Arrow Flight SQL from Python with VeloDB

When you pull a large result set out of an analytical database into Python, the bottleneck often isn't the query - it's the trip home. Classic database drivers serialize results row by row into a format Python then has to reassemble into columns, and on millions of rows that conversion dominates the wall clock. Arrow Flight SQL removes that tax by moving data in columnar Arrow batches end to end. Used against a Doris-based system like VeloDB, it can turn a slow fetch into a fast one without touching the query at all.

What Arrow Flight SQL is, in plain terms

Arrow Flight SQL is a protocol for talking to a database using Apache Arrow as the wire format, carried over Arrow Flight (a gRPC-based transport). Instead of the server encoding rows into a driver-specific format and the client decoding them back into columns, the data stays columnar the whole way - the server sends Arrow record batches and the client receives Arrow, ready to use. No row-to-column reshaping, no per-value serialization overhead. For analytical workloads that ship wide, large result sets, that's where the speedup comes from.

How it fits Doris and VeloDB

Doris (and its commercial form, VeloDB) stores and processes data columnar internally, so emitting Arrow is natural rather than a translation. When the storage engine, the compute, and the transport are all columnar, the result set never has to be flattened into rows and rebuilt - the columnar representation flows from disk to the client's DataFrame. That alignment is why Flight SQL pays off most against columnar engines.

Connecting from Python with ADBC

The cleanest client path is ADBC (Arrow Database Connectivity), the Arrow-native database API. Its Flight SQL driver gives you a familiar connection-and-cursor interface that returns Arrow directly:

import adbc_driver_flightsql.dbapi as flight_sql

conn = flight_sql.connect(
    "grpc://your-velodb-host:port",
    db_kwargs={"username": "user", "password": "pass"},
)
cur = conn.cursor()
cur.execute("SELECT * FROM events WHERE day = '2026-06-01'")
table = cur.fetch_arrow_table()      # Arrow Table, zero row-by-row decode
df = table.to_pandas()               # or hand straight to Polars / DuckDB

The key call is fetch_arrow_table() - you receive Arrow and convert (or don't) on your terms, instead of paying for a row format you never wanted.

What actually moves the needle

  • Fetch Arrow, not rows. Use fetch_arrow_table() and keep data columnar; convert to pandas/Polars only at the edge.
  • Select only the columns you need. Columnar transport rewards narrow projections - don't SELECT * if you use three columns.
  • Push filters and aggregation to the server. Move less data; the fastest transfer is the one you avoid.
  • Mind batch sizes. Very small result sets won't show the benefit; the win grows with volume.

Flight SQL vs classic drivers

A traditional driver (ODBC/JDBC-style) is fine for transactional, small-result, row-oriented access. Flight SQL wins specifically when you're moving large analytical result sets into a columnar Python tool - the bigger and wider the result, the larger the gap, because you're skipping the serialization and reassembly that classic drivers can't avoid.

Common pitfalls

  • Converting to pandas immediately and discarding the Arrow advantage - stay in Arrow/Polars if you can.
  • Selecting everything when you need a few columns, paying transfer cost for data you drop.
  • Expecting speedups on tiny queries - the overhead savings only matter at scale.
  • Network or auth misconfiguration - Flight is gRPC; make sure ports and TLS are set up for it.

The takeaway

Arrow Flight SQL is a quiet, high-leverage upgrade: same SQL, same data, far less time spent reshaping result sets between the database and your analysis. Against a columnar engine like Doris/VeloDB, fetching Arrow directly with ADBC keeps data in its native shape from storage to DataFrame - and the larger your result sets, the more that single change is worth.

Leave a Comment