Polar Code 🎭

Command Palette

Search for a command to run...

09
Pièce N°09

SCENE 9 : Optimisation et Maintenance - L'Entretien de la Machine

La ville vieillit. Les ralentissements aux carrefours. Les fuites dans les canalisations. Les index qui se fragmentent, les statistiques qui mentent. La base de données aussi a besoin d'entretien. Sinon, elle s'effondre.

Le Planificateur de Requêtes – Le Cerveau qui Décide

Quand vous lancez une requête, vous ne dites pas comment la faire.
Le planificateur (query planner) décide.
C'est lui qui choisit les index, l'ordre des jointures, les algorithmes.
Il lit les statistiques. Parfois, il se trompe.

EXPLAIN – Lire dans les Pensées du Planificateur

-- Voir le plan sans exécuter
EXPLAIN SELECT * FROM client WHERE nom = 'Morand';

-- Voir le plan ET les temps réels
EXPLAIN ANALYZE SELECT * FROM client WHERE nom = 'Morand';

-- Sortie typique :
-- Seq Scan on client  (cost=0.00..1850.00 rows=1 width=45) (actual time=0.012..12.345 rows=1 loops=1)
--   Filter: (nom = 'Morand')
--   Rows Removed by Filter: 99999
-- Planning Time: 0.123 ms
-- Execution Time: 12.456 ms

Traduction :

  • Seq Scan = Lecture séquentielle (mauvais signe sur une grosse table)
  • cost=0.00..1850.00 = Coût estimé (unités arbitraires)
  • actual time=0.012..12.345 = Temps réel (ms)
  • rows=1 = Lignes estimées/retournées
  • Filter = Filtre appliqué après lecture

Les Pièges du Planificateur – Quand le Cerveau Déraille

  1. Statistiques périmées – Il pense qu'il y a 100 lignes, il y en a 1 million.
  2. Paramètres mal estimésWHERE date > CURRENT_DATE - INTERVAL '?' – le ? trompe le planificateur.
  3. Corrélations ignoréesWHERE ville = 'Paris' AND code_postal LIKE '75%' – liées, mais il ne le sait pas.

Solutions – Aider le Cerveau

-- 1. Mettre à jour les statistiques (PostgreSQL)
ANALYZE table;

-- 2. Forcer un index (en dernier recours)
SET enable_seqscan = OFF;
-- Attention : seulement pour la session, pas en production générale

-- 3. Réécrire la requête
-- Au lieu de :
SELECT * FROM commande WHERE EXTRACT(MONTH FROM date_commande) = 3;
-- Écrire :
SELECT * FROM commande 
WHERE date_commande >= '2024-03-01' 
  AND date_commande < '2024-04-01';

-- 4. Index sur expression
CREATE INDEX idx_commande_mois ON commande (EXTRACT(MONTH FROM date_commande));

📊 Les Statistiques – Les Yeux du Planificateur

Le SGBD garde des statistiques sur les tables :

  • Nombre de lignes
  • Distribution des valeurs
  • Corrélations entre colonnes

Tables de Statistiques

PostgreSQL :

-- Voir les statistiques d'une table
SELECT schemaname, tablename, n_live_tup, n_dead_tup, last_analyze, last_autoanalyze
FROM pg_stat_user_tables 
WHERE tablename = 'client';

-- n_dead_tup = lignes mortes (à nettoyer par VACUUM)

MySQL :

SHOW TABLE STATUS LIKE 'client';
-- Rows = estimation, Data_length = taille

ANALYZE – Ré-ouvrir les Yeux

-- Analyser une table
ANALYZE client;

-- Analyser une colonne spécifique (PostgreSQL)
ANALYZE client (ville);

-- Analyser toute la base
ANALYZE VERBOSE;
-- VERBOSE montre ce qu'il fait

Quand faire ANALYZE ?

  • Après un gros INSERT/UPDATE/DELETE
  • Après un VACUUM
  • Quand les performances se dégradent mystérieusement

