Очень похожие запросы, очень разная производительность

9

У меня два очень похожих запроса

Первый запрос:

SELECT count(*)
FROM Audits a
    JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
    and a.TargetTypeId IN 
    (1,2,3,4,5,6,7,8,9,
    11,12,13,14,15,16,17,18,19,
    21,22,23,24,25,26,27,28,29,30,
    31,32,33,34,35,36,37,38,39,
    41,42,43,44,45,46,47,48,49,
    51,52,53,54,55,56,57,58,59,
    61,62,63,64,65,66,67,68,69,
    71,72,73,74,75,76,77,78,79)

Результат: 267479

План: https://www.brentozar.com/pastetheplan/?id=BJWTtILyS


Второй запрос:

SELECT count(*)
FROM Audits a
    JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
    and a.TargetTypeId IN 
    (1,2,3,4,5,6,7,8,9,
    11,12,13,14,15,16,17,18,19,
    21,22,23,24,25,26,27,28,29,
    31,32,33,34,35,36,37,38,39,
    41,42,43,44,45,46,47,48,49,
    51,52,53,54,55,56,57,58,59,
    61,62,63,64,65,66,67,68,69,
    71,72,73,74,75,76,77,78,79)

Результат: 25650

План: https://www.brentozar.com/pastetheplan/?id=S1v79U8kS


Первый запрос занимает около одной секунды, в то время как второй запрос занимает около 20 секунд. Это совершенно нелогично для меня, потому что первый запрос имеет намного большее количество, чем второй. Это на SQL Server 2012

Почему так много различий? Как я могу ускорить второй запрос так же быстро, как первый?


Вот скрипт создания таблицы для обеих таблиц:

CREATE TABLE [dbo].[AuditRelatedIds](
    [AuditId] [bigint] NOT NULL,
    [RelatedId] [uniqueidentifier] NOT NULL,
    [AuditTargetTypeId] [smallint] NOT NULL,
 CONSTRAINT [PK_AuditRelatedIds] PRIMARY KEY CLUSTERED 
(
    [AuditId] ASC,
    [RelatedId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE NONCLUSTERED INDEX [IX_AuditRelatedIdsRelatedId_INCLUDES] ON [dbo].[AuditRelatedIds]
(
    [RelatedId] ASC
)
INCLUDE (   [AuditId]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

ALTER TABLE [dbo].[AuditRelatedIds]  WITH CHECK ADD  CONSTRAINT [FK_AuditRelatedIds_AuditId_Audits_Id] FOREIGN KEY([AuditId])
REFERENCES [dbo].[Audits] ([Id])

ALTER TABLE [dbo].[AuditRelatedIds] CHECK CONSTRAINT [FK_AuditRelatedIds_AuditId_Audits_Id]

ALTER TABLE [dbo].[AuditRelatedIds]  WITH CHECK ADD  CONSTRAINT [FK_AuditRelatedIds_AuditTargetTypeId_AuditTargetTypes_Id] FOREIGN KEY([AuditTargetTypeId])
REFERENCES [dbo].[AuditTargetTypes] ([Id])

ALTER TABLE [dbo].[AuditRelatedIds] CHECK CONSTRAINT [FK_AuditRelatedIds_AuditTargetTypeId_AuditTargetTypes_Id]

CREATE TABLE [dbo].[Audits](
    [Id] [bigint] IDENTITY(1,1) NOT NULL,
    [TargetTypeId] [smallint] NOT NULL,
    [TargetId] [nvarchar](40) NOT NULL,
    [TargetName] [nvarchar](max) NOT NULL,
    [Action] [tinyint] NOT NULL,
    [ActionOverride] [tinyint] NULL,
    [Date] [datetime] NOT NULL,
    [UserDisplayName] [nvarchar](max) NOT NULL,
    [DescriptionData] [nvarchar](max) NULL,
    [IsNotification] [bit] NOT NULL,
 CONSTRAINT [PK_Audits] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

SET ANSI_PADDING ON

CREATE NONCLUSTERED INDEX [IX_AuditsTargetId] ON [dbo].[Audits]
(
    [TargetId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

SET ANSI_PADDING ON

CREATE NONCLUSTERED INDEX [IX_AuditsTargetTypeIdAction_INCLUDES] ON [dbo].[Audits]
(
    [TargetTypeId] ASC,
    [Action] ASC
)
INCLUDE (   [TargetId],
    [UserDisplayName]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 100) ON [PRIMARY]

ALTER TABLE [dbo].[Audits]  WITH CHECK ADD  CONSTRAINT [FK_Audits_TargetTypeId_AuditTargetTypes_Id] FOREIGN KEY([TargetTypeId])
REFERENCES [dbo].[AuditTargetTypes] ([Id])

ALTER TABLE [dbo].[Audits] CHECK CONSTRAINT [FK_Audits_TargetTypeId_AuditTargetTypes_Id]
Chocoman
источник
3
Сможем ли мы получить некоторую схему таблицы и детали индекса. Как я уверен, вы заметили, что планы немного отличаются, но, очевидно, это имеет большое значение. Если мы сможем получить эти детали, то, возможно, мы увидим, какие у нас есть варианты.
Кирк Сондерс
2
В качестве очень быстрого совета вместо использования IN создайте TempTable с одним столбцом TINYINT / INT (кластеризованным) с нужными номерами, а затем INNER JOIN к нему. Кроме этого, нам, вероятно, понадобится информация о DDL, как упоминалось выше
@KirkSaunders
2
Есть что-то особенное TargetTypeId = 30? Кажется, что планы разные, потому что это одно значение действительно искажает объем возвращаемых данных (как ожидается, будет).
Аарон Бертран
Я понимаю, что это ужасно педантично, но утверждение «первый запрос возвращает намного больше строк, чем второй». не является правильным. Оба возвращают 1 ряд;)
ypercubeᵀᴹ
1
Я обновил вопрос с помощью операторов создания таблиц для обеих таблиц
Chocoman

Ответы:

8

TL; Dr внизу

Почему был выбран плохой план

Основной причиной выбора одного плана над другим является Estimated total subtreeстоимость.

Эта стоимость была ниже для плохого плана, чем для более эффективного плана.

Общая оценочная стоимость поддерева для плохого плана:

введите описание изображения здесь

Общая оценочная стоимость поддерева для более эффективного плана

введите описание изображения здесь


Оператор оценил расходы

Некоторые операторы могут взять на себя большую часть этой стоимости, и это может стать причиной для оптимизатора выбрать другой путь / план.

В нашем более исполняющем плане, большая часть из Subtreecostрассчитываются на index seek& nested loops operatorвыполнении объединения:

введите описание изображения здесь

В то время как для нашего плохого плана запросов Clustered index seekстоимость оператора ниже

введите описание изображения здесь

Что должно объяснить, почему другой план мог быть выбран.

(И добавив параметр, 30увеличивающий стоимость плохого плана, если он превысил 871.510000расчетную стоимость). Примерное предположение ™

Лучший план выполнения

введите описание изображения здесь

Плохой план

введите описание изображения здесь


Куда это нас приведет?

Эта информация подводит нас к способу навязывания неверного плана запросов в нашем примере (см. DML для почти репликации проблемы OP для данных, использованных для репликации проблемы)

Добавляя INNER LOOP JOINподсказку о присоединении

SELECT count(*)
FROM Audits a
   INNER LOOP JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
    and a.TargetTypeId IN 
    (1,2,3,4,5,6,7,8,9,
    11,12,13,14,15,16,17,18,19,
    21,22,23,24,25,26,27,28,29,
    31,32,33,34,35,36,37,38,39,
    41,42,43,44,45,46,47,48,49,
    51,52,53,54,55,56,57,58,59,
    61,62,63,64,65,66,67,68,69,
    71,72,73,74,75,76,77,78,79)

Он ближе, но имеет некоторые различия в порядке соединения:

введите описание изображения здесь


Переписывание

Моя первая попытка перезаписи могла бы хранить все эти числа во временной таблице:

CREATE TABLE #Numbers(Numbering INT)
INSERT INTO #Numbers(Numbering)
VALUES
(1),(2),(3),(4),(5),(6),(7),(8),(9),(11),(12),(13),(14),(15),(16),(17),(18),(19),
(21),(22),(23),(24),(25),(26),(27),(28),(29),(30),(31),(32),(33),(34),(35),
(36),(37),(38),(39),(41),(42),(43),(44),(45),(46),(47),(48),(49),(51),(52),
(53),(54),(55),(56),(57),(58),(59),(61),(62),(63),(64),(65),(66),(67),(68),
(69),(71),(72),(73),(74),(75),(76),(77),(78),(79);

А потом добавив JOINвместо большогоIN()

SELECT count(*)
FROM Audits a
   INNER LOOP JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
   INNER JOIN #Numbers
   ON Numbering = a.TargetTypeId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1';

Наш план запроса отличается, но еще не исправлен:

введите описание изображения здесь

с огромной оценочной стоимостью оператора на AuditRelatedIdsстоле

введите описание изображения здесь


Вот где я это заметил

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

Я могу воссоздать ваш план, отключив оптимизированные растровые фильтры с помощью флагов трассировки 7497и7498

SELECT count(*)
FROM Audits a 
   INNER JOIN AuditRelatedIds  ari ON a.Id = ari.AuditId 
   INNER JOIN #Numbers
   ON Numbering = a.TargetTypeId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
OPTION (QUERYTRACEON 7497, QUERYTRACEON 7498);

Больше информации об оптимизированных растровых фильтрах здесь .

введите описание изображения здесь

Это означает, что без использования растровых фильтров оптимизатор считает, что лучше сначала присоединиться к #numberтаблице, а затем присоединиться к AuditRelatedIdsтаблице.

При форсировании заказа OPTION (QUERYTRACEON 7497, QUERYTRACEON 7498, FORCE ORDER);мы видим, почему:

введите описание изображения здесь

& введите описание изображения здесь

Фигово


Удаление способности идти параллельно с MaxDop 1

При добавлении MAXDOP 1запрос выполняется быстрее, однопоточный.

И добавив этот индекс

CREATE NONCLUSTERED INDEX [IX_AuditRelatedIdsRelatedId_AuditId] ON [dbo].[AuditRelatedIds]
(
    [RelatedId] ASC,
    [AuditId] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY];

введите описание изображения здесь

При использовании объединения слиянием. введите описание изображения здесь

То же самое верно, когда мы удаляем подсказку о запросе принудительного порядка или не используем таблицу #Numbers и используем IN()вместо нее .

Я бы посоветовал взглянуть на добавление MAXDOP(1)и посмотреть, поможет ли это вашему запросу, при необходимости переписать.

Конечно, вы также должны помнить, что, с моей стороны, он работает еще лучше благодаря оптимизированной фильтрации растровых изображений и фактически использует несколько потоков с хорошим эффектом:

введите описание изображения здесь

введите описание изображения здесь


TL; DR

Ориентировочные затраты будут определять выбранный план, я смог повторить поведение и увидел, что операторы optimized bitmap filters+ были parallellismдобавлены на моем конце, чтобы выполнить запрос быстро и качественно.

Вы можете посмотреть на добавление MAXDOP(1)в свой запрос как способ, как мы надеемся, каждый раз получать один и тот же контролируемый результат, с « merge joinнет» и «плохо» parallellism.

Обновление до более новой версии и использование более высокой версии оценки кардинальности, чем CardinalityEstimationModelVersion="70"может также помочь.

Также может помочь временная таблица чисел для фильтрации нескольких значений.


DML почти повторяет проблему ОП

Я потратил на это больше времени, чем хотел бы признать

set NOCOUNT ON;
DECLARE @I INT = 0
WHILE @I < 56
BEGIN
INSERT INTO  [dbo].[Audits] WITH(TABLOCK) 
([TargetTypeId],
    [TargetId],
    [TargetName],
    [Action],
    [ActionOverride] ,
    [Date] ,
    [UserDisplayName],
    [DescriptionData],
    [IsNotification]) 
SELECT top(500000) CASE WHEN ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 10000 = 30 then 29 ELSE ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 10000 END as rownum2 -- TILL 50 and no 30
,'bla','bla2',1,1,getdate(),'bla3','Bla4',1
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2;
SET @I +=1;
END

-- 'Bad Query matches'
INSERT INTO  [dbo].[AuditRelatedIds] WITH(TABLOCK)
    ([AuditId] ,
    [RelatedId]  ,
    [AuditTargetTypeId])
SELECT
TOP(25650)
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) as rownum1, 
('1DD87CF1-286B-409A-8C60-3FFEC394FDB1') , 
CASE WHEN ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 510 = 30 then 29 ELSE ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 510 END as rownum2 -- TILL 50 and no 30
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2

-- Extra matches with 30
SELECT MAX([Id]) FROM [dbo].[Audits];
--28000001 Upper value

INSERT INTO  [dbo].[Audits] WITH(TABLOCK) 
([TargetTypeId],
    [TargetId],
    [TargetName],
    [Action],
    [ActionOverride] ,
    [Date] ,
    [UserDisplayName],
    [DescriptionData],
    [IsNotification]) 
SELECT top(241829) 30 as rownum2 -- TILL 50 and no 30
,'bla','bla2',1,1,getdate(),'bla3','Bla4',1
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2;



;WITH CTE AS
(SELECT
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) as rownum1, 
('1DD87CF1-286B-409A-8C60-3FFEC394FDB1') as gu , 
30 as rownum2 -- TILL 50 and no 30
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2
CROSS APPLY master.dbo.spt_values spt3
)
--267479 - 25650 = 241829
INSERT INTO  [dbo].[AuditRelatedIds] WITH(TABLOCK)
    ([AuditId] ,
    [RelatedId]  ,
    [AuditTargetTypeId])

SELECT TOP(241829) rownum1,gu,rownum2 FROM CTE
WHERE rownum1 > 28000001
ORDER BY rownum1 ASC;
Рэнди Вертонген
источник
Очень хорошее объяснение! Добавление, MAXDOP 0кажется, исправило это. Большое спасибо!
Chocoman
1
MAXDOP 1 ** (опечатка)
Chocoman
@ Шокоман Отлично!
Рэнди Вертонген
1

Из того, что я могу сказать, основная разница между этими двумя планами заключается в том, что является «Первичным фильтром».

С первой версией выводился основной фильтр, Audit.IDсвязанный с ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'фильтрацией этого списка по тем, кто Audit.TargetTypeIDбыл в списке.

Со второй версией выводился основной фильтр, Audit.IDсвязанный со списком Audit.TargetTypeID.

Поскольку добавление, по- Audit.TargetTypeID = 30видимому, резко увеличило количество записей (267 479 и 25 650 соответственно в соответствии с Первоначальным вопросом). Вероятно, поэтому планы выполнения разные. (Насколько я понимаю) SQL сначала попытается выполнить наиболее избирательную функцию, а затем применить остальные правила. С первой версией, запрос by AuditRelatedID.RelatedIDto find Audit.IDбыл, вероятно, более избирательным, чем попытка использовать Audit.TargetTypeIDзатем find Audit.ID.

К чести ypercube. Вы можете, конечно, обновить, [AuditRelatedIds].[IX_AuditRelatedIdsRelatedId_INCLUDES]чтобы иметь оба RelatedIDи AuditIDкак часть в индексе вместо того, чтобы иметь AuditIDкак часть INCLUDE. Он не должен занимать никакого дополнительного индексного пространства и позволит вам использовать оба столбца в JOINпредложениях. Это может помочь оптимизатору запросов создать один и тот же план выполнения для обоих запросов.

Работая с подобной логикой, может быть некоторое преимущество для индекса, Auditкоторый содержит TargetTypeID ASC, ID ASCфактические упорядоченные / фильтрующие узлы (не как часть INCLUDE). Это должно позволить оптимизатору запросов фильтровать, а Audit.TargetTypeIDзатем быстро присоединиться к AuditReferenceIds.AuditID. Теперь это может привести к тому, что оба запроса выберут менее эффективный план, поэтому я попробую дать рекомендацию ypercube.

Кирк Сондерс
источник