JDBC, ODBC, REST and GraphQL
Why do row cursors break on large results, and who absorbs the mismatch in REST versus GraphQL?
JDBC and ODBC
JDBC (Java) and ODBC (a C-based standard) are standard database driver interfaces. They exist because of an N-by-M problem, not a data problem. Every client tool against every engine would be a combinatorial explosion of integrations. A standard interface in the middle turns N × M into N + M. (For example, 20 tools and 15 engines: 20 × 15 = 300 integrations without a standard, 20 + 15 = 35 with one.) This is the constructive form of Principle B: a shared standard is valuable because many parties adopt it.
What they force: the cursor
They impose a cursor abstraction: connect, prepare, execute, fetch (row by row or in small batches), close. This shape is not neutral:
- It is row-oriented, so a column-oriented engine must transpose its results to fit.
- It is stateful: the connection holds session context (transaction, temporary objects, settings). Losing the connection loses that context.
- It is synchronous: a slow client puts back-pressure on the server, which keeps holding the result.
The result everyone eventually meets: this interface is excellent for thousands of small results and structurally bad for one huge result.
Result egress: only three ways out
There are exactly three ways a large result can leave an engine:
| Way | How it works | Cost behaviour |
|---|---|---|
| Through the cursor | Rows are fetched through the session | Per-row overhead; holds a session; client pace sets the speed |
| Columnar RPC | Batches of columns are sent intact from end to end | Less per-row overhead; still a live connection |
| Unload | The engine writes files to shared storage (such as object storage) and gives the client a pointer | Cost does not depend on how fast the client can read |
Unload is the only one whose cost does not scale with the client's ability to keep up. It decouples production from consumption by putting durable storage between them. Note what that is: buffering, reappearing at the boundary because the two parties cannot be made to run at the same speed. The mismatch is not removed. It is absorbed by a third medium.
-- Typical shape of an unload (syntax differs between engines):
COPY (SELECT * FROM shipments WHERE ship_date >= '2026-01-01')
TO 's3://harborline-exports/2026/' (FORMAT PARQUET);
-- The client then receives a list of files and reads them in parallel.In one sentence: a row cursor is a contract to go at the client's speed, which is fine until the result is bigger than the client's patience.
REST and GraphQL
Here the constraint is different: the callee does not know who the callers are or how many there will be. Under that constraint, statelessness stops being a style preference and becomes the only way to survive. If no request depends on an earlier one:
- any server can answer any request,
- servers can be added and removed freely,
- a dead connection costs only a retry.
Every scaling property that people attribute to REST comes from that one choice.
The price is paid twice
- Context is re-sent on every request. Authentication, identity and position travel again and again.
- Statelessness plus a fixed response shape means the caller often needs several requests to build one answer. When the caller is far away, latency multiplies by the number of round trips.
Under-fetching (not enough data, so more calls) and over-fetching (too much data per call) are two faces of one fact: the callee chose the shape, and the callers' needs vary.
GraphQL: let the caller choose the shape
GraphQL reverses exactly that:
{
shipment(id: 42) {
status
carrier { name }
port { code }
customer { name }
}
}The gain is one round trip for any need. The cost: the server can no longer predict what it will be asked, so the cost of one request is unbounded and unpredictable. Unpredictable cost per request ruins capacity planning (how many servers you need) and the isolation between tenants (one customer's heavy query slows everyone).
So every mature GraphQL service grows the same defences: depth limits, complexity budgets, cost estimation, allow-listed queries. The last one is the tell: it ends by fixing the set of query shapes again, which is to say it re-creates the endpoint. This is the idea that variance costs more than volume, as an interface property: a service can handle a lot of load of a known shape but not a little load of unknown shapes.
In one sentence: whoever chooses the response shape absorbs the mismatch. The server pays in round trips, or the client pays in unpredictable server cost.