Explorer
KNOW-PAT-227

Database Schema Design — Indexing, migrations, connection pooling, normalization

Domaine
database
Type
pattern
Priorité
P1

Database Schema Design

Problème

Les schémas sont conçus au fil de l'eau, sans indexing prévu, sans stratégie de migration. Résultat : performances qui s'effondrent avec le volume, migrations qui cassent la prod.

Solution

Principes de design de schéma + indexing stratégique + migrations réversibles.

1. Règles de normalization

  • 3NF par défaut — une donnée = une place
  • Dénormaliser uniquement sur colonnes calculées fréquemment lues (counters, caches)
  • Pas de JSON pour des données relationnelles — utiliser des tables
  • JSON acceptable pour : metadata flexible, config, payloads d'API externes

2. Indexing — les 5 règles

-- 1. Index sur toutes les FK
CREATE INDEX idx_orders_user_id ON orders(user_id);

-- 2. Index composites dans l'ordre de requête
CREATE INDEX idx_orders_status_created ON orders(status, created_at);

-- 3. Index partiel pour les queries fréquentes
CREATE INDEX idx_users_active ON users(last_login) WHERE deleted_at IS NULL;

-- 4. Index unique pour contraintes métier
CREATE UNIQUE INDEX idx_users_email ON users(email) WHERE deleted_at IS NULL;

-- 5. Pas d'index sur colonnes à faible cardinalité (booléens, statuts avec 2 valeurs)

3. Types — choisir le bon

Donnée Type Pourquoi
ID UUID (Postgres) / BIGINT AUTO_INCREMENT UUID pour distribué, BIGINT pour simplicité
Montant financier DECIMAL(19,4) / NUMERIC Jamais FLOAT — erreurs d'arrondi
Timestamps TIMESTAMPTZ Toujours UTC en DB
Texte court VARCHAR(n) Limite explicite
Texte long TEXT Pas de limite
Booléen BOOLEAN Pas de INT(1)
JSON JSONB (Postgres) Indexable, plus rapide que JSON
Enum VARCHAR + CHECK Plus portable que ENUM natif

4. Migrations réversibles

// Prisma migration — toujours up + down
// Drizzle — forward + rollback

// Règles :
// 1. Jamais de migration destructive en prod (DROP COLUMN, DROP TABLE)
// 2. Ajouter colonne → déployer → backfill → supprimer ancienne (migration suivie)
// 3. Renommer colonne → ajouter nouvelle → dual-write → migrer lectures → supprimer ancienne
// 4. Tester migration sur copy de prod avant

// Exemple : ajout de colonne non-null
// Step 1: ADD COLUMN nullable
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
// Step 2: Backfill
UPDATE users SET phone = '' WHERE phone IS NULL;
// Step 3: SET NOT NULL (migration suivie)
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;

5. Connection pooling

// Postgres — pgbouncer en production
// App — pool de connections, pas une connection par requête

// Prisma (pool intégré)
const prisma = new PrismaClient({
  datasources: {
    db: { url: process.env.DATABASE_URL + '?connection_limit=10' }
  }
});

// Drizzle + postgres-js
import postgres from 'postgres';
const sql = postgres(process.env.DATABASE_URL, {
  max: 10,        // max connections
  idle_timeout: 20,
  connect_timeout: 10
});

6. Soft delete pattern

-- Pas de DELETE physique, utiliser deleted_at
ALTER TABLE users ADD COLUMN deleted_at TIMESTAMPTZ;

-- Queries filtrent automatiquement
-- Via view :
CREATE VIEW users_active AS
SELECT * FROM users WHERE deleted_at IS NULL;

-- Via ORM scope (Prisma middleware ou Drizzle)

7. Audit trail

CREATE TABLE audit_log (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  table_name VARCHAR(50) NOT NULL,
  record_id UUID NOT NULL,
  action VARCHAR(10) NOT NULL, -- INSERT, UPDATE, DELETE
  changed_by UUID REFERENCES users(id),
  changed_at TIMESTAMPTZ DEFAULT NOW(),
  old_values JSONB,
  new_values JSONB
);

-- Trigger automatique ou application-level logging

Anti-patterns

  • SELECT * en production
  • Pas d'index sur les FK
  • FLOAT pour des montants
  • Migration destructive sans plan de rollback
  • Une seule connection partagée (bottleneck)
  • Pas de soft delete sur des données réglementées

Références

  • [[KNOW-PAT-220]] — Secure SDLC Checklist
  • [[KNOW-PAT-221]] — Privacy by Design (retention, deletion)
  • [[KNOW-REF-010]] — Twelve-Factor App (backup, concurrency)