Устранение неполадок с запросами, данными и операциями в mssql-python

Используйте эту статью для диагностики проблем с выполнением запросов, типом данных, производительностью, транзакциями и массовым копированием драйвера mssql-python .

Проблемы с выполнением запросов

Таблица или объект не найден.

Симптомы:

ProgrammingError: [42S02] (208) Invalid object name 'TableName'.

Возможные причины и решения:

  • Некорректный контекст базы данных

    cursor.execute("SELECT DB_NAME()")
    print(cursor.fetchone()[0])
    
  • Схема не указана

    cursor.execute("SELECT * FROM dbo.TableName")
    
  • Таблицы не существует

    cursor.execute("""
        SELECT TABLE_NAME
        FROM INFORMATION_SCHEMA.TABLES
        WHERE TABLE_NAME = 'TableName'
    """)
    

Синтаксическая ошибка

Симптомы:

ProgrammingError: [42000] (102) Incorrect syntax near '...'.

Решения:

  1. Проверьте инструкцию SQL в SQL Server Management Studio (SSMS), чтобы убедиться в правильности синтаксиса.

  2. Используйте параметризованный запрос вместо интерполяции строк:

    # Don't use string interpolation for query parameters.
    cursor.execute(f"SELECT * FROM Production.Product WHERE Name = '{name}'")
    
    # Use parameters.
    cursor.execute(
        "SELECT * FROM Production.Product WHERE Name = %(name)s",
        {"name": name},
    )
    

Ошибки параметров

Симптомы:

ProgrammingError: [07001] Wrong number of parameters

Решения:

  1. Посчитайте заполнители и параметры. Счёты должны совпадать.

  2. Выберите правильный стиль параметра:

    # Qmark style: positional parameters
    cursor.execute(
        "SELECT * FROM Production.Product "
        "WHERE ProductID = ? AND Name LIKE ?",
        (1, "Adjustable%"),
    )
    print(cursor.fetchone())
    
    # Pyformat style: named parameters
    cursor.execute(
        "SELECT * FROM Production.Product "
        "WHERE ProductID = %(id)s AND Name LIKE %(name)s",
        {"id": 1, "name": "Adjustable%"},
    )
    print(cursor.fetchone())
    

Проблемы с типами данных

Ошибки конвертации времени и даты

Симптомы:

DataError: [22007] Invalid datetime format

Solution:

Используйте объекты Python datetime вместо строк.

from datetime import datetime

cursor.execute("CREATE TABLE #Events (EventDate DATETIME)")

# This value raises an error because the date is invalid.
try:
    cursor.execute(
        "INSERT INTO #Events (EventDate) VALUES (%(event_date)s)",
        {"event_date": "2024-13-45"},
    )
except Exception as e:
    print(f"Expected error: {e}")

# Use a Python datetime object.
cursor.execute(
    "INSERT INTO #Events (EventDate) VALUES (%(event_date)s)",
    {"event_date": datetime(2024, 3, 15)},
)
cursor.execute("SELECT EventDate FROM #Events")
print(cursor.fetchone())

Проблемы с десятичной точностью

Симптомы:

Числа выглядят усечёнными или округлёнными неправильно.

Solution:

Использование decimal.Decimal для точных числовых значений:

from decimal import Decimal

cursor.execute("CREATE TABLE #PriceDemo (ListPrice DECIMAL(10, 2))")
cursor.execute(
    "INSERT INTO #PriceDemo (ListPrice) VALUES (%(list_price)s)",
    {"list_price": Decimal("19.99")},
)

Проблемы с кодированием Unicode

Симптомы:

Особые символы выглядят искажёнными или вызывают ошибки.

Решения:

  1. Используйте столбцы nvarchar для данных Unicode в вашей базе данных.

  2. Передавайте строки непосредственно. Драйвер занимается кодированием:

    cursor.execute("CREATE TABLE #UnicodeDemo (Name NVARCHAR(50))")
    cursor.execute(
        "INSERT INTO #UnicodeDemo (Name) VALUES (%(name)s)",
        {"name": "日本語"},
    )
    cursor.execute("SELECT Name FROM #UnicodeDemo")
    print(cursor.fetchone())
    

Проблемы с производительностью

Медленное выполнение запросов

