Workflow · Data
Build a SQL / Data Analyst Copilot
A copilot that turns plain-English questions into validated SQL, runs read-only queries, and returns charts plus plain-language answers.
Problem
Data teams drown in 'quick question' Slack requests. Analysts repeat the same joins. Business users wait days for SQL they could've scoped in a sentence. Self-serve BI helps but non-analysts still bounce off metric definitions and table names.
Final output
An internal web app: analyst types a question, sees generated SQL (editable), runs against read-only replica, gets results table + auto-selected chart + narrative summary. Every query logged with user, SQL, and row count.
Architecture
User question (natural language)
→ Retrieve schema context (relevant tables/columns + metric definitions)
→ Retrieve similar past queries (few-shot)
→ SQL generation LLM → SELECT-only SQL
→ Validation layer (sqlglot parse, table allowlist, row limit injection)
→ Execute on read-only replica / warehouse role
→ Result profiler (row count, column types, sample stats)
→ Insight LLM → 2–3 sentence answer + chart spec
→ Render table + chart in UIStep-by-step
- 01
Schema catalog
Export table/column descriptions, PK/FK relationships, and approved metric definitions (YAML). Refresh nightly from information_schema + your data catalog.
- 02
Few-shot query library
Collect 20–50 canonical queries analysts trust — revenue by month, churn cohort, activation funnel. Embed and retrieve top 3 by question similarity.
- 03
SQL generation
LLM with schema slice + few-shots. Require SELECT-only. Prefer CTEs over nested subqueries. Output: sql, explanation, tables_used, confidence.
- 04
Validation gate
Parse with sqlglot. Reject: INSERT/UPDATE/DELETE, multi-statement, tables outside allowlist. Inject LIMIT 1000 if missing. Block SELECT * on wide tables.
- 05
Read-only execution
Dedicated DB user with SELECT on approved schemas only. 30s query timeout. Log query hash to skip duplicate runs.
- 06
Insight + visualization
Profiler summarizes result shape. LLM picks chart type (bar/line/scatter/table) and writes 2-sentence insight. Flag low row counts and NULL heavy columns.
What you'll build
An internal copilot where analysts ask data questions in plain English, review generated SQL, run it safely against a read-only warehouse, and get answers with charts — without filing a ticket.
The safety model
Question → Generate SQL → Validate → Execute (read-only) → Explain
↑ ↑
schema RAG sqlglot + allowlist
Non-negotiable: the database role can SELECT and nothing else. Validation runs before execution, not after.
Step 1 — Schema catalog
Export metadata your analysts already trust:
tables:
- name: analytics.fct_orders
description: One row per completed order. Partitioned by order_date.
columns:
- name: order_id
type: STRING
description: Primary key
- name: mrr_usd
type: FLOAT
description: Monthly recurring revenue attributed to this order
Pair with metric definitions:
metrics:
- name: net_revenue
sql: "SUM(mrr_usd) - SUM(refund_usd)"
tables: [analytics.fct_orders]
Refresh nightly. Stale schemas produce confident wrong SQL.
Step 2 — Few-shot retrieval
Embed your library of approved queries. When a user asks "monthly churn by plan," retrieve:
churn_cohort_monthly.sqlsubscription_cancellations_by_tier.sql
Few-shots teach join paths the schema alone doesn't convey. See Embeddings Explained and RAG Explained.
Step 3 — Generate SQL
Retrieve only relevant tables — embedding search over table descriptions, not the full warehouse.
The model outputs:
sql— the queryexplanation— what it doesassumptions— "assuming churn = cancelled within 30 days"confidence— flag low-confidence for human review
Show SQL in the UI before running. Let analysts edit inline.
Step 4 — Validation gate
Never skip this layer:
| Check | Action |
|---|---|
| Parse SQL | sqlglot — reject unparseable |
| Statement type | SELECT only |
| Table allowlist | Reject unknown tables |
| Row limit | Inject LIMIT 1000 if absent |
| Timeout | Kill at 30 seconds |
| Cost estimate | Optional — BigQuery bytes scanned |
Rejected queries return a clear error — "table raw_events not in allowlist" — not a warehouse exception.
Step 5 — Execute read-only
-- DB role: analyst_copilot_ro
GRANT SELECT ON SCHEMA analytics TO analyst_copilot_ro;
-- No INSERT, UPDATE, DELETE, CREATE, DROP. Ever.
Run against a replica when possible. Log: user_id, question, sql, row_count, duration_ms, thumbs.
Step 6 — Insight + chart
Profile the result:
- Row count, null rates, numeric ranges
- Sample 10 rows for the insight model
The model returns a headline ("Enterprise churn spiked to 4.2% in May"), chart spec, and caveats.
Render with Streamlit, Plotly, or your internal chart library.
Context engineering matters
The quality ceiling is retrieval, not model size:
- Table descriptions beat raw
information_schemadumps. - Metric definitions prevent "revenue" meaning three different things.
- Approved query few-shots teach join paths.
See Context Engineering Explained for the broader pattern.
MCP alternative
If your team lives in Claude Desktop or Cursor, wrap this as MCP tools:
list_tables— searchable schemagenerate_sql— question → validated SQLrun_query— read-only execution
One implementation, multiple clients. Start with the MCP server from your API workflow and point tools at your warehouse API layer.
Rollout plan
- Week 1 — 3 analysts, 10 approved tables, manual schema.
- Week 2 — Add validation metrics, thumbs up/down.
- Week 4 — Expand allowlist if run-without-edit > 60%.
- Week 8 — Open to PMs with narrower table scope.
Extensions
- Query explain — "why is this slow?" with
EXPLAINoutput interpretation. - Semantic layer — dbt metrics as the source of truth instead of raw tables.
- Scheduled questions — "email me MRR every Monday" from saved queries.
- Anomaly mode — compare this week's result to trailing 4-week average automatically.
The copilot doesn't replace analysts — it absorbs the repetitive 60% so they focus on the questions that need judgment.
Prompt examples
Copy any of these, replace the placeholders, run.
SQL generation system prompt
You are a {{warehouse}} SQL analyst for {{company}}.
Rules:
- SELECT statements only. No DDL, DML, or multiple statements.
- Use fully qualified table names: {{schema}}.table_name
- Prefer explicit column lists — never SELECT * on tables with >20 columns.
- Join using documented FK relationships only.
- Add WHERE filters for partition keys when tables are date-partitioned.
- If the question is ambiguous, state assumptions in "assumptions" field.
- If impossible with available schema, say so — do not invent tables.
Schema (relevant tables):
{{schema_context}}
Similar approved queries:
{{few_shot_queries}}
Output JSON:
{
"sql": string,
"explanation": "one paragraph",
"tables_used": string[],
"assumptions": string[],
"confidence": 0-1
}
Question: {{user_question}}Result insight
Summarize these query results for a business stakeholder.
Question: {{user_question}}
SQL: {{sql}}
Row count: {{row_count}}
Column summary: {{column_stats}}
Sample rows (max 10): {{sample_rows}}
Output JSON:
{
"headline": "one sentence answer",
"insight": "2-3 sentences with specific numbers",
"caveats": string[],
"chart": { "type": "bar"|"line"|"scatter"|"table", "x": string, "y": string }
}
Rules:
- Quote exact numbers from the sample — do not extrapolate beyond returned rows.
- If row_count < 5, warn about small sample.
- If the question isn't answered by these columns, say what's missing.Cost estimate
| Line item | Approx. cost |
|---|---|
| SQL generation (Sonnet, per question) | $0.06 |
| Insight + chart spec (Haiku) | $0.01 |
| Warehouse compute (typical ad-hoc) | $0.02 |
| Per 1,000 questions | $90.00 |
| Streamlit / internal hosting | existing infra |
| Total per run (approx.) | ~$90.09 |
Costs depend on model choice, content length, and how aggressively you cache.
Optimizations
- Retrieve only 5–8 relevant tables via embedding search — not the full 200-table schema.
- Cache results by SQL hash for 1 hour — analysts re-run the same question often.
- Pre-approve 'golden queries' as templates analysts pick instead of free-form gen.
- Use a cheaper model for SQL gen on simple aggregations; escalate for multi-join questions.
- Nightly sync schema metadata — stale column names are the
Common mistakes
- Write access on the DB role — one hallucinated DELETE is a career event. Read-only only.
- Stuffing the entire schema into context — confuses the model and burns tokens.
- No query timeout — a bad join bricks your warehouse slot.
- Skipping the validation layer — LLMs emit plausible but wrong SQL constantly.
- Letting users skip SQL review — show SQL by default until trust is established.
Frequently asked questions
Related on AIKnowHub
Concept
Context Engineering Explained
Prompt engineering is one input. Context engineering is everything around it — what you fetch, what you trim, what you remember, what you let the model see.
Concept
Embeddings Explained
Embeddings turn text into numbers that capture meaning. They power search, RAG, recommendations, and clustering — here's how.
Concept
MCP Explained
The Model Context Protocol is the USB-C of AI tooling. Here's what it is, why it matters, and how to think about it.
Workflow
Build an MCP Server from Your API
Wrap your REST or GraphQL API as an MCP server so Claude Desktop, Cursor, and any MCP client can call it — write once, use everywhere.
Tool Guide
GitHub Copilot Guide
The definitive guide to GitHub Copilot — IDE completions, Copilot Chat, agent mode, CLI, PR features, enterprise policy, pricing, and how it compares to Cursor and Claude Code.
Prompt
Extract Structured Data from PDF
Pull specific structured fields out of an unstructured PDF or document.