Skip to content

Commit bbb8c36

Browse files
committed
feat: updated database + documentation
1 parent b9641c6 commit bbb8c36

9 files changed

Lines changed: 1690 additions & 165 deletions

page/dashboard.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

sandbox/postgres/README.md

Lines changed: 117 additions & 56 deletions
Large diffs are not rendered by default.

sandbox/postgres/architecture_decision_records.md

Lines changed: 209 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,157 @@ descriptives (`mime_type`, `folder_url`, `file_name`, `width`, `height`).
132132

133133
---
134134

135+
## ADR-023 — Éclatement de l'agrégat de commande en composants ECS 1:1
136+
137+
**Statut** : Adopté
138+
139+
### Contexte
140+
141+
Un système de commande réel nécessite des données de natures radicalement
142+
différentes : statut d'exécution (hot, consulté à chaque affichage), données
143+
financières (warm, consultées à la confirmation et à la facturation), informations
144+
de livraison (cold, consultées après expédition). Les stocker dans une table
145+
unique pollue systématiquement le cache CPU avec des données froides lors de
146+
chaque accès chaud.
147+
148+
### Décision
149+
150+
L'entité commande est décomposée en quatre composants denses, reliés par la
151+
même clé primaire (`transaction_id`) :
152+
153+
| Composant | Sémantique schema.org | Fréquence d'accès |
154+
| --------------------------- | --------------------------------- | ----------------- |
155+
| `transaction_core` | `Order` | Très haute |
156+
| `transaction_price` | `PriceSpecification` | Haute |
157+
| `transaction_payment` | `PaymentChargeSpecification` | Moyenne |
158+
| `transaction_delivery` | `ParcelDelivery` | Basse |
159+
| `transaction_item` | `OrderItem` (N lignes) | Haute |
160+
161+
Chaque composant est une relation 1:1 stricte (PK = FK vers `transaction_core`).
162+
163+
### Justification — cache CPU et densité
164+
165+
L'argument "éviter une jointure" en faveur d'une fat table repose sur un coût
166+
mal évalué. Une jointure sur clé primaire entière (`INT4`) est un déréférencement
167+
mémoire trivial sur une ligne déjà en cache — coût de l'ordre de quelques
168+
nanosecondes. La pollution de cache par des colonnes froides a un coût
169+
structurellement récurrent :
170+
171+
- `tracking_number VARCHAR(255)` dans `transaction_core` gonfle chaque tuple
172+
d'au moins 4 bytes d'en-tête varlena, plus le contenu. Sur 500 000 commandes,
173+
un champ non nul de 20 chars ajoute ~12 Mo de bruit sur le heap du composant core.
174+
- Ce bruit réduit mécaniquement le nombre de tuples par page cache, augmentant
175+
les shared_blks_hit nécessaires pour chauffer le working set.
176+
177+
Avec la décomposition, `transaction_core` tient en **32 bytes/tuple** (~258
178+
tuples/page). L'affichage du statut de toutes les commandes d'un client ne charge
179+
jamais le transporteur ni le numéro de facture.
180+
181+
### Arbitrages typologiques (DOD)
182+
183+
**`currency_code SMALLINT`** (2 bytes, pass-by-value) plutôt que `CHAR(3)`
184+
(varlena avec padding) ou `VARCHAR(3)` (varlena). Le code ISO 4217 numérique
185+
(978 = EUR, 840 = USD, 826 = GBP) couvre l'ensemble des devises actives.
186+
Le mapping vers le code alphabétique est délégué à la couche applicative — c'est
187+
une simple table de lookup statique, jamais un accès base de données.
188+
189+
**`tax_rate_bp INT4`** (4 bytes, pass-by-value) plutôt que `NUMERIC(5,2)`
190+
(varlena, arithmétique logicielle). Le taux est stocké en **basis points**
191+
(1 bp = 0,01%). Exemples : 2000 = 20,00% · 550 = 5,50%. L'arithmétique entière
192+
ALU native remplace l'émulation NUMERIC : `(amount_cents * tax_rate_bp) / 10000`
193+
reste dans les registres CPU.
194+
195+
**Tous les montants en `INT8` centimes** (ADR-022) : `shipping_cents`,
196+
`discount_cents`, `tax_cents` suivent la même règle que `price_cents`. Pas de
197+
NUMERIC dans le hot path financier.
198+
199+
### Procédure `create_transaction`
200+
201+
La procédure crée atomiquement les quatre composants : `transaction_core` +
202+
`transaction_price` (devise + montants à zéro) + `transaction_payment` (statut 0)
203+
+ `transaction_delivery` (statut 0). **Il n'existe jamais de transaction_core sans
204+
ses trois composants** — l'invariant est garanti par l'atomicité de la transaction.
205+
Les composants sont mis à jour indépendamment au fil du cycle de vie de la commande.
206+
207+
### Relation avec les autres ADR
208+
209+
- ADR-005 (fragmentation ECS) : même pattern appliqué au domaine Commerce.
210+
- ADR-022 (INT8 centimes) : invariant étendu à tous les montants de `transaction_price`.
211+
- ADR-020 (SECURITY DEFINER) : `create_transaction` est la seule procédure
212+
d'écriture autorisée pour `marius_user` sur `transaction_core` et ses composants.
213+
214+
---
215+
216+
## ADR-022 — Optimisations CPU/mémoire : entiers natifs, VARCHAR, padding documenté, pushdown
217+
218+
**Statut** : Adopté
219+
220+
### Contexte
221+
222+
Audit DOD ciblé sur les composants chauds (`commerce.product_core`,
223+
`commerce.transaction_item`, `org.org_legal`, `identity.auth`,
224+
`commerce.v_transaction`). Quatre ajustements de topologie physique.
225+
226+
### 1. NUMERIC → INT8 centimes (commerce)
227+
228+
Voir ADR-014 pour le raisonnement complet. Résumé :
229+
230+
- `NUMERIC` est varlena → padding d'alignement, tuple deforming indirect, arithmétique logicielle.
231+
- `INT8` est pass-by-value, aligné sur 8 bytes, arithmétique ALU native.
232+
- Gain de densité : ×2 sur `product_core` (24 B/tuple), ×2,2 sur `transaction_item` (20 B/tuple).
233+
- Convention de nommage : suffixe `_cents` visible dans les noms de colonnes et dans les alias de vues.
234+
235+
### 2. `CHAR(n)``VARCHAR(n)` (org.org_legal, commerce.product_identity)
236+
237+
`CHAR(n)` dans PostgreSQL est varlena comme `VARCHAR(n)`. La seule différence est
238+
le padding espace sur écriture et le stripping sur lecture : **surcoût CPU sans
239+
contrepartie**. Les contraintes CHECK avec regex garantissent la longueur exacte et
240+
la conservation des zéros initiaux — `VARCHAR(n)` est strictement suffisant.
241+
242+
Correction étendue à `isbn_ean VARCHAR(13)` dans `commerce.product_identity` pour
243+
la même raison (était `CHAR(13)`).
244+
245+
### 3. Slot de padding libre dans `identity.auth` — documentation
246+
247+
Séquence de types en fin de tuple fixe de `identity.auth` :
248+
```
249+
role_id SMALLINT (2 B, offset 28)
250+
is_banned BOOLEAN (1 B, offset 30)
251+
[padding] — (1 B, offset 31) ← slot libre
252+
password_hash varlena (offset 32, alignement 4 B)
253+
```
254+
255+
Ce byte de padding est structurellement inévitable : la varlena `password_hash`
256+
requiert un alignement sur 4 bytes, et la séquence SMALLINT + BOOLEAN consomme
257+
3 bytes. Le slot à l'offset 31 est documenté comme **emplacement réservé pour un
258+
prochain BOOLEAN** (ex : `is_email_verified`) sans coût marginal.
259+
260+
**Pourquoi pas le type interne `"char"`** : c'est un type système sans opérateurs
261+
de domaine ni coercition standard. Il rend le schéma opaque pour tout DBA et
262+
incompatible avec les outils standards. Pour les états fermés futurs, `SMALLINT`
263+
avec `CHECK (status IN (...))` reste le bon choix.
264+
265+
### 4. Pushdown garanti sur `commerce.v_transaction` — documentation
266+
267+
PostgreSQL inline les vues avant planification : la vue n'est pas une barrière
268+
d'optimisation. `WHERE "identifier" = :id` appliqué sur `v_transaction` se réécrit
269+
en `WHERE t.id = :id` par le query rewriter avant que le planner n'intervienne.
270+
`t.id` figure dans le GROUP BY — le prédicat est poussé avant `json_agg()`, l'index
271+
PK est utilisé, l'agrégation porte sur les lignes de la commande concernée uniquement.
272+
273+
**Vérification recommandée** :
274+
```sql
275+
EXPLAIN (ANALYZE, BUFFERS)
276+
SELECT * FROM commerce.v_transaction WHERE "identifier" = 1;
277+
-- Attendu : Index Scan sur commerce.transaction (PK), pas de Seq Scan.
278+
```
279+
280+
**Invariant d'usage** : `v_transaction` ne doit jamais être appelée sans filtre.
281+
Un `SELECT *` sans `WHERE` agrège toutes les lignes de toutes les transactions.
282+
Documenté dans la définition de la vue.
283+
284+
---
285+
135286
## ADR-005 — Fragmentation ECS : SoA en lieu de AoS
136287

