Dans un moteur de base de données relationnelle, la règle fondamentale de performance est de travailler en mode ensembliste (set-based) : une seule instruction UPDATE ou INSERT INTO ... SELECT est toujours des centaines de fois plus rapide qu'une boucle mettant à jour les lignes une par une.
Cependant, dans l'administration de bases de données et l'ingénierie ETL, les boucles procédurales sont indispensables pour : * Traiter des données par lots (batches) pour ne pas saturer le journal de transactions (WAL). * Générer dynamiquement des partitions ou des tables par date. * Itérer sur un catalogue de tables pour exécuter des scripts de maintenance.
1. La boucle inconditionnelle : LOOP ... EXIT WHEN#
C'est la structure de répétition la plus élémentaire en PL/pgSQL. Elle s'exécute indéfiniment jusqu'à ce qu'une instruction EXIT soit rencontrée.
DO
$$
DECLARE
v_compteur INTEGER := 1;
v_total NUMERIC := 0;
BEGIN
LOOP
v_total := v_total + (v_compteur * 10);
RAISE NOTICE 'Itération % : Total cumulé = %', v_compteur, v_total;
v_compteur := v_compteur + 1;
-- Condition de sortie obligatoire pour éviter la boucle infinie
EXIT WHEN v_compteur > 5;
END LOOP;
RAISE NOTICE 'Sortie de boucle réussie !';
END;
$$ LANGUAGE plpgsql;
[!TIP] Vous pouvez également utiliser l'instruction
CONTINUE WHEN condition;pour interrompre le tour de boucle en cours et passer immédiatement à l'itération suivante sans exécuter les instructions restantes.
2. La boucle conditionnelle : WHILE ... LOOP#
La boucle WHILE évalue une condition booléenne au début de chaque cycle. Si la condition est fausse dès le départ, le corps de la boucle n'est jamais exécuté.
DO
$$
DECLARE
v_stock_disponible INTEGER := 50;
v_taille_lot CONSTANT INTEGER := 12;
v_lots_expedies INTEGER := 0;
BEGIN
WHILE v_stock_disponible >= v_taille_lot LOOP
v_stock_disponible := v_stock_disponible - v_taille_lot;
v_lots_expedies := v_lots_expedies + 1;
RAISE NOTICE 'Lot #% expédié. Stock restant : % unités.',
v_lots_expedies, v_stock_disponible;
END LOOP;
RAISE NOTICE 'Fin des expéditions : reliquat de % unité(s) en stock.', v_stock_disponible;
END;
$$ LANGUAGE plpgsql;
3. La boucle bornée numérique : FOR ... IN 1..N#
Lorsque vous connaissez à l'avance le nombre d'itérations, la boucle FOR est la solution la plus concise.
DO
$$
DECLARE
v_table_cible TEXT;
BEGIN
-- Itération croissante de 1 à 4
FOR i IN 1..4 LOOP
v_table_cible := 'analytics.fct_ventes_t' || i;
RAISE NOTICE 'Génération de l index pour le trimestre T% (Table: %)', i, v_table_cible;
END LOOP;
-- Itération décroissante avec REVERSE et pas personnalisé avec BY
FOR j IN REVERSE 10..2 BY 2 LOOP
RAISE NOTICE 'Décompte pair : %', j;
END LOOP;
END;
$$ LANGUAGE plpgsql;
[!NOTE] Remarquez que la variable de compteur
ioujn'a pas besoin d'être déclarée dans la sectionDECLARE! PostgreSQL crée et détruit automatiquement une variable locale de typeINTEGERpendant toute la durée de la boucleFOR.
4. La boucle sur résultats SQL : FOR record IN SELECT ... LOOP#
C'est sans comparaison la structure la plus utilisée et la plus utile pour un Data Engineer. Elle permet de parcourir les enregistrements d'une requête SQL sans manipuler manuellement de curseur explicite (OPEN, FETCH, CLOSE).
Exemple : Traitement par région et catégorie#
DO
$$
DECLARE
-- Déclaration d'une variable générique pour accueillir chaque ligne
rec RECORD;
v_seuil_critique CONSTANT NUMERIC := 100000.00;
BEGIN
-- Itération sur le résultat d'une requête SQL complexe
FOR rec IN
SELECT
region,
categorie,
SUM(montant) AS ca_total,
COUNT(*) AS nb_transactions
FROM prod.ventes
GROUP BY region, categorie
ORDER BY ca_total DESC
LOOP
IF rec.ca_total >= v_seuil_critique THEN
RAISE NOTICE '[STAR] Région % | Catégorie % : % € (% ventes)',
rec.region, rec.categorie, rec.ca_total, rec.nb_transactions;
ELSE
RAISE NOTICE '[NORMAL] Région % | Catégorie % : % €',
rec.region, rec.categorie, rec.ca_total;
END IF;
END LOOP;
END;
$$ LANGUAGE plpgsql;
5. Cas d'application en production : Traitement de partitions par dates#
Dans un Data Warehouse, vous devez souvent recalculer les 7 derniers jours d'historique pour intégrer les données en retard (latence de reporting).
DO
$$
DECLARE
v_date_traitement DATE;
v_query TEXT;
BEGIN
-- Boucle sur les 7 derniers jours
FOR decallage IN 1..7 LOOP
v_date_traitement := CURRENT_DATE - decallage;
RAISE NOTICE 'Traitement de la partition date : %', v_date_traitement;
-- Construction d'une requête dynamique sécurisée
v_query := format(
'DELETE FROM dwh.fct_daily_snapshot WHERE date_snapshot = %L; ' ||
'INSERT INTO dwh.fct_daily_snapshot (date_snapshot, total_commandes) ' ||
'SELECT date_cmd, count(*) FROM staging.commandes WHERE date_cmd = %L GROUP BY date_cmd;',
v_date_traitement, v_date_traitement
);
-- Exécution de la commande SQL dynamique
EXECUTE v_query;
END LOOP;
RAISE NOTICE 'Recalcul des 7 partitions achevé avec succès !';
END;
$$ LANGUAGE plpgsql;
6. Mise en pratique : Audit de volumétrie des tables d'un schéma#
Objectif#
Écrire un bloc anonyme qui inspecte automatiquement toutes les tables présentes dans le schéma public via la vue système information_schema.tables, compte dynamiquement le nombre de lignes de chacune à l'aide d'une requête EXECUTE ... INTO, et alerte si une table contient plus de 10 000 lignes.
DO
$$
DECLARE
rec RECORD;
v_nb_lignes BIGINT;
v_sql TEXT;
BEGIN
RAISE NOTICE '=========== AUDIT DE VOLUMÉTRIE DU SCHÉMA PUBLIC ===========';
FOR rec IN
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name
LOOP
-- Construction de la requête de comptage dynamique
v_sql := format('SELECT COUNT(*) FROM public.%I', rec.table_name);
-- Exécution et capture du résultat
EXECUTE v_sql INTO v_nb_lignes;
IF v_nb_lignes > 10000 THEN
RAISE WARNING 'Table [ % ] : % lignes (SEUIL DÉPASSÉ)', rec.table_name, v_nb_lignes;
ELSE
RAISE NOTICE 'Table [ % ] : % lignes', rec.table_name, v_nb_lignes;
END IF;
END LOOP;
RAISE NOTICE '=============================================================';
END;
$$ LANGUAGE plpgsql;
Vous maîtrisez désormais les variables, les conditions et les boucles. Dans la prochaine et dernière leçon de programmation, nous encapsulerons toutes ces briques dans des sous-programmes réutilisables : les Fonctions et les Procédures Stockées.
Commentaires