Maîtrisez la vérification du SQL généré par ChatGPT, Copilot. Les 8 erreurs les plus fréquentes, checklist de revue systématique et cas concrets par métier.
Thomas Leroy
📌 Ce qu'il faut retenir
Le SQL généré par IA compile presque toujours — les erreurs sont logiques, pas syntaxiques
Les JOINs incorrects (multiplication de lignes, LEFT JOIN annulé par WHERE) sont les erreurs les plus fréquentes et les plus dangereuses
Les NULL ignorés faussent silencieusement les agrégations et les taux calculés
Utilisez une checklist de revue systématiquement avant d'exécuter du SQL généré par IA
Ne mettez jamais en production du SQL non vérifié, quelle que soit la source
En 2026, la majorité des data analysts utilisent l'IA (ChatGPT, Copilot, Gemini) pour écrire ou accélérer leurs requêtes SQL. C'est un gain de productivité réel. Mais l'IA fait des erreurs — et pas des erreurs de syntaxe évidentes qui empêchent l'exécution. Non, elle fait des erreurs logiques subtiles qui produisent des résultats plausibles mais faux.
Ce guide vous expose les 8 erreurs les plus fréquentes du SQL généré par IA, comment les détecter, et comment les corriger systématiquement.
Pourquoi le SQL généré par IA est dangereux
Le SQL généré par IA compile et s'exécute presque toujours. Il produit un résultat. Et ce résultat a l'air correct. C'est ce qui le rend dangereux : vous pouvez baser des décisions business sur des données fausses sans le savoir.
Les LLM ne « comprennent » pas vos données. Ils ne connaissent pas :
Les cardinalités de vos tables (1:1, 1:N, N:M)
Les valeurs NULL présentes dans vos colonnes
Les cas limites de votre business
Les conventions de nommage spécifiques à votre entreprise
⚠️ Attention
D'après une étude de Stack Overflow 2024, 23% des développeurs ont déployé du code généré par IA sans revue complète. En SQL, cette pratique peut corrompre silencieusement vos métriques business pendant des mois. Une erreur de JOIN peut multiplier les totaux par un facteur de 10 sans que personne le remarque.
Tableau : Impact des erreurs SQL non détectées
Type d'erreur
Détectabilité
Impact business
Délai avant détection
JOIN incorrect (multiplication de lignes)
Très difficile
Métriques multipliées par 10-100×
1-6 mois
NULL ignorés dans agrégation
Difficile
Taux de conversion faux de 15-30%
2-4 semaines
LEFT JOIN devenu INNER JOIN
Très difficile
Clients actifs sous-comptabilisés de 20-50%
1-3 mois
Window function frame incorrecte
Difficile
Tendances inversées ou lissées
3-8 semaines
Mauvais dialecte SQL
Facile
Requête ne s'exécute pas
Immédiat
Division par zéro silencieuse
Très difficile
KPI = NULL sans alerte
2-4 mois
Erreur de logique métier
Très difficile
Décisions business fausses
1-12 mois
Performance catastrophique
Très facile
Timeout, indisponibilité
Immédiat
Les 8 erreurs les plus fréquentes
Erreur 1 : Multiplication de lignes par les JOINs
C'est l'erreur la plus fréquente et la plus dangereuse.
-- Prompt : "Total des ventes et nombre de retours par client"-- SQL généré par l'IA :SELECT
c.name,
SUM(o.amount) AS total_sales,
COUNT(r.id) AS nb_returns
FROM customers c
LEFTJOIN orders o ON c.id = o.customer_id
LEFTJOINreturns r ON c.id = r.customer_id
GROUPBY c.name;
**Le problème** : si un client a 5 commandes et 3 retours, le JOIN produit 15 lignes (5 × 3= produit cartésien partiel). Le total des ventes est multiplié par 3, le nombre de retours par 5.**La correction** — pré-agréger dans des CTEs séparées :
```sqlWITH sales_by_customer AS (
SELECT customer_id, SUM(amount) AS total_sales
FROM orders
GROUPBY customer_id
),
returns_by_customer AS (
SELECT customer_id, COUNT(*) AS nb_returns
FROMreturnsGROUPBY customer_id
)
SELECT
c.name,
COALESCE(s.total_sales, 0) AS total_sales,
COALESCE(r.nb_returns, 0) AS nb_returns
FROM customers c
LEFTJOIN sales_by_customer s ON c.id = s.customer_id
LEFTJOIN returns_by_customer r ON c.id = r.customer_id;
<div class="callout callout-tip"><p class="callout-title">💡 Bon à savoir</p><p><strong>Détecter :</strong> comparez COUNT(*) avant et après le JOIN. Si le nombre de lignes augmente de manière inattendue, il y a une multiplication. <strong>Corriger :</strong> pré-agrégez chaque table dans une CTE séparée avant de joindre. Joignez uniquement des résultats déjà agrégés au niveau de la clé de jointure.</p></div>
### Erreur 2 : LEFTJOIN transformé en INNERJOIN
L'IA ajoute souvent un `WHERE` sur la table de droite, ce qui annule le LEFT JOIN.
```sql
-- L'IA génère :
SELECT c.name, o.order_date
FROM customers c
LEFTJOIN orders o ON c.id = o.customer_id
WHERE o.status ='shipped'; -- élimine les clients sans commande
La correction — déplacer la condition dans le ON :
SELECT c.name, o.order_date
FROM customers c
LEFTJOIN orders o
ON c.id = o.customer_id
AND o.status ='shipped';
💡 Bon à savoir
Détecter : cherchez tout WHERE qui filtre sur une colonne de la table de droite d'un LEFT JOIN. Si la condition est dans WHERE (pas dans ON), elle transforme silencieusement le LEFT JOIN en INNER JOIN. Corriger : déplacez la condition de filtrage de la table de droite dans la clause ON du JOIN.
L'IA utilise fréquemment les window functions sans spécifier la frame :
-- L'IA génère :SELECTdate,
amount,
AVG(amount) OVER (ORDERBYdate) AS moving_avg
FROM sales;
Le problème : sans ROWS BETWEEN, la frame par défaut est RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Avec RANGE, toutes les lignes ayant la même date sont incluses dans le même groupe — ce n'est pas une moyenne mobile.
La correction :
SELECTdate,
amount,
AVG(amount) OVER (
ORDERBYdateROWSBETWEEN6 PRECEDING ANDCURRENTROW
) AS moving_avg_7d
FROM sales;
-- L'IA génère : taux de complétion d'un formulaireSELECT100.0*COUNT(CASEWHEN step ='complete'THEN1END) /COUNT(*) AS completion_rate
FROM form_events;
Le problème : si step peut être NULL, ces lignes ne sont ni comptées dans le numérateur ni correctement gérées. Il faut aussi se demander si COUNT(*) est le bon dénominateur (tous les événements ? ou les utilisateurs uniques ?).
La correction :
SELECT
ROUND(
100.0*COUNT(DISTINCTCASEWHEN step ='complete'THEN user_id END)
/NULLIF(COUNT(DISTINCT user_id), 0),
1
) AS completion_rate
FROM form_events;
💡 Bon à savoir
Détecter : cherchez les divisions (risque de division par zéro), les COUNT(*) utilisés comme dénominateur, et les conditions sur des colonnes nullables sans COALESCE ou IS NULL. Corriger : utilisez NULLIF(dénominateur, 0) pour éviter les divisions par zéro, COUNT(DISTINCT colonne) pour les valeurs uniques réelles, et COALESCE(col, valeur_défaut) pour remplacer les NULL dans les agrégations.
Erreur 5 : Mauvais dialecte SQL
L'IA génère parfois du SQL pour le mauvais dialecte :
-- Syntaxe PostgreSQL, mais vous êtes sur MySQLSELECT DATE_TRUNC('month', order_date) ASmonthFROM orders;
-- En MySQL, il faut :SELECT DATE_FORMAT(order_date, '%Y-%m-01') ASmonthFROM orders;
Ou encore :
-- L'IA utilise ILIKE (PostgreSQL) au lieu de LIKE avec LOWER (MySQL)SELECT*FROM products WHERE name ILIKE '%widget%';
-- En MySQL :SELECT*FROM products WHERELOWER(name) LIKE'%widget%';
Erreur 6 : Agrégation sans les bonnes colonnes dans GROUP BY
-- L'IA génère :SELECT
department,
employee_name,
MAX(salary) AS max_salary
FROM employees
GROUPBY department;
Le problème : employee_name n'est pas dans le GROUP BY. PostgreSQL et SQL Server rejettent cette requête. MySQL en mode non-strict retourne une valeur arbitraire pour employee_name — qui n'est pas forcément l'employé avec le salaire max.
La correction — utiliser une window function :
WITH ranked AS (
SELECT*, RANK() OVER (PARTITIONBY department ORDERBY salary DESC) AS rn
FROM employees
)
SELECT department, employee_name, salary
FROM ranked WHERE rn =1;
L'IA écrit du SQL qui fonctionne sur 1000 lignes mais explose sur 10 millions :
-- L'IA génère : sous-requête corrélée sur chaque ligneSELECT
e.name,
e.salary,
(SELECTAVG(salary) FROM employees e2
WHERE e2.department_id = e.department_id) AS dept_avg
FROM employees e;
La correction — une seule passe avec une window function :
SELECT
name,
salary,
AVG(salary) OVER (PARTITIONBY department_id) AS dept_avg
FROM employees;
Inclure les employés inactifs dans les statistiques
Confondre la date de création et la date de livraison
Utiliser le prix catalogue au lieu du prix réel
-- L'IA génère : CA basé sur le prix catalogueSELECTSUM(p.catalog_price * oi.quantity) AS revenue
FROM order_items oi
JOIN products p ON oi.product_id = p.id;
-- Mais le vrai CA est basé sur le prix effectivement payéSELECTSUM(oi.unit_price * oi.quantity) AS revenue
FROM order_items oi
WHERE order_status ='delivered';
Erreurs par profil d'utilisateur et cas concrets
Erreurs des débutants (< 1 an d'expérience)
Les débutants font souvent confiance aveuglément à l'IA et ne détectent pas les erreurs subtiles.
Exemple : Clara, analyste marketing junior
Clara demande : "Taux de conversion par canal d'acquisition"
-- L'IA génère (erreur subtile) :SELECT
channel,
COUNT(CASEWHEN status ='converted'THEN1END) *100.0/COUNT(*) as conversion_rate
FROM users
GROUPBY channel;
Le problème : COUNT(*) inclut tous les utilisateurs, même ceux qui viennent juste de s'inscrire et n'ont pas encore eu le temps de convertir. Le taux est artificiellement bas.
La correction métier :
SELECT
channel,
COUNT(CASEWHEN status ='converted'THEN1END) *100.0/COUNT(CASEWHEN created_at <=CURRENT_DATE-INTERVAL'7 days'THEN1END) as conversion_rate,
COUNT(*) as total_users,
COUNT(CASEWHEN status ='converted'THEN1END) as converted_users
FROM users
WHERE created_at <=CURRENT_DATE-INTERVAL'7 days'GROUPBY channel
ORDERBY conversion_rate DESC;
Erreurs des intermédiaires (2-4 ans)
Les utilisateurs intermédiaires détectent les erreurs évidentes mais ratent les subtilités métier.
Exemple : Marc, analyste financier
Marc demande : "Évolution du chiffre d'affaires mensuel avec croissance YoY"
-- L'IA génère (logique incomplète) :WITH monthly_revenue AS (
SELECT
DATE_TRUNC('month', order_date) asmonth,
SUM(amount) as revenue
FROM orders
WHERE status ='paid'GROUPBY1
)
SELECTmonth,
revenue,
LAG(revenue, 12) OVER (ORDERBYmonth) as revenue_previous_year,
ROUND((revenue -LAG(revenue, 12) OVER (ORDERBYmonth)) *100.0/LAG(revenue, 12) OVER (ORDERBYmonth), 2) as yoy_growth_pct
FROM monthly_revenue;
Le problème : la requête ne gère pas les remboursements, les annulations tardives, ni les ajustements comptables. En finance, le CA doit être net.
La correction comptable :
WITH transactions AS (
SELECT DATE_TRUNC('month', order_date) asmonth, amount
FROM orders
WHERE status IN ('paid', 'delivered')
UNIONALLSELECT DATE_TRUNC('month', refund_date) asmonth, -refund_amount
FROM refunds
WHERE status ='processed'
),
monthly_revenue AS (
SELECTmonth, SUM(amount) as net_revenue
FROM transactions
GROUPBYmonth
)
SELECTmonth,
net_revenue,
LAG(net_revenue, 12) OVER (ORDERBYmonth) as revenue_previous_year,
ROUND((net_revenue -LAG(net_revenue, 12) OVER (ORDERBYmonth)) *100.0/NULLIF(LAG(net_revenue, 12) OVER (ORDERBYmonth), 0), 2) as yoy_growth_pct
FROM monthly_revenue
ORDERBYmonthDESC;
Erreurs spécifiques à l'e-commerce
Exemple : Léa, e-commerce manager
"Taux de retour par catégorie de produit"
-- L'IA génère (erreur de périmètre) :SELECT
p.category,
COUNT(r.id) *100.0/COUNT(o.id) as return_rate
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
LEFTJOINreturns r ON oi.id = r.order_item_id
GROUPBY p.category;
Le problème : compte les commandes récentes qui n'ont pas encore eu le temps d'être retournées.
La correction :
WITH eligible_orders AS (
-- Seulement les commandes livrées depuis plus de 7 joursSELECT oi.id as order_item_id, p.category
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.status ='delivered'AND o.delivery_date <=CURRENT_DATE-INTERVAL'7 days'AND o.delivery_date >=CURRENT_DATE-INTERVAL'6 months'
)
SELECT
e.category,
COUNT(e.order_item_id) as total_eligible_items,
COUNT(r.id) as returned_items,
ROUND(COUNT(r.id) *100.0/NULLIF(COUNT(e.order_item_id), 0), 2) as return_rate_pct
FROM eligible_orders e
LEFTJOINreturns r ON e.order_item_id = r.order_item_id
AND r.status ='approved'GROUPBY e.category
HAVINGCOUNT(e.order_item_id) >=100ORDERBY return_rate_pct DESC;
Cas réels par type de base de données
PostgreSQL : pièges fréquents
Problème avec les fuseaux horaires :
-- L'IA génère (ignore le fuseau horaire) :SELECT DATE_TRUNC('day', created_at) FROM orders;
-- Correction (prise en compte du fuseau métier) :SELECT DATE_TRUNC('day', created_at ATTIME ZONE 'UTC'ATTIME ZONE 'Europe/Paris') FROM orders;
BigQuery : erreurs typiques
Problème avec les ARRAY et les partitions :
-- L'IA génère (syntaxe incorrecte) :SELECT user_id, tags[0] FROM users;
-- Correction BigQuery :SELECT user_id, tags[OFFSET(0)] FROM users;
MySQL : pièges courants
Problème avec le GROUP BY strict :
-- MySQL 5.7+ en mode strict rejette :SELECT department, name, COUNT(*) FROM employees GROUPBY department;
-- Correction :SELECT department, ANY_VALUE(name), COUNT(*) FROM employees
GROUPBY department;
Checklist de revue complète
Pour tous les profils
Exécution : la requête s'exécute-t-elle sans erreur ?
Résultat : le nombre de lignes est-il plausible ?
NULL : y a-t-il des NULL inattendus ou ignorés ?
Vérification des JOINs (priorité absolue)
Comptabilisez les lignes avant et après chaque JOIN
Si LEFT JOIN : des lignes de la table de gauche ont-elles disparu ?
Les conditions de filtrage de la table de droite sont-elles dans ON ?
Vérification des agrégations
COUNT vs COUNT DISTINCT : le bon est-il utilisé ?
GROUP BY contient toutes les colonnes non-agrégées
Les NULL sont traités dans les calculs de pourcentages
Pas de division par zéro possible (NULLIF)
Vérification business
Les bons statuts sont filtrés (actif/inactif, validé/brouillon)
Les bonnes dates sont utilisées (création/modification/livraison)
Recoupement avec une source alternative (Excel, dashboard)
Tableau récapitulatif : erreurs vs solutions
Erreur
Symptôme
Solution
Multiplication de lignes (JOIN)
COUNT(*) augmente après JOIN
Pré-agréger dans CTEs séparées
LEFT JOIN devenu INNER
Lignes de gauche disparaissent
Déplacer WHERE dans ON
Window function frame
Résultats ne correspondent pas
Spécifier ROWS BETWEEN explicite
NULL ignorés
Totaux manquent de lignes
COALESCE + NULLIF pour division
Mauvais dialecte
Erreur de syntaxe
Adapter aux fonctions de la DB
GROUP BY incomplet
Erreur de colonne
Window functions ou sous-requêtes
Performance mauvaise
Timeout, scan table complet
Remplacer sous-requêtes corrélées par window functions
Toujours tester sur un échantillon : demandez LIMIT 100 et vérifiez manuellement 5-10 lignes
Documenter les hypothèses : écrivez un commentaire explicitant la logique métier
Versioner les requêtes : gardez l'historique des modifications (GitHub, Gitlab)
Automatiser les tests : créez des tests SQL pour vos KPI critiques
-- Exemple : test que le CA mensuel n'a pas changé de > 20%SELECTmonth,
net_revenue,
LAG(net_revenue) OVER (ORDERBYmonth) as prev_month,
CASEWHENABS(net_revenue -LAG(net_revenue) OVER (ORDERBYmonth)) /LAG(net_revenue) OVER (ORDERBYmonth) >0.2THEN'ALERTE'ELSE'OK'ENDas status
FROM monthly_revenue
ORDERBYmonthDESC LIMIT 3;
Erreurs par cas d'usage avancé
Analyse de cohorte (rétention)
L'IA oublie souvent que la rétention doit être mesurée sur un cohort équitable :
-- Faux : mélange des cohortes d'âges différentsSELECT
registration_month,
DATE_TRUNC('month', login_date) as activity_month,
COUNT(DISTINCT user_id) as active_users
FROM users u
LEFTJOIN logins l ON u.id = l.user_id
GROUPBY1, 2;
-- Correct : fenêtre d'observation identique pour tous les cohortsWITH cohorts AS (
SELECT
user_id,
DATE_TRUNC('month', registration_date) as cohort_month
FROM users
WHERE registration_date >='2024-01-01'-INTERVAL'12 months'
),
activity_month_number AS (
SELECT
c.cohort_month,
c.user_id,
(DATE_TRUNC('month', l.login_date)::date- c.cohort_month::date) /30as months_since_signup
FROM cohorts c
LEFTJOIN logins l ON c.user_id = l.user_id
AND l.login_date >= c.cohort_month
)
SELECT
cohort_month,
months_since_signup,
COUNT(DISTINCT user_id) as retained_users
FROM activity_month_number
WHERE months_since_signup >=0AND months_since_signup <=11GROUPBY1, 2;