Преодолеть MERGE JOIN (INDEX SCAN) с явным одиночным значением KEY на FOREIGN KEY

9

Добавлено 7/11 . Проблема заключается в возникновении взаимоблокировок из-за сканирования индекса во время MERGE JOIN. В этом случае транзакция пытается получить S-блокировку для всего индекса в родительской таблице FK, но ранее другая транзакция устанавливает X-блокировку для значения ключа индекса.

Позвольте мне начать с небольшого примера (используется база данных TSQL2012 из 70-461):

CREATE TABLE [Sales].[Orders](
[orderid] [int] IDENTITY(1,1) NOT NULL,
[custid] [int] NULL,
[empid] [int] NOT NULL,
[shipperid] [int] NOT NULL,
... )

Столбцы [custid], [empid], [shipperid]являются соответствующими параметрами [Sales].[Customers], [HR].[Employees], [Sales].[Shippers]. В каждом случае у нас есть кластеризованный индекс по указанному столбцу в таблице parrent.

ALTER TABLE [Sales].[Orders]  WITH CHECK ADD  CONSTRAINT [FK_Orders_Customers] FOREIGN KEY([custid]) REFERENCES [Sales].[Customers] ([custid])
ALTER TABLE [Sales].[Orders]  WITH CHECK ADD  CONSTRAINT [FK_Orders_Employees] FOREIGN KEY([empid]) REFERENCES [HR].[Employees] ([empid])
ALTER TABLE [Sales].[Orders]  WITH CHECK ADD  CONSTRAINT [FK_Orders_Shippers] FOREIGN KEY([shipperid])REFERENCES [Sales].[Shippers] ([shipperid])

Я пытаюсь использовать INSERT [Sales].[Orders] SELECT ... FROMдругую таблицу, [Sales].[OrdersCache]которая имеет ту же структуру, что и [Sales].[Orders]внешние ключи. Еще одна вещь, которая может быть важна, чтобы упомянуть таблицу, [Sales].[OrdersCache]является кластеризованным индексом.

CREATE CLUSTERED INDEX idx_c_OrdersCache ON Sales.OrdersCache ( custid, empid )

Как и ожидалось, когда я пытаюсь вставить небольшие объемы данных, LOOP JOIN работает нормально, делая поиск индекса по внешним ключам.

При больших объемах данных оптимизатор запросов использует MERGE JOIN как наиболее эффективный способ поддержки ключа foregn в запросе.

И это не имеет никакого отношения, кроме использования OPTION (LOOP JOIN) в нашем случае с внешними ключами или INNER LOOP JOIN в явном случае JOIN.

Ниже приведен запрос, который я пытаюсь выполнить в своей среде:

INSERT Sales.Orders (
        custid, empid, shipperid, ... )
SELECT  custid, empid, 2, ...
FROM Sales.OrdersCache

Глядя на план, мы видим, что все 3 внешних ключа проверены с помощью MERGE JOIN. Для меня это неподходящий способ, так как он использует INDEX SCAN с полной блокировкой индекса. MERGE JOIN во время проверки внешних ключей

Использование OPTION (LOOP JOIN) не подходит, поскольку оно стоит почти на 15% дороже, чем MERGE JOIN (я думаю, регрессия будет больше при увеличении объемов данных).

В операторе SELECT вы можете увидеть одно значение shipperidатрибута для всего вставленного набора. По моему мнению, должен быть способ сделать фазу проверки для вставленного набора быстрее, по крайней мере, для неизменяемого атрибута. Что-то вроде:

  • сделать LOOP JOIN, MERGE JOIN, HASH JOIN, если у нас есть неопределенное подмножество для проверки JOIN
  • если есть только одно явное значение проверенного столбца, мы делаем проверку только один раз (INDEX SEEK).

Существует ли какой-либо общий шаблон для преодоления описанной выше ситуации с использованием структур кода, дополнительных объектов DDL и т. Д.?

Добавлено 20/07. Решение. Оптимизатор запросов уже выполняет оптимизацию проверки «один ключ - внешний ключ» с помощью MERGE JOIN. И делает только для таблицы Sales.Shippers, оставляя LOOP JOIN для других объединений в запросе одновременно. Поскольку у меня есть несколько строк в родительской таблице, Query Optimizer использует алгоритм объединения Sort-merge и сравнивает каждую строку во внутренней таблице с родительской таблицей только один раз. Так что это ответ на мой вопрос, есть ли какой-то конкретный механизм для эффективной обработки отдельных значений в наборе во время проверки одного ключа. Это не очень идеальное решение, но именно так SQL Server оптимизирует ситуацию.

Исследование влияния на производительность показало, что в моем случае оператор вставки MERGE JOIN и LOOP JOIN стал примерно равным с 750 одновременно вставленными строками со следующим преимуществом MERGE JOIN (в ресурсе времени ЦП). Поэтому использование OPTION (LOOP JOIN) является подходящим решением для моего бизнес-процесса.

Олег я
источник

Ответы:

8

Использование OPTION (LOOP JOIN) не подходит, так как стоит почти на 15% дороже, чем MERGE JOIN.

Проценты затрат, отображаемые в выходных данных showplan, всегда являются оценками модели оптимизатора, даже в плане после выполнения (фактическом). Эти затраты, вероятно, не отражают фактическую производительность во время выполнения на вашем конкретном оборудовании. Единственный способ убедиться в этом - это проверить альтернативы с вашей рабочей нагрузкой, измерив те показатели, которые наиболее важны для вас (истекшее время, загрузка ЦП и т. Д.).

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

По моему мнению, должен быть способ сделать фазу проверки для вставленного набора быстрее, по крайней мере, для неизменяемого атрибута.

В принципе это имеет смысл, но сегодня в логике построения плана нет такой логики, которую использует проверка внешнего ключа. Текущая логика намеренно очень общая и ортогональная более широкому запросу; специфические оптимизации усложняют тестирование и увеличивают вероятность ошибок в крайнем случае.

Существует ли какой-либо общий шаблон для преодоления описанной выше ситуации с использованием структур кода, дополнительных объектов DDL и т. Д.?

Не то, чтобы я в курсе. Риск взаимоблокировки с проверкой внешнего ключа объединения слиянием хорошо известен , и наиболее часто используемым обходным путем является OPTION (LOOP JOIN)подсказка. Невозможно избежать использования общих блокировок во время проверки внешнего ключа, поскольку они необходимы для корректности даже при уровнях изоляции версий строк.

Нет лучшего общего ответа (чем подсказка присоединения цикла), если вы хотите сохранить возможность для нескольких параллельных процессов добавлять строки в родительскую и дочернюю таблицы транзакционно. Но, если вы хотите сериализовать изменения данных (не считывает), использование sp_getapplock является надежным и простым методом.

Пол Уайт 9
источник