Wähle ein Datenzugriffs- und Analysemuster mit mssql-python

Der Treiber mssql-python bietet mehrere Pfade zum Lesen von Daten aus Microsoft SQL. Jeder Weg passt zu unterschiedlichen Arbeitslasten. Dieser Leitfaden hilft Ihnen, basierend auf Ihrer Datengröße, Ihren Analysebedürfnissen und Ihren Leistungsanforderungen das richtige Modell auszuwählen.

Entscheide nach Arbeitsbelastung

Verwenden Sie diese Tabelle, um Ihren Ausgangspunkt zu finden:

Arbeitsbelastung Empfohlener Pfad Warum?
Anwendungszeilenzugriff (Web-API, CRUD) Methoden zum Abrufen von Cursorn Geringer Overhead, zeilenweise Verarbeitung, keine zusätzlichen Abhängigkeiten.
Kleine bis mittlere Berichtsanfragen pandas Vertraute API zum Filtern, Gruppieren und Visualisieren.
Große Ergebnissätze oder breite Tabellen Pfeilextraktion Null-Kopie-Spaltenübertragung, minimaler Speicheraufwand.
Hochleistungsanalysen Polars mit Pfeil Multithreading-Ausführung auf spaltenorientierten Daten, keine GIL-Kontention.
Ad-hoc-SQL über lokale und entfernte Daten DuckDB mit Pfeil SQL-Analysen auf Arrow-Tabellen, Verknüpfung mit lokalen CSV-/Parquet-Dateien.
Notebook-Erkundung Pandas oder Polare mit Pfeil Wählen Sie basierend auf Teamkenntnis und Datengröße.

Methoden zum Abrufen von Cursorn

Nutze Standard-Cursormethoden, wenn du einen zeilenorientierten Zugriff ohne zusätzliche Abhängigkeiten brauchst. Diese Methode ist die richtige Wahl für Anwendungscode, der jeweils eine Zeile verarbeitet, API-Antworten zurückgibt oder die Anwendungslogik mit Daten versorgt.

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()

Verwendung fetchmany() für speichereffiziente Batch-Verarbeitung großer Ergebnismengen. Verwenden Sie fetchval(), wenn Sie einen einzelnen Wert wie eine Anzahl, ein Maximum oder eine Existenzprüfung benötigen.

Für vollständige Fetch-Methoden-Dokumentation siehe Daten abrufen.

Pfeilextraktion

Verwenden Sie Arrow-Extraktion, wenn Sie Kolumnendaten für Analysen, DataFrame-Aufbau oder den Export nach Parquet benötigen. Arrow ermöglicht eine Zero-Copy-Datenübertragung vom Treiber, wodurch der Zeilen-für-Zeilen-Umwandlungsaufwand beim Erstellen eines DataFrame aus fetchall()vermieden wird.

Tabellen mit Spaltenspeicher-Indizes sind bereits im Spaltenformat in der Datenbank-Engine gespeichert, was die Arrow-Extraktion zu einer natürlichen Lösung für diese Workloads macht.

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")

Verwenden Sie bei großen Ergebnismengen arrow_reader(), um in Stapeln zu streamen, ohne alles in den Speicher zu laden:

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")

Pfeiltabellen sind der Ausgangspunkt für Pandas, Polars und DuckDB. Einmal extrahieren und dann umwandeln:

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)

Für die vollständige Arrow-Dokumentation siehe Apache Arrow Integration.

pandas

Nutzen Sie Pandas, wenn Sie eine vertraute DataFrame-API für Berichterstattung, Ad-hoc-Analyse oder Datenbereinigung benötigen. Pandas funktioniert am besten mit Ergebnismengen, die im Speicher passen (bis zu ein paar Millionen Zeilen, je nach Spaltenbreite).

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"]))

Für größere Ergebnissätze bauen Sie den DataFrame aus Arrow anstelle von fetchall():

cursor.execute("SELECT * FROM Sales.SalesOrderHeader")
arrow_table = cursor.arrow()
df = arrow_table.to_pandas()

Für vollständige Pandas-Muster einschließlich ETL, Zeitreihen und Write-Back siehe Pandas-Integration.

Polars mit Pfeil

Verwenden Sie Polars, wenn Sie schnellere DataFrame-Operationen bei größeren Ergebnismengen benötigen. Polars verwendet Apache Arrow als Speicherformat, sodass die Übertragung von cursor.arrow() ohne Kopieren erfolgt. Polars führt außerdem Operationen auf mehreren Threads aus, was GIL-Konkurrenz bei CPU-lastigen Transformationen vermeidet.

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)

Für das Streaming großer Ergebnissätze:

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)

Für vollständige Polars-Muster siehe Polars-Integration.

DuckDB mit Pfeil

