快速入門:Apache Arrow 搭配 Python 的 mssql-python 驅動程式

在此快速入門中,使用mssql-python驅動程式內建的 Arrow 擷取方法,以欄位式 Apache Arrow 資料表形式取得 SQL Server 資料。 Arrow 的列式記憶體格式可實現高效能分析、與 pandas、Polars 和 DuckDB 的零複製互通,以及無需逐列建立 Python 物件的高效率 Parquet 檔案 I/O。

在 Windows 電腦上,mssql-python 驅動程式不需要任何外部相依性。 驅動程式只需一次 pip 安裝就能安裝所有需要的東西,所以你可以用最新版本的驅動程式來執行新腳本,而不會破壞你沒時間升級和測試的其他腳本。

MSSQL-Python 文件 | MSSQL-Python 原始碼 | 套件(PyPI) | uv

先決條件

  • Python 3.10 或更新版本

  • 如果你還沒有 Python,建議安裝 Python 執行環境Pip 套件管理器 ,從 python.org 安裝。

  • 不想使用自己的系統環境? 遵循容器與本地開發,建立可重現的開發容器或 GitHub Codespaces 環境。

  • Visual Studio Code 使用下列擴充套件:

  • Azure Command-Line 介面(CLI)用於 macOS 和 Linux 的無密碼認證。

  • 如果你還沒有安裝<該軟體>,請依照 安裝說明進行操作。

  • SQL Server、Azure SQL Database 或 Fabric 中的 SQL 資料庫上的資料庫,具有 AdventureWorks2025 範例結構描述和有效的連接字串。

安裝一次性作業系統特定先決條件。 Windows 使用者可以跳過此步驟。 完整平台細節請參見 安裝 mssql-python

apk add libtool krb5-libs krb5-dev

建立 SQL 資料庫

在以下平台建立或連接 SQL 資料庫:

建立專案並執行程式碼

  1. 建立新專案
  2. 新增相依性
  3. 啟動 Visual Studio Code
  4. 更新 pyproject.toml
  5. 更新 main.py
  6. 儲存連接字串
  7. 使用 uv run 執行腳本

建立新專案

  1. 在開發目錄中開啟命令提示字元。 如果你沒有,請建立一個新的目錄,例如 pythonscripts。 避免在 OneDrive 上放置資料夾,因為同步可能會干擾虛擬環境的管理。

  2. 透過使用 建立一個新uv

    uv init arrow-qs
    cd arrow-qs
    

新增依賴性

在同一目錄中安裝 mssql-pythonpython-dotenvpyarrowrich 套件。

uv add mssql-python python-dotenv pyarrow rich

啟動 Visual Studio Code

在相同的目錄中,執行下列命令。

code .

更新 pyproject.toml

  1. pyproject.toml 檔案包含你專案的元資料。 在您最喜歡的編輯器中打開該文件。

  2. 檢閱檔案的內容。 它應該類似於這個例子。 請注意針對 mssql-python 的 Python 版本和相依性,並使用 >= 來定義最低版本。 如果您偏好確切的版本,請將版本號碼之前的 變更 >===。 然後,每個套件的解析版本會儲存在 uv.lock 中。 鎖定檔案確保開發者使用一致的套件版本。 提交 pyproject.tomluv.lock 兩者,並在 CI 中執行經組織核准的相依性掃描工具。 不要直接編輯 uv.lock 檔案。

    [project]
    name = "arrow-qs"
    version = "0.1.0"
    description = "Add your description here"
    readme = "README.md"
    requires-python = ">=3.11"
    dependencies = [
        "mssql-python>=1.5.0",
        "pyarrow>=19.0.0",
        "python-dotenv>=1.1.1",
        "rich>=14.1.0",
    ]
    
  3. 更新描述以更具描述性。

    description = "Fetch SQL Server data as Apache Arrow tables using mssql-python"
    
  4. 儲存並關閉檔案。

