🔝 Retour au Sommaire
La migration vers PostgreSQL 18 est un événement majeur pour votre infrastructure. Comme pour tout changement critique, tester avant de migrer n'est pas optionnel, c'est essentiel. Cette section vous guide à travers les différentes étapes de tests et de validation pour garantir une migration réussie.
Imaginez prendre un avion sans que celui-ci ait été testé :
Sans tests :
✈️ Avion neuf → Embarquement direct → Vol avec passagers
↓
❌ Risque d'accident catastrophique
Avec tests :
✈️ Avion neuf → Tests au sol → Vol d'essai vide → Vol avec équipage réduit
→ Vol d'essai avec quelques passagers → Vol commercial normal
↓
✅ Sécurité maximale
Pour PostgreSQL, c'est identique :
PostgreSQL 18 → Tests en DEV → Tests en STAGING → Tests de charge
→ Migration de préproduction → Migration production
↓
✅ Migration réussie et sécurisée
┌─────────────────────────────────────────────────────────────┐
│ Conséquences d'une migration non testée │
├─────────────────────────────────────────────────────────────┤
│ │
│ ❌ Incompatibilité SQL découverte en production │
│ → Applications qui plantent │
│ → Perte de revenus │
│ │
│ ❌ Performances dégradées inattendues │
│ → Requêtes 10× plus lentes │
│ → Timeout utilisateurs │
│ │
│ ❌ Perte de données ou corruption │
│ → Types de données incompatibles │
│ → Contraintes violées │
│ │
│ ❌ Rollback impossible ou difficile │
│ → Downtime prolongé │
│ → Panique et stress de l'équipe │
│ │
│ 💰 Coût : Millions d'euros potentiels │
│ 😰 Stress : Maximal │
│ 👥 Réputation : Endommagée │
│ │
└─────────────────────────────────────────────────────────────┘
✅ Confiance : L'équipe sait que ça fonctionne
✅ Anticipation : Problèmes découverts avant production
✅ Documentation : Procédures validées et documentées
✅ Rollback préparé : Plan B testé et prêt
✅ Sérénité : Migration en production sans surprises
✅ Business continuity : Service maintenu sans interruption
┌─────────────────────────────────────────────────────────────┐
│ Pyramide des environnements de test │
├─────────────────────────────────────────────────────────────┤
│ │
│ 🏢 PRODUCTION │
│ ┌──────────────────┐ │
│ │ PostgreSQL 18 │ │
│ │ (Migration finale) │
│ └──────────────────┘ │
│ ↑ │
│ ──────────────────── │
│ / \ │
│ │
│ 🔧 STAGING / PRÉPRODUCTION │
│ ┌──────────────────────────────┐ │
│ │ Clone quasi-identique prod │ │
│ │ Tests finaux réalistes │ │
│ │ Validation performance │ │
│ └──────────────────────────────┘ │
│ ↑ │
│ ──────────────────── │
│ / \ │
│ │
│ 💻 DÉVELOPPEMENT │
│ ┌──────────────────────────────────┐ │
│ │ Environnement développeur │ │
│ │ Tests unitaires et intégration │ │
│ │ Découverte des incompatibilités│ │
│ └──────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Caractéristiques :
Données : Sous-ensemble ou données de test (< 1 GB)
Infrastructure : Serveur modeste (2-4 cores, 8 GB RAM)
Objectif : Découvrir rapidement les incompatibilités
Fréquence : Tests quotidiens, multiples itérations
Audience : Développeurs uniquement
Utilisation :
-- DEV : Tester les requêtes SQL sur PG 18
-- Exemple : Vérifier qu'une requête fonctionne toujours
-- Cette requête fonctionnait sur PG 17
SELECT * FROM users WHERE id = ANY('{1,2,3}');
-- Vérifier qu'elle fonctionne toujours sur PG 18
-- Tester les nouvelles fonctionnalités
SELECT * FROM users WHERE created_at > NOW() - INTERVAL '1 day';Avantages :
- Cycle de test rapide (minutes)
- Permet d'itérer rapidement
- Pas d'impact sur la production
Limitations :
- Données limitées (ne détecte pas tous les problèmes)
- Performances non représentatives
- Certains bugs n'apparaissent qu'avec de vraies données
Caractéristiques :
Données : Clone de production ou anonymisées (taille réelle)
Infrastructure : Identique ou proche de production
Objectif : Validation finale avant migration prod
Fréquence : Tests hebdomadaires ou avant chaque release
Audience : Équipe complète (dev, ops, QA, business)
Utilisation :
# Créer un clone de production pour STAGING
# Option 1 : Via pg_dump/restore
pg_dump -h prod-server -U postgres mydb | \
psql -h staging-server -U postgres mydb
# Option 2 : Via snapshot disque (AWS, Azure)
aws rds create-db-snapshot --db-instance-identifier staging-pg18
# Option 3 : Via réplication puis promotion
# (Utiliser réplication physique, puis promouvoir le replica)Tests à effectuer :
- Migration complète avec pg_upgrade
- Tests de charge réalistes
- Validation avec applications réelles
- Tests de rollback
Avantages :
- Données réelles (ou anonymisées)
- Performances réalistes
- Détecte les vrais problèmes
Limitations :
- Coût infrastructure (serveur dédié)
- Synchronisation des données avec prod (peuvent être obsolètes)
Caractéristiques :
Données : Données réelles
Infrastructure : Configuration optimale
Objectif : Service en conditions réelles
Migration : Après validation complète en DEV et STAGING
Principe :
La production n'est PAS un environnement de test !
On y applique uniquement ce qui a été validé en STAGING.
Pour STAGING, il est souvent nécessaire d'anonymiser les données sensibles :
-- Exemple : Anonymiser les données personnelles
-- Sur le clone STAGING après copie
-- Anonymiser emails
UPDATE users
SET email = 'user_' || id || '@test.example.com';
-- Anonymiser noms
UPDATE users
SET
first_name = 'FirstName' || id,
last_name = 'LastName' || id;
-- Anonymiser adresses
UPDATE addresses
SET
street = 'Test Street ' || id,
city = 'TestCity',
postal_code = LPAD(id::text, 5, '0');
-- Supprimer données très sensibles
DELETE FROM payment_methods;
UPDATE orders SET notes = 'Test order'; Outils d'anonymisation :
- pg_anonymize : Extension PostgreSQL
- PostgreSQL Anonymizer : Solution complète
- Scripts personnalisés SQL
Alternative : Générer des données de test réalistes
-- Générer 1 million d'utilisateurs de test
INSERT INTO users (first_name, last_name, email, created_at)
SELECT
'User' || generate_series AS first_name,
'Test' || generate_series AS last_name,
'user' || generate_series || '@test.com' AS email,
NOW() - (random() * INTERVAL '365 days') AS created_at
FROM generate_series(1, 1000000);
-- Générer des commandes aléatoires
INSERT INTO orders (user_id, total, status, created_at)
SELECT
(random() * 1000000)::int + 1 AS user_id,
(random() * 1000)::numeric(10,2) AS total,
(ARRAY['pending', 'paid', 'shipped', 'delivered'])[floor(random() * 4 + 1)] AS status,
NOW() - (random() * INTERVAL '90 days') AS created_at
FROM generate_series(1, 5000000);PostgreSQL 18 peut déprécier certaines fonctionnalités ou changer des comportements :
-- 1. Identifier les fonctions dépréciées
-- Consulter les release notes PostgreSQL 18
-- 2. Rechercher l'utilisation de md5 (déprécié en faveur de scram-sha-256)
SELECT usename, passwd
FROM pg_shadow
WHERE passwd LIKE 'md5%';
-- Si résultats, migrer vers scram-sha-256 avant migration
-- 3. Vérifier les types de données personnalisés
SELECT typname, typtype
FROM pg_type
WHERE typtype = 'c' -- composite types
AND typnamespace::regnamespace::text NOT IN ('pg_catalog', 'information_schema');-- Lister toutes les extensions installées
SELECT
extname AS extension_name,
extversion AS version,
extrelocatable AS relocatable
FROM pg_extension
ORDER BY extname;
-- Vérifier la disponibilité dans PostgreSQL 18
-- Consulter : https://www.postgresql.org/docs/18/Extensions populaires et compatibilité PG 18 :
✅ PostGIS : Compatible (version 3.4+)
✅ pg_stat_statements : Inclus (amélioré)
✅ pgcrypto : Compatible
✅ uuid-ossp : Compatible (mais UUIDv7 natif maintenant)
✅ hstore : Compatible
✅ pg_trgm : Compatible
✅ pgvector : Compatible (version 0.6.0+)
⚠️ Extension_X : Vérifier la documentation spécifique
-- Identifier les requêtes qui utilisent des syntaxes potentiellement problématiques
-- 1. Activer pg_stat_statements (si pas déjà fait)
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- 2. Collecter les requêtes les plus fréquentes
SELECT
query,
calls,
mean_exec_time,
total_exec_time
FROM pg_stat_statements
WHERE query NOT LIKE 'SELECT % FROM pg_%' -- Exclure requêtes système
ORDER BY calls DESC
LIMIT 100;
-- 3. Exporter pour analyse
\copy (SELECT query FROM pg_stat_statements ORDER BY calls DESC LIMIT 1000) TO '/tmp/queries.sql'Vérifier manuellement :
- Utilisation de fonctionnalités dépréciées
- Requêtes non standard
- Utilisations de types spécifiques
# Sur un clone de production en DEV
pg_upgrade \
--old-datadir=/var/lib/postgresql/17/main \
--new-datadir=/var/lib/postgresql/18/main \
--old-bindir=/usr/lib/postgresql/17/bin \
--new-bindir=/usr/lib/postgresql/18/bin \
--check
# Sortie :
# Performing Consistency Checks
# -----------------------------
# Checking cluster versions ok
# Checking database user is the install user ok
# Checking database connection settings ok
# Checking for prepared transactions ok
# ...
# Si "ok" partout → Compatible ✅
# Si erreurs → À corriger avant migration# Python : Vérifier que l'application se connecte à PG 18
import psycopg2
try:
conn = psycopg2.connect(
host="staging-pg18",
database="mydb",
user="appuser",
password="secret"
)
cursor = conn.cursor()
cursor.execute("SELECT version();")
version = cursor.fetchone()
print(f"✅ Connexion réussie : {version[0]}")
cursor.close()
conn.close()
except Exception as e:
print(f"❌ Erreur de connexion : {e}")// Node.js : Test similaire
const { Pool } = require('pg');
const pool = new Pool({
host: 'staging-pg18',
database: 'mydb',
user: 'appuser',
password: 'secret'
});
pool.query('SELECT version()', (err, result) => {
if (err) {
console.error('❌ Erreur:', err);
} else {
console.log('✅ Version:', result.rows[0].version);
}
pool.end();
});# Lancer les tests automatisés contre PostgreSQL 18
# Exemple avec Jest (Node.js)
DATABASE_URL=postgresql://user:pass@staging-pg18:5432/mydb npm test
# Exemple avec pytest (Python)
DATABASE_URL=postgresql://user:pass@staging-pg18:5432/mydb pytest
# Observer les résultats
# ✅ 1250 tests passed
# ❌ 3 tests failed ← À investiguer !Analyser les échecs :
Test failed: test_user_creation
Error: column "uuid" does not exist
→ Investigation : UUID généré différemment en PG 18 ?
→ Solution : Utiliser UUIDv7 natif ou ajuster le code
Créer une checklist de tests fonctionnels :
□ Authentification utilisateur
□ Login avec email/password
□ Login avec OAuth
□ Logout
□ Opérations CRUD basiques
□ Créer un utilisateur
□ Lire les données utilisateur
□ Mettre à jour un profil
□ Supprimer un compte
□ Fonctionnalités métier critiques
□ Créer une commande
□ Payer avec carte bancaire
□ Rechercher des produits
□ Exporter un rapport
□ Fonctionnalités avancées
□ Full-text search
□ Filtres complexes
□ Agrégations et statistiques
Sur PostgreSQL 17 (production actuelle) :
-- 1. Activer le timing
\timing on
-- 2. Exécuter des requêtes représentatives et noter les temps
-- Requête 1 : Recherche utilisateur
SELECT * FROM users WHERE email = 'test@example.com';
-- Time: 2.345 ms
-- Requête 2 : Liste des commandes récentes
SELECT * FROM orders
WHERE created_at > NOW() - INTERVAL '7 days'
ORDER BY created_at DESC
LIMIT 100;
-- Time: 45.678 ms
-- Requête 3 : Agrégation complexe
SELECT
DATE_TRUNC('day', created_at) AS day,
COUNT(*) AS orders_count,
SUM(total) AS revenue
FROM orders
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY day
ORDER BY day;
-- Time: 234.567 msDocumenter dans un fichier baseline.txt :
PostgreSQL 17 - Baseline Performance
====================================
Date: 2024-11-23
Server: production-pg17 (16 cores, 64 GB RAM, NVMe)
Query 1 - User search: 2.345 ms
Query 2 - Recent orders: 45.678 ms
Query 3 - Daily aggregation: 234.567 ms
Query 4 - Complex join: 567.890 ms
...
Sur STAGING PostgreSQL 18 :
# Script de benchmark
#!/bin/bash
# benchmark_pg18.sh
echo "PostgreSQL 18 - Performance Benchmark"
echo "======================================"
date
psql -h staging-pg18 -U postgres -d mydb << 'EOF'
\timing on
-- Query 1
SELECT * FROM users WHERE email = 'test@example.com';
-- Query 2
SELECT * FROM orders
WHERE created_at > NOW() - INTERVAL '7 days'
ORDER BY created_at DESC
LIMIT 100;
-- Query 3
SELECT
DATE_TRUNC('day', created_at) AS day,
COUNT(*) AS orders_count,
SUM(total) AS revenue
FROM orders
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY day
ORDER BY day;
-- Autres requêtes...
EOFRésultats :
PostgreSQL 18 - Performance Benchmark
======================================
Query 1 - User search: 2.123 ms (✅ 9% plus rapide)
Query 2 - Recent orders: 38.456 ms (✅ 16% plus rapide)
Query 3 - Daily aggregation: 198.765 ms (✅ 15% plus rapide)
Query 4 - Complex join: 456.789 ms (✅ 20% plus rapide)
Interprétation :
- Si performances similaires ou meilleures : ✅ Bon signe
- Si dégradation < 10% :
⚠️ Acceptable (peut être optimisé) - Si dégradation > 20% : ❌ Investigation requise
# 1. Initialiser pgbench sur STAGING
pgbench -i -s 100 -h staging-pg18 -U postgres mydb
# s=100 → ~10 GB de données
# 2. Benchmark en lecture seule
pgbench -c 50 -j 4 -T 300 -S -h staging-pg18 -U postgres mydb
# -c 50 : 50 clients concurrents
# -j 4 : 4 threads
# -T 300 : Durée 5 minutes
# -S : Read-only (SELECT only)
# Résultats :
# transaction type: <builtin: select only>
# scaling factor: 100
# number of clients: 50
# number of threads: 4
# duration: 300 s
# number of transactions: 1234567
# tps = 4115.223456 (including connections establishing)
# tps = 4116.789012 (excluding connections establishing)
# 3. Benchmark en lecture/écriture
pgbench -c 50 -j 4 -T 300 -h staging-pg18 -U postgres mydb
# 4. Comparer avec PostgreSQL 17
# Exécuter les mêmes tests sur PG 17 et comparer les TPSBenchmark personnalisé :
# Script SQL personnalisé pour pgbench
cat > custom_benchmark.sql << 'EOF'
-- Simulation de charge réaliste
BEGIN;
-- Insert
INSERT INTO orders (user_id, total, status)
VALUES (random() * 1000000, random() * 1000, 'pending');
-- Update
UPDATE users SET last_login = NOW() WHERE id = :id;
-- Select
SELECT * FROM products WHERE category = 'electronics' LIMIT 10;
COMMIT;
EOF
# Exécuter
pgbench -c 20 -j 4 -T 60 -f custom_benchmark.sql -h staging-pg18 mydb# test_load.py - Simuler une charge applicative réelle
import psycopg2
import random
import time
from concurrent.futures import ThreadPoolExecutor
def simulate_user_session(user_id):
"""Simule une session utilisateur"""
conn = psycopg2.connect(
host="staging-pg18",
database="mydb",
user="appuser",
password="secret"
)
cursor = conn.cursor()
try:
# 1. Login (SELECT)
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
user = cursor.fetchone()
# 2. Browse products (SELECT with JOIN)
cursor.execute("""
SELECT p.*, c.name as category_name
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE p.active = true
LIMIT 20
""")
products = cursor.fetchall()
# 3. Create order (INSERT)
cursor.execute("""
INSERT INTO orders (user_id, total, status)
VALUES (%s, %s, 'pending')
RETURNING id
""", (user_id, random.uniform(10, 500)))
order_id = cursor.fetchone()[0]
# 4. Update profile (UPDATE)
cursor.execute("""
UPDATE users
SET last_activity = NOW()
WHERE id = %s
""", (user_id,))
conn.commit()
return True
except Exception as e:
conn.rollback()
print(f"Error for user {user_id}: {e}")
return False
finally:
cursor.close()
conn.close()
# Simuler 100 utilisateurs concurrents
with ThreadPoolExecutor(max_workers=100) as executor:
start = time.time()
results = list(executor.map(simulate_user_session, range(1, 1001)))
duration = time.time() - start
success_rate = sum(results) / len(results) * 100
print(f"Duration: {duration:.2f}s")
print(f"Success rate: {success_rate:.1f}%")
print(f"Throughput: {len(results)/duration:.2f} sessions/sec")#!/bin/bash
# full_migration_rehearsal.sh
# Simulation complète de la migration
echo "🎬 Répétition générale de la migration"
echo "======================================"
# 1. État initial
echo "📊 État initial"
psql -h staging-pg17 -c "\l+"
psql -h staging-pg17 -d mydb -c "SELECT COUNT(*) FROM users;"
# 2. Arrêt de PostgreSQL 17
echo "⏸️ Arrêt PostgreSQL 17"
sudo systemctl stop postgresql@17-main
# 3. Migration pg_upgrade
echo "🔄 Migration pg_upgrade"
time sudo -u postgres pg_upgrade \
--old-datadir=/var/lib/postgresql/17/main \
--new-datadir=/var/lib/postgresql/18/main \
--old-bindir=/usr/lib/postgresql/17/bin \
--new-bindir=/usr/lib/postgresql/18/bin \
--jobs=8 \
--swap
# 4. Démarrage PostgreSQL 18
echo "▶️ Démarrage PostgreSQL 18"
sudo systemctl start postgresql@18-main
# 5. Validation immédiate
echo "✅ Validation"
psql -h localhost -p 5432 -c "SELECT version();"
psql -h localhost -d mydb -c "SELECT COUNT(*) FROM users;"
# 6. Tests de fumée
echo "💨 Smoke tests"
./smoke_tests.sh
# 7. Mesure du downtime
DOWNTIME=$(grep "Upgrade Complete" pg_upgrade_server.log | \
awk '{print $NF}')
echo "⏱️ Downtime simulé: $DOWNTIME"
echo "✅ Répétition terminée"Résultats de la répétition générale :
=====================================
Arrêt PG 17 : 15 secondes
pg_upgrade --check : 8 minutes
pg_upgrade migration : 45 minutes
Démarrage PG 18 : 30 secondes
Smoke tests : 2 minutes
DOWNTIME TOTAL ESTIMÉ : 47 minutes
Il est crucial de tester le rollback avant la migration en production :
#!/bin/bash
# test_rollback.sh
echo "🔙 Test de rollback"
echo "==================="
# Scénario : Migration PG 18 effectuée, mais problème détecté
# 1. État actuel : PG 18 en production simulée
echo "État actuel : PostgreSQL 18"
psql -h staging-pg18 -c "SELECT version();"
# 2. Simulation d'un problème
echo "⚠️ Problème détecté : Performance dégradée"
# 3. Arrêt PG 18
echo "⏸️ Arrêt PostgreSQL 18"
sudo systemctl stop postgresql@18-main
# 4. Restauration PG 17 depuis .old (si --swap utilisé)
echo "🔄 Restauration PostgreSQL 17"
sudo -u postgres mv /var/lib/postgresql/18/main /var/lib/postgresql/18/main.failed
sudo -u postgres mv /var/lib/postgresql/.old /var/lib/postgresql/17/main
# 5. Redémarrage PG 17
echo "▶️ Redémarrage PostgreSQL 17"
sudo systemctl start postgresql@17-main
# 6. Validation
echo "✅ Validation rollback"
psql -h localhost -c "SELECT version();"
psql -h localhost -d mydb -c "SELECT COUNT(*) FROM users;"
# 7. Mesurer le temps
echo "⏱️ Temps de rollback: < 2 minutes"
echo "✅ Rollback réussi"Important : Le rollback n'est possible que si aucune donnée n'a été modifiée sur PG 18 après la migration.
#!/bin/bash
# post_migration_validation.sh
echo "✅ Validation post-migration PostgreSQL 18"
echo "=========================================="
# 1. Version
echo "1️⃣ Vérification version"
VERSION=$(psql -t -c "SELECT version();" | grep "PostgreSQL 18")
if [ -n "$VERSION" ]; then
echo "✅ Version PostgreSQL 18 confirmée"
else
echo "❌ Version incorrecte !"
exit 1
fi
# 2. Connexions
echo "2️⃣ Vérification connexions"
CONNECTIONS=$(psql -t -c "SELECT count(*) FROM pg_stat_activity;")
echo " Connexions actives : $CONNECTIONS"
# 3. Réplicas (si applicable)
echo "3️⃣ Vérification réplication"
psql -c "SELECT * FROM pg_stat_replication;"
# 4. Intégrité données
echo "4️⃣ Vérification intégrité données"
psql -d mydb << 'EOF'
-- Compter les enregistrements de chaque table
SELECT
schemaname,
tablename,
n_live_tup
FROM pg_stat_user_tables
ORDER BY n_live_tup DESC
LIMIT 20;
EOF
# 5. Extensions
echo "5️⃣ Vérification extensions"
psql -d mydb -c "SELECT extname, extversion FROM pg_extension;"
# 6. Test de lecture
echo "6️⃣ Test de lecture"
psql -d mydb -c "SELECT COUNT(*) FROM users;"
# 7. Test d'écriture
echo "7️⃣ Test d'écriture"
psql -d mydb << 'EOF'
BEGIN;
INSERT INTO test_migration (message, created_at)
VALUES ('Post-migration test', NOW());
SELECT * FROM test_migration WHERE message = 'Post-migration test';
ROLLBACK;
EOF
# 8. Performance simple
echo "8️⃣ Test performance"
psql -d mydb -c "\timing on" -c "SELECT COUNT(*) FROM orders;"
echo ""
echo "✅ Validation post-migration terminée" # smoke_tests.py - Tests rapides post-migration
import psycopg2
import sys
def run_smoke_tests():
"""Exécute les tests de fumée essentiels"""
tests_passed = 0
tests_failed = 0
try:
conn = psycopg2.connect(
host="localhost",
database="mydb",
user="postgres",
password="secret"
)
cursor = conn.cursor()
# Test 1: Version
print("🧪 Test 1: Version PostgreSQL")
cursor.execute("SELECT version();")
version = cursor.fetchone()[0]
if "PostgreSQL 18" in version:
print(" ✅ PASS")
tests_passed += 1
else:
print(f" ❌ FAIL - Version: {version}")
tests_failed += 1
# Test 2: Lecture table users
print("🧪 Test 2: Lecture table users")
cursor.execute("SELECT COUNT(*) FROM users;")
count = cursor.fetchone()[0]
if count > 0:
print(f" ✅ PASS - {count} users")
tests_passed += 1
else:
print(" ❌ FAIL - Aucun utilisateur")
tests_failed += 1
# Test 3: Écriture (transaction rollback)
print("🧪 Test 3: Écriture")
conn.autocommit = False
cursor.execute("""
INSERT INTO users (email, first_name, last_name)
VALUES ('test@migration.com', 'Test', 'Migration')
RETURNING id
""")
user_id = cursor.fetchone()[0]
conn.rollback()
print(f" ✅ PASS - INSERT réussi (rollback)")
tests_passed += 1
# Test 4: Index fonctionnent
print("🧪 Test 4: Index")
cursor.execute("SELECT schemaname, tablename, indexname FROM pg_indexes WHERE schemaname = 'public' LIMIT 5;")
indexes = cursor.fetchall()
if len(indexes) > 0:
print(f" ✅ PASS - {len(indexes)} index trouvés")
tests_passed += 1
else:
print(" ❌ FAIL - Aucun index")
tests_failed += 1
# Test 5: Extensions
print("🧪 Test 5: Extensions")
cursor.execute("SELECT extname FROM pg_extension WHERE extname = 'pg_stat_statements';")
if cursor.fetchone():
print(" ✅ PASS - Extensions chargées")
tests_passed += 1
else:
print(" ⚠️ WARNING - pg_stat_statements non trouvé")
cursor.close()
conn.close()
# Résumé
print("\n" + "="*50)
print(f"Tests réussis : {tests_passed}")
print(f"Tests échoués : {tests_failed}")
if tests_failed > 0:
print("❌ ÉCHEC - Certains tests ont échoué")
sys.exit(1)
else:
print("✅ SUCCÈS - Tous les tests sont passés")
sys.exit(0)
except Exception as e:
print(f"❌ ERREUR CRITIQUE : {e}")
sys.exit(1)
if __name__ == "__main__":
run_smoke_tests()-- Surveiller les requêtes lentes dans les premières heures
-- (pg_stat_statements doit être activé)
-- Top 20 des requêtes les plus lentes
SELECT
substring(query, 1, 100) AS query_preview,
calls,
mean_exec_time,
max_exec_time,
total_exec_time,
stddev_exec_time
FROM pg_stat_statements
WHERE mean_exec_time > 100 -- > 100ms
ORDER BY mean_exec_time DESC
LIMIT 20;
-- Comparer avec la baseline PG 17
-- Si dégradation importante, investiguer#!/bin/bash
# monitor_resources.sh
# À lancer toutes les 5 minutes via cron
echo "📊 Monitoring PostgreSQL 18 - $(date)"
# CPU
echo "CPU:"
top -b -n 1 | grep postgres | head -5
# RAM
echo "RAM:"
free -h | grep Mem
# Disk I/O
echo "Disk I/O:"
iostat -x 1 2 | tail -n +4
# Connexions actives
echo "Connexions:"
psql -t -c "SELECT count(*) FROM pg_stat_activity WHERE state = 'active';"
# Cache hit ratio
echo "Cache Hit Ratio:"
psql -t -c "
SELECT
ROUND(
(sum(blks_hit) * 100.0 / NULLIF(sum(blks_hit + blks_read), 0)), 2
) AS cache_hit_ratio
FROM pg_stat_database;
"
# Locks en attente
LOCKS=$(psql -t -c "SELECT count(*) FROM pg_locks WHERE NOT granted;")
if [ "$LOCKS" -gt "0" ]; then
echo "⚠️ $LOCKS locks en attente"
fi
echo "---"# monitoring_alerts.py - Alertes automatiques
import psycopg2
import smtplib
from email.mime.text import MIMEText
def check_and_alert():
"""Vérifie l'état et envoie des alertes si nécessaire"""
conn = psycopg2.connect(
host="localhost",
database="mydb",
user="postgres"
)
cursor = conn.cursor()
alerts = []
# Alert 1: Connexions > 90% de max_connections
cursor.execute("""
SELECT
count(*) AS current,
(SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max
FROM pg_stat_activity
""")
current, max_conn = cursor.fetchone()
if current > max_conn * 0.9:
alerts.append(f"⚠️ Connexions élevées: {current}/{max_conn}")
# Alert 2: Cache hit ratio < 90%
cursor.execute("""
SELECT
ROUND((sum(blks_hit) * 100.0 / NULLIF(sum(blks_hit + blks_read), 0)), 2)
FROM pg_stat_database
""")
cache_ratio = cursor.fetchone()[0]
if cache_ratio < 90:
alerts.append(f"⚠️ Cache hit ratio faible: {cache_ratio}%")
# Alert 3: Requêtes lentes
cursor.execute("""
SELECT count(*)
FROM pg_stat_activity
WHERE state = 'active'
AND now() - query_start > interval '5 minutes'
""")
long_queries = cursor.fetchone()[0]
if long_queries > 0:
alerts.append(f"⚠️ {long_queries} requêtes longues (> 5 min)")
# Alert 4: Locks en attente
cursor.execute("SELECT count(*) FROM pg_locks WHERE NOT granted")
waiting_locks = cursor.fetchone()[0]
if waiting_locks > 10:
alerts.append(f"⚠️ {waiting_locks} locks en attente")
# Envoyer les alertes
if alerts:
message = "\n".join(alerts)
send_alert(message)
print(message)
else:
print("✅ Tout va bien")
cursor.close()
conn.close()
def send_alert(message):
"""Envoie une alerte par email"""
# Configuration email
msg = MIMEText(message)
msg['Subject'] = '⚠️ Alerte PostgreSQL 18 Migration'
msg['From'] = 'monitoring@example.com'
msg['To'] = 'dba@example.com'
# Envoyer (exemple avec SMTP)
# server = smtplib.SMTP('smtp.example.com')
# server.send_message(msg)
# server.quit()
if __name__ == "__main__":
check_and_alert()-- Comparer les statistiques d'exécution avec PG 17
-- (nécessite d'avoir sauvegardé les stats de PG 17 avant migration)
-- Créer une table pour archiver les stats PG 17
CREATE TABLE pg17_baseline AS
SELECT
query,
calls,
mean_exec_time,
total_exec_time
FROM pg_stat_statements;
-- Après migration vers PG 18, comparer
SELECT
pg17.query,
pg17.mean_exec_time AS pg17_mean_time,
pg18.mean_exec_time AS pg18_mean_time,
ROUND(
((pg18.mean_exec_time - pg17.mean_exec_time) / pg17.mean_exec_time * 100)::numeric,
2
) AS percent_change
FROM pg17_baseline pg17
JOIN pg_stat_statements pg18 ON pg17.query = pg18.query
WHERE pg17.calls > 100 -- Seulement les requêtes fréquentes
ORDER BY ABS(pg18.mean_exec_time - pg17.mean_exec_time) DESC
LIMIT 50;
-- Identifier les régressions de performance
SELECT *
FROM (
-- Même requête que ci-dessus
) AS comparison
WHERE percent_change > 20 -- > 20% plus lent
ORDER BY percent_change DESC; # regression_tests.py - Tests de régression automatisés
import psycopg2
import json
import time
def load_baseline(filename='baseline_pg17.json'):
"""Charge les performances baseline de PG 17"""
with open(filename, 'r') as f:
return json.load(f)
def run_performance_tests(baseline):
"""Exécute les tests de performance et compare avec baseline"""
conn = psycopg2.connect(
host="localhost",
database="mydb",
user="postgres"
)
cursor = conn.cursor()
results = []
regressions = []
for test in baseline['tests']:
query = test['query']
expected_time = test['mean_time_ms']
# Exécuter la requête 10 fois et moyenner
times = []
for _ in range(10):
start = time.time()
cursor.execute(query)
cursor.fetchall()
duration = (time.time() - start) * 1000 # En ms
times.append(duration)
actual_time = sum(times) / len(times)
difference = ((actual_time - expected_time) / expected_time) * 100
result = {
'query': query[:100],
'expected': expected_time,
'actual': actual_time,
'difference_pct': difference
}
results.append(result)
# Régression si > 20% plus lent
if difference > 20:
regressions.append(result)
print(f"❌ RÉGRESSION: {query[:50]}... ({difference:.1f}% plus lent)")
elif difference < -10:
print(f"✅ AMÉLIORATION: {query[:50]}... ({abs(difference):.1f}% plus rapide)")
else:
print(f"✅ OK: {query[:50]}... ({difference:.1f}%)")
# Rapport final
print("\n" + "="*70)
print(f"Tests total: {len(results)}")
print(f"Régressions: {len(regressions)}")
if regressions:
print("\n⚠️ RÉGRESSIONS DÉTECTÉES:")
for reg in regressions:
print(f" - {reg['query']}")
print(f" Attendu: {reg['expected']:.2f}ms")
print(f" Actuel: {reg['actual']:.2f}ms")
print(f" Différence: +{reg['difference_pct']:.1f}%")
else:
print("\n✅ Aucune régression détectée")
cursor.close()
conn.close()
return len(regressions) == 0
if __name__ == "__main__":
baseline = load_baseline()
success = run_performance_tests(baseline)
exit(0 if success else 1)-- Installation
CREATE EXTENSION pgtap;
-- Exemple de tests
BEGIN;
SELECT plan(5); -- Nombre de tests prévus
-- Test 1: La table existe
SELECT has_table('public', 'users', 'Table users should exist');
-- Test 2: La colonne existe
SELECT has_column('public', 'users', 'email', 'Column email should exist');
-- Test 3: L'index existe
SELECT has_index('public', 'users', 'idx_users_email', 'Index on email should exist');
-- Test 4: Contrainte PK
SELECT has_pk('public', 'users', 'users should have a primary key');
-- Test 5: Nombre d'enregistrements
SELECT ok(
(SELECT count(*) FROM users) > 0,
'users table should not be empty'
);
SELECT * FROM finish();
ROLLBACK; # Analyser les logs PostgreSQL 18
pgbadger /var/log/postgresql/postgresql-18-main.log \
-o /var/www/html/pgbadger_report.html
# Générer un rapport HTML avec :
# - Requêtes les plus lentes
# - Requêtes les plus fréquentes
# - Distribution des temps de réponse
# - Erreurs et warnings# Installer
apt-get install check-pgactivity
# Vérifier les connexions
check_pgactivity -s connections --critical=90% --warning=80%
# Vérifier les backends bloqués
check_pgactivity -s backends_status --critical=5 --warning=2
# Vérifier le temps des transactions
check_pgactivity -s oldest_xact --critical=1h --warning=30m
# Intégrer dans un système de monitoring (Nagios, Icinga, etc.)#!/bin/bash
# comprehensive_test_suite.sh
# Suite de tests complète pour validation migration
echo "🧪 Suite de tests PostgreSQL 18 Migration"
echo "========================================="
FAILED=0
# Test 1: Version
echo "Test 1: Version PostgreSQL"
if psql -t -c "SELECT version();" | grep -q "PostgreSQL 18"; then
echo "✅ PASS"
else
echo "❌ FAIL"
FAILED=$((FAILED + 1))
fi
# Test 2: Extensions
echo "Test 2: Extensions critiques"
for ext in pg_stat_statements uuid-ossp pgcrypto; do
if psql -t -c "SELECT 1 FROM pg_extension WHERE extname = '$ext';" | grep -q 1; then
echo "✅ $ext présent"
else
echo "❌ $ext manquant"
FAILED=$((FAILED + 1))
fi
done
# Test 3: Tables critiques
echo "Test 3: Tables critiques"
for table in users orders products; do
COUNT=$(psql -t -d mydb -c "SELECT count(*) FROM $table;")
if [ "$COUNT" -gt 0 ]; then
echo "✅ $table: $COUNT rows"
else
echo "❌ $table: vide"
FAILED=$((FAILED + 1))
fi
done
# Test 4: Index
echo "Test 4: Index critiques"
MISSING_INDEXES=$(psql -t -d mydb -c "
SELECT count(*)
FROM pg_indexes
WHERE schemaname = 'public'
AND indexname IN ('idx_users_email', 'idx_orders_user_id', 'idx_products_category')
")
if [ "$MISSING_INDEXES" -eq 3 ]; then
echo "✅ Tous les index présents"
else
echo "❌ Index manquants"
FAILED=$((FAILED + 1))
fi
# Test 5: Performance baseline
echo "Test 5: Performance baseline"
TIME=$(psql -t -d mydb -c "\timing on" -c "SELECT count(*) FROM orders;" 2>&1 | grep "Time" | awk '{print $2}')
if [ $(echo "$TIME < 1000" | bc) -eq 1 ]; then
echo "✅ Performance OK ($TIME ms)"
else
echo "⚠️ Performance degradée ($TIME ms)"
fi
# Résumé
echo ""
echo "========================================="
if [ "$FAILED" -eq 0 ]; then
echo "✅ SUCCÈS - Tous les tests sont passés"
exit 0
else
echo "❌ ÉCHEC - $FAILED test(s) échoué(s)"
exit 1
fi# Rapport de Test - Migration PostgreSQL 18
## Informations générales
- **Date du test** : 2024-11-23
- **Environnement** : STAGING
- **Testeur** : Jean Dupont
- **Version source** : PostgreSQL 17.2
- **Version cible** : PostgreSQL 18.0
- **Taille base** : 1.5 TB
- **Méthode migration** : pg_upgrade --swap --jobs=8
## Tests pré-migration
### Analyse de compatibilité
- ✅ pg_upgrade --check : OK
- ✅ Extensions compatibles : OK
- ✅ Types de données : OK
- ⚠️ 3 tables sans PK (corrigé)
### Tests applications
- ✅ Connexion : OK
- ✅ Tests unitaires : 1247/1250 passés
- ❌ 3 tests échoués :
- test_uuid_generation (résolu: utiliser gen_random_uuid())
- test_json_aggregation (résolu: syntaxe JSON ajustée)
- test_full_text_search (en cours d'investigation)
### Tests performance
- ✅ Baseline établie
- ✅ Benchmark pgbench : +15% TPS vs PG 17
## Tests migration
### Répétition générale
- ✅ Migration complète : OK
- ✅ Durée : 47 minutes
- ✅ Downtime estimé : 50 minutes (avec marge)
- ✅ Rollback testé : OK (< 2 minutes)
## Tests post-migration
### Validation immédiate
- ✅ Version : PostgreSQL 18.0
- ✅ Connexions : OK
- ✅ Intégrité données : OK
- ✅ Extensions : Toutes présentes
- ✅ Smoke tests : OK
### Validation continue (24h)
- ✅ Monitoring : Aucune alerte
- ✅ Cache hit ratio : 97.5% (excellent)
- ✅ Requêtes lentes : Aucune régression
- ⚠️ 1 requête +12% plus lente (acceptable)
## Problèmes rencontrés
### Problème 1 : Tests unitaires UUID
**Description** : 3 tests échouaient car UUID généré différemment
**Solution** : Utiliser gen_random_uuid() natif PG 18
**Statut** : ✅ Résolu
### Problème 2 : Full-text search
**Description** : 1 test FTS échoue
**Solution** : En cours d'investigation
**Statut** : ⚠️ En cours
## Recommandations
1. ✅ GO pour migration production
2. ⚠️ Résoudre le problème FTS avant (non bloquant)
3. 📊 Prévoir monitoring renforcé 48h post-migration
4. 🔄 Conserver Blue 2 semaines minimum
## Conclusion
**Migration PostgreSQL 18 validée pour PRODUCTION**
Signature : _________________
Date : _________________ □ Planifier suffisamment de temps (2-4 semaines minimum)
□ Impliquer toutes les parties prenantes (dev, ops, QA, business)
□ Provisionner environnements dédiés (DEV, STAGING)
□ Documenter l'état actuel (baseline)
□ Préparer les scripts de test automatisés
□ Former l'équipe sur PostgreSQL 18 (nouveautés)
□ Commencer par DEV (itérations rapides)
□ Progresser vers STAGING (tests réalistes)
□ Documenter chaque problème rencontré
□ Mesurer systématiquement les performances
□ Tester les cas limites et edge cases
□ Valider le rollback fonctionne
□ Compiler un rapport de test complet
□ Obtenir l'approbation des stakeholders
□ Planifier la migration production (date, fenêtre)
□ Communiquer largement (équipes, utilisateurs)
□ Préparer le plan de communication de crise
□ Documenter les procédures (runbook)
┌─────────────────────────────────────────────────────────────┐
│ Checklist Complète - Validation Migration PostgreSQL 18 │
├─────────────────────────────────────────────────────────────┤
│ │
│ PHASE 1 : PRÉ-MIGRATION │
│ □ pg_upgrade --check : OK │
│ □ Compatibilité extensions vérifiée │
│ □ Compatibilité applications testée │
│ □ Performance baseline documentée │
│ □ Tests de charge effectués │
│ □ Rollback testé et validé │
│ │
│ PHASE 2 : MIGRATION │
│ □ Répétition générale réussie │
│ □ Downtime mesuré et acceptable │
│ □ Procédures documentées │
│ □ Équipe formée et prête │
│ │
│ PHASE 3 : POST-MIGRATION │
│ □ Validation immédiate (T+0) : OK │
│ □ Smoke tests passés │
│ □ Monitoring actif │
│ □ Performance validée (T+24h) │
│ □ Aucune régression détectée (T+7j) │
│ □ Rapport final rédigé │
│ │
│ ✅ Migration validée pour PRODUCTION │
│ │
└─────────────────────────────────────────────────────────────┘
Les tests et la validation sont la clé du succès d'une migration PostgreSQL 18. Investir le temps nécessaire dans les tests permet d'éviter les catastrophes en production.
- Tester, tester, tester : Jamais trop de tests
- Environnements multiples : DEV → STAGING → PROD
- Tests automatisés : Scripts réutilisables et fiables
- Documentation : Tracer chaque étape et problème
- Performance : Comparer systématiquement avec baseline
- Rollback : Toujours testé avant migration prod
Projet petit (< 100 GB) :
- Tests DEV : 1 semaine
- Tests STAGING : 1 semaine
- Total : 2 semaines minimum
Projet moyen (100 GB - 1 TB) :
- Tests DEV : 2 semaines
- Tests STAGING : 2 semaines
- Total : 4 semaines (1 mois)
Projet large (> 1 TB) :
- Tests DEV : 3 semaines
- Tests STAGING : 3 semaines
- Total : 6-8 semaines (2 mois)
"Weeks of coding can save hours of planning."
"If you don't have time to do it right, when will you have time to do it over?" - John Wooden
"Test early, test often, test in production... wait, no, not that last one." - DevOps Wisdom
Pour approfondir :
- Section 19.3.1 à 19.3.4 : Techniques de migration avancées
- Section 19.4 : Troubleshooting et gestion de crises
- Section 14 : Observabilité et monitoring
- Section 13 : Optimisation et performance
Note : Tester une migration peut sembler long et fastidieux, mais c'est comme apprendre à conduire : vous passez du temps sur le simulateur et avec un moniteur avant de conduire seul sur l'autoroute. Les tests vous donnent la confiance et l'expérience nécessaires pour réussir la migration en production sans surprises désagréables. Ne sautez jamais cette étape !