Modélisation Dimensionnelle
Workflow — 4 décisions dans l'ordre
1. Identifier le processus métier
Choisir UN processus à la fois (ventes, commandes, facturation, trafic web).
Ne pas mélanger deux processus dans une même table de faits à ce stade.
2. Définir le grain
La décision la plus critique. "1 ligne = ?" doit s'énoncer en une phrase.
| Grain |
Exemple |
| Grain fin (transactionnel) |
1 ligne par ligne de commande |
| Grain moyen |
1 ligne par commande |
| Grain agrégé |
1 ligne par client par mois |
Règle : toujours choisir le grain le plus fin techniquement supportable.
Les agrégats peuvent toujours être calculés à la requête ; l'inverse est impossible.
3. Identifier les dimensions
Questions guides : Qui ? Quoi ? Où ? Quand ? Comment ?
Chaque dimension répond à l'une de ces questions pour décrire le fait.
4. Identifier les mesures (faits)
Ne retenir que les mesures numériques cohérentes avec le grain défini.
Classer chaque mesure : additive / semi-additive / non-additive.
| Additivité |
Définition |
Exemple |
| Additive |
Somme valide sur toutes dimensions |
Quantité vendue, chiffre d'affaires |
| Semi-additive |
Somme valide sur certaines dimensions seulement |
Solde de compte (pas sur le temps) |
| Non-additive |
Pas de somme utile |
Taux, ratios, prix unitaire |
Schéma en étoile — Structure SQL
-- Table de faits
CREATE TABLE fact_sales (
sale_key BIGINT IDENTITY PRIMARY KEY,
date_key INT NOT NULL REFERENCES dim_date(date_key),
product_key INT NOT NULL REFERENCES dim_product(product_key),
customer_key INT NOT NULL REFERENCES dim_customer(customer_key),
store_key INT NOT NULL REFERENCES dim_store(store_key),
-- Mesures additives
quantity INT NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
discount_amount DECIMAL(10,2) NOT NULL DEFAULT 0,
net_amount DECIMAL(10,2) NOT NULL,
tax_amount DECIMAL(10,2) NOT NULL,
total_amount DECIMAL(10,2) NOT NULL,
-- Clés dégénérées (identifiants source sans dimension propre)
invoice_number VARCHAR(50),
line_number INT
);
-- Dimension Date (pré-remplie, jamais via ETL en temps réel)
CREATE TABLE dim_date (
date_key INT PRIMARY KEY, -- Format YYYYMMDD
full_date DATE NOT NULL,
day_of_week INT NOT NULL, -- 1=Lundi ... 7=Dimanche
day_name VARCHAR(10) NOT NULL,
day_of_month INT NOT NULL,
week_of_year INT NOT NULL,
month_number INT NOT NULL,
month_name VARCHAR(10) NOT NULL,
quarter INT NOT NULL,
year INT NOT NULL,
is_weekend BIT NOT NULL,
is_holiday BIT NOT NULL,
fiscal_year INT,
fiscal_quarter INT
);
Remplir dim_date en SQL (script one-shot)
-- Génère toutes les dates entre deux bornes
WITH dates AS (
SELECT CAST('2010-01-01' AS DATE) AS d
UNION ALL
SELECT DATEADD(DAY, 1, d) FROM dates WHERE d < '2030-12-31'
)
INSERT INTO dim_date (date_key, full_date, day_of_week, day_name,
day_of_month, week_of_year, month_number, month_name,
quarter, year, is_weekend)
SELECT
CONVERT(INT, FORMAT(d, 'yyyyMMdd')),
d,
DATEPART(WEEKDAY, d),
DATENAME(WEEKDAY, d),
DAY(d),
DATEPART(WEEK, d),
MONTH(d),
DATENAME(MONTH, d),
DATEPART(QUARTER, d),
YEAR(d),
CASE WHEN DATEPART(WEEKDAY, d) IN (1,7) THEN 1 ELSE 0 END
FROM dates
OPTION (MAXRECURSION 10000);
Slowly Changing Dimensions (SCD)
Critères de choix
| Type |
Conserver l'historique ? |
Volume delta |
À utiliser si… |
| Type 1 |
Non |
Faible |
Correction d'erreur, attribut sans valeur analytique (ex: code postal format) |
| Type 2 |
Oui, complet |
Moyen-élevé |
Segment client, territoire vendeur, catégorie produit |
| Type 3 |
Partiel (1 seule transition) |
Faible |
Réorganisation connue à l'avance avec comparaison avant/après |
| Type 4 (mini-dimension) |
Oui, séparé |
Attributs très volatils |
Profil comportemental changeant fréquemment |
| Type 6 (hybride 1+2+3) |
Oui + snapshot courant |
Complexe |
Besoin de navigation historique ET valeur courante dénormalisée |
SCD Type 2 — Pattern complet
CREATE TABLE dim_customer (
customer_key INT IDENTITY PRIMARY KEY, -- Surrogate key
customer_id VARCHAR(50) NOT NULL, -- Business key (NK)
name VARCHAR(200) NOT NULL,
email VARCHAR(200),
city VARCHAR(100),
segment VARCHAR(50),
effective_date DATE NOT NULL,
expiration_date DATE NOT NULL DEFAULT '9999-12-31',
is_current BIT NOT NULL DEFAULT 1
);
-- ETL : mise à jour SCD Type 2
BEGIN TRANSACTION;
-- Étape 1 : expirer l'enregistrement actuel
UPDATE dim_customer
SET expiration_date = CAST(GETDATE() AS DATE),
is_current = 0
WHERE customer_id = 'CUST-123'
AND is_current = 1;
-- Étape 2 : insérer la nouvelle version
INSERT INTO dim_customer (customer_id, name, email, city, segment, effective_date)
VALUES ('CUST-123', 'Jean Dupont', 'jean@new.com', 'Lyon', 'Premium', CAST(GETDATE() AS DATE));
COMMIT;
-- Requête sur une période historique
SELECT f.total_amount, c.segment
FROM fact_sales f
JOIN dim_customer c ON c.customer_key = f.customer_key
AND c.effective_date <= f.sale_date
AND f.sale_date < c.expiration_date;
Types de tables de faits
| Type |
Grain |
Cas d'usage |
| Transactionnelle |
1 ligne par événement atomique |
Ventes, clics, transactions |
| Snapshot périodique |
1 ligne par entité par période |
Solde mensuel, stock fin de journée |
| Snapshot cumulatif |
1 ligne par cycle de vie |
Pipeline commande (créée → validée → expédiée → livrée) |
| Sans faits (factless) |
Intersection sans mesure |
Présence à un événement, éligibilité à une promotion |
Snapshot cumulatif — colonnes types
CREATE TABLE fact_order_pipeline (
order_key INT PRIMARY KEY,
order_id VARCHAR(50),
-- Une date_key par étape
created_date_key INT REFERENCES dim_date(date_key),
approved_date_key INT REFERENCES dim_date(date_key),
shipped_date_key INT REFERENCES dim_date(date_key),
delivered_date_key INT REFERENCES dim_date(date_key),
-- Lag entre étapes (recalculé à chaque mise à jour)
days_to_approve INT,
days_to_ship INT,
days_to_deliver INT,
order_amount DECIMAL(12,2)
);
Schéma flocon (snowflake) — quand l'utiliser
Normaliser une dimension (ex: dim_product → dim_category → dim_department) :
- Pour : réduit la redondance, cohérence de mise à jour.
- Contre : jointures supplémentaires, requêtes plus lentes, lisibilité dégradée.
Règle 2026 : préférer le star schema sauf si le volume de la dimension est > 10M lignes et que les attributs normalisables sont très stables. Les outils BI modernes (Power BI, Tableau, Looker) sont optimisés star.
Anti-patterns et garde-fous
| Anti-pattern |
Symptôme |
Correction |
| Grain mixte |
Mesures incomparables dans une même fact |
Séparer en deux tables de faits |
| Clé métier comme FK |
JOIN lent, problèmes SCD |
Toujours utiliser les surrogate keys |
| Dimension fourre-tout |
dim_misc, dim_attributes avec 80 colonnes |
Décomposer en dimensions thématiques |
| Mesure non-additive stockée brute |
Requêtes fausses (SUM de taux) |
Stocker numérateur + dénominateur, calculer le ratio en vue |
| SCD Type 2 sur tout |
Table de 100M lignes pour une dim de 10k clients |
Choisir SCD Type 1 pour attributs sans valeur historique |
| dim_date absente |
Filtres dates via CAST sur la fact |
Toujours créer et pré-remplir dim_date |
| NULL en FK |
Ruptures de jointure silencieuses |
Créer une ligne "inconnu" (key = -1) dans chaque dimension |
Bonnes pratiques 2026
- Nommage : préfixe
fact_ / dim_ / bridge_ ; clés avec suffixe _key ; business keys avec _id ou _bk.
- Surrogate keys :
INT IDENTITY (OLTP < 2 milliards) ou BIGINT sinon. Ne jamais exposer la surrogate key aux utilisateurs BI.
- Ligne "inconnu" : insérer
customer_key = -1, customer_id = 'UNKNOWN' dans chaque dimension pour absorber les NULLs ETL.
- Date clé : format
YYYYMMDD comme INT — plus rapide pour les range scans que DATE.
- Indexes : clustered sur la PK de la fact ; columnstore non-clustered pour les agrégations analytiques (SQL Server / Synapse).
- Partitionnement : partitionner les grandes facts par
date_key (partition par année ou trimestre).
- Modèle bus : définir une matrice bus (processus × dimension) avant de coder pour identifier les dimensions conformées réutilisables.
- Tests de cohérence : vérifier
COUNT(DISTINCT surrogate_key) = COUNT(*) sur les dimensions et l'absence de NULLs sur les FKs de la fact après chaque chargement ETL.
Communication Rules — MANDATORY
- Ultra-concise. No filler, no preamble, no pleasantries.
- Never say "happy to help", "sure!", "great question", "let me", or similar.
- Tool first, talk second. Act before explaining.
- Result first. Lead with outcome, not process.
- Stop when done. No summary, no recap, no trailing commentary.
- No politeness wrappers. Direct and blunt.
- Minimum words. If one word works, do not use ten.
- No unsolicited explanations.
- No emoji unless asked.
1---2name: data-dimensional-modeling3description: Modélisation dimensionnelle pour le data warehousing — schéma en étoile, flocon, tables de faits et dimensions, slowly changing dimensions. À utiliser quand l'utilisateur conçoit un data warehouse, modélise des faits/dimensions ou met en place du reporting analytique. Se déclenche aussi avec "schéma en étoile", "star schema", "table de faits", "dimension", "data warehouse", "SCD", "slowly changing dimension", "modélisation dimensionnelle". Also triggers on "fact and dimension tables", "data warehouse modeling".4---56# Modélisation Dimensionnelle78## Workflow — 4 décisions dans l'ordre910### 1. Identifier le processus métier11Choisir UN processus à la fois (ventes, commandes, facturation, trafic web).12Ne pas mélanger deux processus dans une même table de faits à ce stade.1314### 2. Définir le grain15La décision la plus critique. "1 ligne = ?" doit s'énoncer en une phrase.1617| Grain | Exemple |18|-------|---------|19| Grain fin (transactionnel) | 1 ligne par ligne de commande |20| Grain moyen | 1 ligne par commande |21| Grain agrégé | 1 ligne par client par mois |2223> Règle : toujours choisir le grain le plus fin techniquement supportable.24> Les agrégats peuvent toujours être calculés à la requête ; l'inverse est impossible.2526### 3. Identifier les dimensions27Questions guides : Qui ? Quoi ? Où ? Quand ? Comment ?28Chaque dimension répond à l'une de ces questions pour décrire le fait.2930### 4. Identifier les mesures (faits)31Ne retenir que les mesures numériques cohérentes avec le grain défini.32Classer chaque mesure : additive / semi-additive / non-additive.3334| Additivité | Définition | Exemple |35|-----------|-----------|---------|36| **Additive** | Somme valide sur toutes dimensions | Quantité vendue, chiffre d'affaires |37| **Semi-additive** | Somme valide sur certaines dimensions seulement | Solde de compte (pas sur le temps) |38| **Non-additive** | Pas de somme utile | Taux, ratios, prix unitaire |3940---4142## Schéma en étoile — Structure SQL4344```sql45-- Table de faits46CREATE TABLE fact_sales (47 sale_key BIGINT IDENTITY PRIMARY KEY,48 date_key INT NOT NULL REFERENCES dim_date(date_key),49 product_key INT NOT NULL REFERENCES dim_product(product_key),50 customer_key INT NOT NULL REFERENCES dim_customer(customer_key),51 store_key INT NOT NULL REFERENCES dim_store(store_key),5253 -- Mesures additives54 quantity INT NOT NULL,55 unit_price DECIMAL(10,2) NOT NULL,56 discount_amount DECIMAL(10,2) NOT NULL DEFAULT 0,57 net_amount DECIMAL(10,2) NOT NULL,58 tax_amount DECIMAL(10,2) NOT NULL,59 total_amount DECIMAL(10,2) NOT NULL,6061 -- Clés dégénérées (identifiants source sans dimension propre)62 invoice_number VARCHAR(50),63 line_number INT64);6566-- Dimension Date (pré-remplie, jamais via ETL en temps réel)67CREATE TABLE dim_date (68 date_key INT PRIMARY KEY, -- Format YYYYMMDD69 full_date DATE NOT NULL,70 day_of_week INT NOT NULL, -- 1=Lundi ... 7=Dimanche71 day_name VARCHAR(10) NOT NULL,72 day_of_month INT NOT NULL,73 week_of_year INT NOT NULL,74 month_number INT NOT NULL,75 month_name VARCHAR(10) NOT NULL,76 quarter INT NOT NULL,77 year INT NOT NULL,78 is_weekend BIT NOT NULL,79 is_holiday BIT NOT NULL,80 fiscal_year INT,81 fiscal_quarter INT82);83```8485### Remplir dim_date en SQL (script one-shot)8687```sql88-- Génère toutes les dates entre deux bornes89WITH dates AS (90 SELECT CAST('2010-01-01' AS DATE) AS d91 UNION ALL92 SELECT DATEADD(DAY, 1, d) FROM dates WHERE d < '2030-12-31'93)94INSERT INTO dim_date (date_key, full_date, day_of_week, day_name,95 day_of_month, week_of_year, month_number, month_name,96 quarter, year, is_weekend)97SELECT98 CONVERT(INT, FORMAT(d, 'yyyyMMdd')),99 d,100 DATEPART(WEEKDAY, d),101 DATENAME(WEEKDAY, d),102 DAY(d),103 DATEPART(WEEK, d),104 MONTH(d),105 DATENAME(MONTH, d),106 DATEPART(QUARTER, d),107 YEAR(d),108 CASE WHEN DATEPART(WEEKDAY, d) IN (1,7) THEN 1 ELSE 0 END109FROM dates110OPTION (MAXRECURSION 10000);111```112113---114115## Slowly Changing Dimensions (SCD)116117### Critères de choix118119| Type | Conserver l'historique ? | Volume delta | À utiliser si… |120|------|--------------------------|--------------|----------------|121| **Type 1** | Non | Faible | Correction d'erreur, attribut sans valeur analytique (ex: code postal format) |122| **Type 2** | Oui, complet | Moyen-élevé | Segment client, territoire vendeur, catégorie produit |123| **Type 3** | Partiel (1 seule transition) | Faible | Réorganisation connue à l'avance avec comparaison avant/après |124| **Type 4** (mini-dimension) | Oui, séparé | Attributs très volatils | Profil comportemental changeant fréquemment |125| **Type 6** (hybride 1+2+3) | Oui + snapshot courant | Complexe | Besoin de navigation historique ET valeur courante dénormalisée |126127### SCD Type 2 — Pattern complet128129```sql130CREATE TABLE dim_customer (131 customer_key INT IDENTITY PRIMARY KEY, -- Surrogate key132 customer_id VARCHAR(50) NOT NULL, -- Business key (NK)133 name VARCHAR(200) NOT NULL,134 email VARCHAR(200),135 city VARCHAR(100),136 segment VARCHAR(50),137138 effective_date DATE NOT NULL,139 expiration_date DATE NOT NULL DEFAULT '9999-12-31',140 is_current BIT NOT NULL DEFAULT 1141);142143-- ETL : mise à jour SCD Type 2144BEGIN TRANSACTION;145146-- Étape 1 : expirer l'enregistrement actuel147UPDATE dim_customer148SET expiration_date = CAST(GETDATE() AS DATE),149 is_current = 0150WHERE customer_id = 'CUST-123'151 AND is_current = 1;152153-- Étape 2 : insérer la nouvelle version154INSERT INTO dim_customer (customer_id, name, email, city, segment, effective_date)155VALUES ('CUST-123', 'Jean Dupont', 'jean@new.com', 'Lyon', 'Premium', CAST(GETDATE() AS DATE));156157COMMIT;158159-- Requête sur une période historique160SELECT f.total_amount, c.segment161FROM fact_sales f162JOIN dim_customer c ON c.customer_key = f.customer_key163 AND c.effective_date <= f.sale_date164 AND f.sale_date < c.expiration_date;165```166167---168169## Types de tables de faits170171| Type | Grain | Cas d'usage |172|------|-------|------------|173| **Transactionnelle** | 1 ligne par événement atomique | Ventes, clics, transactions |174| **Snapshot périodique** | 1 ligne par entité par période | Solde mensuel, stock fin de journée |175| **Snapshot cumulatif** | 1 ligne par cycle de vie | Pipeline commande (créée → validée → expédiée → livrée) |176| **Sans faits (factless)** | Intersection sans mesure | Présence à un événement, éligibilité à une promotion |177178### Snapshot cumulatif — colonnes types179180```sql181CREATE TABLE fact_order_pipeline (182 order_key INT PRIMARY KEY,183 order_id VARCHAR(50),184 -- Une date_key par étape185 created_date_key INT REFERENCES dim_date(date_key),186 approved_date_key INT REFERENCES dim_date(date_key),187 shipped_date_key INT REFERENCES dim_date(date_key),188 delivered_date_key INT REFERENCES dim_date(date_key),189 -- Lag entre étapes (recalculé à chaque mise à jour)190 days_to_approve INT,191 days_to_ship INT,192 days_to_deliver INT,193 order_amount DECIMAL(12,2)194);195```196197---198199## Schéma flocon (snowflake) — quand l'utiliser200201Normaliser une dimension (ex: `dim_product → dim_category → dim_department`) :202- **Pour** : réduit la redondance, cohérence de mise à jour.203- **Contre** : jointures supplémentaires, requêtes plus lentes, lisibilité dégradée.204205> Règle 2026 : préférer le star schema sauf si le volume de la dimension est > 10M lignes et que les attributs normalisables sont très stables. Les outils BI modernes (Power BI, Tableau, Looker) sont optimisés star.206207---208209## Anti-patterns et garde-fous210211| Anti-pattern | Symptôme | Correction |212|-------------|---------|-----------|213| **Grain mixte** | Mesures incomparables dans une même fact | Séparer en deux tables de faits |214| **Clé métier comme FK** | JOIN lent, problèmes SCD | Toujours utiliser les surrogate keys |215| **Dimension fourre-tout** | dim_misc, dim_attributes avec 80 colonnes | Décomposer en dimensions thématiques |216| **Mesure non-additive stockée brute** | Requêtes fausses (SUM de taux) | Stocker numérateur + dénominateur, calculer le ratio en vue |217| **SCD Type 2 sur tout** | Table de 100M lignes pour une dim de 10k clients | Choisir SCD Type 1 pour attributs sans valeur historique |218| **dim_date absente** | Filtres dates via CAST sur la fact | Toujours créer et pré-remplir dim_date |219| **NULL en FK** | Ruptures de jointure silencieuses | Créer une ligne "inconnu" (key = -1) dans chaque dimension |220221---222223## Bonnes pratiques 2026224225- **Nommage** : préfixe `fact_` / `dim_` / `bridge_` ; clés avec suffixe `_key` ; business keys avec `_id` ou `_bk`.226- **Surrogate keys** : `INT IDENTITY` (OLTP < 2 milliards) ou `BIGINT` sinon. Ne jamais exposer la surrogate key aux utilisateurs BI.227- **Ligne "inconnu"** : insérer `customer_key = -1, customer_id = 'UNKNOWN'` dans chaque dimension pour absorber les NULLs ETL.228- **Date clé** : format `YYYYMMDD` comme `INT` — plus rapide pour les range scans que `DATE`.229- **Indexes** : clustered sur la PK de la fact ; columnstore non-clustered pour les agrégations analytiques (SQL Server / Synapse).230- **Partitionnement** : partitionner les grandes facts par `date_key` (partition par année ou trimestre).231- **Modèle bus** : définir une matrice bus (processus × dimension) avant de coder pour identifier les dimensions conformées réutilisables.232- **Tests de cohérence** : vérifier `COUNT(DISTINCT surrogate_key) = COUNT(*)` sur les dimensions et l'absence de NULLs sur les FKs de la fact après chaque chargement ETL.233234235## Communication Rules — MANDATORY236237- Ultra-concise. No filler, no preamble, no pleasantries.238- Never say "happy to help", "sure!", "great question", "let me", or similar.239- Tool first, talk second. Act before explaining.240- Result first. Lead with outcome, not process.241- Stop when done. No summary, no recap, no trailing commentary.242- No politeness wrappers. Direct and blunt.243- Minimum words. If one word works, do not use ten.244- No unsolicited explanations.245- No emoji unless asked.