Technical Publication
Architecture of Information SystemsArrow, ADBC & the Death of ODBC
research series

Arrow, ADBC & the Death of ODBC

1 July 2026Revision 027 min readShreyas Agarwal
Dry Read

Modern Analytical Transport: Arrow, ADBC, and the Death of ODBC

This is the article several earlier chapters were quietly building toward.

In the previous article, Backpressure: Teaching Your Gateway How to Say "No", we watched a gateway learn to say no — shedding, delaying, and rejecting ingestion before an unbounded queue could turn a legitimate spike into an outage. That article closed with a question this series raised much earlier and never actually resolved: how fast can data go back out, once something needs to read it?

Go back to Article 9 of this series, in the article that introduced the hidden cost of moving data. That article named the tax precisely: every time information crosses an execution boundary, it pays a toll in serialization CPU cycles, network bandwidth, and deserialization latency. It offered three mitigations — Apache Arrow's zero-copy in-memory format, predicate pushdown, and Parquet-style compression — and then moved on, because the series still had sockets, storage engines, and edge infrastructure left to cover. Arrow was named. It was never actually unpacked.

Historical Context

Article 9 described the tax and named the fix. It did not explain how the fix actually works, or why the tool most analysts use every day to query a database — the ODBC driver — is itself one of the biggest sources of the exact tax that article was warning about. This is where that gap closes.

The Irony ODBC Was Hiding

Open Database Connectivity has been the default bridge between analytical tools and databases for over three decades, and for most of that time it has been a genuinely good solution to a genuinely hard problem: giving any client, written in any language, a uniform way to talk to any database. That uniformity came from a specific design choice — ODBC exposes results as a cursor over rows, fetched one row (or one small batch of rows) at a time, each value individually typed and marshaled into a generic C-level representation the driver understands, then unmarshaled again into whatever structure the client application actually wants.

That sentence should sound familiar. It is, almost verbatim, the row-oriented transport path Article 9 diagrammed as the source of the serialization tax: the database serializes native tuples into a wire format, the format crosses a boundary, the client deserializes it back into objects, and only then does anything useful happen with the data.

OBSObservation

Power BI's own internal engine, VertiPaq, is columnar — it was built specifically to exploit the performance advantages of column-oriented storage that Article 9 described. And yet the standard path from a columnar database to that columnar engine still runs through an ODBC cursor: a row-by-row interface that flattens both ends' columnar structure into rows, ships it across the wire that way, and forces the columnar engine on the receiving side to rebuild the very structure that existed at the source and was thrown away in transit.

This is the specific, satisfying complication that makes this the payoff to Article 9's setup rather than a repeat of it: the lesson was never just "row-oriented transport is slow." It's that a row-shaped pipe between two column-shaped systems throws away the columnar advantage twice — once when the source serializes columns into rows to fit the driver's model, and again when the destination has to reassemble rows back into columns to fit its own engine. The tax gets paid at both ends of a trip that never needed to leave columnar format in the first place.

Apache Arrow: One Memory Layout, Not One Per Language

Apache Arrow's actual contribution is narrower and more useful than "a fast columnar format" makes it sound. Arrow specifies an exact, language-independent in-memory columnar layout — how a column of integers, strings, or timestamps is arranged in memory, down to the byte. Because the specification is shared, a process written in C++, a process written in Python, and a process written in Go can all read the same block of Arrow-formatted memory without translating it into their own native representation first.

That's the detail that actually eliminates the tax, rather than merely shrinking it. Traditional serialization crosses a boundary by converting a structure into an intermediate wire format and converting it back — two conversions, even when the format in between is efficient. Arrow's premise is that if both sides already agree on the memory layout, the "conversion" step can disappear entirely. The bytes that meant something on one side of the boundary mean the same thing, unchanged, on the other side. This is what Article 9 called zero-copy deserialization without fully explaining it: not a faster deserialization step, but the removal of the deserialization step.

text
ODBC path (row-oriented, two serialization boundaries):
Columnar DB engine -> flatten to rows -> ODBC wire format -> row cursor
   -> client unmarshals rows -> re-assemble into columns for VertiPaq

