Eu tenho o que é, para mim, uma pergunta interessante sobre SARGability. Neste caso, trata-se de usar um predicado na diferença entre duas colunas de data. Aqui está a configuração:
USE [tempdb]
SET NOCOUNT ON
IF OBJECT_ID('tempdb..#sargme') IS NOT NULL
BEGIN
DROP TABLE #sargme
END
SELECT TOP 1000
IDENTITY (BIGINT, 1,1) AS ID,
CAST(DATEADD(DAY, [m].[severity] * -1, GETDATE()) AS DATE) AS [DateCol1],
CAST(DATEADD(DAY, [m].[severity], GETDATE()) AS DATE) AS [DateCol2]
INTO #sargme
FROM sys.[messages] AS [m]
ALTER TABLE [#sargme] ADD CONSTRAINT [pk_whatever] PRIMARY KEY CLUSTERED ([ID])
CREATE NONCLUSTERED INDEX [ix_dates] ON [#sargme] ([DateCol1], [DateCol2])
O que vejo com bastante frequência é algo assim:
/*definitely not sargable*/
SELECT
* ,
DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
[#sargme] AS [s]
WHERE
DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) >= 48;
...que definitivamente não é SARGable. Isso resulta em uma varredura de índice, lê todas as 1000 linhas, não é bom. Linhas estimadas fedem. Você nunca colocaria isso em produção.
Seria bom se pudéssemos materializar CTEs, porque isso nos ajudaria a tornar isso, bem, mais SARGable-er, tecnicamente falando. Mas não, temos o mesmo plano de execução do topo.
/*would be nice if it were sargable*/
WITH [x] AS ( SELECT
* ,
DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) AS [ddif]
FROM
[#sargme] AS [s])
SELECT
*
FROM
[x]
WHERE
[x].[ddif] >= 48;
E, claro, como não estamos usando constantes, esse código não muda nada e nem é meio SARGable. Sem graça. Mesmo plano de execução.
/*not even half sargable*/
SELECT
* ,
DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
[#sargme] AS [s]
WHERE
[s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])
Se você estiver com sorte e estiver obedecendo a todas as opções ANSI SET em suas strings de conexão, poderá adicionar uma coluna computada e pesquisá-la ...
ALTER TABLE [#sargme] ADD [ddiff] AS
DATEDIFF(DAY, DateCol1, DateCol2) PERSISTED
CREATE NONCLUSTERED INDEX [ix_dates2] ON [#sargme] ([ddiff], [DateCol1], [DateCol2])
SELECT [s].[ID] ,
[s].[DateCol1] ,
[s].[DateCol2]
FROM [#sargme] AS [s]
WHERE [ddiff] >= 48
Isso resultará em uma busca de índice com três consultas. O estranho é onde adicionamos 48 dias a DateCol1. A consulta DATEDIFF
na WHERE
cláusula, o CTE
, e a consulta final com um predicado na coluna computada fornecem um plano muito melhor com estimativas muito melhores e tudo mais.
O que me leva à pergunta: em uma única consulta, existe uma maneira SARGable de realizar essa pesquisa?
Sem tabelas temporárias, sem variáveis de tabela, sem alterar a estrutura da tabela e sem exibições.
Estou bem com junções automáticas, CTEs, subconsultas ou várias passagens pelos dados. Pode funcionar com qualquer versão do SQL Server.
Evitar a coluna computada é uma limitação artificial porque estou mais interessado em uma solução de consulta do que em qualquer outra coisa.
Apenas adicionando isso rapidamente para que exista como uma resposta (embora eu saiba que não é a resposta que você deseja).
Uma coluna computada indexada geralmente é a solução certa para esse tipo de problema.
Isto:
Para esclarecer esse último ponto, a coluna calculada não precisa ser persistida neste caso:
Agora a consulta:
...dá o seguinte plano trivial :
Como disse Martin Smith, se você tiver conexões usando as opções de conjunto erradas, poderá criar uma coluna regular e manter o valor calculado usando gatilhos.
Tudo isso realmente importa (desafio do código à parte) se houver um problema real a ser resolvido, é claro, como Aaron diz em sua resposta .
É divertido pensar nisso, mas não conheço nenhuma maneira de conseguir o que você deseja razoavelmente, dadas as restrições da questão. Parece que qualquer solução ideal exigiria uma nova estrutura de dados de algum tipo; o mais próximo que temos é a aproximação de 'índice de função' fornecida por um índice em uma coluna computada não persistente como acima.
Arriscando-me a ser ridicularizado por alguns dos maiores nomes da comunidade do SQL Server, vou arriscar o pescoço e dizer não.
Para que sua consulta seja SARGable, você basicamente teria que construir uma consulta que pudesse identificar uma linha inicial em um intervalo de linhas consecutivas em um índice. Com o índice
ix_dates
, as linhas não são ordenadas pela diferença de data entreDateCol1
eDateCol2
, portanto, suas linhas de destino podem ser espalhadas em qualquer lugar do índice.Autojunções, passagens múltiplas, etc., todos têm em comum que incluem pelo menos uma varredura de índice, embora uma junção (loop aninhado) possa usar uma busca de índice. Mas não vejo como seria possível eliminar o Scan.
Quanto a obter estimativas de linha mais precisas, não há estatísticas sobre a diferença de datas.
A construção CTE recursiva bastante feia a seguir elimina tecnicamente a varredura de toda a tabela, embora introduza uma junção de loop aninhada e um número (potencialmente muito grande) de buscas de índice.
Ele cria um spool de índice contendo cada
DateCol1
na tabela e, em seguida, executa uma busca de índice (varredura de intervalo) para cada um delesDateCol1
eDateCol2
que são pelo menos 48 dias à frente.Mais E/S, tempo de execução um pouco mais longo, estimativa de linha ainda distante e chance zero de paralelização por causa da recursão: acho que essa consulta pode ser útil se você tiver um número muito grande de valores em relativamente poucos distintos, consecutivos
DateCol1
(mantendo o número de Seeks baixo).Eu tentei um monte de variações malucas, mas não encontrei nenhuma versão melhor do que uma das suas. O principal problema é que seu índice se parece com isso em termos de como date1 e date2 são classificados juntos. A primeira coluna ficará em uma bela linha arquitetada, enquanto o espaço entre elas será muito irregular. Você quer que isso pareça mais com um funil do que realmente será:
Não há realmente nenhuma maneira de tornar isso pesquisável para um determinado delta (ou intervalo de deltas) entre os dois pontos. E quero dizer uma única busca que é executada uma vez + uma varredura de alcance, não uma busca que é executada para cada linha. Isso envolverá uma verificação e/ou classificação em algum momento, e essas são coisas que você deseja evitar obviamente. É uma pena que você não possa usar expressões como
DATEADD
/DATEDIFF
em índices filtrados ou realizar quaisquer possíveis modificações de esquema que permitam uma classificação no produto da diferença de data (como calcular o delta no momento da inserção/atualização). Como está, esse parece ser um daqueles casos em que uma varredura é, na verdade, o método de recuperação ideal.Você disse que esta consulta não era divertida, mas se você olhar mais de perto, esta é de longe a melhor (e seria ainda melhor se você deixasse de fora a saída escalar de computação):
A razão é que evitar
DATEDIFF
potencialmente reduz alguma CPU em comparação com um cálculo apenas contra a coluna de chave não principal no índice e também evita algumas conversões implícitas desagradáveis paradatetimeoffset(7)
(não me pergunte por que elas estão lá, mas estão). Aqui está aDATEDIFF
versão:E aqui está o sem
DATEDIFF
:Também encontrei resultados um pouco melhores em termos de duração quando alterei o índice para apenas incluir
DateCol2
(e quando ambos os índices estavam presentes, o SQL Server sempre escolheu aquele com uma chave e uma coluna de inclusão versus várias chaves). Para esta consulta, como temos que varrer todas as linhas para encontrar o intervalo de qualquer maneira, não há benefício em ter a segunda coluna de data como parte da chave e classificada de qualquer maneira. E, embora eu saiba que não podemos obter uma busca aqui, há algo inerentemente bom em não impedir a capacidade de obter uma forçando cálculos na coluna de chave principal e apenas executá-los nas colunas secundárias ou incluídas.Se fosse eu, e desistisse de encontrar a solução sargable, sei qual escolheria - aquela que faz o SQL Server dar menos trabalho (mesmo que o delta seja quase inexistente). Ou melhor ainda, eu relaxaria minhas restrições sobre mudança de esquema e coisas do gênero.
E o quanto tudo isso importa? Não sei. Fiz a tabela com 10 milhões de linhas e todas as variações de consulta acima ainda foram concluídas em menos de um segundo. E isso está em uma VM em um laptop (concedido, com SSD).
Todas as maneiras que pensei para tornar essa cláusula WHERE sargable são complexas e parecem trabalhar em busca de índice como um objetivo final, e não como um meio. Então, não, não acho que seja (pragmaticamente) possível.
Eu não tinha certeza se "não alterar a estrutura da tabela" significava nenhum índice adicional. Aqui está uma solução que evita completamente as varreduras de índice, mas resulta em MUITAS buscas de índice separadas, ou seja, uma para cada data DateCol1 possível no intervalo Min/Max de valores de data na tabela. (Ao contrário de Daniel, que resulta em uma busca para cada data distinta que realmente aparece na tabela). Teoricamente é candidato ao paralelismo porque evita a recursão. Mas, honestamente, é difícil ver uma distribuição de dados em que isso seja mais rápido do que apenas digitalizar e fazer DATEDIFF. (Talvez um DOP muito alto?) E... o código é feio. Acho que esse esforço conta como um "exercício mental".
Resposta do Community Wiki originalmente adicionada pelo autor da pergunta como uma edição da pergunta
Depois de deixar isso parado um pouco e algumas pessoas realmente inteligentes entrarem em contato, meu pensamento inicial sobre isso parece correto: não há uma maneira sã e SARGable de escrever essa consulta sem adicionar uma coluna, seja computada ou mantida por meio de algum outro mecanismo, ou seja, gatilhos.
Eu tentei algumas outras coisas e tenho algumas outras observações que podem ou não ser interessantes para quem está lendo.
First, re-running the setup using a regular table rather than a temp table
Here's the new setup:
Then, running the first query, it uses the ix_dates index, and scans, just like before. No change here. This seems redundant, but stick with me.
Run the CTE query again, still the same...
Alright! Run the not-even-half-sargable query again:
Now add the computed column, and re-run all three, along with the query that hits the computed column:
If you stuck with me to here, thanks. This is the interesting observation portion of the post.
Running a query with an undocumented trace flag by Fabiano Amorim to see which statistics each query used is pretty cool. Seeing that no plan touched a statistics object until the computed column was created and indexed seemed odd.
Heck, even the query that hit the computed column ONLY didn't touch a statistics object until I ran it a few times and it got simple parameterization. So even though they all initially scanned the ix_dates index, they used hard-coded cardinality estimates (30% of the table) rather than any statistics object available to them.
Um outro ponto que levantou uma sobrancelha aqui é que, quando adicionei apenas o índice não clusterizado, todos os planos de consulta examinaram o HEAP, em vez de usar o índice não clusterizado em ambas as colunas de data.
Obrigado a todos que responderam. Vocês são todos maravilhosos.