01 — SQL et texte-vers-SQL
Un modèle qui ne connaît pas ton schéma invente tes colonnes. Lui donner le schéma, c'est 80 % du travail.
Temps de lecture : 15 min | Niveau : Intermédiaire
Ce que tu sauras faire après
- Fournir ton schéma au modèle dans un format qu'il exploite vraiment.
- Faire écrire une requête complexe sans te retrouver avec des colonnes inventées.
- Faire optimiser une requête lente et savoir vérifier que l'optimisation est réelle.
- Faire expliquer une requête héritée de quelqu'un d'autre.
- Traduire une requête d'un dialecte SQL à un autre sans casser la logique.
1. Le vrai problème du texte-vers-SQL
Tu écris « donne-moi le chiffre d'affaires par mois ». Le modèle ne sait pas :
- dans quelle table sont les ventes ;
- si le montant est HT ou TTC ;
- si la colonne s'appelle
amount,montant,revenueouca_ttc; - si les commandes annulées sont dans la même table ;
- quel est ton dialecte SQL.
Il va donc deviner. Et une devinette SQL s'exécute parfois sans erreur. C'est le pire cas : tu as un chiffre, il est faux, rien ne clignote.
La documentation Anthropic sur le prompting le dit sans détour :
« Think of Claude as a brilliant but new employee who lacks context on your norms and workflows. The more precisely you explain what you want, the better the result. »
Et sur l'ancrage dans des documents fournis :
« Ground responses in quotes: For long document tasks, ask Claude to quote relevant parts of the documents first before carrying out its task. »
Appliqué au SQL, « les documents », c'est ton DDL.
2. Donner le schéma : la seule méthode qui marche
AVANT (mauvais)
Écris-moi une requête SQL qui donne le CA par mois et par magasin.Résultat typique : SELECT DATE_TRUNC('month', order_date), store_name, SUM(total) FROM orders GROUP BY 1,2. Trois noms de colonnes sur quatre sont inventés.
APRÈS (bon)
Tu colles le CREATE TABLE réel. Pas une description en prose : le DDL.
-- Schéma fourni au modèle
CREATE TABLE analytics.fct_commandes (
commande_id STRING NOT NULL, -- clé primaire
magasin_code STRING, -- 2 caractères, ex '03'
date_commande DATE, -- date de prise de commande
montant_ttc NUMERIC, -- en euros, TTC
statut STRING -- 'validee' | 'annulee' | 'en_attente'
)
PARTITION BY date_commande
CLUSTER BY magasin_code;
CREATE TABLE analytics.dim_magasin (
magasin_code STRING NOT NULL,
magasin_libelle STRING,
region STRING
);Trois choses changent tout :
- Les commentaires
--: ils portent le sens métier que le nom de colonne ne porte pas (montant_ttcen euros, TTC). - Les valeurs possibles d'une colonne d'état : le modèle saura filtrer
statut = 'validee'au lieu de tout additionner. - Le partitionnement et le clustering : le modèle saura qu'il doit filtrer sur
date_commandepour ne pas scanner toute la table.
Structurer avec des balises XML
La doc Anthropic recommande d'emballer chaque type de contenu dans sa propre balise :
« XML tags help Claude parse complex prompts unambiguously, especially when your prompt mixes instructions, context, examples, and variable inputs. »
Concrètement :
<schema>
{{COLLE TON DDL ICI}}
</schema>
<regles_metier>
- Le CA exclut toujours les commandes avec statut = 'annulee'.
- L'exercice fiscal va du 1er juillet au 30 juin.
</regles_metier>
<question>
CA mensuel par région sur l'exercice en cours.
</question>Un détail qui compte quand le schéma est gros : la doc Anthropic recommande de placer les données longues en haut du prompt, au-dessus de la question. Schéma d'abord, question ensuite.
3. Écrire une requête complexe
Pour une requête à plusieurs niveaux (fenêtrage, CTE, anti-jointure), ajoute deux contraintes qui changent la qualité du résultat.
Contrainte 1 — imposer la structure en CTE. Une requête en CTE nommées est relisible ; une requête en sous-requêtes imbriquées ne l'est pas.
Contrainte 2 — demander les hypothèses avant le code. Tu forces le modèle à écrire ce qu'il a supposé. Tu lis les hypothèses, tu corriges celles qui sont fausses, puis seulement tu lis le SQL.
AVANT
Requête pour les clients qui ont commandé en janvier mais pas en février.APRÈS
<schema>{{DDL}}</schema>
Écris une requête GoogleSQL (BigQuery) qui liste les clients ayant commandé
en janvier 2026 mais pas en février 2026.
Contraintes :
- Utilise des CTE nommées, une par étape logique. Pas de sous-requête imbriquée.
- Filtre sur la colonne de partitionnement dans CHAQUE CTE qui lit une table partitionnée.
- Exclus les commandes annulées.
Avant le SQL, liste sous <hypotheses> toutes les hypothèses que tu as dû faire
sur les données. Si le schéma ne te permet pas de répondre, dis-le au lieu
d'inventer un nom de colonne.Pourquoi c'est mieux : le modèle a le schéma, il connaît la règle métier sur les annulations, il sait qu'il doit filtrer la partition, et il a le droit de dire qu'il lui manque une information. Cette dernière phrase est la technique de « l'issue de secours » que recommande Microsoft :
« Give the model an "out". It can sometimes be helpful to give the model an alternative path if it's unable to complete the assigned task. »
4. Optimiser une requête — et vérifier que c'est vrai
C'est là que la plupart des gens se font piéger. Le modèle te dira « cette version est plus performante ». Il ne l'a pas mesurée. Il raisonne sur des motifs, pas sur ton plan d'exécution.
Vérification 1 — le plan d'exécution
Sur PostgreSQL et MySQL : EXPLAIN donne le plan estimé, EXPLAIN ANALYZE exécute vraiment la requête et mesure.
Sur Snowflake, attention : EXPLAIN existe (USING JSON, USING TABULAR, USING TEXT) mais il ne rend que le plan logique. La doc est claire : « EXPLAIN compiles the SQL statement, but does not execute it, so EXPLAIN does not require a running warehouse. » Il n'y a donc pas d'EXPLAIN ANALYZE : pour les temps réellement passés, tu ouvres le Query Profile dans Snowsight.
Sur BigQuery, il n'y a pas de mot-clé EXPLAIN. Tu ouvres le bouton Execution Details dans la console, ou tu récupères le plan via la méthode d'API jobs.get, sous statistics.query.queryPlan.
Les champs du plan BigQuery à regarder en premier :
| Champ | Ce qu'il te dit |
|---|---|
recordsRead | Nombre de lignes lues en entrée de l'étape |
recordsWritten | Nombre de lignes produites en sortie |
shuffleOutputBytes | Octets échangés entre étapes |
shuffleOutputBytesSpilled | Octets débordés sur disque : signe d'une étape trop grosse |
computeRatioAvg / waitRatioAvg | Où part le temps : calcul, ou attente de slots |
Un recordsRead énorme suivi d'un recordsWritten minuscule veut dire : tu lis beaucoup pour jeter beaucoup. C'est un filtre à remonter plus tôt.
Vérification 2 — la volumétrie et le coût
Sur BigQuery en facturation à la demande, tu paies les octets lus. Trois réflexes, tous documentés par Google :
- Le dry run. L'option
--dry_rundebqvalide la requête et estime les octets traités, sans rien exécuter ni facturer. Retiens que c'est une estimation haute. La doc Google le dit mot pour mot : « The estimate of the number of bytes that is billed for a query is an upper bound, and can be higher than the actual number of bytes billed after running the query. » Tu peux donc payer moins que l'estimation, jamais plus. - Le plafond
maximum bytes billed. Si l'estimation dépasse ta limite, « the query fails without incurring a charge ». - Le piège du
LIMIT. La doc est formelle : « For non-clustered tables, don't use a LIMIT clause as a method of cost control », parce qu'il « doesn't affect the amount of data that is read ». UnLIMIT 10ne rend pas ta requête moins chère.
# Estimer avant d'exécuter
bq query --dry_run --use_legacy_sql=false \
"SELECT magasin_code, SUM(montant_ttc) FROM analytics.fct_commandes
WHERE date_commande BETWEEN '2026-01-01' AND '2026-01-31' GROUP BY 1"
# Plafonner la facture d'une requête (10 Go)
bq query --maximum_bytes_billed=10000000000 --use_legacy_sql=false "SELECT ..."Google range les facteurs de performance en six familles. Voici le libellé exact de la doc, et ce que tu cherches à réduire dans chacune :
| Famille (libellé Google) | Ce que tu réduis |
|---|---|
| Input data and data sources (I/O) | Les octets lus par la requête |
| Communication between nodes (shuffling) | Les octets échangés entre les étapes |
| Computation | Le travail de calcul (CPU) |
| Outputs (materialization) | Les octets écrits |
| Capacity and concurrency | La concurrence entre requêtes sur les slots |
| Query patterns | Les mauvaises habitudes d'écriture SQL |
Les quatre premières sont celles sur lesquelles tu agis directement en réécrivant une requête. Reprends ce vocabulaire dans ton prompt : le modèle s'y aligne et te rend une liste structurée au lieu d'un fourre-tout.
5. Expliquer une requête existante
Cas très fréquent : tu hérites d'une requête de 300 lignes, sans commentaire, écrite par quelqu'un qui est parti. L'IA est excellente ici, parce que la source de vérité (le SQL) est entièrement dans le prompt. Le risque d'hallucination est faible.
Demande une sortie en trois blocs : intention métier, étape par étape, pièges détectés. Le troisième bloc est le plus utile. Les pièges classiques à faire chercher :
- un
LEFT JOINneutralisé par une condition sur la table de droite placée dans leWHEREau lieu duON; COUNT(colonne)qui ignore lesNULL, alors queCOUNT(*)ne les ignore pas ;UNIONqui dédoublonne en silence là oùUNION ALLétait voulu ;- une jointure sur une clé non unique qui duplique les lignes et gonfle les sommes.
6. Traduire entre dialectes
Traduire PostgreSQL → BigQuery ou SQL Server → Snowflake est un bon usage, à condition de nommer explicitement les deux dialectes et de demander une liste des constructions non traduisibles.
Les points qui cassent en silence, à faire vérifier systématiquement :
- Dates :
DATE_TRUNC,DATEADD,EXTRACTn'ont pas les mêmes signatures. - Division : certains moteurs renvoient un entier, d'autres un flottant.
NULLdans les tris :NULLS FIRST/NULLS LASTn'est pas le défaut partout.- Sensibilité à la casse des identifiants.
- Fenêtrage :
QUALIFYexiste sur BigQuery et Snowflake, pas sur PostgreSQL.
Toujours terminer par : exécute les deux requêtes sur le même jeu de données et compare le nombre de lignes et les sommes. Une traduction non comparée n'est pas une traduction validée.
🧰 Templates à copier-coller
Template A — Écrire une requête
Tu es ingénieur data senior, expert en [DIALECTE_SQL].
<schema>
[COLLE_ICI_LES_CREATE_TABLE_AVEC_COMMENTAIRES]
</schema>
<regles_metier>
- [REGLE_1_EX_LE_CA_EXCLUT_LES_ANNULATIONS]
- [REGLE_2_EX_EXERCICE_FISCAL_1ER_JUILLET_AU_30_JUIN]
</regles_metier>
<question>
[FORMULE_TA_QUESTION_EN_UNE_PHRASE]
</question>
Contraintes :
- Dialecte : [DIALECTE_SQL].
- Structure en CTE nommées, une par étape logique.
- Filtre obligatoire sur la colonne de partitionnement : [NOM_COLONNE_PARTITION].
- Granularité attendue du résultat : une ligne par [GRANULARITE].
Produis dans cet ordre :
1. <hypotheses> : toutes les hypothèses que tu as dû faire.
2. <sql> : la requête.
3. <verification> : une requête de contrôle simple qui permet de vérifier
l'ordre de grandeur du résultat.
Si le schéma ne suffit pas, écris "information manquante : [...]" au lieu
d'inventer un nom de colonne.Template B — Optimiser
Voici une requête [DIALECTE_SQL] qui met [DUREE_ACTUELLE] et lit [VOLUME_LU].
<requete>
[COLLE_LA_REQUETE]
</requete>
<schema>
[DDL_AVEC_PARTITIONNEMENT_ET_CLUSTERING]
</schema>
<plan_execution>
[COLLE_LE_PLAN_OU_ECRIS_"non disponible"]
</plan_execution>
Propose des optimisations classées dans cet ordre, avec le vocabulaire Google :
1. input data and data sources (I/O) : réduire les octets lus
2. communication between nodes (shuffling) : réduire les octets échangés
3. computation : réduire le travail de calcul
4. outputs (materialization) : réduire les octets écrits
Pour chaque proposition, donne : le changement, le gain attendu, et le risque
de changer le résultat. Marque clairement celles qui modifient la sémantique.
Ne prétends pas connaître le gain réel : indique comment je dois le mesurer.Template C — Expliquer une requête héritée
<requete>
[COLLE_LA_REQUETE]
</requete>
Explique-la en trois parties :
1. <intention> : en 3 phrases maximum, ce que cette requête calcule, en langage métier.
2. <etapes> : un tableau | étape | ce qu'elle fait | granularité en sortie |.
3. <pieges> : tout ce qui peut produire un résultat surprenant
(LEFT JOIN neutralisé par un WHERE, COUNT qui ignore les NULL,
UNION qui dédoublonne, jointure qui duplique des lignes).
N'affirme rien qui ne soit pas visible dans la requête. Si le sens d'une
colonne est ambigu sans le schéma, dis-le.Template D — Traduire un dialecte
Traduis cette requête de [DIALECTE_SOURCE] vers [DIALECTE_CIBLE].
<requete_source>
[COLLE_LA_REQUETE]
</requete_source>
Exigences :
- Conserve exactement la même sémantique, y compris le traitement des NULL
et l'ordre de tri.
- Rends un tableau | construction source | équivalent cible | risque de
différence de résultat |.
- Signale séparément, sous <non_traduisible>, toute construction sans
équivalent direct, avec la solution de contournement proposée.
- Termine par la requête de comparaison que je dois lancer pour vérifier
que les deux versions renvoient le même résultat.A retenir
- Sans DDL commenté dans le prompt, le modèle invente des colonnes : le schéma n'est pas optionnel.
- Emballe schéma, règles métier et question dans des balises XML distinctes, et mets le schéma en haut.
- Exige les hypothèses avant le SQL, et donne toujours au modèle le droit de dire « information manquante ».
- Une optimisation proposée par l'IA n'est pas mesurée : valide par le plan d'exécution et par un contrôle de volumétrie.
- Sur BigQuery :
--dry_runpour estimer (estimation haute),maximum bytes billedpour plafonner, etLIMITne réduit pas le coût sur une table non clusterisée. EXPLAIN ANALYZEexiste sur PostgreSQL et MySQL, pas sur Snowflake ni sur BigQuery : nomme le bon outil de mesure selon ton moteur.
Sources
- Prompting best practices — Claude Docs
- Prompt engineering overview — Claude Docs
- Prompt engineering techniques — Microsoft Learn
- BigQuery introduction — Google Cloud
- Controlling costs in BigQuery — Google Cloud
- Estimate query costs — Google Cloud
- EXPLAIN — Snowflake Documentation
- Query plan and timeline — Google Cloud
- Optimize query performance — Google Cloud
- Canner/WrenAI - code d'exemple sur le texte-vers-SQL gouverné par une couche sémantique :
docs/core/get_started/quickstart.md - googleapis/mcp-toolbox - le serveur MCP qui donne le schéma et l'exécution SQL à ton agent, sans coller le DDL à la main :
docs/BIGQUERY_README.md