Arrow-native path (columnar, zero-copy):
Columnar DB engine -> Arrow record batches -> shared memory layout
   -> client reads Arrow batches directly -> VertiPaq ingests columns as-is

ADBC: Giving Arrow a Driver Interface

Arrow solves the memory layout problem. It does not, by itself, replace the connection-and-query interface that ODBC and JDBC provide — something still has to open a connection, send a query, and hand back a result set through a standard API that tool vendors can build against without writing a bespoke integration per database. That's the gap Arrow Database Connectivity fills. ADBC is a driver API, structured similarly to ODBC in spirit — connect, execute, fetch — but with one deliberate difference: the result of a query is not a row cursor. It's a stream of Arrow record batches.

python
# ODBC-style cursor: rows arrive one at a time, marshaled into Python objects
cursor = odbc_connection.execute("SELECT region, revenue FROM gold_revenue")
for row in cursor.fetchmany(1000):
    process_row(row)  # every row individually unmarshaled

# ADBC: the result set streams in as Arrow record batches — already columnar,
# already in the client's memory in the format it will actually be used in
reader = adbc_connection.execute_query("SELECT region, revenue FROM gold_revenue")
for batch in reader:  # pyarrow.RecordBatch — no row reconstruction needed
    process_batch(batch)

A client library that speaks ADBC against a database engine that emits Arrow natively — DuckDB, several modern warehouse connectors, and an increasing share of the Arrow Flight SQL ecosystem — never constructs a row object at all. The query result arrives already shaped the way an analytical consumer wants it, and tools that ingest Arrow directly, including newer Power BI and Tableau connectors, load it straight into their own columnar stores without the flatten-and-reassemble round trip ODBC required.

DEC — Decision · ACCEPTEDaccepted

Prefer Arrow-native transport — ADBC or Arrow Flight SQL — over legacy ODBC/JDBC row-cursor drivers for any analytical client connection where both the source engine and the destination tool support it. Reserve ODBC for tools and databases that have not yet adopted an Arrow-native path.

EVDEvidence

Eliminating the row-flatten-and-reassemble round trip does not just remove CPU cycles spent marshaling values — it removes an entire class of allocation and garbage-collection pressure on the client side, the same pattern this series identified when unresolved Promises and orphaned objects were shown to be a distinct failure mode from classic unfreed memory. Fewer intermediate row objects means less retained state competing for the same heap.

TRD — Trade-off
Gain

Removes the exact serialization/deserialization round trip Article 9 identified as the dominant cost of moving analytical data, and does so without requiring either end to abandon a columnar internal representation.

Cost

ADBC and Arrow-native connectors are newer than ODBC and JDBC; not every database engine or BI tool has shipped support yet, which means most organizations will run both paths side by side for some time rather than retiring ODBC outright.

A More Important Lesson

This article is not really about Arrow or ADBC. It is about a debt this series opened several chapters ago and is only now paying down. Article 9 named the serialization tax and pointed at the fix without walking through it, because the series had other constraints left to cover first — sockets, storage engines, edge infrastructure. Every one of those detours turned out to matter for understanding this one properly: a driver interface is just a boundary, a boundary is just a place data gets serialized, and the fix for a boundary tax was always going to be removing the serialization step, not making it faster.

That's the shape worth remembering past this specific pair of technologies. The tax Article 9 described wasn't really about JSON versus Parquet, or rows versus columns. It was about how many times the same information gets rebuilt from scratch on its way from where it lives to where it's needed. Arrow and ADBC don't make that rebuilding faster. They make it stop happening.

Looking Ahead

Fast, zero-copy transport solves one problem and immediately creates the conditions for another. Once a query stops being expensive, the obvious next move is to avoid running it twice — to cache the result and serve it again without going back to the source at all. That sounds like a purely additive optimization sitting harmlessly in front of the transport layer this article just fixed.

It isn't. Caching is where this series' hardest, least visible failures live, and it deserves the scrutiny every other boundary in this series has already received.

Next in Track 01: Spatial Intelligence: Maps Are Not Tables.