Digamos
Você tem um procedimento armazenado que aceita matrizes de data e hora, que são carregadas em uma tabela temporária e usadas para filtrar uma coluna de data e hora em uma tabela.
- Pode haver qualquer número de valores inseridos como datas de início e término.
- Os intervalos de datas podem se sobrepor às vezes , mas não é uma condição com a qual eu contaria regularmente.
- Também é possível fornecer datas com horários.
Qual é a maneira mais eficiente de escrever uma consulta para realizar a filtragem?
configurar
USE StackOverflow2013;
CREATE TABLE
#d
(
dfrom datetime,
dto datetime,
PRIMARY KEY (dfrom, dto)
)
INSERT
#d
(
dfrom,
dto
)
SELECT
dfrom = '2013-11-20',
dto = '2013-12-05'
UNION ALL
SELECT
dfrom = '2013-11-27',
dto = '2013-12-12';
CREATE INDEX
p
ON dbo.Posts
(CreationDate)
WITH
(SORT_IN_TEMPDB = ON, DATA_COMPRESSION = PAGE);
consulta
O melhor que consegui foi usar EXISTS
assim:
SELECT
c = COUNT_BIG(*)
FROM dbo.Posts AS p
WHERE EXISTS
(
SELECT
1/0
FROM #d AS d
WHERE p.CreationDate BETWEEN d.dfrom
AND d.dto
);
O que resulta em um plano de execução bastante triste:
Nested Loops é o único operador de junção disponível, já que não temos um predicado de igualdade.
O que procuro é uma sintaxe alternativa que produza um tipo diferente de junção.
Obrigado!
Juntar
Você pode descobrir que uma junção oferece desempenho adequado, apesar de ainda usar loops aninhados. Esta é uma variação da resposta atualizada de David Browne :
Isso dura cerca de 150 ms para mim.
Remova sobreposições e junte-se
Se você tiver linhas sobrepostas significativas, pode valer a pena reduzi-las a intervalos distintos e não sobrepostos, conforme descrito na resposta de Martin Smith . Minha implementação de uma ideia semelhante é:
Isso dura cerca de 40ms para mim.
Busca dinâmica sem junção
Outra abordagem é gerar intervalos literais dinamicamente:
Com os dados de amostra, isso produz:
Qual SQL Server simplifica para uma única busca de intervalo:
Isso dura cerca de 35ms para mim.
Em geral, o otimizador simplificará os intervalos tanto quanto possível (assim como faria um Merge Interval ). Não parece haver um limite para o número de buscas de intervalo separadas em um único operador de busca de índice . Fiquei entediado depois de 1440 buscas .
Visualização indexada
Em vez de contar linhas repetidamente, pode ser útil armazenar e manter contagens agrupadas em uma visualização indexada. A implementação a seguir usa granularidade horária:
The view takes around 2s to create on my laptop. At hour granularity, the indexed view holds 47,469 rows for my copy of the StackOverflow2013 database's Posts table with 17,142,169 rows.
The majority of the work can be done from the view. Only part-hour periods at the start and end of any range not covered by any other range needs to be processed separately. For example:
The three queries above complete in less than 1 ms overall. There are no part-hour periods in the sample data. Performance will decrease somewhat with more and larger part periods, or if a less precise indexed view granularity is used.
Se você tiver um número pequeno de datas, poderá materializar uma lista ordenada de todas as datas válidas, por exemplo
E então obtenha uma junção de mesclagem. Ou vá na outra direção, por exemplo
E você pode apenas COUNT_BIG(*) se pré-processar #D para mesclar intervalos sobrepostos.
Usar SQL Dinâmico para gerar uma série de
UNION
instruções parece eliminar oNested Loops
e resulta emHash Match
:Plano de Execução Real
Parece correr muito rápido do meu lado. É claro que isso poderia resultar em uma longa lista de
UNION
cláusulas, dependendo de quantas linhas existem na#d
tabela temporária. YMMV.What I would really like is to be able to get a plan like
Where SQL Server uses its built in merge interval operator to combine overlapping ranges.
I don't think this is currently possible though to get this driven by a table in this case.
My attempt to manually simulate this is below
If I got the overlapping range logic correct (which has taken a few stabs at it so far but hopefully is now correct) then this should collapse down the distinct ranges efficiently and then seek exactly the needed rows for those ranges from Posts.
dfrom, dto
(which conveniently is the order of the PK) and keep track of largestdto
value seen in preceding rows (MaxDtoSoFar
).dfrom
in a row is <=MaxDtoSoFar
then we are continuing an existing interval and setIsIntervalStart
to0
otherwise we are starting a new interval and set that to1
.1
and fill in the relevantMaxDtoSoFar
value.MaxDtoSoFar
and notGREATEST(dto, MaxDtoSoFar)
.NB: The above method does do the interval merging with a single ordered clustered index scan and no sort operations but does have a wide execution plan with multiple of each of Segment/Sequence Project/Window Spool/ Stream Aggregate operators.
Paul White provided an alternative method in the comments that can use batch mode Window Aggregates exclusively and has a much more streamlined plan (as well as likely being simpler SQL)
First I'd like to apologize for the odd query plan in my answer. My computer was recently hacked and I've been unable to remove the SSMS plugin.
Você pode melhorar bastante o desempenho dividindo cada data válida em sua própria linha. O truque é também transportar informações de tempo para excluir dados nos terminais, se necessário. Por exemplo, considere o intervalo de datas '20230709 18:00:00' a '20230711 04:00:00'. Para esse intervalo, você incluiria linhas com data de 20230709 com hora >= 18:00:00, todas as linhas para 20230710 e linhas com data de 20230711 com hora <= 04:00:00.
Abaixo estão os mesmos dados de amostra, juntamente com uma tabela temporária extra que resume os intervalos de datas da maneira que descrevi anteriormente:
Esta consulta é executada em 64 ms na minha máquina:
Para uma aceleração relativa de cerca de 300X. Aqui está o plano de consulta:
Existe um conceito interessante chamado árvore de intervalo relacional estático, Itzik escreveu algumas coisas sobre ele, mas acho que houve algum problema em sua postagem no blog com o código de exemplo, então nunca consegui fazê-lo funcionar, de qualquer forma, encontrei um link de exemplo e há mais se você usar esses termos para uma pesquisa na web
https://lucient.com/blog/a-static-relational-interval-tree/#
Seems your objective is to find the number of matching posts, that are in the given date ranges. Given you don't have more information on data distribution and quantity structure, it is hard to give proper recommendations.
How about the obvious, assuming that there's primary key "id" in Posts:
and creating an index on Posts(CreationDate, id).