Há algo que não tenho certeza de como resolver na consulta que tenho.
Primeiro, as definições:
Tabela de serviços de correio. Com um registro.
CREATE TABLE [dbo].[CS](
[ServiceID] [int] IDENTITY(1,1) NOT NULL,
[CSID] [nvarchar](6) NULL,
[CSDescription] [varchar](50) NULL,
[OperatingDays] [int] NULL,
[DefaultService] [bit] NULL,
CONSTRAINT [CourierServices_PK] PRIMARY KEY CLUSTERED
(
[ServiceID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF,
ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90
) ON [PRIMARY]
) ON [PRIMARY]
GO
SET IDENTITY_INSERT [dbo].[CS] ON
INSERT [dbo].[CS] ([ServiceID], [CSID], [OperatingDays], [DefaultService])
VALUES (1, N'RM48', 2, 1)
SET IDENTITY_INSERT [dbo].[CS] OFF
SET ANSI_PADDING ON
GO
/****** Object: Index [ix_CourierServices] Script Date: 19/04/2017 14:27:03 ******/
CREATE NONCLUSTERED INDEX [ix_CourierServices] ON [dbo].[CS]
(
[CSID] ASC,
[DefaultService] ASC,
[OperatingDays] ASC
)
INCLUDE ( [CSDescription]) 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]
GO
BD de calendário e tabela, código do Genius Jim Horn :
CREATE TABLE [dbo].[days](
[PKDate] [date] NOT NULL,
[calendar_year] [smallint] NULL,
[calendar_quarter] [tinyint] NULL,
[calendar_quarter_desc] [varchar](10) NULL,
[calendar_month] [tinyint] NULL,
[calendar_month_name_long] [varchar](30) NULL,
[calendar_month_name_short] [varchar](10) NULL,
[calendar_week_in_year] [tinyint] NULL,
[calendar_week_in_month] [tinyint] NULL,
[calendar_day_in_year] [smallint] NULL,
[calendar_day_in_week] [tinyint] NULL,
[calendar_day_in_month] [tinyint] NULL,
[dmy_name_long] [varchar](30) NULL,
[dmy_name_long_with_suffix] [varchar](30) NULL,
[day_name_long] [varchar](10) NULL,
[day_name_short] [varchar](10) NULL,
[continuous_year] [tinyint] NULL,
[continuous_quarter] [smallint] NULL,
[continuous_month] [smallint] NULL,
[continuous_week] [smallint] NULL,
[continuous_day] [int] NULL,
[description] [varchar](100) NULL,
[is_weekend] [tinyint] NULL,
[is_holiday] [tinyint] NULL,
[is_workday] [tinyint] NULL,
[is_event] [tinyint] NULL,
PRIMARY KEY CLUSTERED
(
[PKDate] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF,
ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object: Index [ix_days] Script Date: 19/04/2017 14:38:47 ******/
CREATE NONCLUSTERED INDEX [ix_days] ON [dbo].[days]
(
[PKDate] ASC
)
INCLUDE ( [is_weekend],
[is_holiday],
[is_workday],
[is_event]) 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]
GO
Agora, estou executando uma consulta que faz referência a ambas as tabelas por bit de código:
Select
OID
,case when
Cast(o.[CreationDate] as time) > '16:00:00'
then (select top 1 [PKDate] from [calendar].[dbo].days
where is_weekend <> 1 and is_holiday <>1 and
PKDate > cast(o.[CreationDate] as date)
order by PKDate asc)
else (select top 1 [PKDate] from [calendar].[dbo].days
where is_weekend <> 1 and is_holiday <>1 and
PKDate >= Cast(o.[CreationDate] as date)
order by PKDate asc)
end OperatingDate
,case when
Cast(o.[CreationDate] as time) > '16:00:00'
then (select top 1 [PKDate] from [calendar].[dbo].days
where is_weekend <> 1 and is_holiday <>1 and
PKDate > dateadd(day,isnull(
(select top 1 [operatingdays]
from [dbo].[CS]
where DefaultService = 1)
,2)+1,Cast(o.[CreationDate] as date))
order by PKDate asc)
else (select top 1 [PKDate] from [calendar].[dbo].days
where is_weekend <> 1 and is_holiday <>1 and
PKDate > dateadd(day,isnull(
(select top 1 [operatingdays]
from [dbo].[CS]
where DefaultService = 1)
,2), Cast(o.[CreationDate] as date))
order by PKDate asc)
end EstimatedDeliveryDate
,(select dateadd(day,3,o.[CreationDate])) DeliveryDate
From o
Agora a pergunta é, relacionada a scans de índice e número de execuções: por que os 2 BILHÕES? Ou 6 bilhões? A saída de toda a consulta é, reconhecidamente, 1,7 milhão de linhas, mas isso não explica os números insanos mostrados no plano de consulta:
https://www.brentozar.com/pastetheplan/?id=H1iahxHAe
Se eu puder martelar todas essas varreduras, posso reduzir significativamente o tempo de consulta, mas antes de tudo: como interpreto esses números para encontrar uma solução?
A tabela de dias contém 7,6 k linhas (para cobrir os anos 2000-2020).
Vamos começar olhando para o canto superior direito do plano. Essa parte calcula a
OperatingDate
coluna:Como recebemos de volta 1,72 M de linhas para o conjunto de linhas externas, podemos esperar cerca de 1,72 M de buscas de índice
ix_days
. Isso é de fato o que acontece. Existem 478k linhas para as quaiso.[CreationDate] as time) > '16:00:00'
aCASE
instrução envia 478k buscas para um ramo e o restante para o outro.Observe que o índice que você possui não é o mais eficiente possível para essa consulta. Só podemos fazer um predicado de busca contra
PKDate
. O resto dos filtros são aplicados como um predicado. Isso significa que a busca pode percorrer muitas linhas antes de encontrar uma correspondência. Presumo que a maioria dos dias em sua tabela de calendário não sejam fins de semana ou feriados, portanto, pode não fazer uma diferença prática para essa consulta. No entanto, você pode definir um índice emis_weekend, is_holiday, PKDate
. Isso deve permitir que você procure imediatamente a primeira linha desejada.Para deixar o ponto mais claro, vamos passar por um exemplo simples:
Vamos para a parte mais interessante que é a ramificação para calcular a
DeliveryDate
coluna. Vou incluir apenas metade dele:Suspeito que o que você esperava que o otimizador fizesse fosse calcular isso como um escalar:
E para usar o valor disso para fazer um índice procure usando
ix_days
. Infelizmente, o otimizador não faz isso. Em vez disso, ele aplica uma meta de linha ao índice e faz uma varredura. Para cada linha retornada da varredura, ele verifica se o valor corresponde ao filtro em relação a[dbo].[CS]
. A varredura é interrompida assim que encontra uma linha correspondente. O SQL Server estimou que só recuperaria 3,33 linhas em média da verificação até encontrar uma correspondência. Se isso fosse verdade, você veria cerca de 1,5 milhão de execuções contra[dbo].[CS]
. Em vez disso, o otimizador fez 2 bilhões de execuções na tabela, então a estimativa foi mais de 1.000 vezes errada.Como regra geral, você deve examinar cuidadosamente quaisquer varreduras no lado interno de um loop aninhado. Claro, existem algumas consultas para as quais é isso que você deseja. E só porque você tem uma busca não significa que a consulta será eficiente. Por exemplo, se uma busca retornar muitas linhas, pode não haver muita diferença em fazer uma varredura. Você não postou a consulta completa aqui, mas vou passar por algumas ideias que podem ajudar.
Esta consulta é um pouco estranha:
É não determinista porque você tem
TOP
semORDER BY
. No entanto, a própria tabela tem 1 linha e você sempre recupera o mesmo valor para cada linha deo
. Se possível, eu tentaria salvar o valor dessa consulta em uma variável local e usar isso na consulta. Isso deve economizar um total de 8 bilhões de varreduras novamente[dbo].[CS]
e eu esperaria ver uma busca de índice em vez de uma varredura de índice contraix_days
. Consegui simular alguns dados na minha máquina. Aqui está parte do plano de consulta:Agora temos todas as buscas e essas buscas não devem processar muitas linhas extras. No entanto, a consulta real pode ser mais complicada do que isso, então talvez você não possa usar uma variável.
Digamos que eu escreva uma condição de filtro diferente que não use
TOP
. Em vez disso, usareiMIN
. O SQL Server é capaz de processar essa subconsulta de maneira mais eficiente. O TOP pode impedir certas transformações de consulta. Aqui está minha subconsulta:Veja como pode ser o plano:
Agora faremos apenas cerca de 1,5 milhão de varreduras na
CS
mesa. também obtemos uma busca de índice muito mais eficiente em relação aoix_days
índice que é capaz de usar os resultados da subconsulta:Claro, não estou dizendo que você deve reescrever seu código para usar isso. Provavelmente retornará resultados incorretos. O ponto importante é que você pode obter as buscas de índice que deseja com uma subconsulta. Você só precisa escrever sua subconsulta da maneira correta.
For one more example, let's assume that you absolutely need to keep the
TOP
operator in the subquery. It might be possible to add a redundant filter againstPkDate
to get better performance. I'm going to assume that the results of the subquery are non-negative and small. That means that this query will be equivalent:This changes the plan to use seeks:
It's important to realize that the seeks may return more just one row. The important point is that SQL Server can start seeking at
o.[CreationDate]
. If there's a large gap in the dates then the index seek will process many extra rows and the query will not be as efficient.Você está obtendo esses números da junção de loop aninhado .
No seu, este é um exemplo de como você obtém cerca de 2B registros.
Outra como você obtém 5B+.
Alguns links sobre como você pode evitar grandes associações de loops aninhados:
The information that both other answers try to convey but fail (only partly due to assumptions that I understand exactly what they say) is this:
With the query written the way it was in the question the observed performance was inevitable.
While it was fancy and mostly easy to see the purpose it was simply too heavy for the optimizer to work magic on it. It wasn't quite the nested loop problem SqlWorldWide indicated, but the subqueries simply had to be executed for each row and since they were index seeks and scans they multiplied, and multiplied... and multiplied.
What I ended up having was this:
In addition to streamlining the query - which still is not optimal - I've also reworked the calendar.dbo.days table's indexes. Dropped the constraint (which I really didn't have to, but what the hell, it might cause more problems further down the line) and added this:
Admito que é principalmente para que eu possa utilizar mais plenamente a tabela de calendário (eu mencionei que Jim Horn é um gênio?), mas no momento em que minhas contas as pessoas viram, elas querem cada vez mais as guloseimas que ela armazena ... em todos os lugares .
A conclusão é que, embora seja importante observar todos os aspectos da consulta: lógica, índices, predicados etc., às vezes a única maneira sensata de melhorar as coisas é alterar o código. No meu caso, o tempo de execução da consulta completa (várias inserções, atualizações e CTEs) agora termina em cerca de 2 minutos, em comparação com 15 minutos antes.