137288
**Statut** : Adopté
@@ -610,20 +761,49 @@ est direct sur les tables à forte volumétrie (`content_to_tag`, `transaction_i
610761

611762
---
612763

613-
## ADR-014 — `NUMERIC(12,2)` pour tous les montants monétaires
764+
## ADR-014 — `INT8` centimes pour tous les montants monétaires
614765

615-
**Statut** : Adopté
766+
**Statut** : Adopté (révisé — ADR-022)
616767

617-
### Justification
768+
### Décision
769+
770+
Les montants monétaires (`price_cents` dans `product_core`,
771+
`unit_price_snapshot_cents` dans `transaction_item`) sont stockés en **centimes
772+
de la devise de référence** sous forme d'entier `INT8`. La conversion décimale
773+
est déléguée à la couche applicative.
774+
775+
### Pourquoi pas NUMERIC
776+
777+
`NUMERIC` est varlena dans PostgreSQL sans exception, quelle que soit la précision
778+
déclarée. Conséquences directes :
618779

619-
`FLOAT8` introduit des erreurs d'arrondi binaire sur les représentations
620-
décimales exactes (`0.1 + 0.2 ≠ 0.3`). Inacceptable pour des montants financiers
621-
archivés dans `unit_price_snapshot` — le prix capturé doit être exact et stable.
780+
- **Padding d'alignement** : le moteur insère des bytes de remplissage entre les
781+
colonnes à taille fixe et l'en-tête varlena de NUMERIC. Ce padding est invisible
782+
dans `\d` et permanent.
783+
- **Tuple deforming** : NUMERIC déclenche un appel indirect via parsing de l'en-tête
784+
varlena à chaque lecture de tuple, même pour une comparaison triviale.
785+
- **Arithmétique** : les opérations sur NUMERIC sont émulées en base 10000 en
786+
logiciel. `unit_price * quantity` dans `v_transaction` ne touche pas l'ALU.
622787

623-
`NUMERIC` sans précision produit un stockage variable non contrôlé.
788+
### Pourquoi INT8 et non INT4
624789

625-
`NUMERIC(12,2)` : précision décimale exacte, plage jusqu'à 9 999 999 999,99
626-
(~10 Md€), stockage interne de 6–8 bytes pour les montants courants.
790+
INT4 max = 2 147 483 647 centimes ≈ 21,4 M€. Insuffisant pour les transactions
791+
B2B (équipements industriels, licences). INT8 max ≈ 92 000 Md€. Le surcoût est
792+
nul : INT8 est pass-by-value sur toutes les architectures 64 bits.
793+
794+
### Gain de densité
795+
796+
| Table | Avant (NUMERIC) | Après (INT8) | Tuples/page |
797+
| --------------------------- | --------------- | ------------ | ----------- |
798+
| `commerce.product_core` | ~48 B | 24 B | ~341 (×2) |
799+
| `commerce.transaction_item` | ~44 B | 20 B | ~409 (×2,2) |
800+
801+
### Convention de nommage
802+
803+
Les colonnes suffixées `_cents` rendent l'invariant d'unité visible à tout lecteur
804+
du schéma sans documentation supplémentaire. La vue expose le suffixe
805+
(`"priceCents"`, `"unitPriceCents"`, `"totalPriceCents"`) pour que le contrat API
806+
soit sans ambiguïté.
627807

628808
---
629809

@@ -634,33 +814,39 @@ archivés dans `unit_price_snapshot` — le prix capturé doit être exact et st
634814
### Décision
635815

636816
Les lignes de commande sont une table dédiée :
637-
`commerce.transaction_item(transaction_id, product_id, quantity, unit_price_snapshot)`.
817+
`commerce.transaction_item(unit_price_snapshot_cents, transaction_id, product_id, quantity)`.
638818

639819
### Justification
640820

641821
Une liste d'identifiants sérialisée en colonne varchar rend impossible toute FK
642822
référentielle, tout agrégat par produit et tout historique de prix. Le champ
643-
`unit_price_snapshot NUMERIC(12,2)` capture le prix au moment de l'INSERT : le
644-
prix courant peut évoluer sans altérer l'historique des commandes passées.
823+
`unit_price_snapshot_cents INT8` capture le prix en centimes au moment de l'INSERT :
824+
le prix courant peut évoluer sans altérer l'historique des commandes passées.
645825

646826
`quantity INT4` (et non `SMALLINT`) : SMALLINT max = 32 767. Les commandes B2B
647827
peuvent dépasser ce volume. INT4 couvre jusqu'à ~2,1 milliards.
648828

649829
---
650830

651-
## ADR-010 — Typage `CHAR(9)` / `CHAR(14)` pour DUNS et SIRET
831+
## ADR-010 — `VARCHAR(n)` pour DUNS, SIRET et ISBN/EAN
652832

653-
**Statut** : Adopté
833+
**Statut** : Adopté (révisé — ADR-022)
834+
835+
### Décision
836+
837+
Les identifiants de longueur fixe (`duns VARCHAR(9)`, `siret VARCHAR(14)`,
838+
`isbn_ean VARCHAR(13)`) utilisent `VARCHAR(n)` et non `CHAR(n)`.
654839

655840
### Justification
656841

657-
DUNS (9 chiffres) et SIRET (14 chiffres) sont des identifiants non arithmétiques :
658-
**les zéros initiaux sont significatifs**. Un type entier les perd silencieusement.
659-
`VARCHAR` n'impose pas la longueur fixe. `CHAR(n)` est le seul type garantissant
660-
longueur fixe et conservation des zéros initiaux.
842+
`CHAR(n)` dans PostgreSQL est varlena au même titre que `VARCHAR(n)` — il n'offre
843+
aucun stockage fixe. La seule différence opérationnelle est le **padding espace**
844+
à l'écriture et le **stripping** à la lecture : surcoût CPU pur sans contrepartie
845+
en densité ni en performance de recherche.
661846

662-
Validation par contrainte CHECK avec regex (`'^[0-9]{9}$'`, `'^[0-9]{14}$'`) :
663-
coût fixe à l'INSERT, zéro coût à la lecture, pas de trigger.
847+
L'invariant de longueur exacte est garanti par les contraintes CHECK avec regex.
848+
`VARCHAR(n)` avec CHECK est strictement équivalent en termes de validation, et
849+
absent de tout overhead de padding.
664850

665851
---
666852

@@ -692,9 +878,10 @@ au même titre que les `REVOKE` qui les suivent.
692878
| `identity.person_identity` | ~74 B | ~110 |
693879
| `identity.person_biography` | 44 B | ~185 |
694880
| `org.org_hierarchy` | 40 B | ~204 |
695-
| `commerce.transaction_item` | 44 B | ~186 |
881+
| `commerce.product_core` | 24 B | ~341 |
882+
| `commerce.transaction_item` | 20 B | ~409 |
696883
| `geo.place_core` (minimal) | 61 B | ~134 |
697884

698885
---
699886

700-
*Architecture ECS/DOD · PostgreSQL 18 · Projet Marius · 21 décisions*
887+
*Architecture ECS/DOD · PostgreSQL 18 · Projet Marius · 23 décisions*

0 commit comments

Comments
 (0)