Verwenden Sie DuckDB, wenn Sie SQL-Analysen auf extrahierten Daten durchführen, Serverdaten mit lokalen CSV- oder Parquet-Dateien verbinden oder Ergebnisse in Dateiformate exportieren müssen. DuckDB arbeitet auf Arrow-Tabellen mit Null-Kopierzugriff.

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())

Serverdaten mit einer lokalen Datei verknüpfen:

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 nach Parquet:

cursor.execute("SELECT * FROM Sales.SalesOrderHeader")
orders = cursor.arrow()

duckdb.sql("COPY orders TO 'orders.parquet' (FORMAT PARQUET)")

Für vollständige DuckDB-Muster siehe DuckDB-Integration.

Microsoft SQL-Funktionen, die die Lesepfadentscheidungen beeinflussen

Die Datenbank-Engine verfügt über Funktionen, die direkt beeinflussen, welcher Lesepfad am besten funktioniert. Berücksichtigen Sie diese Merkmale bei der Wahl Ihres Ansatzes:

ColumnStore-Indizes

Tabellen mit Spaltenspeicher-Indizes speichern Daten im Spaltenformat. Die Arrow-Extraktion ist die naheliegende Übergabe für diese Tabellen, da die Daten in der Engine bereits spaltenorientiert vorliegen. Wenn Ihre Analytics-Abfragen breite Tabellen mit Millionen von Zeilen scannen, sorgt ein nicht-geclusterter Columnstore-Index auf Serverseite kombiniert mit Arrow-Extraktion auf der Client-Seite für den besten End-to-End-Durchsatz.

Indizierte Ansichten

Indexierte Ansichten berechnen und speichern aggregierte oder zusammengefügte Ergebnisse auf dem Server. Wenn deine Pandas- oder Polars-Analyse wiederholt dieselbe Aggregation berechnet, solltest du stattdessen eine indexierte Ansicht erstellen und diese Ansicht abfragen. Der Server hält die Ansicht automatisch aufrecht, wenn sich die zugrunde liegenden Daten ändern.

Abfragespeicher

Abfragespeicher verfolgt Abfrageausführungsstatistiken im Zeitverlauf. Nutzen Sie es, um zu identifizieren, welche Abfragen teuer genug sind, um eine Arrow-Extraktion und lokale DataFrame-Analyse im Vergleich zu einem direkten Cursor-Lesen zu rechtfertigen. Wenn eine Abfrage innerhalb von Millisekunden läuft, ist der Cursor-Fetch in Ordnung. Wenn es Millionen von Zeilen scannt, könnten Arrow-Extraktion und lokale Analyse die Serverbelastung verringern.

Intelligente Abfrageverarbeitung

Die intelligenten Abfrageverarbeitungsfunktionen von Microsoft SQL, wie adaptive Joins, Batch-Modus im Rowstore und Memory Grant Feedback, optimieren die Abfrageausführung automatisch. Diese Funktionen funktionieren unabhängig davon, welchen Kundenlesepfad Sie wählen, aber sie kommen großen analytischen Abfragen am meisten zugute. Du musst keine Hinweise oder Ausführungspläne für die meisten Workloads optimieren.

Große Ergebnismengen streamen

Für Ergebnismengen, die nicht ins Gedächtnis passen, verwenden Sie Streaming-Muster:

Cursorbasiertes Streaming mit 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

Pfeilbasiertes Streaming zu 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-Muster, die man vermeiden sollte

Anti-Muster Problem Besserer Ansatz
fetchall() dann gilt pd.DataFrame() für große Tabellen Lädt alle Zeilen zweimal in den Speicher (einmal als Tupel, einmal als DataFrame). Verwenden Sie cursor.arrow() und dann arrow_table.to_pandas().
Arrow in Pandas umwandeln, nur um Zeilen zu filtern Verschwendet Speicher für die vollständige pandas-Kopie. Filtern Sie in SQL (WHERE-Klausel) oder verwenden Sie Polars/DuckDB direkt auf der Arrow-Tabelle.
SELECT * Wenn du drei Spalten brauchst Überträgt unnötige Daten vom Server. Listen Sie nur die Spalten auf, die Sie benötigen.
Aufbau eines DataFrame zur Berechnung COUNT(*) Der Server berechnet Aggregate schneller als Python. Verwenden Sie SELECT COUNT(*) und fetchval().
Eröffnung einer neuen Verbindung pro Abfrage Das Erstellen einer Verbindung ist selbst unter Berücksichtigung des Overheads durch das Pooling teuer. Wiederverwenden Sie Verbindungen innerhalb einer logischen Arbeitseinheit.
Kettenpfeil –> Pandas –> Polare Jede Konvertierung kopiert Daten. Wechsle direkt zu deinem Zielformat: Arrow -> Polars oder Arrow -> pandas.