Используйте mssql-python с DuckDB

DuckDB — это процессуемый SQL-аналитический движок, который может напрямую запрашивать таблицы Apache Arrow без копирования данных. Объединение DuckDB с драйвером mssql-python позволяет вам:

  • Запускайте аналитические SQL-запросы на наборах результатов Microsoft SQL без загрузки данных в pandas или Polars.
  • Выполняйте запросы к таблицам Arrow в памяти без дополнительных затрат на копирование.
  • Соедините данные Microsoft SQL с локальными файлами (CSV, Parquet, JSON) в одном запросе DuckDB.
  • Экспортируйте данные Microsoft SQL в Parquet, CSV или другие форматы через DuckDB.

Необходимые условия

  • Python 3.10 или более поздней версии.
  • Пакеты mssql-python, duckdb и pyarrow. Установите всё с помощью pip install mssql-python duckdb pyarrow.
  • Установите единовременные предварительные условия для операционной системы. Пользователи Windows могут пропустить этот шаг. Для полной информации о платформе см. Установить mssql-python.
    apk add libtool krb5-libs krb5-dev
    

Создание базы данных SQL

Создайте или подключитесь к SQL-базе данных на одной из следующих платформ:

Примеры в этой статье используют запрос к AdventureWorks примерной базе данных. Если у вас его ещё нет, посмотрите примеры баз данных AdventureWorks.

Установка зависимостей

pip install mssql-python duckdb pyarrow

Выполнение запросов к данным Microsoft SQL в DuckDB

Основной рабочий процесс: выполнить запрос с помощью mssql-python, получить результаты в виде таблицы Arrow, затем запросить эту таблицу Arrow с помощью DuckDB SQL.

Основная схема

Начните с установления соединения и получения данных в виде таблицы Arrow.

import duckdb
import mssql_python

conn = mssql_python.connect(
    "Server=<server>.database.windows.net;"
    "Database=<database>;"
    "Authentication=ActiveDirectoryDefault;"
    "Encrypt=yes"
)
cursor = conn.cursor()
# Fetch Microsoft SQL data as Arrow
cursor.execute("SELECT * FROM Production.Product WHERE ListPrice > 0")
products = cursor.arrow()

# Query the Arrow table with DuckDB
result = duckdb.sql("""
    SELECT Color, COUNT(*) AS ProductCount, AVG(ListPrice) AS AvgPrice
    FROM products
    GROUP BY Color
    ORDER BY ProductCount DESC
""")
print(result.fetchdf())

DuckDB обращается к таблице Arrow products по имени её переменной в Python. Данные не копируются в хранилище DuckDB.

Агрегат и фильтр

Используйте SQL DuckDB для группировки и агрегирования данных Arrow.

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

# Top customers by total spend
top_customers = duckdb.sql("""
    SELECT
        CustomerID,
        COUNT(*) AS OrderCount,
        SUM(TotalDue) AS TotalSpent,
        AVG(TotalDue) AS AvgOrderValue
    FROM orders
    GROUP BY CustomerID
    HAVING SUM(TotalDue) > 10000
    ORDER BY TotalSpent DESC
    LIMIT 20
""")
print(top_customers.fetchdf())

Присоединяйтесь к нескольким результатам Microsoft SQL

Получите несколько таблиц из Microsoft SQL и объедините их в DuckDB без написания кросс-серверного запроса.

# Fetch two tables
cursor.execute("SELECT * FROM Production.Product")
products = cursor.arrow()

cursor.execute("SELECT * FROM Production.ProductSubcategory")
subcategories = cursor.arrow()

# Join in DuckDB
result = duckdb.sql("""
    SELECT
        s.Name AS Subcategory,
        COUNT(*) AS ProductCount,
        ROUND(AVG(p.ListPrice), 2) AS AvgPrice
    FROM products p
    JOIN subcategories s ON p.ProductSubcategoryID = s.ProductSubcategoryID
    GROUP BY s.Name
    ORDER BY AvgPrice DESC
""")
print(result.fetchdf())

Соедините данные Microsoft SQL с локальными файлами

DuckDB может читать файлы CSV, Parquet и JSON нативно. Объедините данные SQL Server с локальными файлами в одном запросе.

Объединить через CSV-файл

Загрузите CSV-файл и объедините его с данными из Microsoft SQL.

import csv
from pathlib import Path

cursor.execute("SELECT CustomerID, PersonID FROM Sales.Customer")
customers = cursor.arrow()

csv_path = Path("customer_regions.csv")
with csv_path.open("w", newline="", encoding="utf-8") as file:
    writer = csv.writer(file)
    writer.writerow(["CustomerID", "Region", "Segment"])
    writer.writerows([
        (1, "West", "Premium"),
        (2, "East", "Standard"),
        (3, "Central", "Basic"),
    ])

try:
    result = duckdb.sql("""
        SELECT c.CustomerID, c.PersonID, f.Region, f.Segment
        FROM customers c
        JOIN read_csv_auto('customer_regions.csv') f ON c.CustomerID = f.CustomerID
    """)
    print(result.fetchdf())