Возможные причины и решения:

  • Отсутствующие индексы: Проверьте план выполнения запросов в SSMS.

  • Большие наборы результатов: Используйте fetchmany() вместо fetchall():

    cursor.arraysize = 1000
    while True:
        rows = cursor.fetchmany()
        if not rows:
            break
        process_rows(rows)
    
  • Пулирование соединений отключено: Включите пулинг:

    import mssql_python
    
    mssql_python.pooling(max_size=20, idle_timeout=300)
    

Проблемы с памятью при больших результатах

Симптомы:

Процессу Python не хватает памяти.

Решения:

  1. Передавайте результаты потоком вместо загрузки всех строк в память.

    cursor.execute("SELECT * FROM LargeTable")
    for row in cursor:
        process_row(row)
    
  2. Используйте серверную пагинацию.

    page_size = 1000
    offset = 0
    
    while True:
        cursor.execute(
            "SELECT * FROM LargeTable ORDER BY ID "
            "OFFSET ? ROWS FETCH NEXT ? ROWS ONLY",
            (offset, page_size),
        )
        rows = cursor.fetchall()
        if not rows:
            break
        process_rows(rows)
        offset += page_size
    

Вопросы транзакций

Область видимости временной таблицы при автокоммите

Временные таблицы сессии (#tablename), которые вы создаёте внутри транзакции, исчезают при откате транзакции. Такое поведение часто вызывает путаницу, когда автокоммит отключён, а это по умолчанию:

conn = mssql_python.connect(connection_string)
cursor = conn.cursor()

cursor.execute("CREATE TABLE #TempData (ID INT, Name NVARCHAR(50))")
cursor.execute("INSERT INTO #TempData VALUES (1, 'test')")

# An explicit rollback or an error removes #TempData.
conn.rollback()

# This statement fails with "Invalid object name '#TempData'".
cursor.execute("SELECT * FROM #TempData")

Сделайте коммит сразу после создания временной таблицы или используйте режим автокоммита:

cursor.execute("CREATE TABLE #TempData (ID INT, Name NVARCHAR(50))")
conn.commit()

cursor.execute("INSERT INTO #TempData VALUES (1, 'test')")
conn.commit()

DDL-операторы, требующие режима автофиксации, например CREATE DATABASE, не работают внутри открытой транзакции. Включите автокоммит перед тем, как запускать их:

conn.autocommit = True
cursor.execute("CREATE DATABASE TestDB")
conn.autocommit = False

Сделка не была совершена

Симптомы:

Изменения данных не сохраняются после закрытия соединения.

Solution:

Если используется autocommit=False, который используется по умолчанию, вызовите commit():

cursor.execute("CREATE TABLE #Products (Name NVARCHAR(100))")
cursor.execute(
    "INSERT INTO #Products (Name) VALUES (%(name)s)",
    {"name": "Widget"},
)
conn.commit()

Или используйте режим автоподтверждения:

conn = mssql_python.connect(connection_string, autocommit=True)

Ошибки взаимоблокировки

Симптомы:

OperationalError: [40001] (1205) Transaction ... was deadlocked on lock resources with another process

Solution:

Повторная логика решает немедленный сбой, но повторяющиеся тупики указывают на конструктивную проблему. Соберите граф взаимоблокировок и проанализируйте инструкции и типы блокировок. Распространённые исправления включают следующие изменения:

  • Измените порядок операций так, чтобы конкурирующие транзакции получали блокировки в одной и той же последовательности.
  • Сократьте объем транзакций.
  • Добавьте соответствующие индексы, чтобы сократить время блокировки.

Для полного обзора анализа тупиков смотрите руководство по Deadlocks. Если вы используете База данных SQL Azure, см. Анализ и предотвращение взаимоблокировок.

Проблемы с массовой нагрузкой

Нарушения ограничений во время массового копирования

Симптомы:

RuntimeError: CHECK constraint ... Conflict occurred in database ...
RuntimeError: Cannot insert duplicate key ... violation of PRIMARY KEY constraint

Причина:

Данные в вашей партии нарушают ограничения таблицы, такие как ограничения первичного ключа, уникального, проверочного или внешнего ключа.

Solution:

Проверьте данные перед загрузкой. Для больших наборов данных загрузите данные в промежуточную таблицу, а затем объедините их с целевой таблицей:

cursor.execute("CREATE TABLE ##Staging (ID INT, Name NVARCHAR(100))")
cursor.bulkcopy("##Staging", rows)

# Check for duplicate rows before the merge.
cursor.execute("""
    SELECT s.ID
    FROM ##Staging AS s
    INNER JOIN dbo.Target AS t
        ON s.ID = t.ID
""")
dupes = cursor.fetchall()
if dupes:
    print(f"Skipping {len(dupes)} duplicate rows")

# Insert rows that don't exist in the target.
cursor.execute("""
    INSERT INTO dbo.Target (ID, Name)
    SELECT s.ID, s.Name
    FROM ##Staging AS s
    WHERE NOT EXISTS (
        SELECT 1
        FROM dbo.Target AS t
        WHERE t.ID = s.ID
    )
""")
conn.commit()

Для сценариев upsert с промежуточными таблицами см. Шаблоны загрузки и перемещения данных.

Ошибки отображения столбцов

Симптомы:

RuntimeError: Bulk copy failure - column count mismatch

Причина:

Количество столбцов в ваших данных не совпадает с количеством столбцов целевой таблицы или столбцы расположены в неправильном порядке.

Solution:

Убедитесь, что ваши данные соответствуют схеме таблицы по порядку столбцов и их количеству:

from decimal import Decimal

cursor.execute("""
    SELECT COLUMN_NAME, DATA_TYPE
    FROM INFORMATION_SCHEMA.COLUMNS
    WHERE TABLE_NAME = 'MyTable'
    ORDER BY ORDINAL_POSITION
""")
for col in cursor.fetchall():
    print(col)

rows = [
    (1, "Widget", Decimal("19.99")),
    (2, "Gadget", Decimal("29.99")),
]
cursor.bulkcopy("dbo.MyTable", rows)

Несоответствия типов при массовом копировании

Симптомы:

Данные загружаются, но значения усечаны, округлены или некорректны.

Причина:

Значения Python не могут быть однозначно сопоставлены с типами целевого столбца. Распространённые примеры включают float значения, загруженные в десятичные столбцы, что может терять точность, а также увеличенные строки, загруженные в столбцы фиксированной длины.

Solution:

Используйте типы Python, соответствующие вашей схеме:

from decimal import Decimal

rows = [
    # Use Decimal for decimal and numeric columns.
    (1, "Widget", Decimal("19.99")),
    # Avoid float values because they can lose precision.
    # (1, "Widget", 19.99),
]
cursor.bulkcopy("dbo.Products", rows)

Сбои связывания типа NumPy

Симптомы:

Параметры тихо сбывают или вызывают ошибки типов данных, когда вы используете целочисленные или плавающие типы NumPy.

Причина:

Типы NumPy, такие как numpy.int64 и numpy.int32, не проходят проверку isinstance(x, int) в NumPy 2.x. Механизм вывода типов драйвера не распознаёт их, из-за чего возникает непредсказуемое поведение.

Solution:

Преобразуйте значения NumPy в родные типы Python перед тем, как их привязать:

import numpy as np

cursor.execute(
    "SELECT * FROM Production.Product WHERE ProductID = %(product_id)s",
    {"product_id": int(np.int64(42))},
)

for _, row in df.iterrows():
    cursor.execute(
        "INSERT INTO #Orders (ProductID, Qty) "
        "VALUES (%(product_id)s, %(qty)s)",
        {
            "product_id": int(row["ProductID"]),
            "qty": int(row["Qty"]),
        },
    )

Для больших наборов данных используйте пути интеграции Arrow или pandas . Эти пути обеспечивают внутреннее преобразование типов.

Массовое копирование с временными таблицами

Симптомы:

cursor.bulkcopy("#TempTable", data) вызывает RuntimeError: Invalid object name '#TempTable'.

Причина:

bulkcopy() Не могу разрешить временные таблицы сессий (#tablename) из-за ограничений поиска метаданных. Глобальные временные таблицы (##tablename) и постоянные таблицы работают.

Solution:

Используйте глобальную временную таблицу или обычную таблицу стадирования:

# A global temp table is visible to all sessions and is dropped
# when the last session disconnects.
cursor.execute("CREATE TABLE ##Staging (ID INT, Name NVARCHAR(50))")
cursor.bulkcopy("##Staging", rows)

# Alternatively, use a permanent staging table.
cursor.execute("CREATE TABLE dbo.Staging (ID INT, Name NVARCHAR(50))")
cursor.bulkcopy("dbo.Staging", rows)

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

cursor.execute("CREATE TABLE #Staging (ID INT, Name NVARCHAR(50))")
cursor.executemany(
    "INSERT INTO #Staging (ID, Name) VALUES (?, ?)",
    rows,
)