Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Driver mssql-python menyediakan beberapa fitur dan pola untuk mengoptimalkan performa aplikasi SQL Server, termasuk pengumpulan koneksi, pengoptimalan kueri, dan operasi massal.
Manajemen koneksi
Gunakan pengumpulan koneksi
Pengumpulan koneksi sudah terpasang. Saat Anda memanggil conn.close(), koneksi dikembalikan ke pool untuk digunakan kembali alih-alih dihancurkan, sehingga pemanggilan connect() berikutnya tidak perlu melakukan handshake yang memakan banyak sumber daya:
import mssql_python
def get_data():
conn = mssql_python.connect(
"Server=<server>.database.windows.net;Database=<database>;"
"Authentication=ActiveDirectoryDefault;Encrypt=yes"
)
try:
cursor = conn.cursor()
cursor.execute("SELECT TOP 10 Name, ListPrice FROM Production.Product")
return cursor.fetchall()
finally:
conn.close()
Mengonfigurasi ukuran kumpulan untuk beban kerja
Sesuaikan ukuran kumpulan berdasarkan persyaratan konkurensi Anda. Jika aplikasi Anda menangani banyak pengguna secara simultan, tingkatkan kumpulan. Untuk beban kerja yang lebih ringan, kumpulan yang lebih kecil menghemat sumber daya server:
import mssql_python
mssql_python.pooling(
max_size=50, # Default is 100; reduce or increase for your workload
idle_timeout=600 # Seconds before idle connections are recycled
)
Gunakan kembali koneksi dalam operasi
Membuka koneksi baru untuk setiap kueri menambahkan overhead bahkan dengan pengumpulan. Sebagai gantinya, tahan satu koneksi selama durasi operasi logis:
# Bad: New connection per query
def bad_pattern(product_ids):
for pid in product_ids:
conn = mssql_python.connect(connection_string)
cursor = conn.cursor()
cursor.execute("SELECT Name, ListPrice FROM Production.Product WHERE ProductID = %(pid)s", {"pid": pid})
row = cursor.fetchone()
process(row)
conn.close()
# Good: Single connection for all queries
def good_pattern(product_ids):
conn = mssql_python.connect(connection_string)
cursor = conn.cursor()
try:
for pid in product_ids:
cursor.execute("SELECT Name, ListPrice FROM Production.Product WHERE ProductID = %(pid)s", {"pid": pid})
row = cursor.fetchone()
process(row)
finally:
conn.close()
Jaga agar koneksi tetap terbuka di layanan yang berjalan lama
Server web, pekerja antrean, dan tugas terjadwal yang berjalan terus-menerus sebaiknya mempertahankan koneksi tetap terbuka daripada membuka dan menutup koneksi untuk setiap operasi. Membuka koneksi melibatkan jabat tangan TCP, negosiasi TLS, dan otentikasi, yang dapat memakan waktu 50-200 ms tergantung pada jarak jaringan dan metode autentikasi. Untuk pekerja antrean yang memproses ribuan pesan per jam, overhead itu bertambah dengan cepat.
Jaga agar koneksi tetap terbuka selama masa pakai pekerja dan sambungkan kembali saat koneksi gagal. Tidur di antara iterasi untuk menghindari memukul server saat antrean kosong:
import mssql_python
import time
def run_worker(connection_string: str, poll_interval: float = 1.0):
conn = None
try:
while True:
try:
if conn is None:
conn = mssql_python.connect(connection_string)
cursor = conn.cursor()
cursor.execute("SELECT TOP 1 * FROM dbo.JobQueue WHERE Status = 'Pending' ORDER BY CreatedDate")
job = cursor.fetchone()
if job:
try:
process_job(job)
cursor.execute("UPDATE dbo.JobQueue SET Status = 'Done' WHERE JobID = %(job_id)s", {"job_id": job[0]})
except Exception:
cursor.execute("UPDATE dbo.JobQueue SET Status = 'Failed' WHERE JobID = %(job_id)s", {"job_id": job[0]})
conn.commit()
else:
time.sleep(poll_interval) # No work available, wait before polling again
except mssql_python.OperationalError:
# Connection lost, reconnect on next iteration
conn = None
time.sleep(poll_interval)
finally:
if conn is not None:
conn.close()
Dengan pengumpulan koneksi diaktifkan (default), kumpulan menangani koneksi menganggur untuk Anda. Tetapi jika Anda menonaktifkan pengumpulan atau menggunakan satu koneksi khusus, atur Connection Timeout dan Command Timeout di string koneksi Anda untuk mendeteksi koneksi kedaluwarsa lebih awal daripada macet.
Pengoptimalan kueri
Mengambil hanya data yang diperlukan
Memilih hanya kolom yang digunakan aplikasi Anda mengurangi transfer jaringan, konsumsi memori, dan waktu eksekusi kueri.
# Bad: Select all columns
cursor.execute("SELECT * FROM Sales.SalesOrderHeader WHERE CustomerID = %(customer_id)s", {"customer_id": 1})
# Good: Select specific columns
cursor.execute("""
SELECT SalesOrderID, OrderDate, TotalDue
FROM Sales.SalesOrderHeader
WHERE CustomerID = %(customer_id)s
""", {"customer_id": 1})
Gunakan metode fetch yang sesuai
Driver menyediakan beberapa metode pengambilan data. Gunakan salah satu yang cocok dengan ukuran hasil Anda:
-
fetchval()mengembalikan satu nilai skalar dengan overhead minimal. -
fetchall()memuat seluruh kumpulan hasil ke dalam memori, yang berfungsi dengan baik untuk tabel kecil. -
fetchmany(n)mengambil baris secara bertahap, sehingga penggunaan memori tetap konstan untuk kumpulan hasil yang besar.
Ukuran batch yang tepat bergantung pada lebar baris fetchmany(). Untuk baris sempit (beberapa kolom kecil, masing-masing kira-kira 1 KB), 1.000 baris menyimpan setiap batch sekitar 1 MB memori. Untuk baris yang lebih lebar dengan string besar atau kolom biner, gunakan ukuran batch yang lebih kecil. Mulailah dengan 1.000 dan sesuaikan berdasarkan data Anda.
def process_batch(rows):
# Example: print each row. Replace with your own logic.
for row in rows:
print(row)
# Single value
cursor.execute("SELECT COUNT(*) FROM Production.Product")
count = cursor.fetchval()
# Small result set
cursor.execute("SELECT ProductCategoryID, Name FROM Production.ProductCategory")
categories = cursor.fetchall()
# Large result set - process in batches
cursor.execute("SELECT SalesOrderID, OrderDate, TotalDue FROM Sales.SalesOrderHeader")
while True:
batch = cursor.fetchmany(1000)
if not batch:
break
process_batch(batch)
Gunakan paginasi server-side
Alih-alih mengambil semua baris dan mengiris di Python, gunakan OFFSET/FETCH NEXT untuk mengambil hanya halaman yang Anda butuhkan.
def get_page(cursor, page: int, page_size: int = 50) -> list:
"""Get paginated results efficiently."""
offset = (page - 1) * page_size
cursor.execute("""
SELECT ProductID, Name, ListPrice
FROM Production.Product
ORDER BY ProductID
OFFSET %(offset)s ROWS
FETCH NEXT %(page_size)s ROWS ONLY
""", {"offset": offset, "page_size": page_size})
return cursor.fetchall()
Gunakan SET NOCOUNT ON
Secara default, SQL Server mengirimkan pesan "baris yang terpengaruh" setelah setiap pernyataan DML.
SET NOCOUNT ON menyembunyikan pesan-pesan ini dan mengurangi lalu lintas jaringan. Ini adalah pengaturan tingkat sesi, jadi atur sekali setelah tersambung daripada menyematkannya di setiap kueri.
# Set once after connecting
cursor.execute("SET NOCOUNT ON")
# All subsequent statements on this connection skip the row-count message
cursor.execute(
"INSERT INTO Log (Message) VALUES (%(message)s)",
{"message": "Log entry"}
)
Pilih metode sisipan yang tepat
Driver menyediakan tiga cara untuk menyisipkan data, masing-masing sesuai dengan skala yang berbeda:
| Metode | Jumlah baris | Mengapa |
|---|---|---|
execute() |
1 baris setiap panggilan | Gunakan untuk operasi baris tunggal seperti pengiriman formulir atau penangan API di mana Anda memerlukan ID yang dimasukkan segera. |
executemany() |
~10 - 1.000 baris | Menggunakan pengikatan parameter berdasarkan kolom untuk throughput yang lebih baik daripada loop. Mengirim setiap baris sebagai pernyataan berparameter. |
bulkcopy() |
100 baris atau lebih | Menggunakan protokol penyisipan massal TDS, yang jauh lebih efisien daripada penyisipan baris demi baris. Terbaik untuk pemuatan data, migrasi, dan pemrosesan batch. |
Untuk detail dan contoh selengkapnya, lihat Pola pemuatan dan pergerakan data.
Penyisipan tunggal dengan execute()
Gunakan untuk sisipan satu kali di mana Anda membutuhkan hasilnya segera.
Production.Product memiliki beberapa kolom NOT NULL tanpa nilai bawaan, jadi pernyataan INSERT mencantumkan semuanya:
from datetime import datetime
cursor.execute(
"""
INSERT INTO Production.Product
(Name, ProductNumber, SafetyStockLevel, ReorderPoint,
StandardCost, ListPrice, DaysToManufacture, SellStartDate)
VALUES (%(name)s, %(number)s, %(safety)s, %(reorder)s,
%(cost)s, %(price)s, %(days)s, %(start)s)
""",
{
"name": "Widget", "number": "WG-1001",
"safety": 100, "reorder": 75,
"cost": 12.50, "price": 19.99,
"days": 1, "start": datetime(2024, 1, 1),
},
)
conn.commit()
Penyisipan batch dengan executemany()
executemany() mengikat parameter berdasarkan kolom dan mengirimkannya secara efisien. Gunakan ini untuk batch berukuran sedang alih-alih memanggil execute() dalam perulangan. Perhatikan bahwa executemany() memerlukan penanda posisi ? dengan daftar tuple, sementara execute() mendukung parameter bernama ?%(name)s dengan dikte. Lihat Kueri berparameter untuk detail tentang setiap gaya.
rows = [
("Widget A", "WG-1001", 100, 75, 12.50, 19.99, 1, datetime(2024, 1, 1)),
("Widget B", "WG-1002", 100, 75, 15.00, 24.99, 1, datetime(2024, 1, 1)),
("Widget C", "WG-1003", 100, 75, 18.00, 29.99, 1, datetime(2024, 1, 1)),
]
cursor.executemany(
"""
INSERT INTO Production.Product
(Name, ProductNumber, SafetyStockLevel, ReorderPoint,
StandardCost, ListPrice, DaysToManufacture, SellStartDate)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
rows,
)
conn.commit()
Salinan massal untuk beban besar
Jika throughput lebih penting daripada kontrol per baris, beralihlah ke bulkcopy(). Ini mengalirkan baris data melalui protokol penyisipan massal TDS dan menghindari overhead per baris pada pernyataan berparameter. Titik peralihan yang tepat saat bulkcopy() mengungguli executemany() bergantung pada lebar baris dan latensi jaringan, tetapi biasanya berada pada kisaran seratusan baris. Untuk batch yang sangat kecil, executemany() lebih sederhana karena bulkcopy() membuat koneksi internal terpisah dan melakukan secara otomatis.
Tidak seperti execute() dan executemany(), bulkcopy() memetakan nilai ke kolom berdasarkan posisi, bukan dengan INSERT daftar kolom. Berikan column_mappings untuk menentukan nama kolom tujuan tempat data dimuat, sehingga tuple pada sumber sejajar dengan kolom yang benar, bukan dengan kolom identitas pertama pada tabel:
result = cursor.bulkcopy(
"Production.Product",
rows,
column_mappings=["Name", "ProductNumber", "SafetyStockLevel", "ReorderPoint",
"StandardCost", "ListPrice", "DaysToManufacture", "SellStartDate"],
)
print(f"Copied {result['rows_copied']} rows")
Untuk beban yang sangat besar, gunakan generator untuk menghindari pemuatan seluruh himpunan data ke dalam memori dan atur batch_size untuk melakukan secara berkala:
import csv
def csv_rows(path):
with open(path, newline="") as f:
reader = csv.reader(f)
next(reader) # Skip header
for row in reader:
yield tuple(row)
cursor.bulkcopy(
"Production.Product",
csv_rows("products.csv"),
column_mappings=["Name", "ProductNumber", "SafetyStockLevel", "ReorderPoint",
"StandardCost", "ListPrice", "DaysToManufacture", "SellStartDate"],
batch_size=5000,
)
Strategi tembolok
Untuk data referensi yang jarang berubah (kategori, tabel pencarian, konfigurasi), cache hasil dalam aplikasi Anda alih-alih mengkueri setiap permintaan.
functools.lru_cache Python menyediakan memoisasi sederhana, tetapi cache-nya disimpan terus hingga proses dimulai ulang. Jika data yang mendasarinya dapat berubah, gunakan cachetools.TTLCache untuk me-refresh secara otomatis setelah batas waktu:
from cachetools import TTLCache, cached
category_cache = TTLCache(maxsize=1, ttl=300) # Refresh every 5 minutes
@cached(category_cache)
def get_categories(connection_string: str) -> tuple:
conn = mssql_python.connect(connection_string)
try:
cursor = conn.cursor()
cursor.execute("SELECT ProductCategoryID, Name FROM Production.ProductCategory")
return cursor.fetchall()
finally:
conn.close()
Pengoptimalan jaringan
Minimalkan perjalanan pulang pergi
Setiap kueri memerlukan satu perjalanan pulang-pergi melalui jaringan ke server. Gabungkan kueri terkait ke dalam satu batch dan gunakan nextset() untuk maju melalui kumpulan hasil:
# Bad: Multiple round trips
cursor.execute("SELECT CustomerID, AccountNumber FROM Sales.Customer WHERE CustomerID = %(customer_id)s", {"customer_id": 1})
customer = cursor.fetchone()
cursor.execute("SELECT SalesOrderID, OrderDate, TotalDue FROM Sales.SalesOrderHeader WHERE CustomerID = %(customer_id)s", {"customer_id": 1})
orders = cursor.fetchall()
cursor.execute("SELECT COUNT(*) FROM Sales.SalesOrderDetail WHERE SalesOrderID IN (SELECT SalesOrderID FROM Sales.SalesOrderHeader WHERE CustomerID = %(customer_id)s)", {"customer_id": 1})
detail_count = cursor.fetchval()
# Good: Single round trip
cursor.execute("""
SELECT CustomerID, AccountNumber FROM Sales.Customer WHERE CustomerID = %(customer_id)s;
SELECT SalesOrderID, OrderDate, TotalDue FROM Sales.SalesOrderHeader WHERE CustomerID = %(customer_id)s;
SELECT COUNT(*) FROM Sales.SalesOrderDetail WHERE SalesOrderID IN (SELECT SalesOrderID FROM Sales.SalesOrderHeader WHERE CustomerID = %(customer_id)s);
""", {"customer_id": 1})
customer = cursor.fetchone()
cursor.nextset()
orders = cursor.fetchall()
cursor.nextset()
detail_count = cursor.fetchval()
Gunakan pemrosesan sisi server untuk logika yang kompleks
Dorong agregasi dan pemfilteran ke SQL Server alih-alih mengambil baris mentah dan memprosesnya dalam Python. Server mengembalikan satu baris ringkasan, bukan berpotensi ribuan baris detail:
cursor.execute("""
SELECT p.Name, COUNT(sod.SalesOrderDetailID) AS OrderCount, SUM(sod.LineTotal) AS TotalSales
FROM Production.Product p
JOIN Sales.SalesOrderDetail sod ON p.ProductID = sod.ProductID
WHERE p.ProductID = %(product_id)s
GROUP BY p.Name
""", {"product_id": 707})
Hindari operasi kursor yang berselang-seling
Driver mssql-python tidak mendukung Beberapa Set Hasil Aktif (MARS). Hanya satu kursor yang dapat memiliki kueri aktif per koneksi. Ambil kumpulan hasil pertama sepenuhnya sebelum menjalankan kueri berikutnya, atau gunakan koneksi kedua:
connection_string = (
"Server=<server>.database.windows.net;Database=<database>;"
"Authentication=ActiveDirectoryDefault;Encrypt=yes"
)
# Option 1: Fetch first, then query (single connection)
# Warning: This is an N+1 pattern. Each iteration is a round trip.
# Use this only when the JOIN in Option 2 is not possible.
conn = mssql_python.connect(connection_string)
cursor = conn.cursor()
cursor.execute("SELECT ProductID FROM Production.Product WHERE ProductSubcategoryID = 1")
product_ids = [row[0] for row in cursor.fetchall()]
for pid in product_ids:
cursor.execute("SELECT ProductID, LocationID, Quantity FROM Production.ProductInventory WHERE ProductID = %(product_id)s", {"product_id": pid})
inventory = cursor.fetchone()
conn.close()
# Option 2: Use a JOIN instead of N+1 queries (preferred)
conn = mssql_python.connect(connection_string)
cursor = conn.cursor()
cursor.execute("""
SELECT p.ProductID, p.Name, i.Quantity
FROM Production.Product p
LEFT JOIN Production.ProductInventory i ON p.ProductID = i.ProductID
WHERE p.ProductSubcategoryID = 1
""")
results = cursor.fetchall()
conn.close()
Manajemen memori
Memproses hasil besar dalam potongan
Memuat tabel jutaan baris ke dalam daftar menghabiskan memori sebanding dengan kumpulan hasil penuh. Gunakan OFFSET dan FETCH NEXT untuk melakukan paginasi data di sisi server dan memproses satu chunk data pada satu waktu.
def quote_id(identifier: str) -> str:
"""Quote a possibly schema-qualified SQL identifier to prevent SQL injection."""
return ".".join("[" + part.replace("]", "]]") + "]" for part in identifier.split("."))
def process_large_table(cursor, table: str, columns: list[str], key_column: str, processor, chunk_size: int = 10000):
"""Process large table without loading all data."""
safe_table = quote_id(table)
safe_key = quote_id(key_column)
col_list = ", ".join(quote_id(c) for c in columns)
cursor.execute(f"SELECT COUNT(*) FROM {safe_table}")
total = cursor.fetchval()
offset = 0
while offset < total:
cursor.execute(f"""
SELECT {col_list} FROM {safe_table}
ORDER BY {safe_key}
OFFSET ? ROWS
FETCH NEXT ? ROWS ONLY
""", (offset, chunk_size))
chunk = cursor.fetchall()
processor(chunk)
offset += chunk_size
print(f"Processed {min(offset, total)}/{total}")
# key_column must be unique, otherwise rows can be duplicated or skipped across pages
process_large_table(
cursor,
"Production.TransactionHistory",
["TransactionID", "ProductID", "Quantity", "ActualCost"],
"TransactionID",
lambda chunk: None, # replace with your row-processing logic
)
Menggunakan generator untuk streaming
Pembungkus fetchmany() generator Python menjaga penggunaan memori tetap konstan terlepas dari ukuran tabel. Pemanggil mengulangi baris demi baris tanpa memuat kumpulan hasil lengkap. Untuk sumber ekstra besar, gabungkan tabel dengan UNION ALL dan streaming hasil gabungan dengan cara yang sama.
def stream_query(cursor, query: str, params: dict = None, batch_size: int = 1000):
cursor.execute(query, params or {})
while True:
batch = cursor.fetchmany(batch_size)
if not batch:
break
for row in batch:
yield row
# Union the live and archive transaction tables into one extra-large result set
query = """
SELECT TransactionID, ProductID, Quantity, ActualCost FROM Production.TransactionHistory
UNION ALL
SELECT TransactionID, ProductID, Quantity, ActualCost FROM Production.TransactionHistoryArchive
"""
count = 0
for row in stream_query(cursor, query, batch_size=5000):
count += 1
print(f"Streamed {count} rows")
Bersihkan sumber daya dengan segera
Koneksi yang tidak tertutup mengikat sumber daya server dan dapat menghabiskan kumpulan koneksi. Gunakan pengelola konteks untuk menjamin pembersihan bahkan ketika pengecualian terjadi.
from contextlib import contextmanager
@contextmanager
def database_connection(connection_string: str):
conn = mssql_python.connect(connection_string)
try:
yield conn
finally:
conn.close()
with database_connection(connection_string) as conn:
cursor = conn.cursor()
cursor.execute("SELECT TOP 10 Name, ListPrice FROM Production.Product")
data = cursor.fetchall()
Pantau penggunaan memori
Set hasil yang besar, cache berumur panjang, dan objek koneksi semuanya menghabiskan memori. Jika aplikasi Anda berjalan sebagai layanan, kebocoran memori akibat kursor yang tidak ditutup atau cache yang tidak dibatasi pada akhirnya dapat menyebabkan proses diakhiri oleh OS atau runtime kontainer.
Gunakan modul Python tracemalloc untuk menjepret memori dan menemukan alokasi terbesar.
import tracemalloc
tracemalloc.start()
# ... run your workload ...
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")
for stat in top_stats[:10]:
print(stat)
Sumber umum pertumbuhan memori yang tidak terduga meliputi:
- Memanggil
fetchall()pada kueri yang mengembalikan jutaan baris. Gunakanfetchmany()atau generator sebagai gantinya. - Menyimpan hasil kueri dalam cache tanpa
maxsizeatau TTL. Cache akan terus bertambah sampai proses dimulai ulang. - Membuat kursor dalam loop tanpa menutupnya. Setiap kursor yang terbuka menyimpan kumpulan hasilnya dalam memori.
Pengoptimalan rencana indeks dan kueri
Periksa kinerja kueri pada sisi server
Gunakan SET STATISTICS TIME ON dan SET STATISTICS IO ON untuk melihat berapa lama kueri yang dibutuhkan di server dan berapa banyak data yang mereka baca. Pembacaan logikal yang tinggi biasanya menunjukkan adanya indeks yang hilang. Jalankan pernyataan ini di SQL Server Management Studio atau ekstensi MSSQL untuk Visual Studio Code, di mana output muncul di panel Pesan:
SET STATISTICS TIME ON;
SET STATISTICS IO ON;
SELECT * FROM Production.Product WHERE ProductSubcategoryID = 1;
SET STATISTICS TIME OFF;
SET STATISTICS IO OFF;
Anda akan melihat output seperti:
Table 'Product'. Scan count 1, logical reads 3
SQL Server Execution Times: CPU time = 0 ms, elapsed time = 1 ms.
Jika Anda melihat pembacaan logis tinggi atau pemindaian tabel, pertimbangkan untuk menambahkan indeks.
Gunakan petunjuk kueri sebagai solusi taktis
Petunjuk kueri menggantikan indeks pengoptimal kueri dan pilihan strategi gabungan. Dalam produksi, hal ini berguna sebagai tambalan cepat yang berisiko rendah ketika kueri tiba-tiba mengalami penurunan kinerja. Anda dapat segera menyebarkan petunjuk dalam kode aplikasi Anda untuk menstabilkan kueri saat Anda menyelidiki akar penyebab (indeks yang hilang, statistik kedaluwarsa, atau perubahan skema).
Hindari meninggalkan petunjuk di tempatnya secara permanen. Ketika distribusi data atau skema berubah, petunjuk hardcode dapat memperburuk keadaan. Perlakukan mereka sebagai sementara dan tinjau kembali setelah masalah yang mendasarinya diselesaikan:
cursor.execute("""
SELECT * FROM Production.Product WITH (INDEX(AK_Product_Name))
WHERE ProductSubcategoryID = %(subcategory_id)s
""", {"subcategory_id": 1})
Gunakan OPTION (RECOMPILE) untuk melewati paket cache yang buruk
SQL Server menyimpan paket kueri dalam cache berdasarkan kumpulan nilai parameter pertama yang dilihatnya. Jika distribusi data sangat bervariasi antar panggilan, paket yang di-cache mungkin berkinerja buruk untuk beberapa nilai. Masalah ini, yang disebut parameter sniffing, sering muncul sebagai kueri yang "dulunya cepat" tiba-tiba memakan waktu beberapa detik atau menit.
OPTION (RECOMPILE)memaksa SQL Server untuk membangun rencana baru untuk setiap eksekusi, yang merupakan perbaikan langsung yang efektif yang dapat Anda terapkan tanpa perubahan sisi server. Komprominya adalah biaya kompilasi kecil untuk setiap pemanggilan, tetapi untuk kueri yang jarang dijalankan atau mengembalikan kumpulan hasil dengan ukuran yang bervariasi, biaya tersebut dapat diabaikan dibandingkan dengan menjalankan rencana eksekusi yang buruk.
Setelah menstabilkan masalah, Anda dapat meluangkan waktu untuk menerapkan perbaikan permanen seperti menulis ulang kueri, menambahkan indeks yang difilter, atau menggunakan panduan paket:
cursor.execute("""
SELECT * FROM Sales.SalesOrderHeader
WHERE OrderDate > %(start_date)s
OPTION (RECOMPILE)
""", {"start_date": start_date})
Pemantauan kinerja
Atur waktu pertanyaan Anda
Untuk menemukan operasi yang lambat, bungkus kueri dengan time.perf_counter():
import time
start = time.perf_counter()
cursor.execute("SELECT SalesOrderID, OrderDate, TotalDue FROM Sales.SalesOrderHeader WHERE CustomerID = %(customer_id)s", {"customer_id": customer_id})
rows = cursor.fetchall()
elapsed = time.perf_counter() - start
print(f"Query returned {len(rows)} rows in {elapsed:.3f}s")
Untuk tampilan yang lebih luas tentang tempat aplikasi Anda menghabiskan waktu, gunakan modul bawaan cProfile Python:
python -m cProfile -s cumtime my_app.py
Tampilan ini menunjukkan waktu kumulatif per panggilan fungsi, yang membantu Anda mengidentifikasi apakah kelambatan dalam eksekusi kueri, pemrosesan data, atau latensi jaringan.
Menggunakan Query Store untuk analisis sisi server
Pengaturan waktu di sisi klien menunjukkan berapa lama kueri berlangsung dari sudut pandang aplikasi Anda, tetapi mencakup latensi jaringan, waktu eksekusi server, dan pemrosesan klien. Query Store menangkap rencana eksekusi dan statistik runtime di server, sehingga Anda dapat melihat dengan tepat bagaimana SQL Server mengeksekusi setiap kueri, seberapa sering berjalan, dan bagaimana performanya berubah dari waktu ke waktu.
Query Store sangat berguna untuk mengidentifikasi pengendusan parameter, regresi rencana, dan kueri yang menghabiskan sumber daya server paling banyak. Anda dapat membuat kueri pada view sys.query_store_runtime_stats dan sys.query_store_plan tersebut secara langsung, atau menggunakan laporan Query Store bawaan di SQL Server Management Studio.
Menggunakan Laporan Dasbor Performa
Laporan Dasbor Performa di SQL Server Management Studio memberikan gambaran umum real-time tentang kesehatan SQL Server, termasuk jenis tunggu saat ini, kueri mahal aktif, dan tren CPU/IO. Gunakan fitur ini untuk dengan cepat mengidentifikasi hambatan tanpa menulis kueri langsung pada DMV.
Daftar Periksa Kinerja
Koneksi
- [ ] Aktifkan pengumpulan koneksi.
- [ ] Tentukan ukuran pool sesuai beban kerja Anda.
- [ ] Gunakan kembali koneksi dalam operasi.
- [ ] Jaga agar koneksi tetap terbuka di layanan yang berjalan lama.
Queries
- [ ] Pilih hanya kolom yang Anda butuhkan.
- [ ] Gunakan metode pengambilan yang sesuai untuk setiap kueri.
- [ ] Terapkan paginasi sisi server.
- [ ] Atur
SET NOCOUNT ONsekali setelah terhubung. - [ ] Minimalkan round trip dengan mengelompokkan kueri secara batch.
Sisipan
- [ ] Gunakan
execute()untuk sisipan baris tunggal. - [ ] Gunakan
executemany()untuk batch kecil hingga sedang (~10-1.000 baris). - [ ] Gunakan
bulkcopy()saat throughput lebih penting daripada kontrol per baris.
Penggunaan Cache
- [ ] Simpan data referensi dengan TTL agar tidak menyajikan hasil usang.
Resources
- [ ] Proses hasil besar dalam potongan atau dengan generator.
- [ ] Segera bersihkan sambungan.
- [ ] Pantau penggunaan memori pada layanan yang berjalan dalam jangka waktu lama dengan
tracemalloc.