Додано 7/11 . Проблема в тупикових ситуаціях виникає через сканування індексу під час MERGE JOIN. У цьому випадку транзакція намагається отримати блокування S на весь індекс у батьківській таблиці FK, але раніше інша транзакція ставить X lock на ключове значення індексу.
Дозвольте розпочати з невеликого прикладу (TSQL2012 DB із 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]
відповідно. У кожному випадку у нас є кластерний індекс на згаданому стовпчику в батьківській таблиці.
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 використовується оптимізатором запитів як найефективніший спосіб підтримати передбачуваний ключ у запиті.
І це не має нічого спільного, крім використання 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 з блокуванням цілого індексу.
Використання OPTION (LOOP JOIN) не підходить, оскільки коштує майже на 15% дорожче, ніж МЕРГОВЕ ПРИЄДНАННЯ (я думаю, що регрес буде більшим із збільшенням обсягів даних).
У операторі SELECT ви можете бачити одне значення shipperid
атрибута для всього вставленого набору. На мою думку, повинен бути спосіб зробити фазу перевірки для вставленого набору швидшою, принаймні, для атрибута, що не змінюється. Щось на зразок:
- зробіть LOOP JOIN, MERGE JOIN, HASH JOIN, якщо у нас є невизначений підмножина для валідації JOIN
- якщо є лише одне явне значення перевіреного стовпця, ми робимо перевірку лише один раз (INDEX SEEK).
Чи існує якась загальна модель, що перевершує ситуацію, що вища, використовуючи структуру коду, додаткові об'єкти DDL тощо?
Додано 20/07. Рішення. Оптимізатор запитів вже робить оптимізацію перевірки "єдиного ключа - зовнішнього ключа" за допомогою MERGE JOIN. І робить лише для Sales.Shippers таблиці, залишаючи LOOP JOIN для чергового приєднання до запиту одночасно. Оскільки в батьківській таблиці є декілька рядків Оптимізатор запитів використовує алгоритм сортування-об’єднання об'єднань і порівнює кожен рядок у внутрішній таблиці з батьківською таблицею лише один раз. Отже, це відповідь на моє запитання, чи є якийсь конкретний механізм для ефективної обробки окремих значень у наборі під час перевірки одного ключа. Це не дуже ідеальне рішення, але саме таким чином SQL Server оптимізує справу.
Дослідження про вплив на ефективність показало, що в моєму випадку оператор вставки MERGE JOIN та LOOP JOIN став приблизно рівним 750 одночасно вставлених рядків із наступною перевагою MERGE JOIN (у часовому ресурсі процесора). Тож використання OPTION (LOOP JOIN) - це відповідне рішення для мого бізнес-процесу.