更新 main.py

  1. 開啟名為 main.py的檔案。 它應該類似於這個例子。

    def main():
        print("Hello from arrow-qs!")
    
    if __name__ == "__main__":
        main()
    
  2. 將整個 main.py 內容替換成以下程式碼。

    """Fetch SQL Server data as Apache Arrow tables using mssql-python."""
    
    from os import getenv
    
    import pyarrow as pa
    import pyarrow.parquet as pq
    from dotenv import load_dotenv
    from mssql_python import connect, Connection
    from rich.console import Console
    from rich.table import Table
    
    console = Console()
    
    
    def get_connection() -> Connection:
        """Create a connection using the connection string from .env."""
        load_dotenv()
        conn_str = getenv("SQL_CONNECTION_STRING")
        if not conn_str:
            raise ValueError("SQL_CONNECTION_STRING not set in .env file")
        return connect(conn_str)
    
    
    def fetch_arrow_table(conn: Connection) -> pa.Table:
        """Run a query and return the full result as an Arrow Table."""
        cursor = conn.cursor()
        cursor.execute("""
            SELECT
                p.ProductID,
                p.Name,
                p.ProductNumber,
                p.Color,
                p.StandardCost,
                p.ListPrice,
                p.Size,
                p.Weight,
                p.SellStartDate,
                pc.Name AS Category
            FROM SalesLT.Product AS p
            INNER JOIN SalesLT.ProductCategory AS pc
                ON p.ProductCategoryID = pc.ProductCategoryID
            ORDER BY p.ListPrice DESC
        """)
        arrow_table = cursor.arrow()
        cursor.close()
        return arrow_table
    
    
    def fetch_arrow_batches(conn: Connection) -> pa.Table:
        """Stream results one batch at a time using arrow_batch()."""
        cursor = conn.cursor()
        cursor.execute("""
            SELECT
                c.CustomerID,
                c.CompanyName,
                c.EmailAddress,
                COUNT(soh.SalesOrderID) AS OrderCount,
                SUM(soh.SubTotal + soh.TaxAmt + soh.Freight) AS TotalSpent
            FROM SalesLT.Customer AS c
            LEFT OUTER JOIN SalesLT.SalesOrderHeader AS soh
                ON c.CustomerID = soh.CustomerID
            GROUP BY
                c.CustomerID,
                c.CompanyName,
                c.EmailAddress
            ORDER BY TotalSpent DESC
        """)
        batches = []
        while True:
            batch = cursor.arrow_batch()
            if batch is None or batch.num_rows == 0:
                break
            batches.append(batch)
        cursor.close()
    
        if not batches:
            return pa.table({})
    
        return pa.Table.from_batches(batches)
    
    
    def fetch_with_reader(conn: Connection) -> pa.Table:
        """Use arrow_reader() to stream results as a RecordBatchReader."""
        cursor = conn.cursor()
        cursor.execute("""
            SELECT
                soh.SalesOrderID,
                soh.OrderDate,
                (soh.SubTotal + soh.TaxAmt + soh.Freight) AS TotalDue,
                c.CompanyName
            FROM SalesLT.SalesOrderHeader AS soh
            INNER JOIN SalesLT.Customer AS c
                ON soh.CustomerID = c.CustomerID
            ORDER BY soh.OrderDate DESC
        """)
        reader = cursor.arrow_reader()
        arrow_table = reader.read_all()
        cursor.close()
        return arrow_table
    
    
    def display_arrow_table(arrow_table: pa.Table, title: str, max_rows: int = 10) -> None:
        """Display an Arrow table using rich formatting."""
        rich_table = Table(title=title)
    
        for name in arrow_table.column_names:
            rich_table.add_column(name, style="bright_white")
    
        for i in range(min(max_rows, arrow_table.num_rows)):
            row = [str(arrow_table.column(col)[i].as_py()) for col in range(arrow_table.num_columns)]
            rich_table.add_row(*row)
    
        if arrow_table.num_rows > max_rows:
            rich_table.add_row(*[f"... ({arrow_table.num_rows - max_rows} more rows)" if col == 0 else "" for col in range(arrow_table.num_columns)])
    
        console.print(rich_table)
        console.print(f"\n[dim]Schema: {arrow_table.num_columns} columns, {arrow_table.num_rows} rows[/dim]\n")
    
    
    def save_to_parquet(arrow_table: pa.Table, file_path: str) -> None:
        """Save an Arrow table to a Parquet file."""
        pq.write_table(arrow_table, file_path)
        console.print(f"[green]Saved {arrow_table.num_rows} rows to {file_path}[/green]\n")
    
    
    def main() -> None:
        conn = get_connection()
    
        # 1. Fetch entire result as an Arrow Table with cursor.arrow()
        console.rule("[bold]cursor.arrow() - Full table fetch[/bold]")
        products = fetch_arrow_table(conn)
        display_arrow_table(products, "Products (Top 10 by List Price)")
    
        # 2. Stream results in batches with cursor.arrow_batch()
        console.rule("[bold]cursor.arrow_batch() - Batch streaming[/bold]")
        customers = fetch_arrow_batches(conn)
        display_arrow_table(customers, "Customers by Total Spent")
    
        # 3. Use RecordBatchReader with cursor.arrow_reader()
        console.rule("[bold]cursor.arrow_reader() - RecordBatchReader[/bold]")
        orders = fetch_with_reader(conn)
        display_arrow_table(orders, "Recent Orders")
    
        # 4. Save to Parquet
        console.rule("[bold]Save to Parquet[/bold]")
        save_to_parquet(products, "products.parquet")
    
        # 5. Read back from Parquet and verify
        loaded = pq.read_table("products.parquet")
        console.print(f"[green]Read back {loaded.num_rows} rows from products.parquet[/green]")
        console.print(f"[dim]Schema: {loaded.schema}[/dim]\n")
    
        conn.close()
    
    
    if __name__ == "__main__":
        main()
    

儲存連接字串

  1. 開啟 .gitignore 檔案,並為 .env 檔案新增排除項目。 您的檔案應該類似於此範例。 請務必儲存並在完成後將其關閉。

    # Python-generated files
    __pycache__/
    *.py[oc]
    build/
    dist/
    wheels/
    *.egg-info
    
    # Virtual environments
    .venv
    
    # Connection strings and secrets
    .env
    
    # Generated data files
    *.parquet
    
  2. 在目前目錄中,建立名為 .env的新檔案。

  3. .env 檔案中,新增一個名為 SQL_CONNECTION_STRING 的連接字串項目。 將此處的範例替換為您的實際連接字符串值。

    SQL_CONNECTION_STRING="Server=<server_name>;Database=<database_name>;Encrypt=yes;TrustServerCertificate=no;Authentication=ActiveDirectoryInteractive"
    

    Important

    保持本地化 .env ,且不使用原始碼控制。 對於 CI 和已部署的環境,請從平台的祕密儲存區中注入連線字串或其組成祕密,而不要在不同機器之間複製 .env

    Tip

    你使用的 連接字串 很大程度取決於你連接的 SQL 資料庫類型。 如果您要連線到 Azure SQL 資料庫Fabric 中的 SQL 資料庫,請使用 [連接字串] 索引標籤中的 ODBC 連接字串。您可能需要根據您的案例調整驗證類型。 如需連接字串及其語法的詳細資訊,請參閱 連接字串語法參考

使用 uv run 執行腳本

Tip

在 macOS 上,兩者都ActiveDirectoryInteractiveActiveDirectoryDefault適用於 Microsoft Entra 認證。 ActiveDirectoryInteractive 每次執行腳本時都會提示你登入。 為避免重複登入提示,請透過 Azure CLI 執行 az login,然後使用 ActiveDirectoryDefault,重複使用快取的憑證。

  • 在先前的終端機視窗中,或開啟至相同目錄的新終端機視窗中,執行下列命令。

    uv run main.py
    

    該腳本示範了三種 Arrow 擷取方法:

    • cursor.arrow() 回傳包含所有列的 complete pyarrow.Table 。 最適合用於中小型結果集,且需要將完整資料集載入記憶體的情況。

    • cursor.arrow_batch() 一次傳回一個 pyarrow.RecordBatch。 最適合大型結果集,想要逐步處理資料而不把所有資料載入記憶體。

    • cursor.arrow_reader() 會回傳用於串流的 pyarrow.RecordBatchReader。 最適合用於管線式處理,或直接傳給可接受讀取器的程式庫。

    腳本也會將產品資料儲存為 Parquet 檔案,並再讀取回來以驗證往返流程。

程式碼運作方式

  1. 連線:腳本從 .env 檔案載入連線字串,並使用 mssql_python.connect() 建立連線。

  2. 完整資料表擷取cursor.arrow() 執行查詢並回傳整個結果集為 pyarrow.Table。 驅動程式透過 Arrow C 資料介面在 C++ 層中轉換資料,繞過 Python 物件建立以提升效能。

  3. 批次串流cursor.arrow_batch() 每次通話回傳一個 pyarrow.RecordBatch 。 迴圈會收集批次直到沒有更多列,然後將它們合併成單一表格。 這種方法適用於大型資料集或想獨立處理每批資料時。

  4. RecordBatchReadercursor.arrow_reader() 回傳一個 pyarrow.RecordBatchReader,一個標準的 Arrow 介面,許多函式庫直接接受。 呼叫 reader.read_all() 會將整個串流匯入一個資料表。

  5. Parquet I/Opyarrow.parquet.write_table() 將 Arrow 表格儲存為壓縮後的 Parquet 檔案。 此格式保留欄位類型並支援高效的部分讀取。

下一步

善用這些文章持續學習:

  • Arrow 整合,適用於進階 Arrow 使用模式,包括批次處理、記憶體管理及函式庫互通性。
  • pandas 整合 ,直接將查詢結果載入 DataFrames。
  • Polars 整合,可從 Arrow 原生查詢建立 Polars 資料框。