finally:
    csv_path.unlink(missing_ok=True)

Объединение с файлом Parquet

Загрузите файл Parquet и объедините его с данными из Microsoft SQL.

from pathlib import Path

import pyarrow as pa
import pyarrow.parquet as pq

cursor.execute("SELECT ProductID, Name, ListPrice FROM Production.Product")
products = cursor.arrow()

parquet_path = Path("order_history.parquet")
order_history = pa.table({
    "ProductID": [1, 2, 680],
    "OrderDate": ["2024-06-01", "2024-03-15", "2024-01-10"],
    "Quantity": [10, 5, 3],
})
pq.write_table(order_history, parquet_path)

try:
    result = duckdb.sql("""
        SELECT p.Name, p.ListPrice, h.OrderDate, h.Quantity
        FROM products p
        JOIN read_parquet('order_history.parquet') h ON p.ProductID = h.ProductID
        WHERE h.OrderDate >= '2024-01-01'
    """)
    print(result.fetchdf())
finally:
    parquet_path.unlink(missing_ok=True)

Экспорт данных Microsoft SQL

Используйте оператор COPY DuckDB для экспорта данных из Microsoft SQL в различные файловые форматы.

Экспорт в Parquet

Экспорт данных в формат Apache Parquet.

cursor.execute("SELECT * FROM Production.Product")
products = cursor.arrow()

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

Экспорт в CSV

Экспорт данных в файл значений с разделёнными запятыми:

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

duckdb.sql("COPY orders TO 'orders.csv' (FORMAT CSV, HEADER)")

Экспортный разделённый паркет

Экспорт данных в разделённые файлы Parquet для распределённой аналитики:

import shutil
from pathlib import Path

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

output_dir = Path("sales_data")
shutil.rmtree(output_dir, ignore_errors=True)

duckdb.sql("""
    COPY (SELECT *, YEAR(OrderDate) AS OrderYear FROM orders)
    TO 'sales_data'
    (FORMAT PARQUET, PARTITION_BY (OrderYear))
""")

Потоковая передача больших наборов результатов

Для больших наборов данных используйте arrow_reader(), чтобы обрабатывать данные потоковыми пакетами, не загружая все строки в память одновременно:

cursor.execute("SELECT * FROM Production.TransactionHistory")
reader = cursor.arrow_reader(batch_size=50000)

# Process each batch with DuckDB
total_rows = 0
for batch in reader:
    result = duckdb.sql("""
        SELECT ProductID, SUM(ActualCost) AS TotalCost
        FROM batch
        GROUP BY ProductID
    """)
    total_rows += batch.num_rows
    print(f"Processed {total_rows} rows")

Накопление результатов стриминга

Для агрегирования по всем партиям регистрируйте каждую партию в постоянном соединении DuckDB и накапливайте результаты постепенно.

cursor.execute("SELECT * FROM Production.TransactionHistory")
reader = cursor.arrow_reader(batch_size=50000)

duck = duckdb.connect()
duck.execute("CREATE TABLE transactions (ProductID INT, ActualCost DOUBLE, Quantity INT)")

for batch in reader:
    duck.execute("INSERT INTO transactions SELECT ProductID, ActualCost, Quantity FROM batch")

# Query the accumulated data
result = duck.sql("""
    SELECT ProductID, SUM(ActualCost) AS TotalCost, SUM(Quantity) AS TotalQty
    FROM transactions
    GROUP BY ProductID
    ORDER BY TotalCost DESC
    LIMIT 10
""")
print(result.fetchdf())
duck.close()

Советы по производительности

Пусть Microsoft SQL возьмёт на себя тяжёлую работу

Microsoft SQL быстрее подходит для фильтрации, объединений и агрегирования, чем перемещение всех сырых данных по проводу. Используйте DuckDB для вторичного анализа уже полученных наборов результатов, а не как замену оптимизации запросов SQL Server.

# Suboptimal: Pull all rows, filter in DuckDB
cursor.execute("SELECT * FROM Sales.SalesOrderHeader")
orders = cursor.arrow()
result = duckdb.sql("SELECT * FROM orders WHERE TotalDue > 1000")

# Better: Filter in Microsoft SQL, analyze in DuckDB
cursor.execute("SELECT * FROM Sales.SalesOrderHeader WHERE TotalDue > 1000")
orders = cursor.arrow()
result = duckdb.sql("SELECT CustomerID, SUM(TotalDue) FROM orders GROUP BY CustomerID")

Используйте Arrow для всех операций чтения

Передача на основе стрелок предотвращает создание промежуточных объектов Python, что снижает использование памяти и повышает пропускную способность. При передаче данных в DuckDB предпочитайте cursor.arrow() вместо ручного построчного преобразования.

Используйте потоковые потоки для больших наборов данных

Для наборов результатов, превышающих доступную память, используйте arrow_reader() с параметром batch_size для постепенной обработки данных.