Friday, July 31, 2026
HomePythonIntroducing Apache Arrow Assist in mssql-python

Introducing Apache Arrow Assist in mssql-python


c1014e61 a66d 4807 ab58 655671044f49 image

Reviewed by Sumit Sarabhai

Fetching 1,000,000 rows from SQL Server right into a Polars DataFrame used to imply 1,000,000 Python objects, 1,000,000 GC allocations, after which throwing all of it away to construct a DataFrame. Not anymore. mssql-python now helps fetching SQL Server knowledge straight as Apache Arrow buildings – a sooner and extra memory-efficient path for anybody working with SQL Server knowledge in Polars, Pandas, DuckDB, or another Arrow-native library. This function was contributed by neighborhood developer Felix Graßl (@ffelixg), and we’re thrilled to ship it.

Key Phrases

API (Software Programming Interface): a source-code contract that defines methods to name a operate or library.

ABI (Software Binary Interface): a binary-level contract that specifies how compiled code is specified by reminiscence. Two packages constructed in several languages can share an ABI and change knowledge straight – no serialization is required.

Arrow C Information Interface: Apache Arrow’s ABI specification – the usual that makes zero-copy knowledge change between languages potential.

What Is Apache Arrow?

The important thing perception behind Apache Arrow is zero-copy language interoperability. Arrow defines a steady shared-memory structure – the Arrow C Information Interface, a cross-language ABI – that any language can produce or devour by exchanging a pointer, with no serialization, no copies, and no re-parsing. A C++ database driver and a Python DataFrame library can work on the very same reminiscence with out both one figuring out concerning the different.

Constructed on high of that, Arrow makes use of a columnar in-memory format: as a substitute of representing a desk as a listing of rows, every row a group of Python objects, Arrow shops all values for a column contiguously in a typed buffer. Nulls are tracked in a compact bitmap somewhat than per-cell None objects.

For a database driver, this implies your complete fetch loop can run in C++ and write values straight into Arrow buffers – no Python object creation per row, no garbage-collector stress. The DataFrame library receives a pointer to that reminiscence and might start working on it instantly. Crucially, subsequent operations – filters, joins, aggregations – additionally work in-place on those self same buffers. A Polars pipeline studying from mssql-python by no means must materialize intermediate Python objects at any stage, making Arrow the best basis for high-throughput knowledge processing pipelines.

For customers of mssql-python, this interprets into 4 concrete advantages:

  • Velocity: The columnar fetch path avoids Python object creation per row, which ought to make fetching noticeably sooner for a lot of SQL Server sorts – particularly temporal sorts like DATETIME and DATETIMEOFFSET, the place Python-side per-value conversions are eradicated totally.
  • Decrease reminiscence utilization: A column of 1 million integers is a single contiguous C array, not 1,000,000 particular person Python objects.
  • Seamless interoperability: Polars, Pandas (by way of ArrowDtype), DuckDB, Hugging Face datasets, and lots of different libraries all communicate Arrow natively. Zero-copy hand-off between mssql-python and people instruments.
  • Purely additive: Your present fetchone, fetchmany, and fetchall code is totally unaffected. You choose in solely the place you want it.

The Arrow Fetch APIs

Three APIs have been added to the Cursor object.

1. cursor.arrow_batch(batch_size=8192)pyarrow.RecordBatch

Fetches the following batch of as much as batch_size rows as an Arrow RecordBatch and advances the cursor. RecordBatches are the constructing block for extra high-level Arrow knowledge sorts like tables and the batch reader interface.

import mssql_python

conn   = mssql_python.join(conn_str)
cursor = conn.cursor()
cursor.execute("SELECT * FROM SalesData")

partial_data = cursor.arrow_batch(batch_size=50000)
course of(partial_data)   # pyarrow.RecordBatch

2. cursor.arrow(batch_size=8192)pyarrow.Desk

Eagerly fetches your complete consequence set right into a single Arrow Desk. That is the best path and works properly for analytics queries the place the consequence matches comfortably in reminiscence. Nonetheless, as a result of it materialises the total consequence set directly, it will possibly trigger excessive peak RAM utilization or out-of-memory errors on very massive or unbounded queries. For big exports or ETL workloads, desire cursor.arrow_reader() (streaming, fetches lazily) or cursor.arrow_batch() (fetch one batch at a time). In each circumstances, batch_size is a tuning knob: bigger batches enhance throughput however improve peak reminiscence; smaller batches scale back reminiscence at the price of barely extra per-batch overhead.

