У меня два очень похожих запроса
Первый запрос:
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]
TargetTypeId = 30
? Кажется, что планы разные, потому что это одно значение действительно искажает объем возвращаемых данных (как ожидается, будет).Ответы:
TL; Dr внизу
Почему был выбран плохой план
Основной причиной выбора одного плана над другим является
Estimated total subtree
стоимость.Эта стоимость была ниже для плохого плана, чем для более эффективного плана.
Общая оценочная стоимость поддерева для плохого плана:
Общая оценочная стоимость поддерева для более эффективного плана
Оператор оценил расходы
Некоторые операторы могут взять на себя большую часть этой стоимости, и это может стать причиной для оптимизатора выбрать другой путь / план.
В нашем более исполняющем плане, большая часть из
Subtreecost
рассчитываются наindex seek
&nested loops operator
выполнении объединения:В то время как для нашего плохого плана запросов
Clustered index seek
стоимость оператора нижеЧто должно объяснить, почему другой план мог быть выбран.
(И добавив параметр,
30
увеличивающий стоимость плохого плана, если он превысил871.510000
расчетную стоимость). Примерное предположение ™Лучший план выполнения
Плохой план
Куда это нас приведет?
Эта информация подводит нас к способу навязывания неверного плана запросов в нашем примере (см. DML для почти репликации проблемы OP для данных, использованных для репликации проблемы)
Добавляя
INNER LOOP JOIN
подсказку о присоединенииОн ближе, но имеет некоторые различия в порядке соединения:
Переписывание
Моя первая попытка перезаписи могла бы хранить все эти числа во временной таблице:
А потом добавив
JOIN
вместо большогоIN()
Наш план запроса отличается, но еще не исправлен:
с огромной оценочной стоимостью оператора на
AuditRelatedIds
столеВот где я это заметил
Причиной, по которой я не могу напрямую воссоздать ваш план, является оптимизированная фильтрация растровых изображений.
Я могу воссоздать ваш план, отключив оптимизированные растровые фильтры с помощью флагов трассировки
7497
и7498
Больше информации об оптимизированных растровых фильтрах здесь .
Это означает, что без использования растровых фильтров оптимизатор считает, что лучше сначала присоединиться к
#number
таблице, а затем присоединиться кAuditRelatedIds
таблице.При форсировании заказа
OPTION (QUERYTRACEON 7497, QUERYTRACEON 7498, FORCE ORDER);
мы видим, почему:&
Фигово
Удаление способности идти параллельно с MaxDop 1
При добавлении
MAXDOP 1
запрос выполняется быстрее, однопоточный.И добавив этот индекс
При использовании объединения слиянием.
То же самое верно, когда мы удаляем подсказку о запросе принудительного порядка или не используем таблицу #Numbers и используем
IN()
вместо нее .Я бы посоветовал взглянуть на добавление
MAXDOP(1)
и посмотреть, поможет ли это вашему запросу, при необходимости переписать.Конечно, вы также должны помнить, что, с моей стороны, он работает еще лучше благодаря оптимизированной фильтрации растровых изображений и фактически использует несколько потоков с хорошим эффектом:
TL; DR
Ориентировочные затраты будут определять выбранный план, я смог повторить поведение и увидел, что операторы
optimized bitmap filters
+ былиparallellism
добавлены на моем конце, чтобы выполнить запрос быстро и качественно.Вы можете посмотреть на добавление
MAXDOP(1)
в свой запрос как способ, как мы надеемся, каждый раз получать один и тот же контролируемый результат, с «merge join
нет» и «плохо»parallellism
.Обновление до более новой версии и использование более высокой версии оценки кардинальности, чем
CardinalityEstimationModelVersion="70"
может также помочь.Также может помочь временная таблица чисел для фильтрации нескольких значений.
DML почти повторяет проблему ОП
Я потратил на это больше времени, чем хотел бы признать
источник
MAXDOP 0
кажется, исправило это. Большое спасибо!Из того, что я могу сказать, основная разница между этими двумя планами заключается в том, что является «Первичным фильтром».
С первой версией выводился основной фильтр,
Audit.ID
связанный сari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
фильтрацией этого списка по тем, ктоAudit.TargetTypeID
был в списке.Со второй версией выводился основной фильтр,
Audit.ID
связанный со спискомAudit.TargetTypeID
.Поскольку добавление, по-
Audit.TargetTypeID = 30
видимому, резко увеличило количество записей (267 479 и 25 650 соответственно в соответствии с Первоначальным вопросом). Вероятно, поэтому планы выполнения разные. (Насколько я понимаю) SQL сначала попытается выполнить наиболее избирательную функцию, а затем применить остальные правила. С первой версией, запрос byAuditRelatedID.RelatedID
to findAudit.ID
был, вероятно, более избирательным, чем попытка использоватьAudit.TargetTypeID
затем findAudit.ID
.К чести ypercube. Вы можете, конечно, обновить,
[AuditRelatedIds].[IX_AuditRelatedIdsRelatedId_INCLUDES]
чтобы иметь обаRelatedID
иAuditID
как часть в индексе вместо того, чтобы иметьAuditID
как частьINCLUDE
. Он не должен занимать никакого дополнительного индексного пространства и позволит вам использовать оба столбца вJOIN
предложениях. Это может помочь оптимизатору запросов создать один и тот же план выполнения для обоих запросов.Работая с подобной логикой, может быть некоторое преимущество для индекса,
Audit
который содержитTargetTypeID ASC, ID ASC
фактические упорядоченные / фильтрующие узлы (не как частьINCLUDE
). Это должно позволить оптимизатору запросов фильтровать, аAudit.TargetTypeID
затем быстро присоединиться кAuditReferenceIds.AuditID
. Теперь это может привести к тому, что оба запроса выберут менее эффективный план, поэтому я попробую дать рекомендацию ypercube.источник