Raw SQL or Curated Queries? Giving an Agent Your Database

A two-line tool description can hand an agent unbounded access to your data and your query planner. The guardrails matter more than the choice — and one cheap planner call prevents most of the damage.

The fastest way to make an analytics agent useful is to hand it SQL. It's also the fastest way to give it an unbounded surface over your data, your query planner, and your database's stability — in one tool with a two-line description.

Three shapes of database access are worth considering, and the choice between them is less important than the guardrails all three need.

The three options

Raw SQL on the real schema. Maximum flexibility. The agent must know your schema, which means the schema goes in the context — often a lot of tokens — and every query it composes is arbitrary.

Raw SQL on curated views. Same interface, narrower surface. Views expose the columns and rows you're willing to have queried, with joins already resolved and sensitive fields absent.

Parameterized query tools. No SQL at all. revenue_by_month(from, to, segment) returns a fixed shape. The agent chooses which question to ask, never how to ask it.

Where each one breaks

Raw on the real schema fails on schema knowledge before it fails on anything else. A model working from dumped DDL gets joins subtly wrong — the right columns, the wrong relationship — and produces a plausible number nobody can check. Add unconstrained scans, no row limits, and columns nobody meant to expose, and the failure modes range from wrong answers to a query that pins the database.

Curated views solve the exposure and the join-correctness problems, since the hard joins are baked in. What remains is that arbitrary SQL over a view is still arbitrary SQL — an expensive aggregate is still expensive. And views need maintaining as the schema evolves, which is real recurring work.

Parameterized tools are safe, fast, and cheap. They fail at the edge of what you anticipated: the first question outside the parameter space is unanswerable, and the agent has no way to get closer. ⚠️ Watch for the agent contorting available parameters to approximate a question they don't cover — that produces confidently wrong answers with no error anywhere.

Choosing

  • A known, repeated question set — dashboards, standard reports, high volume → parameterized tools. Cheapest and most predictable.
  • Genuine exploration by people who'd otherwise write SQL themselves → SQL over curated views.
  • Raw SQL on the primary with a broad role → not a configuration to run, at any level of prompt sophistication.

✅ The guardrails, which matter more than the choice

Whatever shape you pick, the same protections apply, and they belong in code:

A read-only role on a replica. Not "the agent is instructed not to write." A role that cannot write, on a database whose load doesn't affect production traffic.

Server-side statement timeout and row limit. Enforced by the database, not appended to the query by your handler — an agent-composed query can override a LIMIT you concatenated, and cannot override a session setting.

Parse and reject, don't prompt. Run the SQL through a parser and refuse anything that isn't a single SELECT. DDL, DML, multiple statements, and transaction control all fail at the gate, regardless of what any text in the context asked for.

💡 Cost-check with EXPLAIN before executing. The technique that prevents most database incidents here:

plan = db.explain(sql)
if plan.estimated_cost > COST_CEILING:
    return {error: "Query too expensive (est. cost 4.2M, ceiling 500K). "
                   "Add a date filter or narrow the columns.",
            retryable: true}

The agent gets an actionable message and reformulates. Nothing expensive ever runs. This costs one cheap planner call per query and is the single highest-value guard in the list.

Honest result envelopes. Return rows, row_count, total_matched, and truncated. A result silently cut at the row limit and treated as the complete set is the same silent-partial failure that turns up everywhere else in agent design.

Give it the schema it needs, not the schema you have

For any SQL-shaped option, what you put in the context decides accuracy more than the model does. A dumped DDL is the worst version — verbose, and it says nothing about meaning.

A curated schema description beats it substantially:

orders
  id            uuid
  placed_at     timestamptz   -- UTC; use date_trunc for monthly grouping
  status        text          -- pending|shipped|delivered|cancelled
  total_cents   bigint        -- cents, not dollars
  customer_id   uuid          -- join: customers.id
  
  note: cancelled orders retain their total; exclude them from revenue

Units, enumerated values, join hints, and the business rules that a correct query has to encode. → That last line is the kind of thing that separates a right answer from a plausible one, and it exists nowhere in the DDL.

🔍 What to log

Every query, its estimated and actual cost, rows returned, duration — and every rejected query with the reason. The rejection log is the more useful of the two: it shows what the agent tried to ask and couldn't, which is a direct list of the views or parameters worth adding.

The hybrid that holds up

Parameterized tools for the questions you know are asked, covering most volume at low cost and zero risk. SQL over curated views as the escape hatch, behind the full guardrail set, for the long tail.

The agent takes the cheap path when it fits and has somewhere to go when it doesn't — which is the same shape that works for tool design generally.

The takeaway

The interesting decision isn't SQL versus not-SQL; it's whether the surface is bounded by something the model can't reach. Read-only role, replica, server-side timeouts and row caps, a parser that rejects anything but a SELECT, and an EXPLAIN cost ceiling before execution. Then write the schema description for a reader who needs to know what the columns mean — and log what the agent asked for and was refused, because that's your roadmap.

Keep reading

Similar posts

Matched on shared tags and category — the more bars, the stronger the overlap with what you just read.