Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Драйвер mssql-python предоставляет несколько функций и шаблонов для оптимизации производительности приложений SQL Server, включая пул соединений, оптимизацию запросов и массовые операции.
Управление подключениями
Использование пула соединений
Пул соединений встроен. Когда вы вызываете conn.close(), соединение возвращается в пул для повторного использования, а не уничтожается, поэтому при последующих вызовах connect() пропускается ресурсоёмкая процедура установки соединения:
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()
Настройка размера пула для рабочей нагрузки
Регулируйте размер пула в зависимости от ваших требований к параллельности. Если ваше приложение обслуживает множество одновременных пользователей, увеличите пул. Для более лёгких рабочих нагрузок меньший пул экономит ресурсы сервера:
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
)
Повторное использование соединений внутри операций
Открытие нового соединения для каждого запроса приводит к дополнительным накладным расходам даже при использовании пула соединений. Вместо этого удерживать одно соединение на протяжении всей логической операции:
# 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()
Сохраняйте открытые соединения в долгосрочных сервисах
Веб-серверы, работники очередей и запланированные задачи, выполняющиеся непрерывно, должны держать соединения открытыми, а не подключаться и отключаться при каждой операции. Открытие соединения включает рукопожатие по TCP, согласование TLS и аутентификацию, что может занять от 50 до 200 мс в зависимости от расстояния до сети и метода аутентификации. Для работника очереди, обрабатывающего тысячи сообщений в час, эти накладные расходы быстро накапливаются.
Поддерживайте соединение открытым в течение всего времени работы воркера и переподключайтесь при разрыве соединения. Спите между итерациями, чтобы не нагружать сервер, когда очередь пуста:
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()
При включённом пуле соединений (по умолчанию) пул сам управляет неактивными соединениями. Но если вы отключите пул или используете одно выделенное соединение, установите Connection Timeout и Command Timeout в строка подключения так, чтобы устаревшие соединения выявлялись заранее, а не зависали.
Оптимизация запросов
Получайте только необходимые данные
Выбор только столбцов, которые используется приложением, снижает передачу сети, потребление памяти и время выполнения запросов.
# 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})
Используйте соответствующие методы извлечения
Драйвер предоставляет несколько методов получения информации. Используйте тот, который соответствует размеру вашего результата:
-
fetchval()возвращает одно скалярное значение с минимальными накладными расходами. -
fetchall()загружает весь набор результатов в память, что хорошо работает для небольших таблиц. -
fetchmany(n)извлекает строки пакетами, поддерживая постоянный расход памяти даже для больших наборов результатов.
Подходящий размер пакета fetchmany() зависит от ширины строк. Для узких строк (несколько небольших столбцов, примерно по 1 КБ каждый) 1 000 строк обеспечивают объём каждой партии около 1 МБ памяти. Для более широких строк с большими строками или двоичными столбцами используйте меньший размер партии. Начните с 1000 и скорректируйте значение на основе ваших данных.
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)
Используйте серверную пагинацию
Вместо того чтобы получать все строки и делать срезы в Python, используйте OFFSET/FETCH NEXT, чтобы извлечь только нужную страницу.
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()
Использовать SET NOCOUNT ON
По умолчанию SQL Server отправляет сообщение о количестве затронутых строк после каждой инструкции DML.
SET NOCOUNT ON подавляет эти сообщения и снижает сетевой трафик. Это настройка сессионного уровня, поэтому устанавливайте его один раз после подключения, а не встраивайте в каждый запрос.
# 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"}
)
Выберите правильный метод вставки
Драйвер предлагает три способа вставки данных, каждый из которых соответствует разной шкале:
| Метод | Число строк | Почему |
|---|---|---|
execute() |
1 строка для каждого вызова | Используйте их для одностроковых операций, таких как отправка форм или обработчики API, где вставленный идентификатор нужен сразу же. |
executemany() |
~10-1 000 рядов | Используется привязка параметров по столбцам для повышения пропускной способности по сравнению с использованием цикла. Отправляет каждую строку в виде параметризованной инструкции. |
bulkcopy() |
Сотни строк и более | Использует протокол TDS bulk insert, который значительно более эффективен, чем построчные вставки. Лучше всего подходит для загрузки данных, миграций и пакетной обработки. |
Для более подробной информации и примеров см. раздел «Загрузка данных и паттерны передвижения».
Одиночная вставка с execute()
Используйте разовые вставки, где результат нужен сразу.
Production.Product имеет несколько столбцов NOT NULL без значений по умолчанию, поэтому в операторе INSERT перечислены все эти столбцы:
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()
Пакетные вставки с executemany()
executemany() связывает параметры по столбцам и эффективно отправляет их. Используйте это для пакетов среднего размера вместо вызова execute() в цикле. Обратите внимание, что executemany() требует позиционных ? маркеров и списка кортежей, тогда как execute() поддерживает как ?, так и именованные %(name)s параметры со словарями. См. раздел «Параметризованные запросы » для подробностей каждого стиля.
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()
Массовое копирование при больших объёмах
Если пропускная способность важнее, чем управление отдельными строками, переключитесь на bulkcopy(). Это передаёт строки через протокол пакетной вставки TDS и позволяет избежать накладных расходов на каждую строку, связанных с параметризованными выражениями. Точная точка, в которой bulkcopy() превосходит executemany() по производительности, зависит от ширины строк и сетевой задержки, но обычно это порядка нескольких сотен строк. Для очень маленьких пакетов executemany() проще, потому что bulkcopy() создаёт отдельное внутреннее соединение и автоматически подтверждает транзакцию.
В отличие от execute() и executemany(), bulkcopy() сопоставляет значения со столбцами по позиции, а не по списку столбцов INSERT. Передайте column_mappings название столбцам назначения, которые вы загружаете, чтобы исходные кортежи совпадали с нужными столбцами, а не с ведущим столбцем идентичности таблицы:
result = cursor.bulkcopy(
"Production.Product",
rows,
column_mappings=["Name", "ProductNumber", "SafetyStockLevel", "ReorderPoint",
"StandardCost", "ListPrice", "DaysToManufacture", "SellStartDate"],
)
print(f"Copied {result['rows_copied']} rows")
Для очень больших загрузок используйте генератор, чтобы избежать загрузки всего набора данных в память, и настройте batch_size периодическое фиксирование:
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,
)
Стратегии кэширования
Для эталонных данных, которые редко меняются (категории, таблицы поиска, конфигурация), кэшируйте результаты в вашем приложении вместо запросов по каждому запросу.
functools.lru_cache в Python обеспечивает простую мемоизацию, но кэш сохраняется бессрочно, пока процесс не будет перезапущен. Если базовые данные могут измениться, используйте cachetools.TTLCache автоматическое обновление после определённого времени:
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()
Оптимизация сети
Минимизировать круговые поездки
Каждый запрос — это сетевой круговой переход к серверу. Объедините связанные запросы в одну партию и используйте nextset() для продвижения по наборам результатов:
# 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()
Использование серверной обработки для сложной логики
Отправляйте агрегацию и фильтрацию в SQL Server вместо того, чтобы загружать сырые строки и обрабатывать их на Python. Сервер возвращает одну сводную строку вместо потенциально тысяч строк деталей:
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})
Избегайте чередующихся операций курсора
Драйвер mssql-python не поддерживает несколько активных наборов результатов (MARS). Только один курсор может иметь активный запрос на каждое соединение. Полностью получите первый набор результатов перед выполнением следующего запроса или используйте второе соединение:
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()
Управление памятью
Обрабатывать большие результаты по частям
Загрузка таблицы с миллионами строк в список требует объёма памяти, пропорционального всему набору результатов. Используйте OFFSET и FETCH NEXT пагинируйте данные на стороне сервера, обрабатывая по одному блоку за раз.
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
)
Используйте генераторы для стриминга
Обёртка fetchmany() генератора Python поддерживает постоянное использование памяти независимо от размера таблицы. Вызывающий повторяет порядок за строкой, не загружая полный набор результатов. Для очень большого источника объедините таблицы с помощью UNION ALL и передавайте объединённый результат в потоковом режиме таким же образом.
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")
Быстро очищайте ресурсы
Незакрытые соединения связывают ресурсы сервера и могут исчерпать пул соединений. Используйте менеджер контекста, чтобы гарантировать очистку даже при возникновении исключений.
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()
Наблюдение за использованием памяти
Большие наборы результатов, долгоживущие кэши и объекты соединения потребляют память. Если ваше приложение работает как сервис, утечки памяти из незакрытых курсоров или неограниченных кэшей могут в конечном итоге привести к тому, что процесс будет остановлен операционной системой или процессом выполнения контейнера.
Используйте модуль Python tracemalloc для создания снимков памяти и поиска крупнейших выделений памяти.
import tracemalloc
tracemalloc.start()
# ... run your workload ...
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")
for stat in top_stats[:10]:
print(stat)
Распространённые источники неожиданного роста памяти включают:
- Вызов
fetchall()для запроса, который возвращает миллионы строк. Используйте вместо этогоfetchmany()или генератор. - Кэширование результатов запросов без
maxsizeили TTL. Размер кэша увеличивается до перезапуска процесса. - Создание курсоров в цикле без их закрытия. Каждый открытый курсор хранит свой набор результатов в памяти.
Оптимизация индекса и плана запросов
Проверьте производительность серверных запросов
Используйте SET STATISTICS TIME ON и SET STATISTICS IO ON чтобы посмотреть, сколько времени занимают запросы на сервере и сколько данных они читают. Высокое число логических чтений обычно указывает на отсутствие индекса. Запустите эти операторы в SQL Server Management Studio или в расширении MSSQL для Visual Studio Code, где вывод отображается в панели сообщений:
SET STATISTICS TIME ON;
SET STATISTICS IO ON;
SELECT * FROM Production.Product WHERE ProductSubcategoryID = 1;
SET STATISTICS TIME OFF;
SET STATISTICS IO OFF;
Выходные данные должны отображаться следующим образом:
Table 'Product'. Scan count 1, logical reads 3
SQL Server Execution Times: CPU time = 0 ms, elapsed time = 1 ms.
Если вы видите большое число логических чтений или сканирование таблиц, рассмотрите возможность создания индекса.
Используйте подсказки по запросам как тактическое решение
Подсказки запроса переопределяют выбор индексов и стратегий соединения, сделанный оптимизатором запросов. В продакшене это ценное быстрое и низкорисковое решение, когда производительность запроса внезапно деградирует. Вы можете сразу развернуть подсказку в коде приложения, чтобы стабилизировать запрос, пока вы исследуете коренную причину (отсутствующие индексы, устаревшая статистика или изменения схемы).
Избегайте оставлять подсказки навсегда. Когда меняется распределение данных или схема, жёсткая подсказка может усугубить ситуацию. Воспринимайте их как временные и возвращайтесь к ним после устранения основной проблемы:
cursor.execute("""
SELECT * FROM Production.Product WITH (INDEX(AK_Product_Name))
WHERE ProductSubcategoryID = %(subcategory_id)s
""", {"subcategory_id": 1})
Используйте OPTION (RECOMPILE), чтобы обойти плохие кэшированные планы
SQL Server кэширует планы запросов на основе первого набора значений параметров, которые видит. Если распределение данных сильно варьируется между звонками, кэшированный план может плохо работать для некоторых значений. Эта задача, называемая поиском параметров, часто появляется как запрос, который «раньше был быстрым», внезапно занимающий секунды или минуты.
OPTION (RECOMPILE)заставляет SQL Server разрабатывать новый план для каждой работы, что является эффективным и немедленным решением, которое можно развернуть без каких-либо изменений на стороне сервера. Компромисс — небольшая стоимость компиляции за один вызов, но для запросов, которые выполняются редко или возвращают наборы результатов переменного размера, эта стоимость незначительна по сравнению с неудачным планом.
Когда вы стабилизируете ситуацию, можно не торопясь внедрить окончательное решение, например переписав запрос, добавив отфильтрованные индексы или используя подсказки плана выполнения:
cursor.execute("""
SELECT * FROM Sales.SalesOrderHeader
WHERE OrderDate > %(start_date)s
OPTION (RECOMPILE)
""", {"start_date": start_date})
Отслеживание производительности
Отпределяйте время для запросов
Чтобы найти медленные операции, оберните запросы с 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")
Для более широкого понимания того, как ваше приложение тратит время, используйте встроенный cProfile модуль Python:
python -m cProfile -s cumtime my_app.py
Этот вид показывает накопленное время на вызов функции, что помогает определить, есть ли замедление при выполнении запросов, обработке данных или сетевой задержке.
Используйте хранилище запросов для серверного анализа
Тайминг на стороне клиента показывает, сколько времени занимает запрос с точки зрения вашего приложения, но он объединяет сетевую задержку, время выполнения сервера и обработку клиента. хранилище запросов фиксирует планы выполнения и статистику во время выполнения на сервере, чтобы вы могли точно видеть, как SQL Server выполнял каждый запрос, как часто он запускался и как менялась его производительность со временем.
хранилище запросов особенно полезен для выявления отслеживания параметров, регрессий планов и запросов, потребляющих наибольшую часть ресурсов сервера. Вы можете напрямую выполнять запросы к представлениям sys.query_store_runtime_stats и sys.query_store_plan или использовать встроенные отчёты хранилище запросов в SQL Server Management Studio.
Используйте отчёты Performance Dashboard
Отчёты Performance Dashboard в SQL Server Management Studio предоставляют обзор состояния SQL Server в реальном времени, включая текущие типы ожидания, активные дорогие запросы и тенденции CPU/IO. Используйте их для быстрого выявления узких мест, не отправляя запросы напрямую к DMV.
Контрольный список производительности
Подключение
- [ ] Включить пул соединений.
- [ ] Определи размер бассейна под свою нагрузку.
- [ ] Повторное использование соединений в операциях.
- [ ] Поддерживайте открытые соединения в длительно работающих службах.
Queries
- [ ] Выберите только нужные столбцы.
- [ ] Используйте подходящий метод получения для каждого запроса.
- [ ] Реализуйте страницирование на стороне сервера.
- [ ] Установите
SET NOCOUNT ONтолько один раз после подключения. - [ ] Минимизируйте круговые поездки, разделяя запросы.
Вставки
- [ ] Используйте
execute()для однострочных вставок. - [ ] Используйте
executemany()для малых и средних партий (~10–1 000 строк). - [ ] Используйте
bulkcopy()тогда, когда пропускная способность важнее, чем контроль на ряд.
Кэширование
- [ ] Кэшировать эталонные данные с помощью TTL, чтобы избежать показа устаревших результатов.
Resources
- [ ] Обрабатывайте крупные результаты кусками или с помощью генераторов.
- [ ] Быстро убирайте соединения.
- [ ] Отслеживайте использование памяти с помощью
tracemallocв длительно работающих сервисах.