🤖 Agents IA

agent-database-query-subagent

Sous-agent spécialisé dans les requêtes base de données — SQL generation, exécution et analyse de résultats.

⚡ Installation & lancement en 1 commande

Copiez-collez dans votre terminal : le skill s'installe dans ~/.claude/skills et Claude Code se lance directement dessus.

macOS / Linux
curl -fsSL https://raw.githubusercontent.com/khalilbenaz/claude-skills-collection/main/install.sh | sh -s -- agent-database-query-subagent --launch
Windows (PowerShell)
iex "& { $(iwr -useb https://raw.githubusercontent.com/khalilbenaz/claude-skills-collection/main/install.ps1) } agent-database-query-subagent -Launch"

🚀 Déjà installé ?

claude "/agent-database-query-subagent"

Ou tapez /agent-database-query-subagent dans une session Claude Code, ou décrivez simplement votre besoin — le skill se déclenche automatiquement via le skill-router.

🔑 Déclencheurs automatiques

Le skill s'active automatiquement quand votre demande contient :

sous-agent DBdatabase agentSQL agentagent base de donnéesquery agentagent qui requêtetext-to-SQL agentNL2SQL

📦 Installation manuelle

git clone https://github.com/khalilbenaz/claude-skills-collection.git cp -r claude-skills-collection/skills/agent-database-query-subagent ~/.claude/skills/

Payload du plugin : skills/agent-database-query-subagent · source éditable : agent-skills/database-query-subagent

📖 Manuel

Database Query Sub-Agent

Cas d'usage

Déléguer à ce sous-agent toute interrogation DB depuis un agent parent : NL2SQL (question → SQL), analyse de données complexes, BI assistée par IA, drill-down conversationnel multi-tour. Ne pas utiliser pour des mutations — ce sous-agent est en lecture seule par défaut.


Workflow (10 étapes)

1. Validation des inputs

Recevoir et valider avant toute génération de SQL :

required = ["question", "connection.db_type", "connection.host", "connection.database"]
# Tester la connexion : ping + SELECT 1
# Si échec → retourner immédiatement errors=[{"type": "connection_error", ...}]

Defaults : read_only=True, max_rows=1000, timeout_s=30.


2. Découverte du schéma

Si schema non fourni, l'inférer automatiquement :

-- PostgreSQL / MySQL
SELECT table_name, column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position;

-- SQLite
SELECT name, sql FROM sqlite_master WHERE type='table';

-- SQL Server
SELECT t.name, c.name, tp.name, c.is_nullable
FROM sys.tables t
JOIN sys.columns c ON t.object_id = c.object_id
JOIN sys.types tp ON c.user_type_id = tp.user_type_id;

Construire un DDL simplifié (max ~2 000 tokens) à injecter dans le prompt de génération.


3. NL → SQL (génération)

Prompt structuré :

Schéma DDL :
<DDL des tables pertinentes uniquement>

Question : <question utilisateur>
Dialecte : <db_type>
Contraintes : lecture seule, LIMIT max_rows

Règles :
- Préférer les CTEs aux sous-requêtes imbriquées
- Alias explicites sur toutes les colonnes ambiguës
- Pas de SELECT * sur tables volumineuses
- Exemples few-shot si disponibles en session_context

Critères de sélection des tables pertinentes : similarité sémantique entre la question et les noms de tables/colonnes (embedding cosine > 0.7, ou matching de mots-clés en fallback).


4. Validation avant exécution

import sqlglot

def validate_query(sql: str, db_type: str, schema: dict, read_only: bool) -> list[str]:
    errors = []
    # 1. Parse syntaxique
    try:
        parsed = sqlglot.parse_one(sql, dialect=db_type)
    except sqlglot.errors.ParseError as e:
        errors.append(f"syntax_error: {e}")
        return errors

    # 2. Vérifier colonnes et tables vs schéma
    for table in parsed.find_all(sqlglot.exp.Table):
        if table.name not in schema["tables"]:
            errors.append(f"unknown_table: {table.name}")

    # 3. Bloquer mutations si read_only
    if read_only:
        forbidden = (sqlglot.exp.Drop, sqlglot.exp.Delete,
                     sqlglot.exp.Update, sqlglot.exp.Insert,
                     sqlglot.exp.Create, sqlglot.exp.AlterTable)
        for node in parsed.walk():
            if isinstance(node, forbidden):
                errors.append(f"mutation_blocked: {type(node).__name__}")
    return errors

En cas d'erreur de validation : retourner sans exécuter, inclure la requête invalide dans errors[].query.


5. Exécution sécurisée

from sqlalchemy import create_engine, text
from sqlalchemy.pool import NullPool

engine = create_engine(conn_url, poolclass=NullPool,
                       connect_args={"connect_timeout": 5})

