Strip the vocabulary away and an agent’s retrieval pipeline is a query plan: access paths, joins, sorts, limits — hand-written in Python and executed by an LLM one tool call at a time. Databases solved this problem fifty years ago. This post is about why the industry keeps rediscovering it, what the hand-rolled version actually costs, and what it looks like to push the plan down into an engine built to run it.
TL;DR: The retrieval and context-assembly logic inside an agent framework — which store to hit, in what order, how to merge and rank the results — is a query plan written by hand and executed by an LLM one tool call at a time. This reinvents the navigational database, the architecture the relational model replaced in the 1970s, minus everything that made databases trustworthy: no consistent snapshot across reads, no cost model, no automatic re-optimization as data changes. The fix isn’t a smarter framework. It’s giving the agent a declarative surface — one query that spans point lookups, aggregates, and vector similarity, planned by an actual planner against one internally coherent snapshot — and letting the framework do the job it’s genuinely good at: control flow.
Here is a retrieval step from a production agent, the kind every framework tutorial teaches you to build. Search the vector store for similar cases. Take the top twenty IDs and look each one up in Postgres. Fetch the last-hour transaction counter from Redis. Merge the results in Python, dedupe them, rerank them, and truncate the list to fit the context window.
Now show that sequence to someone who builds databases for a living. They will recognize it immediately, because they have a name for every line: an index scan feeding a nested-loop join, a point lookup against a second table, a sort, and a `LIMIT`. That’s not an orchestration pattern. That’s a physical query plan — hand-written, frozen into application code, and executed by a large language model at one tool call per operator.
The agent ecosystem has invented a rich vocabulary for this work: retrieval pipelines, context engineering, tool orchestration, memory management. The vocabulary obscures what the work actually is. It is query planning. And the reason it feels hard is that query planning is hard — hard enough that the database community spent fifty years building machinery to do it automatically, machinery your framework is quietly reimplementing without the parts that make it correct.
The Retrieval Code You Wrote Is a Physical Plan
A query planner is the component of a database that turns a declarative statement of what data you need into an executable plan for how to get it — choosing access paths, join order, and algorithms using statistics about the data, and executing the result against a single consistent snapshot. It exists because hand-writing data-access plans in application code was tried, at industrial scale, for two decades — and lost.
Every mainstream agent framework asks the developer (or the LLM, at runtime) to make exactly the decisions a planner makes. The correspondence isn’t loose analogy. It’s one-to-one:
Read the right-hand column top to bottom and the claim in this post’s title stops sounding polemical. The retrieval half of your agent graph is a plan tree. The difference is who wrote it, what executes it, and what guarantees it carries — and on all three counts, the hand-rolled version is the one from 1968.
This is a different failure than the one orchestration frameworks are usually criticized for. LangGraph, CrewAI, and their peers coordinate control flow — which agent runs next, how handoffs happen — and the standard critique is that they don’t manage shared state. True, but this post is about the other half: even within a single agent’s single step, the data access itself has been pushed up into imperative application code, where the database community learned the hard way it doesn’t belong.
What your framework calls it
What a database calls it
Tool call to a store
Table or index access
Vector search, top-k
Index scan with a distance operator
Looking up each result ID
Nested-loop join (the N+1 kind)
Merging results in Python
Hash join, minus the hash table
Reranking
`ORDER BY`
Truncating to the context budget
`LIMIT`
A fixed RAG pipeline
A frozen physical plan
The retrieval subgraph in your DAG
A plan tree
A Redis layer for hot values
A materialized view, maintained by hand
We Have Been Here Before
The pattern isn’t new. It’s the third or fourth time the industry has run this exact loop.
In the 1960s, databases were navigational. IMS and CODASYL systems stored records connected by physical pointers, and the application programmer wrote the traversal: start at this record, follow this link, check this field, follow the next link. The programmer chose the access path. It worked — and it rotted. When the data grew or the access pattern shifted, every hand-written traversal in every program had to be found and rewritten. Codd’s 1970 relational model won not because tables were prettier than pointers, but because declarative beat imperative: state what you want, and let a planner — armed with statistics about the actual data — decide how to get it, again and again, differently as the data changes.
In the 2000s we forgot, and wrote MapReduce jobs by hand: imperative plans over distributed data, each one lovingly tuned, each one rotting as the data drifted. Within a few years Hive, Spark SQL, and Presto had pushed that work back under a declarative surface, and today almost nobody hand-writes the plan.
In the 2010s we forgot again, and hand-wired stream topologies — this operator keys by user, that one windows by hour, this cache holds the running count. Streaming SQL and incrementally maintained views are that generation’s pushdown, still in progress.
Each cycle has the same shape. A new workload arrives; there’s no engine for it yet, so engineers do the planning by hand in application code; the pain compounds as the hand-written plans multiply and rot; then the planning gets pushed down into a declarative engine, and everyone quietly agrees the imperative era was a mistake. Agent frameworks are at step two of this cycle. The retrieval graphs being written today are this generation’s navigational code — and “context engineering,” in its current form, is partly a discipline built around doing a planner’s job manually.
What the Denial Costs
If this were only an aesthetic complaint — “you’re reinventing something old” — it wouldn’t matter. Engineers reinvent things; sometimes the new context justifies it. The problem is that the hand-rolled planner is missing the three properties that made real planners trustworthy, and each absence has a concrete production cost.
No snapshot. A database join, whatever its physical form, reads from a single consistent view of the data. Your agent’s hand-rolled join does not. The vector store reflects its last reindex. The Redis counter is a few seconds behind its pipeline. The Postgres read is current as of its moment, not the others’. The Python merge then joins rows from different versions of reality and hands the result to the model as if it were one coherent world. This is the retrieval gap, and for agents it’s worse than for dashboards, because the agent acts on the merged result — and under concurrency, two agent steps reading the same fragmented stores at slightly different moments will confidently disagree with each other.
The world’s most expensive executor. In a database, dispatching the next plan operator costs nanoseconds. In an agent graph, when the LLM drives retrieval, dispatching the next operator is an inference round trip: the model reads the tool result, reasons about it, and emits the next call — hundreds of milliseconds and a growing token bill per operator. A five-step retrieval chain executes in seconds and dollars what an engine executes in milliseconds and microcents. And the shape is usually worse than five steps, because the hand-rolled nested-loop join is the N+1 kind: top twenty IDs means twenty lookups. You would reject this executor for any other data workload in your company. It became acceptable for agents only because the vocabulary hid what it was executing.
No cost model, no re-optimization. A planner chooses between an index seek and a scan using statistics about the actual data, and chooses differently next quarter when the distribution shifts. Your retrieval graph has no statistics and no cost model. The developer guessed an access path at design time, or the LLM guesses one at runtime by vibes — and neither guess updates itself when the data grows tenfold or the hot keys move. Hand-written plans don’t just start wrong; they rot, silently, in exactly the way navigational code rotted. Nobody re-tunes the retrieval subgraph, because nobody thinks of it as a plan that needs re-tuning.
Derived state maintained by hand. The counters, aggregates, and velocity features an agent needs are the same derived context every decision system needs, and in the hand-rolled world each one is a little pipeline — a stream job feeding a cache, each lagging by its own amount, each a snapshot of a different moment. That’s the preparation gap stacked on top of the retrieval gap: not only do your reads span systems, but the pre-computed values inside those systems were computed at different times from different event sets.
What Pushdown Looks Like for an Agent
The resolution of every previous cycle wasn’t a better way to hand-write plans. It was a declarative surface with an engine underneath. For agents, that means the step where context gets assembled should be one query against one engine, not a chain of tool calls the model narrates its way through.
Here is the hand-rolled version — the plan from this post’s opening, as it appears in a thousand codebases:
python
profile = postgres.get_customer(cid) # point lookup
exposure = redis.get(f"exposure:1h:{cid}") # hand-maintained counter
hits = vectorstore.search(query_embedding, k=20) # semantic scan
cases = [postgres.get_case(h.id) for h in hits] # N+1 nested-loop join
context = rerank(merge(profile, exposure, cases))[:BUDGET] # sort + limit
Five operators, four systems, four freshness levels, executed step by step with the model in the loop. Here is the same plan, expressed declaratively against a substrate that can run all of it:
sql
SELECT
c.credit_limit,
c.risk_tier,
-- derived state, computed on demand against committed data
(SELECT COALESCE(SUM(amount), 0)
FROM authorizations a
WHERE a.customer_id = c.id
AND a.created_at > now() - interval '1 hour') AS exposure_1h,
sim.case_summary,
sim.outcome
FROM customers c,
LATERAL (
SELECT case_summary, outcome
FROM fraud_cases
ORDER BY embedding <=> $2 -- vector similarity, same plan
LIMIT 5
) sim
WHERE c.id = $1;
One statement. The point lookup, the aggregate, and the semantic search are operators in a single plan tree, chosen and ordered by a planner with statistics, executed against one internally coherent snapshot — every part of the answer reflects the same set of ingested events, so the join can’t straddle versions of reality. One round trip replaces five tool calls, which means the model spends its inference budget on the decision instead of on narrating data plumbing.
This is the Context Lake pattern, and the properties that make it work for agents are worth naming precisely:
Hybrid tables serve point lookups and analytical scans from the same store, so “fetch this customer” and “aggregate their last hour” don’t live in different systems. Many derived signals — sums, counts, velocities — can be computed on demand at query time against committed data, which removes the hand-maintained cache entirely for those cases.
Incrementally maintained views cover the derived state that’s too hot to compute per-query. They converge in sub-seconds and are maintained inside the engine — deliberately asynchronous to stay out of the write path, but maintained from the same event stream as everything else, not by a fleet of per-team pipelines each lagging differently.
Semantic operators extend the same declarative surface to LLM-computed signals — classify, extract, summarize as query operators — so even the “call the model on each row” step becomes something the planner schedules rather than something your application loops over.
Note what this does not require: it does not require retrieval to be static. Declarative never meant pre-planned — the agent generates a different query every step, exactly as it generated a different tool-call chain every step. What changes is the division of labor. The model decides what it needs. The planner decides how to get it. That division — intent above, access paths below — is the entire lesson of the relational era, and it maps onto agents without modification.
Your Framework Still Has a Job
None of this means the agent framework disappears. It means the framework stops moonlighting.
Control flow is real work: which step runs next, when to loop and when to stop, where the human approval gates sit, what the agent is allowed to do, how a multi-agent handoff carries state. That’s coordination, and frameworks are genuinely good at it. The same is true of tool boundaries for actions — writing to external systems, sending the email, moving the money. Those are irreducibly imperative, and they raise their own transactional questions that deserve their own machinery.
The denial is specifically about data access. So the test is simple: look at each node in your agent graph and ask whether it exists to fetch, filter, join, aggregate, or rank data. If it does, it’s a plan operator wearing an agent costume — and every one you push down into the engine makes the remaining graph smaller, faster, cheaper, and honest about what it is. The frameworks that thrive in the next cycle will be the ones that embrace this: thin control-flow layers over a declarative context surface, not imperative data-plumbing runtimes with a planner-shaped hole in the middle.
The database industry has run this experiment for you, at scale, over fifty years, with the same result every time. Declarative wins. Plans belong to planners. The only question is how many hand-written retrieval graphs your team maintains before the pushdown happens anyway.
Frequently Asked Questions
Key Takeaways
The retrieval logic in your agent graph is a physical query plan — tool calls are table accesses, the Python merge is a join, the reranker is a sort, the context budget is a `LIMIT` — hand-written and executed by an LLM one operator at a time.
This is the navigational-database era replayed. Hand-chosen access paths in application code lost to declarative queries and planners in the 1970s, again with MapReduce in the 2000s, again with hand-wired stream topologies in the 2010s. Agent frameworks are the current generation’s imperative phase.
The hand-rolled version is missing what made planners trustworthy: no consistent snapshot across reads (joins straddle versions of reality), no cost model (access paths chosen by guesswork and never re-tuned), and the most expensive plan executor ever built — an inference round trip per operator.
Pushdown means one query against one engine: point lookups, aggregates, vector similarity, and even LLM-computed signals as operators in a single plan, executed against one internally coherent snapshot. The model decides what it needs; the planner decides how to get it.
Frameworks keep the job they’re good at — control flow, gates, actions. The test for every node in your graph: if it exists to fetch, join, filter, aggregate, or rank data, it’s a plan operator wearing an agent costume. Push it down.
AI AgentsQuery PlanningAgent FrameworksDatabasesContext LakeArchitecture