cursor.execute("SELECT customer_id, order_date, quantity FROM Orders")
desk = cursor.arrow()

# Zero-copy conversion to Polars
import polars as pl
df = pl.DataFrame(desk)

# Or to Pandas with Arrow-backed dtypes
import pandas as pd
df = desk.to_pandas(types_mapper=pd.ArrowDtype)

3. cursor.arrow_reader(batch_size=8192)pyarrow.RecordBatchReader

Returns a lazy RecordBatchReader. Batches are fetched solely when the reader is iterated, enabling streaming over very massive consequence units. RecordBatchReader can also be accepted straight by DuckDB, Lance, and different Arrow-native libraries.

cursor.execute("SELECT * FROM LargeEventLog")
reader = cursor.arrow_reader(batch_size=100000)

for batch in reader:
    sink.write(batch)

Testing

We validated the Arrow fetch path in opposition to the usual Python row fetch path throughout a variety of SQL Server sorts — numeric, temporal, string, and UUID – for each single-column and vast (20-column) tables. The total take a look at script and outcomes can be found within the Assets part; we encourage you to run them by yourself {hardware} to see the distinction on your workload.

In our testing, the Arrow path was persistently sooner for many SQL Server sorts. Temporal sorts confirmed the biggest good points: sorts like DATETIME and DATETIMEOFFSET profit considerably as a result of the Arrow path handles timezone normalization and worth encoding totally in C++, eliminating per-value Python-side conversions. DATETIMEOFFSET specifically confirmed among the most pronounced speedups we noticed.

JSON Serialization Bonus

The Arrow path may also profit API workloads that serialize outcomes to JSON. As a substitute of fetchall() + json.dumps(), fetch by way of cursor.arrow(), wrap in a Polars DataFrame, and name df.write_json() – your complete pipeline bypasses Python objects and might be noticeably sooner, particularly for sorts like DATETIMEOFFSET.

NVARCHAR on Linux

Our Linux exams present longer fetch instances for NVARCHAR as a result of present UTF-16 → UTF-8 conversion path. On Home windows, NVARCHAR fetches persistently sooner with Arrow. A repair is focused for a follow-up launch.

Getting Began

Set up or improve mssql-python, then add pyarrow:

pip set up mssql-python pyarrow

For IDE sort hints and static sort checking:

pip set up pyarrow-stubs

Then swap in cursor.arrow() wherever you’d have referred to as fetchall() and transformed to a DataFrame. Your present code is totally unaffected — Arrow help is only additive.

import mssql_python
import polars as pl

conn   = mssql_python.join(conn_str)
cursor = conn.cursor()

cursor.execute("SELECT * FROM dbo.LargeSalesTable")
df = pl.DataFrame(cursor.arrow())

print(df.describe())

What’s Subsequent

One identified space we’re actively engaged on to enhance is NVARCHAR efficiency on Linux. SQL Server returns Unicode string knowledge in UTF-16 encoding, which the driving force should convert to UTF-8 earlier than handing it to Arrow. On Home windows this conversion makes use of a local system API that may be very quick, however the present Linux code path goes by way of a slower chain of intermediate steps. In consequence, NVARCHAR columns on Linux present longer fetch instances in comparison with the Python fetch path — the alternative of each different sort. A repair utilizing a extra environment friendly codec is in progress for a follow-up launch. On Home windows, our exams present NVARCHAR fetching noticeably sooner with Arrow, and Linux will comply with.

A Be aware of Thanks

This function was contributed by Felix Graßl (@ffelixg), the writer of zodbc, his personal Zig-based ODBC driver. His deep familiarity with ODBC and Arrow made this an intensive, well-tested contribution overlaying each Linux and Home windows, and all three fetch patterns. We’re very grateful for his work and the care he delivered to this function.

Assets

Strive It and Share Your Suggestions! 

We invite you to: 

  1. Test-out the mssql-python driver and combine it into your tasks. 
  2. Share your ideas: Open points, counsel options, and contribute to the mission. 
  3. Be part of the dialog: GitHub Discussions | SQL Server Tech Neighborhood
RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments