參數化查詢對於以下需求至關重要:
- 安全性:防止 SQL 注入攻擊
- 效能:支援查詢計畫重用
- 正確性:正確處理特殊字元與資料型態
mssql-python 驅動程式預設使用 pyformat 帶有 %(name)s 佔位符的參數樣式,但如果你偏好其他參數樣式,也支援其他參數樣式。
基本參數化查詢
具名參數
使用命名的佔位符來傳遞查詢參數:
import mssql_python
conn = mssql_python.connect(
"Server=<server>.database.windows.net;"
"Database=<database>;"
"Authentication=ActiveDirectoryDefault;"
"Encrypt=yes"
)
cursor = conn.cursor()
# Single parameter
cursor.execute(
"SELECT * FROM Production.Product WHERE ProductSubcategoryID = %(category)s",
{"category": 5}
)
# Multiple parameters
cursor.execute(
"SELECT * FROM Production.Product WHERE ProductSubcategoryID = %(cat)s AND ListPrice > %(price)s",
{"cat": 5, "price": 10.00}
)
參數重用
你可以多次引用同一個參數:
cursor.execute("""
SELECT * FROM Production.Product
WHERE (Name LIKE %(search)s OR ProductNumber LIKE %(search)s)
AND ProductSubcategoryID = %(cat)s
""", {"search": "%Road%", "cat": 2})
參數中的資料型別
字串參數
字串會自動加上引號,特殊字元則會安全地逸出:
# Strings are automatically quoted
cursor.execute(
"SELECT * FROM Person.EmailAddress WHERE EmailAddress = %(email)s",
{"email": "ken0@adventure-works.com"}
)
# Special characters are escaped
cursor.execute(
"SELECT * FROM Person.Person WHERE LastName = %(name)s",
{"name": "O'Brien"} # Apostrophe handled safely
)
數值參數
視所需精確度而定,將數值以整數、小數或浮點數形式傳遞:
from decimal import Decimal
# Integer
cursor.execute("SELECT * FROM Production.Product WHERE ProductID = %(id)s", {"id": 42})
# Decimal for financial precision
cursor.execute(
"SELECT * FROM Production.Product WHERE ListPrice >= %(min)s AND ListPrice <= %(max)s",
{"min": Decimal("10.00"), "max": Decimal("100.00")}
)
# Float
cursor.execute(
"SELECT * FROM Production.Product WHERE Weight > %(threshold)s",
{"threshold": 15.0}
)
日期/時間參數
使用 Python 的datetime模組傳遞日期、日期時間和時間值:
from datetime import date, datetime, time
# Date
cursor.execute(
"SELECT * FROM Sales.SalesOrderHeader WHERE OrderDate >= %(date)s AND OrderDate < DATEADD(day, 1, %(date)s)",
{"date": date(2014, 3, 15)}
)
# Datetime
cursor.execute(
"SELECT * FROM Sales.SalesOrderHeader WHERE ModifiedDate >= %(start)s AND ModifiedDate < %(end)s",
{"start": datetime(2014, 3, 1), "end": datetime(2014, 4, 1)}
)
# Time
cursor.execute(
"SELECT * FROM HumanResources.Shift WHERE StartTime >= %(time)s",
{"time": time(9, 0, 0)}
)
NULL 為無
傳遞 None 以插入或更新資料庫中的 NULL 值:
# Insert NULL
cursor.execute("""
CREATE TABLE #NullDemo (ID INT IDENTITY, Name NVARCHAR(50), Email NVARCHAR(100))
""")
cursor.execute(
"INSERT INTO #NullDemo (Name, Email) VALUES (%(name)s, %(email)s)",
{"name": "Guest", "email": None}
)
# Query with NULL
cursor.execute(
"UPDATE #NullDemo SET Email = %(email)s WHERE ID = %(id)s",
{"email": None, "id": 1}
)
二元參數
將二進位資料以 bytes 物件形式插入:
# Binary data
hash_value = b'\x00\x01\x02\x03'
cursor.execute("""
CREATE TABLE #HashDemo (ID INT IDENTITY, DocumentHash VARBINARY(256))
""")
cursor.execute(
"INSERT INTO #HashDemo (DocumentHash) VALUES (%(hash)s)",
{"hash": hash_value}
)
建立動態查詢
條件式 WHERE 子句
根據可選的搜尋條件動態建構 WHERE 子句:
def search_products(cursor, name: str | None = None,
category: int | None = None,
min_price: float | None = None) -> list:
"""Build query with optional conditions."""
conditions = []
params = {}
if name:
conditions.append("Name LIKE %(name)s")
params["name"] = f"%{name}%"
if category:
conditions.append("ProductSubcategoryID = %(category)s")
params["category"] = category
if min_price is not None:
conditions.append("ListPrice >= %(min_price)s")
params["min_price"] = min_price
query = "SELECT TOP 10 * FROM Production.Product"
if conditions:
query += " WHERE " + " AND ".join(conditions)
cursor.execute(query, params)
return cursor.fetchall()
# Usage
products = search_products(cursor, name="Road", min_price=10.0)
包含多個值的 IN 子句
動態建構 IN 子句,並為每個值設置佔位符。 切勿使用字串格式直接注入數值:
def get_products_by_ids(cursor, product_ids: list[int]) -> list:
"""Query with IN clause using qmark (?) placeholders."""
if not product_ids:
return []
placeholders = ", ".join("?" for _ in product_ids)
query = f"SELECT * FROM Production.Product WHERE ProductID IN ({placeholders})"
cursor.execute(query, tuple(product_ids))
return cursor.fetchall()
# Usage
products = get_products_by_ids(cursor, [1, 5, 10, 15])
相同模式也適用於 pyformat (%(name)s) 佔位符:
def get_products_by_ids(cursor, product_ids: list[int]) -> list:
"""Query with IN clause using pyformat placeholders."""
if not product_ids:
return []
# Create named parameters for each ID
params = {f"id{i}": id for i, id in enumerate(product_ids)}
placeholders = ", ".join(f"%(id{i})s" for i in range(len(product_ids)))
query = f"SELECT * FROM Production.Product WHERE ProductID IN ({placeholders})"
cursor.execute(query, params)
return cursor.fetchall()
# Usage
products = get_products_by_ids(cursor, [1, 5, 10, 15])
動態欄位選擇
在動態建立 SELECT 清單前,先使用允許清單來驗證欄位,並保留過濾器值作為參數:
def get_employee(cursor, employee_id: int, columns: list[str] | None = None) -> dict:
"""Get employee with specified columns."""
# Allow list of permitted columns
allowed = {"BusinessEntityID", "LoginID", "JobTitle", "HireDate", "SalariedFlag"}
if columns:
# Validate columns against allow list
safe_columns = [c for c in columns if c in allowed]
if not safe_columns:
raise ValueError("No valid columns specified")
column_list = ", ".join(safe_columns)
else:
column_list = "*"
# ID is always a parameter, never interpolated
query = f"SELECT {column_list} FROM HumanResources.Employee WHERE BusinessEntityID = %(id)s"
cursor.execute(query, {"id": employee_id})
return cursor.fetchone()
排序順序
使用允許清單來驗證排序欄位,然後再插值:
def get_products_sorted(cursor, sort_by: str = "Name",
descending: bool = False) -> list:
"""Get products with validated sort order."""
# Allow list of permitted sort columns
allowed_sorts = {"Name", "ListPrice", "SellStartDate", "ProductID"}
if sort_by not in allowed_sorts:
sort_by = "Name" # Default
direction = "DESC" if descending else "ASC"
# sort_by and direction are validated, safe to interpolate
query = f"SELECT TOP 10 * FROM Production.Product ORDER BY {sort_by} {direction}"
cursor.execute(query)
return cursor.fetchall()
INSERT 運作
單一嵌件
插入一筆使用參數化值的資料列:
cursor.execute("""
CREATE TABLE #ParamInsert (ID INT IDENTITY, Name NVARCHAR(50), Price DECIMAL(10,2), CategoryID INT)
""")
cursor.execute("""
INSERT INTO #ParamInsert (Name, Price, CategoryID)
VALUES (%(name)s, %(price)s, %(category)s)
""", {"name": "New Widget", "price": 29.99, "category": 5})
conn.commit()
插入並回傳識別值
插入新列後,請使用 OUTPUT 取得產生的身份值:
cursor.execute("""
CREATE TABLE #IdentDemo (ProductID INT IDENTITY, Name NVARCHAR(50), Price DECIMAL(10,2), CategoryID INT)
""")
cursor.execute("""
INSERT INTO #IdentDemo (Name, Price, CategoryID)
OUTPUT INSERTED.ProductID
VALUES (%(name)s, %(price)s, %(category)s)
""", {"name": "New Widget", "price": 29.99, "category": 5})
new_id = cursor.fetchval()
conn.commit()
print(f"Created product with ID: {new_id}")
使用 executemany 進行批次插入
可用 executemany() 一個參數化的語句有效插入多列:
Tip
對於大量資料,bulkcopy() 比 executemany() 更快,因為它使用大量插入通訊協定,而非個別的 INSERT 陳述式。 請參閱 批量副本。
cursor.execute("""
CREATE TABLE #BatchDemo (ID INT IDENTITY, Name NVARCHAR(50), Price DECIMAL(10,2), CategoryID INT)
""")
products = [
{"name": "Widget A", "price": 19.99, "cat": 1},
{"name": "Widget B", "price": 29.99, "cat": 1},
{"name": "Widget C", "price": 39.99, "cat": 2},
]
cursor.executemany("""
INSERT INTO #BatchDemo (Name, Price, CategoryID)
VALUES (%(name)s, %(price)s, %(cat)s)
""", products)
conn.commit()
UPDATE 運作
根據條件,使用參數化的 WHERE 子句更新單列或多列:
# Create temp table with sample data
cursor.execute("""
CREATE TABLE #UpdDemo (
ID INT IDENTITY, Name NVARCHAR(50),
Price DECIMAL(10,2), CategoryID INT, ModifiedAt DATETIME
)
""")
cursor.execute("""
INSERT INTO #UpdDemo (Name, Price, CategoryID)
VALUES ('Widget X', 25.00, 5), ('Widget Y', 30.00, 5), ('Gadget Z', 50.00, 3)
""")
# Single row update
cursor.execute("""
UPDATE #UpdDemo
SET Price = %(price)s, ModifiedAt = %(modified)s
WHERE ID = %(id)s
""", {"price": 34.99, "modified": datetime.now(), "id": 1})
# Conditional update
cursor.execute("""
UPDATE #UpdDemo
SET Price = Price * %(multiplier)s
WHERE CategoryID = %(category)s
""", {"multiplier": 1.1, "category": 5})
conn.commit()
DELETE 運作
根據參數化的過濾條件刪除資料表中的列:
# Create temp table with sample data
cursor.execute("""
CREATE TABLE #DelDemo (
ID INT IDENTITY, Name NVARCHAR(50), Status NVARCHAR(20), OrderDate DATE
)
""")
cursor.execute("""
INSERT INTO #DelDemo (Name, Status, OrderDate)
VALUES ('Order1', 'Active', '2024-06-01'), ('Order2', 'Cancelled', '2022-05-01'),
('Order3', 'Cancelled', '2022-11-01')
""")
# Delete single row
cursor.execute(
"DELETE FROM #DelDemo WHERE ID = %(id)s",
{"id": 1}
)
# Delete with conditions
cursor.execute("""
DELETE FROM #DelDemo
WHERE Status = %(status)s AND OrderDate < %(date)s
""", {"status": "Cancelled", "date": date(2023, 1, 1)})
conn.commit()
安全性考慮
切勿插值使用者輸入
務必使用參數來安全避開使用者輸入:
# DANGEROUS - SQL injection vulnerability!
user_input = "'; DROP TABLE Users;--"
query = f"SELECT * FROM Person.Person WHERE LastName = '{user_input}'" # DON'T DO THIS
# SAFE - always use parameters
cursor.execute(
"SELECT * FROM Person.Person WHERE LastName = %(name)s",
{"name": user_input} # Input is safely escaped
)
驗證資料表與欄位名稱
使用允許清單來驗證無法參數化的表格與欄位識別碼:
def query_table(cursor, table: str, columns: list[str]):
"""Query with validated table and column names."""
# Allow list of permitted tables
allowed_tables = {"Person.Person", "Production.Product", "Sales.SalesOrderHeader"}
if table not in allowed_tables:
raise ValueError(f"Invalid table: {table}")
# Allow list of permitted columns per table
allowed_columns = {
"Person.Person": {"BusinessEntityID", "FirstName", "LastName"},
"Production.Product": {"ProductID", "Name", "ListPrice"},
"Sales.SalesOrderHeader": {"SalesOrderID", "CustomerID", "TotalDue"},
}
safe_columns = [c for c in columns if c in allowed_columns.get(table, set())]
if not safe_columns:
raise ValueError("No valid columns")
# Safe to interpolate after validation
query = f"SELECT TOP 5 {', '.join(safe_columns)} FROM {table}"
cursor.execute(query)
return cursor.fetchall()
使用預存程序來執行複雜作業
儲存程序增加了另一層保護,並使複雜的商業邏輯能在伺服器端執行:
cursor.execute("""
EXECUTE dbo.uspGetEmployeeManagers @BusinessEntityID = %(id)s
""", {"id": 5})
rows = cursor.fetchall()
效能優點
查詢計畫快取
使用參數化查詢時,SQL Server 會重複使用相同的執行計畫,跨越不同參數值,而不是為每個查詢編譯新的計畫。
for product_id in range(1, 100):
cursor.execute(
"SELECT * FROM Production.Product WHERE ProductID = %(id)s",
{"id": product_id}
)
備妥語句
對於經常執行的查詢,使用預先準備好的陳述。 驅動程式會自動預先準備陳述式,因此以不同參數執行相同的查詢範本時,可受益於預先準備。
query = "SELECT Name, ListPrice FROM Production.Product WHERE ProductSubcategoryID = %(cat)s"
for category in [1, 2, 3, 4, 5]:
cursor.execute(query, {"cat": category})
products = cursor.fetchall()