RAG sur Données SQL : Interrogez vos Bases de Données en Langage Naturel
Implémentez le Text-to-SQL avec RAG pour interroger vos bases de données en langage naturel. Comparaison d'approches, benchmarks Spider, sécurité et code complet.
RAG sur Données SQL : Interrogez vos Bases de Données en Langage Naturel
80% des données d'entreprise vivent dans des bases de données relationnelles. Pourtant, la plupart des systèmes RAG ignorent complètement les données structurées, se limitant aux documents textuels. Le RAG sur données SQL permet à vos utilisateurs de poser des questions comme "Quel est notre meilleur client ce trimestre ?" et d'obtenir une réponse précise directement depuis votre base de données, sans écrire une seule ligne de SQL.
TL;DR
- Text-to-SQL convertit les questions en langage naturel en requêtes SQL exécutables
- Trois approches : génération SQL directe, RAG-augmented SQL, hybride (SQL + documents)
- Benchmarks Spider : les meilleurs modèles atteignent 85%+ d'exactitude d'exécution
- Sécurité critique : SQL injection, accès aux données sensibles, validation obligatoire
- Outils : LangChain SQL agents, LlamaIndex NLSQLTableQueryEngine, Vanna.AI
- Cas d'usage : analytics conversationnel, reporting automatisé, support e-commerce
Pourquoi le RAG sur données SQL ?
Les bases de données contiennent les données les plus précieuses et les plus à jour d'une entreprise. Mais elles sont inaccessibles aux non-techniciens.
| Source de données | RAG classique | RAG SQL | Avantage SQL |
|---|---|---|---|
| Documents PDF | Excellent | N/A | - |
| FAQ/Articles | Excellent | N/A | - |
| Ventes/Commandes | Impossible | Excellent | Données en temps réel |
| Stock/Inventaire | Impossible | Excellent | Toujours à jour |
| Analytics/Métriques | Impossible | Excellent | Calculs précis |
| Données clients | Impossible | Excellent | Personnalisation |
Exemples de questions auxquelles seul le SQL peut répondre
"Quel est le chiffre d'affaires du mois dernier ?"
→ SELECT SUM(amount) FROM orders WHERE date >= '2026-02-01'
"Quels produits sont en rupture de stock ?"
→ SELECT name FROM products WHERE stock = 0
"Qui sont nos 10 meilleurs clients par CA ?"
→ SELECT customer_name, SUM(amount) as total
FROM orders GROUP BY customer_name
ORDER BY total DESC LIMIT 10
"Combien de tickets support sont ouverts depuis plus de 48h ?"
→ SELECT COUNT(*) FROM tickets
WHERE status = 'open'
AND created_at < NOW() - INTERVAL '48 hours'
Les trois approches
Approche 1 : Génération SQL directe par LLM
Le LLM reçoit le schéma de la base et génère directement la requête SQL.
DEVELOPERpythonfrom langchain_openai import ChatOpenAI from langchain_community.utilities import SQLDatabase # Connexion à la base db = SQLDatabase.from_uri( "postgresql://user:password@localhost:5432/mydb", include_tables=["orders", "customers", "products"], sample_rows_in_table_info=3 # Exemples de données ) llm = ChatOpenAI(model="gpt-4o", temperature=0) def generate_sql(question: str) -> str: """Génère une requête SQL à partir d'une question.""" schema = db.get_table_info() prompt = f"""Tu es un expert SQL PostgreSQL. Génère une requête SQL pour répondre à la question. Schéma de la base de données : {schema} Règles : - Utilise UNIQUEMENT les tables et colonnes du schéma - Retourne du SQL valide PostgreSQL - Limite les résultats à 50 lignes maximum - N'utilise JAMAIS DROP, DELETE, UPDATE, INSERT, ALTER Question : {question} Requête SQL :""" response = llm.invoke(prompt) return response.content.strip().strip("```sql").strip("```") # Exemple sql = generate_sql("Quel est le CA du mois dernier ?") # SELECT SUM(amount) FROM orders # WHERE date >= date_trunc('month', CURRENT_DATE - INTERVAL '1 month') # AND date < date_trunc('month', CURRENT_DATE)
Approche 2 : RAG-Augmented SQL
Enrichit le contexte du LLM avec des exemples de requêtes similaires (few-shot) et la documentation du schéma.
DEVELOPERpythonfrom langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS from langchain_core.documents import Document # Base de connaissances d'exemples SQL sql_examples = [ Document( page_content="Question: Chiffre d'affaires du mois dernier\n" "SQL: SELECT SUM(amount) FROM orders " "WHERE date >= date_trunc('month', CURRENT_DATE - INTERVAL '1 month') " "AND date < date_trunc('month', CURRENT_DATE)", metadata={"category": "revenue", "tables": "orders"} ), Document( page_content="Question: Top 10 clients par chiffre d'affaires\n" "SQL: SELECT c.name, SUM(o.amount) as total " "FROM orders o JOIN customers c ON o.customer_id = c.id " "GROUP BY c.name ORDER BY total DESC LIMIT 10", metadata={"category": "customers", "tables": "orders,customers"} ), Document( page_content="Question: Produits en rupture de stock\n" "SQL: SELECT name, sku FROM products WHERE stock = 0 " "ORDER BY name", metadata={"category": "inventory", "tables": "products"} ), # ... des dizaines d'exemples ] # Index vectoriel des exemples embeddings = OpenAIEmbeddings(model="text-embedding-3-small") example_store = FAISS.from_documents(sql_examples, embeddings) class RAGAugmentedSQL: def __init__(self, db, llm, example_store): self.db = db self.llm = llm self.retriever = example_store.as_retriever( search_kwargs={"k": 3} ) def generate(self, question: str) -> str: # 1. Récupérer des exemples similaires examples = self.retriever.invoke(question) examples_text = "\n\n".join([ doc.page_content for doc in examples ]) # 2. Générer avec few-shot schema = self.db.get_table_info() prompt = f"""Tu es un expert SQL PostgreSQL. Schéma : {schema} Exemples de requêtes similaires : {examples_text} Génère la requête SQL pour cette question. Question : {question} Requête SQL :""" response = self.llm.invoke(prompt) return response.content.strip().strip("```sql").strip("```") rag_sql = RAGAugmentedSQL(db, llm, example_store) sql = rag_sql.generate("Quels sont les 5 produits les plus vendus ?")
Approche 3 : Hybride (SQL + Documents)
Combine les données SQL et les documents textuels pour des réponses enrichies.
DEVELOPERpythonclass HybridSQLDocumentRAG: """RAG hybride combinant SQL et documents.""" def __init__(self, sql_chain, doc_retriever, llm): self.sql_chain = sql_chain self.doc_retriever = doc_retriever self.llm = llm async def query(self, question: str) -> str: # 1. Classifier la question needs_sql = await self._needs_sql(question) needs_docs = await self._needs_docs(question) context_parts = [] # 2. Récupérer les données SQL si nécessaire if needs_sql: sql_result = await self.sql_chain.arun(question) context_parts.append( f"Données de la base :\n{sql_result}" ) # 3. Récupérer les documents si nécessaire if needs_docs: docs = await self.doc_retriever.aget_relevant_documents( question ) doc_context = "\n".join([d.page_content for d in docs]) context_parts.append( f"Documentation :\n{doc_context}" ) # 4. Générer la réponse combinée context = "\n\n---\n\n".join(context_parts) prompt = f"""Réponds à la question en combinant les données de la base de données et la documentation. {context} Question : {question} Réponse (en langage naturel, pas en SQL) :""" return (await self.llm.ainvoke(prompt)).content async def _needs_sql(self, question: str) -> bool: """Détermine si la question nécessite des données SQL.""" keywords = [ "combien", "total", "chiffre", "ventes", "stock", "clients", "commandes", "meilleur", "pire", "moyenne", "dernier mois", "ce trimestre", "cette année" ] return any(kw in question.lower() for kw in keywords) async def _needs_docs(self, question: str) -> bool: """Détermine si la question nécessite de la documentation.""" keywords = [ "comment", "pourquoi", "politique", "procédure", "guide", "tutoriel", "expliquer", "fonctionnement" ] return any(kw in question.lower() for kw in keywords)
Comparaison des approches
| Critère | SQL Direct | RAG-Augmented SQL | Hybride |
|---|---|---|---|
| Précision SQL | 70-75% | 80-85% | 80-85% |
| Questions factuelles | Excellent | Excellent | Excellent |
| Questions conceptuelles | Impossible | Impossible | Bon |
| Setup time | 30 min | 2-4h | 4-8h |
| Maintenance | Faible | Moyenne | Élevée |
| Coût par requête | $0.005 | $0.008 | $0.012 |
| Latence | ~2s | ~3s | ~4s |
| Sécurité | Critique | Critique | Critique |
Benchmarks sur le dataset Spider
Le dataset Spider est le benchmark standard pour le Text-to-SQL.
| Modèle / Approche | Exactitude d'exécution | Exactitude SQL exacte |
|---|---|---|
| GPT-4o (zero-shot) | 72.3% | 67.8% |
| GPT-4o (few-shot, 5 exemples) | 79.1% | 74.5% |
| GPT-4o + RAG exemples | 83.4% | 78.9% |
| Claude 3.5 Sonnet (few-shot) | 80.7% | 76.2% |
| DIN-SQL + GPT-4 | 85.3% | 81.1% |
| DAIL-SQL + GPT-4 | 86.6% | 82.4% |
| Spécialiste fine-tuné | 88.2% | 84.7% |
Implémentation complète avec LangChain
DEVELOPERpythonfrom langchain_community.utilities import SQLDatabase from langchain_community.agent_toolkits import SQLDatabaseToolkit from langchain_openai import ChatOpenAI from langchain.agents import create_sql_agent from langchain.agents.agent_types import AgentType # 1. Configuration de la base db = SQLDatabase.from_uri( "postgresql://user:password@localhost:5432/ecommerce", include_tables=[ "orders", "order_items", "customers", "products", "categories" ], sample_rows_in_table_info=3 ) # 2. LLM llm = ChatOpenAI(model="gpt-4o", temperature=0) # 3. Toolkit SQL toolkit = SQLDatabaseToolkit(db=db, llm=llm) # 4. Agent SQL agent = create_sql_agent( llm=llm, toolkit=toolkit, agent_type=AgentType.OPENAI_FUNCTIONS, verbose=True, max_iterations=10, handle_parsing_errors=True, extra_tools=[], prefix="""Tu es un agent qui interagit avec une base de données SQL. Tu dois répondre aux questions en langage naturel. RÈGLES DE SÉCURITÉ : - N'exécute JAMAIS de requêtes qui modifient la base (INSERT, UPDATE, DELETE, DROP) - Limite toujours les résultats (LIMIT 50 max) - Ne révèle JAMAIS les mots de passe ou données sensibles - Si une question est ambiguë, demande des précisions RÈGLES DE FORMAT : - Réponds en français - Formate les montants en euros - Formate les dates en format français (JJ/MM/AAAA) """ ) # 5. Utilisation result = agent.invoke({ "input": "Quel est le CA total du mois dernier " "et combien de commandes avons-nous reçu ?" }) print(result["output"])
Implémentation avec LlamaIndex
DEVELOPERpythonfrom llama_index.core import SQLDatabase, VectorStoreIndex from llama_index.core.query_engine import NLSQLTableQueryEngine from llama_index.core.indices.struct_store import SQLTableRetrieverQueryEngine from sqlalchemy import create_engine # 1. Connexion engine = create_engine( "postgresql://user:password@localhost:5432/ecommerce" ) sql_database = SQLDatabase( engine, include_tables=[ "orders", "customers", "products" ] ) # 2. Query engine simple query_engine = NLSQLTableQueryEngine( sql_database=sql_database, tables=["orders", "customers", "products"], llm=llm ) response = query_engine.query( "Quels sont les 5 clients avec le plus de commandes ?" ) print(response.response) print(f"SQL généré : {response.metadata['sql_query']}") # 3. Query engine avancé avec retrieval de tables # (utile quand il y a beaucoup de tables) table_node_mapping = sql_database.get_table_node_mapping() table_schema_index = VectorStoreIndex( list(table_node_mapping.values()) ) query_engine = SQLTableRetrieverQueryEngine( sql_database=sql_database, table_retriever=table_schema_index.as_retriever( similarity_top_k=3 ), llm=llm )
Sécurité : la partie critique
Le RAG SQL expose directement votre base de données. La sécurité n'est pas optionnelle.
1. Validation des requêtes
DEVELOPERpythonimport re from typing import Tuple class SQLValidator: """Valide les requêtes SQL générées avant exécution.""" FORBIDDEN_KEYWORDS = [ "DROP", "DELETE", "UPDATE", "INSERT", "ALTER", "TRUNCATE", "CREATE", "GRANT", "REVOKE", "EXEC", "EXECUTE", "xp_", "sp_", "--", "/*", "*/", "UNION ALL" ] MAX_RESULT_LIMIT = 100 def validate(self, sql: str) -> Tuple[bool, str]: """Valide une requête SQL. Retourne (is_valid, reason).""" sql_upper = sql.upper().strip() # 1. Vérifier que c'est un SELECT if not sql_upper.startswith("SELECT"): return False, "Seules les requêtes SELECT sont autorisées" # 2. Vérifier les mots-clés interdits for keyword in self.FORBIDDEN_KEYWORDS: if keyword.upper() in sql_upper: return False, f"Mot-clé interdit détecté : {keyword}" # 3. Vérifier la présence d'un LIMIT if "LIMIT" not in sql_upper: return False, "La requête doit contenir un LIMIT" # 4. Vérifier que le LIMIT est raisonnable limit_match = re.search(r"LIMIT\s+(\d+)", sql_upper) if limit_match: limit_val = int(limit_match.group(1)) if limit_val > self.MAX_RESULT_LIMIT: return False, f"LIMIT trop élevé ({limit_val} > {self.MAX_RESULT_LIMIT})" # 5. Vérifier les sous-requêtes imbriquées if sql_upper.count("SELECT") > 3: return False, "Trop de sous-requêtes imbriquées" return True, "Requête validée" validator = SQLValidator() is_valid, reason = validator.validate(sql) if not is_valid: raise SecurityError(f"Requête rejetée : {reason}")
2. Utilisateur base de données en lecture seule
DEVELOPERsql-- Créer un utilisateur en lecture seule CREATE USER rag_readonly WITH PASSWORD 'strong_password'; GRANT CONNECT ON DATABASE ecommerce TO rag_readonly; GRANT USAGE ON SCHEMA public TO rag_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA public TO rag_readonly; -- Exclure les tables sensibles REVOKE SELECT ON users FROM rag_readonly; REVOKE SELECT ON payment_methods FROM rag_readonly; REVOKE SELECT ON api_keys FROM rag_readonly;
3. Masquage des données sensibles
DEVELOPERpythonclass DataMasker: """Masque les données sensibles dans les résultats.""" PATTERNS = { "email": (r"[\w.-]+@[\w.-]+\.\w+", "***@***.***"), "phone": (r"\+?\d{10,15}", "**********"), "iban": (r"[A-Z]{2}\d{2}[A-Z0-9]{11,30}", "****"), "card": (r"\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}", "****-****-****-****"), } def mask(self, text: str) -> str: """Masque les données sensibles dans le texte.""" for pattern_name, (pattern, replacement) in self.PATTERNS.items(): text = re.sub(pattern, replacement, text) return text
Optimisation des performances
| Technique | Impact | Complexité |
|---|---|---|
| Cache des requêtes SQL identiques | -60% latence | Faible |
| Index sur les colonnes fréquemment filtrées | -70% temps exécution | Faible |
| Vues matérialisées pour les agrégations courantes | -80% temps exécution | Moyenne |
| Few-shot examples en RAG | +15% précision SQL | Moyenne |
| Schéma annoté (descriptions de colonnes) | +10% précision SQL | Faible |
| Fine-tuning du modèle sur votre schéma | +20% précision SQL | Élevée |
FAQ
Le Text-to-SQL est-il fiable pour la production ?
Avec les bons garde-fous, oui. Les modèles actuels atteignent 80-85% de précision avec du few-shot. Les 15-20% d'erreurs sont principalement sur des requêtes complexes avec des jointures multiples. En production, ajoutez systématiquement une validation de la requête, une exécution en lecture seule, et un mécanisme de feedback pour corriger les erreurs récurrentes.
Comment gérer les schémas avec des centaines de tables ?
N'exposez jamais toutes les tables au LLM. Utilisez un retriever de schéma qui sélectionne les 3-5 tables pertinentes pour chaque question. LlamaIndex SQLTableRetrieverQueryEngine fait exactement cela. Annotez vos tables avec des descriptions claires pour aider le retriever.
Le RAG SQL peut-il gérer les requêtes analytiques complexes (GROUP BY, HAVING, sous-requêtes) ?
GPT-4o et Claude gèrent bien les GROUP BY et ORDER BY simples. Les sous-requêtes corrélées et les fonctions fenêtre (WINDOW) restent difficiles. Pour les cas complexes, créez des vues matérialisées qui simplifient les requêtes, ou utilisez des exemples few-shot spécifiques à votre domaine.
Comment protéger les données sensibles ?
Triple protection : (1) utilisateur base de données en lecture seule sans accès aux tables sensibles, (2) validation de la requête SQL avant exécution (pas de UNION, pas de tables interdites), (3) masquage des données sensibles dans les résultats (emails, téléphones, IBAN). Ne faites jamais confiance au LLM pour respecter les consignes de confidentialité.
Peut-on combiner RAG SQL et RAG documentaire dans le même chatbot ?
Oui, c'est même recommandé. L'approche hybride permet de répondre à des questions comme "Quel est notre CA du mois et comment l'améliorer ?" en combinant les données SQL (CA réel) et la documentation (stratégies d'amélioration). Le classificateur de requête route vers le bon pipeline, ou combine les deux pour les questions mixtes.
Conclusion
Le RAG sur données SQL ouvre un monde de possibilités en rendant les bases de données accessibles en langage naturel. C'est la pièce manquante pour les entreprises dont les données les plus précieuses vivent dans PostgreSQL, MySQL ou SQL Server.
Points clés :
- L'approche RAG-augmented (few-shot + exemples similaires) offre le meilleur rapport précision/effort
- La sécurité est non-négociable : lecture seule, validation, masquage
- L'approche hybride (SQL + documents) couvre tous les types de questions
- Le schéma annoté est le facteur le plus important pour la précision
- Les vues matérialisées simplifient les requêtes complexes
Ailog permet de connecter vos bases de données à vos chatbots pour des réponses précises et en temps réel. Essayez gratuitement et donnez à vos utilisateurs le pouvoir d'interroger vos données sans connaître le SQL.
Ressources
- LangChain SQL Agent - Documentation officielle
- LlamaIndex NLSQLTableQueryEngine - Guide LlamaIndex
- Spider Dataset - Benchmark Text-to-SQL
- Vanna.AI - Framework Text-to-SQL open source
- Knowledge Base Entreprise - Construire une base de connaissances
- RAG E-commerce - RAG pour le e-commerce
Tags
Articles connexes
Fondamentaux du Retrieval : Comment fonctionne la recherche RAG
Maîtrisez les bases du retrieval dans les systèmes RAG : embeddings, recherche vectorielle, chunking et indexation pour des résultats pertinents.
GraphRAG : La Révolution qui Rend le RAG Traditionnel Obsolète
Découvrez GraphRAG de Microsoft : knowledge graphs + recherche vectorielle pour mieux répondre aux questions multi-hop et globales. Architecture, comparaison et implémentation complète.
RAG Adaptatif : L'IA qui Choisit Automatiquement la Meilleure Stratégie
Implémentez le RAG adaptatif pour router intelligemment entre no-retrieval, single-step et multi-step retrieval. Une précision proche du multi-étapes avec bien moins d'appels de recherche.