with engine.connect() as conn:
    # Lecture seule au niveau transaction
    conn.execute(text("SET TRANSACTION READ ONLY"))          # PostgreSQL / MySQL
    # SQL Server : utiliser un login sans droits DML

    # Timeout
    conn.execute(text(f"SET statement_timeout = {timeout_s * 1000}"))  # PostgreSQL ms

    # LIMIT automatique si absent
    if "LIMIT" not in sql.upper() and db_type != "sqlserver":
        sql += f" LIMIT {max_rows}"
    elif db_type == "sqlserver" and "TOP" not in sql.upper():
        sql = sql.replace("SELECT", f"SELECT TOP {max_rows}", 1)

    result = conn.execute(text(sql))
    rows = [dict(r) for r in result.fetchmany(max_rows + 1)]
    truncated = len(rows) > max_rows
    return rows[:max_rows], truncated

6. Traitement des résultats

Sérialisation sûre pour JSON :

import math
from decimal import Decimal
from datetime import date, datetime

def serialize_row(row: dict) -> dict:
    out = {}
    for k, v in row.items():
        if v is None:
            out[k] = None
        elif isinstance(v, (datetime, date)):
            out[k] = v.isoformat()
        elif isinstance(v, Decimal):
            out[k] = float(v)
        elif isinstance(v, float) and math.isnan(v):
            out[k] = None  # NaN non serializable JSON
        elif isinstance(v, bytes):
            out[k] = v.hex()
        else:
            out[k] = v
    return out

Calculer les stats pour colonnes numériques : {min, max, mean, std, null_count, p25, p50, p75}.


7. Analyse du plan d'exécution

Lancer EXPLAIN si row_count > 10 000 ou execution_time_s > 2 :

EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) <votre requête>;  -- PostgreSQL
EXPLAIN FORMAT=JSON <votre requête>;                       -- MySQL

Signaux d'alerte à détecter dans le plan :

Inclure dans optimization_hints uniquement si gain estimé > 50 %.


8. Dialectes SQL — différences clés

FeaturePostgreSQLMySQLSQLiteSQL ServerBigQuery
LimiteLIMIT nLIMIT nLIMIT nTOP nLIMIT n
Date nowNOW()NOW()datetime('now')GETDATE()CURRENT_TIMESTAMP
Regex~ / ~*REGEXPLIKE seulementLIKE / PATINDEXREGEXP_CONTAINS
JSON-> ->>JSON_EXTRACTjson_extract()JSON_VALUEJSON_EXTRACT_SCALAR
ILIKEouinon (insensible par défaut)nonCOLLATELOWER()
CTE récursiveoui≥ 8.0≥ 3.35ouioui

Pour MongoDB : traduire en pipeline d'agrégation ($match$group$project), pas de SQL.


9. Contexte conversationnel multi-tour

class QuerySession:
    def __init__(self):
        self.history: list[dict] = []  # [{"question", "sql", "row_count", "columns"}]
        self.last_result_columns: list[str] = []
        self.last_filter: dict = {}

    def resolve_anaphora(self, question: str) -> str:
        """Remplacer 'eux', 'les mêmes', 'parmi ceux-là' par le contexte précédent."""
        if self.history and any(p in question.lower() for p in ["eux", "ceux-là", "les mêmes"]):
            last = self.history[-1]
            return f"Parmi les résultats de '{last['question']}' ({last['sql']}), {question}"
        return question

Passer session_context (sérialisé) dans chaque call et le retourner mis à jour dans l'output.


10. Génération du rapport

Retourner systématiquement :


Interface contractuelle

Input :

{
  "question": str,           # Obligatoire — NL ou SQL direct
  "schema": dict | None,     # Optionnel — inféré si absent
  "connection": {
    "db_type": str,          # "postgresql"|"mysql"|"sqlite"|"sqlserver"|"bigquery"|"mongodb"
    "host": str,
    "port": int | None,
    "database": str,
    "username": str,
    "password": str,         # Jamais loggué
    "ssl": bool              # Défaut: True
  },
  "read_only": bool,         # Défaut: True
  "max_rows": int,           # Défaut: 1000
  "timeout_s": int,          # Défaut: 30
  "session_context": dict | None,
  "explain_results": bool    # Défaut: True
}

Output :

{
  "query": str,
  "results": list[dict],
  "row_count": int,
  "execution_time_s": float,
  "explanation": str,
  "stats": {"col": {"min": float, "max": float, "mean": float, "null_count": int}},
  "optimization_hints": list[str],
  "visualization_suggestion": str,
  "follow_up_questions": list[str],
  "session_context": dict,
  "errors": list[{"type": str, "message": str, "query": str}],
  "truncated": bool
}

Dépendances Python :

sqlalchemy>=2.0
sqlglot>=25.0
psycopg2-binary>=2.9
pymysql>=1.1
pyodbc>=5.0
pandas>=2.2
pydantic>=2.0

Garde-fous et anti-patterns

Ne jamais faire :

Pièges courants :

Bonnes pratiques 2026 :