Nóta
Teastaíonn údarú chun rochtain a fháil ar an leathanach seo. Is féidir leat triail a bhaint as shíniú isteach nó eolairí a athrú.
Teastaíonn údarú chun rochtain a fháil ar an leathanach seo. Is féidir leat triail a bhaint as eolairí a athrú.
The mssql-python driver provides multiple paths for reading data from Microsoft SQL. Each path fits different workloads. This guide helps you choose the right one based on your data size, analysis needs, and performance requirements.
Decide by workload
Use this table to find your starting point:
| Workload | Recommended path | Why |
|---|---|---|
| Application row access (web API, CRUD) | Cursor fetch methods | Low overhead, row-at-a-time processing, no extra dependencies. |
| Small to medium reporting queries | pandas | Familiar API for filtering, grouping, and visualization. |
| Large result sets or wide tables | Arrow extraction | Zero-copy columnar transfer, minimal memory overhead. |
| High-performance analytics | Polars with Arrow | Multithreaded execution on columnar data, no GIL contention. |
| Ad hoc SQL over local and remote data | DuckDB with Arrow | SQL analytics on Arrow tables, join with local CSV/Parquet files. |
| Notebook exploration | pandas or Polars with Arrow | Choose based on team familiarity and data size. |
Cursor fetch methods
Use standard cursor methods when you need row-oriented access with no extra dependencies. This method is the right choice for application code that processes one row at a time, returns API responses, or feeds application logic.
import mssql_python
conn = mssql_python.connect(
server="<server>.database.windows.net",
database="<database>",
authentication="ActiveDirectoryDefault",
encrypt="yes"
)
cursor = conn.cursor()
# fetchone(): Process rows one at a time
cursor.execute("SELECT ProductID, Name, ListPrice FROM Production.Product WHERE ListPrice > %(threshold)s", {"threshold": 100})
row = cursor.fetchone()
while row:
print(f"{row.Name}: ${row.ListPrice:.2f}")
row = cursor.fetchone()
# fetchmany(): Process in batches
cursor.execute("SELECT ProductID, Name FROM Production.Product")
while True:
batch = cursor.fetchmany(100)
if not batch:
break
for row in batch:
print(row.Name)
# fetchval(): Get a single scalar value
cursor.execute("SELECT COUNT(*) FROM Production.Product")
count = cursor.fetchval()
Use fetchmany() for memory-efficient batch processing of large result sets. Use fetchval() when you need a single value like a count, max, or exists check.
For complete fetch method documentation, see Retrieve data.
Arrow extraction
Use Arrow extraction when you need columnar data for analytics, DataFrame construction, or export to Parquet. Arrow provides zero-copy data transfer from the driver, which avoids the row-by-row conversion overhead of building a DataFrame from fetchall().
Tables with columnstore indexes are already stored in columnar format in the database engine, making Arrow extraction a natural fit for those workloads.
cursor.execute("""
SELECT ProductID, Name, ListPrice, Color
FROM Production.Product
WHERE ListPrice > 0
""")
# Get a single Arrow table
arrow_table = cursor.arrow()
print(f"{arrow_table.num_rows} rows, {arrow_table.num_columns} columns")
For large result sets, use arrow_reader() to stream batches without loading everything into memory:
cursor.execute("SELECT * FROM Sales.SalesOrderHeader")
# Stream Arrow record batches
reader = cursor.arrow_reader(batch_size=10000)
for batch in reader:
# Each batch is a pyarrow.RecordBatch
print(f"Batch: {batch.num_rows} rows")
Arrow tables are the starting point for pandas, Polars, and DuckDB. Extract once, then convert:
cursor.execute("""
SELECT ProductID, Name, ListPrice, Color
FROM Production.Product
WHERE ListPrice > 0
""")
arrow_table = cursor.arrow()
# Arrow -> pandas
df = arrow_table.to_pandas()
# Arrow -> Polars (zero-copy)
import polars as pl
df = pl.from_arrow(arrow_table)
For complete Arrow documentation, see Apache Arrow integration.
pandas
Use pandas when you need a familiar DataFrame API for reporting, ad hoc analysis, or data cleanup. pandas works best with result sets that fit in memory (up to a few million rows, depending on column width).
cursor.execute("""
SELECT p.Name, p.ListPrice, pc.Name AS Category
FROM Production.Product p
JOIN Production.ProductSubcategory ps ON p.ProductSubcategoryID = ps.ProductSubcategoryID
JOIN Production.ProductCategory pc ON ps.ProductCategoryID = pc.ProductCategoryID
WHERE p.ListPrice > 0
""")
import pandas as pd
rows = cursor.fetchall()
columns = [desc[0] for desc in cursor.description]
df = pd.DataFrame.from_records(rows, columns=columns)
# Analyze
print(df.groupby("Category")["ListPrice"].agg(["mean", "count"]))
For larger result sets, build the DataFrame from Arrow instead of fetchall():
cursor.execute("SELECT * FROM Sales.SalesOrderHeader")
arrow_table = cursor.arrow()
df = arrow_table.to_pandas()
For complete pandas patterns including ETL, time series, and write-back, see pandas integration.
Polars with Arrow
Use Polars when you need faster DataFrame operations on larger result sets. Polars uses Apache Arrow as its memory format, so the transfer from cursor.arrow() is zero-copy. Polars also runs operations on multiple threads, which avoids GIL contention on CPU-heavy transformations.
import polars as pl
cursor.execute("""
SELECT ProductID, Name, ListPrice, Color
FROM Production.Product
WHERE ListPrice > 0
""")
arrow_table = cursor.arrow()
df = pl.from_arrow(arrow_table)
# Filter and aggregate
result = (
df.filter(pl.col("ListPrice") > 100)
.group_by("Color")
.agg(pl.col("ListPrice").mean().alias("AvgPrice"))
.sort("AvgPrice", descending=True)
)
print(result)
For streaming large result sets:
cursor.execute("SELECT * FROM Sales.SalesOrderHeader")
reader = cursor.arrow_reader(batch_size=50000)
frames = []
for batch in reader:
frames.append(pl.from_arrow(batch))
df = pl.concat(frames)
For complete Polars patterns, see Polars integration.
DuckDB with Arrow
Use DuckDB when you need to run SQL analytics on extracted data, join server data with local CSV or Parquet files, or export results to file formats. DuckDB operates on Arrow tables with zero-copy access.
import duckdb
cursor.execute("""
SELECT ProductID, Name, ListPrice, Color
FROM Production.Product
WHERE ListPrice > 0
""")
products = cursor.arrow()
# Run DuckDB SQL on the Arrow table
result = duckdb.sql("""
SELECT Color, AVG(ListPrice) AS AvgPrice, COUNT(*) AS Count
FROM products
WHERE Color IS NOT NULL
GROUP BY Color
ORDER BY AvgPrice DESC
""")
print(result.fetchdf())
Join server data with a local file:
cursor.execute("SELECT CustomerID, TerritoryID FROM Sales.Customer")
customers = cursor.arrow()
# Join with a local CSV file
result = duckdb.sql("""
SELECT c.CustomerID, c.TerritoryID, l.Region
FROM customers c
JOIN read_csv_auto('regions.csv') l ON c.TerritoryID = l.TerritoryID
""")
Export to Parquet:
cursor.execute("SELECT * FROM Sales.SalesOrderHeader")
orders = cursor.arrow()
duckdb.sql("COPY orders TO 'orders.parquet' (FORMAT PARQUET)")
For complete DuckDB patterns, see DuckDB integration.
Microsoft SQL features that affect read-path decisions
The database engine has features that directly affect which read path works best. Consider these features when choosing your approach:
Columnstore indexes
Tables with columnstore indexes store data in columnar format. Arrow extraction is the natural handoff for these tables because the data is already columnar in the engine. If your analytics queries scan wide tables with millions of rows, a nonclustered columnstore index on the server side combined with Arrow extraction on the client side gives the best end-to-end throughput.
Indexed views
Indexed views precompute and store aggregated or joined results on the server. If your pandas or Polars analysis repeatedly computes the same aggregation, consider creating an indexed view and querying that view instead. The server automatically maintains the view as underlying data changes.
Query Store
Query Store tracks query execution statistics over time. Use it to identify which queries are expensive enough to justify an Arrow extract and local DataFrame analysis versus a direct cursor read. If a query runs in milliseconds, cursor fetch is fine. If it scans millions of rows, Arrow extraction and local analysis might reduce server load.
Intelligent query processing
Microsoft SQL's intelligent query processing features, such as adaptive joins, batch mode on rowstore, and memory grant feedback, automatically optimize query execution. These features work regardless of which client read path you choose, but they benefit large analytical queries the most. You don't need to tune hints or execution plans for most workloads.
Stream large result sets
For result sets that don't fit in memory, use streaming patterns:
Cursor-based streaming with fetchmany():
cursor.execute("SELECT * FROM Sales.SalesOrderHeader")
while True:
batch = cursor.fetchmany(5000)
if not batch:
break
for row in batch:
print(row[0]) # Process each row
Arrow-based streaming to Parquet:
import pyarrow.parquet as pq
cursor.execute("SELECT * FROM Sales.SalesOrderHeader")
reader = cursor.arrow_reader(batch_size=50000)
writer = None
for batch in reader:
if writer is None:
writer = pq.ParquetWriter("orders.parquet", batch.schema)
writer.write_batch(batch)
if writer:
writer.close()
Anti-patterns to avoid
| Anti-pattern | Problem | Better approach |
|---|---|---|
fetchall() then pd.DataFrame() for large tables |
Loads all rows into memory twice (once as tuples, once as DataFrame). | Use cursor.arrow() then arrow_table.to_pandas(). |
| Converting Arrow to pandas just to filter rows | Wastes memory on the full pandas copy. | Filter in SQL (WHERE clause) or use Polars/DuckDB on the Arrow table directly. |
SELECT * when you need three columns |
Transfers unnecessary data from the server. | List only the columns you need. |
Building a DataFrame to compute COUNT(*) |
The server computes aggregates faster than Python. | Use SELECT COUNT(*) and fetchval(). |
| Opening a new connection per query | Connection creation is expensive even with pooling overhead. | Reuse connections within a logical unit of work. |
| Chaining Arrow -> pandas -> Polars | Each conversion copies data. | Go directly to your target format: Arrow -> Polars or Arrow -> pandas. |