🧹 Nettoyage : VACUUM et OPTIMIZE

Les SGBD n'effacent pas physiquement les données supprimées.
Ils les marquent comme "mortes". En attendant le nettoyeur.

VACUUM (PostgreSQL) – Le Nettoyeur de Nuit

Problème : Les DELETE et UPDATE (qui sont des DELETE+INSERT) laissent des lignes mortes.
Elles prennent de la place, ralentissent les scans.

-- Voir les lignes mortes
SELECT schemaname, relname, n_dead_tup, last_vacuum, last_autovacuum
FROM pg_stat_user_tables;

-- Nettoyage manuel
VACUUM client;
-- Libère l'espace pour réutilisation, ne le rend pas au OS

-- Nettoyage complet (rend l'espace au OS)
VACUUM FULL client;
-- Bloque la table, plus long

-- VACUUM avec analyse
VACUUM ANALYZE client;
-- Nettoie ET met à jour les statistiques

Autovacuum – Le nettoyeur automatique.
Normalement actif. Vérifiez qu'il tourne :

-- Vérifier l'autovacuum
SELECT name, setting FROM pg_settings WHERE name LIKE '%autovacuum%';

-- Problèmes courants :
-- 1. autovacuum = off (à ne JAMAIS faire)
-- 2. Seuil trop haut (par défaut : 50 tuples morts + 20% de la table)

OPTIMIZE TABLE (MySQL) – Le Défragmenteur

-- Défragmenter et réorganiser
OPTIMIZE TABLE client, commande, produit;

-- Equivalent à :
-- 1. Recréer la table
-- 2. Recopier les données
-- 3. Reconstruire les index
-- Bloquant. À faire pendant une fenêtre de maintenance.

Pourcentage de Fragmentation

-- MySQL : voir la fragmentation
SELECT 
    table_name,
    data_free / 1024 / 1024 as fragmentation_mb,
    (data_free / (data_length + index_length)) * 100 as fragmentation_pct
FROM information_schema.tables
WHERE table_schema = 'ma_base'
  AND data_length > 0
ORDER BY fragmentation_pct DESC;

-- > 30% = OPTIMIZE nécessaire

📈 Monitoring – Les Capteurs de la Ville

Métriques Clés à Surveiller

  1. Taille des bases/tables
-- PostgreSQL
SELECT 
    schemaname,
    tablename,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as taille_totale,
    pg_size_pretty(pg_relation_size(schemaname||'.'||tablename)) as taille_table,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename) - 
                   pg_relation_size(schemaname||'.'||tablename)) as taille_indexes
FROM pg_tables
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
  1. Cache Hit Ratio – Combien de lectures viennent du cache
-- PostgreSQL
SELECT 
    sum(heap_blks_read) as disk_reads,
    sum(heap_blks_hit) as cache_hits,
    (sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read) + 0.0001)) * 100 as hit_ratio
FROM pg_statio_user_tables;

-- > 99% = bon
-- < 95% = peut-être besoin de plus de RAM
  1. Longues requêtes
-- PostgreSQL 9.6+
SELECT 
    pid,
    now() - query_start as duree,
    query,
    state
FROM pg_stat_activity 
WHERE state = 'active' 
  AND now() - query_start > interval '5 minutes'
ORDER BY duree DESC;

-- Les tuer si nécessaire
SELECT pg_cancel_backend(pid); -- Annule la requête
SELECT pg_terminate_backend(pid); -- Tue la connexion
  1. Verrous (Locks) – Les embouteillages
-- PostgreSQL
SELECT 
    locktype,
    relation::regclass,
    mode,
    granted,
    pid,
    age(now(), query_start) as age
FROM pg_locks
JOIN pg_stat_activity ON pg_locks.pid = pg_stat_activity.pid
WHERE NOT granted OR age(now(), query_start) > interval '1 minute';

Les Logs – Le Journal de Bord

PostgreSQL :

# postgresql.conf
log_destination = 'stderr'
logging_collector = on
log_directory = 'pg_log'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_min_duration_statement = 1000  # Log les requêtes > 1s

MySQL :

# my.cnf
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2  # Secondes
log_queries_not_using_indexes = 1

Analyser les logs lents :

# PostgreSQL
pgbadger postgresql-*.log -o rapport.html

# MySQL
mysqldumpslow -s t /var/log/mysql/slow.log

🔧 Maintenance Routine – Le Calendrier de l'Entretien

Quotidien

  • Vérifier les espaces disque
  • Vérifier les erreurs dans les logs
  • Surveiller les longues requêtes

Hebdomadaire

-- PostgreSQL
VACUUM ANALYZE;  -- Pour les tables petites/moyennes
-- ou laisser l'autovacuum faire

-- MySQL
OPTIMIZE TABLE table_critique; -- Si fragmentation > 30%
ANALYZE TABLE table_critique;

Mensuel

  • Backup complet + restauration test
  • Vérifier la croissance des tables
  • Purger les logs/archives
  • Vérifier la version, les patches de sécurité

Script de Maintenance Automatisé

-- maintenance.sql (PostgreSQL)
BEGIN;

-- 1. VACUUM des tables avec beaucoup de dead tuples
DO $$
DECLARE
    r RECORD;
BEGIN
    FOR r IN 
        SELECT schemaname, relname 
        FROM pg_stat_user_tables 
        WHERE n_dead_tup > 1000
          AND last_autovacuum < now() - interval '1 day'
    LOOP
        EXECUTE format('VACUUM ANALYZE %I.%I', r.schemaname, r.relname);
        RAISE NOTICE 'VACUUM de %.% fait', r.schemaname, r.relname;
    END LOOP;
END $$;

-- 2. Réindexer les index avec plus de 30% de délétions
DO $$
DECLARE
    r RECORD;
BEGIN
    FOR r IN 
        SELECT schemaname, tablename, indexname
        FROM pg_stat_user_indexes 
        WHERE idx_scan = 0  -- Jamais utilisé
    LOOP
        EXECUTE format('DROP INDEX IF EXISTS %I.%I', r.schemaname, r.indexname);
        RAISE NOTICE 'Index %.% supprimé (inutilisé)', r.schemaname, r.indexname;
    END LOOP;
END $$;

-- 3. Nettoyer les anciennes données
DELETE FROM logs_acces WHERE date < now() - interval '90 days';
DELETE FROM sessions WHERE expire < now() - interval '7 days';

COMMIT;

🚨 Les Situations d'Urgence

1. La Base est Pleine

-- Voir l'espace
SELECT pg_size_pretty(pg_database_size('ma_base'));

-- Solutions rapides :
-- a) Supprimer les données temporaires
-- b) Augmenter l'espace
-- c) Archivage d'urgence

-- Trouver les grosses tables
SELECT * FROM pg_ls_dir('.') 
WHERE pg_stat_file(pg_ls_dir).size > 1000000000; -- >1GB

2. Une Requête Tue le Serveur

-- Identifier le coupable
SELECT pid, query, now() - query_start as age
FROM pg_stat_activity 
WHERE state = 'active'
ORDER BY age DESC;

-- Le tuer
SELECT pg_cancel_backend(12345);  -- Gentil
SELECT pg_terminate_backend(12345); -- Brutal

3. Corruption de Données

-- PostgreSQL : vérifier l'intégrité
CHECKPOINT;  -- Force l'écriture sur disque
-- Redémarrer avec fsync = on (toujours on en production!)

-- MySQL
CHECK TABLE client, commande;
REPAIR TABLE table_corrompue;

4. Perte de Performances Brutale

Check-list :

  1. ANALYZE récent ?
  2. VACUUM tourne ?
  3. Espace disque libre ?
  4. Pas de verrous bloquants ?
  5. Cache hit ratio OK ?
  6. Pas de nouvelle requête gourmande ?

🎯 Optimisation Avancée – Les Raccourcis du Vétéran

Partitionnement – Découper la Bête

Pour les tables de > 100 Go.
Découper par date, par région, par catégorie.

-- PostgreSQL 12+
CREATE TABLE commande (
    id SERIAL,
    date_commande DATE NOT NULL,
    client_id INTEGER,
    montant DECIMAL(10,2)
) PARTITION BY RANGE (date_commande);

-- Partitions mensuelles
CREATE TABLE commande_2024_01 PARTITION OF commande
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE commande_2024_02 PARTITION OF commande
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

-- Avantages :
-- 1. DELETE vieilles données = DROP PARTITION (instantané)
-- 2. Index plus petits
-- 3. Parallelisation

Tables Temporaires vs CTE vs Sous-requêtes

-- Mauvais : CTE materialisée (PostgreSQL < 12)
WITH ventes AS (
    SELECT client_id, SUM(montant) as total
    FROM commande
    GROUP BY client_id
)
SELECT * FROM ventes WHERE total > 1000;
-- La CTE est exécutée, matérialisée, puis relue

-- Meilleur : Sous-requête
SELECT * FROM (
    SELECT client_id, SUM(montant) as total
    FROM commande
    GROUP BY client_id
) AS ventes WHERE total > 1000;
-- Peut être intégrée dans la requête principale

-- Temporaire : pour les requêtes complexes multi-étapes
CREATE TEMP TABLE ventes_intermediaire AS ...;
-- Vie pendant la session, propre à chaque utilisateur

Configuration du Serveur – Les Réglages Fins

PostgreSQL (postgresql.conf) :

shared_buffers = 25% de la RAM  # Cache
work_mem = 4-64MB  # Mémoire par opération de tri
maintenance_work_mem = 256MB-1GB  # Pour VACUUM, CREATE INDEX
effective_cache_size = 50-75% de la RAM  # Estimation pour le planificateur

MySQL (my.cnf) :

innodb_buffer_pool_size = 70-80% de la RAM  # Le plus important
innodb_log_file_size = 1-4GB  # Logs transactionnels
query_cache_type = 0  # Désactivé dans MySQL 8+

📚 Checklist de Survie en Production

Avant Mise en Production

  • Index sur les clés étrangères
  • ANALYZE après peuplement initial
  • Backup configuré et testé
  • Monitoring configuré (alertes disque, RAM, CPU)
  • Utilisateurs avec droits minimum

Surveillance Quotidienne

  • Espace disque libre (> 20%)
  • Pas d'erreurs dans les logs
  • Temps de réponse moyen < seuil
  • Cache hit ratio > 99%

Hebdomadaire

  • Vérifier la fragmentation
  • Vérifier les index inutilisés
  • Purge des données temporaires
  • Vérifier la croissance

Mensuel

  • Test de restauration de backup
  • REINDEX des tables critiques
  • Révision des performances
  • Mise à jour des statistiques long terme

Épilogue – La Ville qui Dure

L'optimisation, ce n'est pas une action, c'est une discipline.
La maintenance, ce n'est pas un incident, c'est une routine.
Une base de données bien entretenue dure des années.
Une base négligée s'effondre au premier pic de charge.

Les 10 Commandements :

  1. Tu ANALYZEras après chaque gros changement.
  2. Tu surveilleras l'espace disque.
  3. Tu ne désactiveras pas l'autovacuum.
  4. Tu feras des backups et les testeras.
  5. Tu utiliseras les droits minimum.
  6. Tu monitoreras les requêtes lentes.
  7. Tu partitionneras les grosses tables.
  8. Tu documenteras chaque trigger, chaque procédure.
  9. Tu garderas les logs assez longtemps, mais pas trop.
  10. Tu planifieras la croissance.

La ville est maintenant entre tes mains.
Elle a ses lois, ses automatismes, ses pièges.
Elle dort d'un œil, mais elle veille.
Comme toi.