Database
 sql >> Base de Dados >  >> RDS >> Database

Qual é a maneira mais eficiente de cortar o tempo de datetime?


A necessidade mais comum de retirar o tempo de um valor datetime é obter todas as linhas que representam pedidos (ou visitas ou acidentes) que ocorreram em um determinado dia. No entanto, nem todas as técnicas utilizadas para isso são eficientes ou mesmo seguras.

TL;versão DR


Se você deseja uma consulta de intervalo seguro com bom desempenho, use um intervalo aberto ou, para consultas de um dia no SQL Server 2008 e superior, use CONVERT(DATE) :
DECLARE @today DATETIME;
 
-- only on <= 2005:
 
SET @today = DATEADD(DAY, DATEDIFF(DAY, '20000101', CURRENT_TIMESTAMP), '20000101');
 
-- or on 2008 and above:
 
SET @today = CONVERT(DATE, CURRENT_TIMESTAMP);
 
-- and then use an open-ended range in the query:
 
...
WHERE OrderDate >= @today 
  AND OrderDate < DATEADD(DAY, 1, @today);
 
-- you can also do this (again, in SQL Server 2008 and above):
 
...
WHERE CONVERT(DATE, OrderDate) = @today;

Algumas ressalvas:
  • Tenha cuidado com o DATEDIFF abordagem, pois existem algumas anomalias de estimativa de cardinalidade que podem ocorrer (consulte esta postagem do blog e a pergunta do Stack Overflow que o estimulou para obter mais informações).
  • Embora o último ainda use potencialmente uma busca de índice (diferente de todas as outras expressões não sargáveis ​​que já encontrei), você precisa ter cuidado ao converter a coluna em uma data antes de comparar. Essa abordagem também pode gerar estimativas de cardinalidade fundamentalmente erradas. Veja esta resposta de Martin Smith para mais detalhes.

De qualquer forma, continue lendo para entender por que essas são as duas únicas abordagens que eu recomendo.

Nem todas as abordagens são seguras


Como um exemplo inseguro, vejo este muito usado:
WHERE OrderDate BETWEEN DATEDIFF(DAY, 0, GETDATE()) 
  AND DATEADD(MILLISECOND, -3, DATEDIFF(DAY, 0, GETDATE()) + 1);

Existem alguns problemas com essa abordagem, mas o mais notável é o cálculo do "fim" de hoje - se o tipo de dados subjacente for SMALLDATETIME , esse intervalo final será arredondado para cima; se for DATETIME2 , teoricamente você pode perder dados no final do dia. Se você escolher minutos ou nanossegundos ou qualquer outro intervalo para acomodar o tipo de dados atual, sua consulta começará a ter um comportamento estranho caso o tipo de dados mude mais tarde (e vamos ser honestos, se alguém alterar o tipo dessa coluna para ser mais ou menos granular, eles não estão correndo por aí verificando todas as consultas que o acessam). Ter que codificar dessa maneira, dependendo do tipo de dados de data/hora na coluna subjacente, é fragmentado e propenso a erros. É muito melhor usar intervalos de datas abertos para isso:

Eu falo muito mais sobre isso em alguns posts antigos do blog:
  • O que BETWEEN e o diabo têm em comum?
  • Maus hábitos a serem eliminados:tratamento incorreto de consultas de data/intervalo

Mas eu queria comparar o desempenho de algumas das abordagens mais comuns que vejo por aí. Sempre usei intervalos abertos e, desde o SQL Server 2008, podemos usar CONVERT(DATE) e ainda utilizar um índice nessa coluna, que é bastante poderoso.
SELECT CONVERT(CHAR(8), CURRENT_TIMESTAMP, 112);
SELECT CONVERT(CHAR(10), CURRENT_TIMESTAMP, 120);
SELECT CONVERT(DATE, CURRENT_TIMESTAMP);
SELECT DATEADD(DAY, DATEDIFF(DAY, '19000101', CURRENT_TIMESTAMP), '19000101');
SELECT CONVERT(DATETIME, DATEDIFF(DAY, '19000101', CURRENT_TIMESTAMP));
SELECT CONVERT(DATETIME, CONVERT(INT, CONVERT(FLOAT, CURRENT_TIMESTAMP)));
SELECT CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, CURRENT_TIMESTAMP)));

Um teste de desempenho simples


Para realizar um teste de desempenho inicial muito simples, fiz o seguinte para cada uma das declarações acima, definindo uma variável para a saída do cálculo 100.000 vezes:
SELECT SYSDATETIME();
GO
 
DECLARE @d DATETIME = [conversion method];
GO 100000
 
SELECT SYSDATETIME();
GO

Eu fiz isso três vezes para cada método, e todos eles rodaram no intervalo de 34 a 38 segundos. Então, estritamente falando, existem diferenças muito insignificantes nesses métodos ao realizar as operações na memória:


Um teste de desempenho mais elaborado


Eu também queria comparar esses métodos com diferentes tipos de dados (DATETIME , SMALLDATETIME e DATETIME2 ), contra um índice clusterizado e um heap, e com e sem compactação de dados. Então, primeiro eu criei um banco de dados simples. Por meio de experimentação, determinei que o tamanho ideal para lidar com 120 milhões de linhas e toda a atividade de log que poderia ocorrer (e para evitar que eventos de crescimento automático interferissem no teste) era um arquivo de dados de 20 GB e um log de 3 GB:
CREATE DATABASE [Datetime_Testing]
ON PRIMARY 
( 
  NAME = N'Datetime_Testing_Data', 
  FILENAME = N'D:\DATA\Datetime_Testing.mdf', 
  SIZE = 20480000KB , MAXSIZE = UNLIMITED, FILEGROWTH = 102400KB 
)
LOG ON 
( 
  NAME = N'Datetime_Testing_Log', 
  FILENAME = N'E:\LOGS\Datetime_Testing_log.ldf', 
  SIZE = 3000000KB , MAXSIZE = UNLIMITED, FILEGROWTH = 20480KB );

Em seguida, criei 12 tabelas:
-- clustered index with no compression:
 
CREATE TABLE dbo.smalldatetime_nocompression_clustered(dt SMALLDATETIME);
CREATE CLUSTERED INDEX x ON dbo.smalldatetime_nocompression_clustered(dt);
 
-- heap with no compression:
 
CREATE TABLE dbo.smalldatetime_nocompression_heap(dt SMALLDATETIME);
 
-- clustered index with page compression:
 
CREATE TABLE dbo.smalldatetime_compression_clustered(dt SMALLDATETIME) 
WITH (DATA_COMPRESSION = PAGE);
 
CREATE CLUSTERED INDEX x ON dbo.smalldatetime_compression_clustered(dt)
WITH (DATA_COMPRESSION = PAGE);
 
-- heap with page compression:
 
CREATE TABLE dbo.smalldatetime_compression_heap(dt SMALLDATETIME)
WITH (DATA_COMPRESSION = PAGE);

[Em seguida, repita novamente para DATETIME e DATETIME2.]

Em seguida, inseri 10.000.000 de linhas em cada tabela. Eu fiz isso criando uma visão que geraria as mesmas 10.000.000 datas de cada vez:
CREATE VIEW dbo.TenMillionDates
AS
 SELECT TOP (10000000) d = DATEADD(MINUTE, ROW_NUMBER() OVER 
   (ORDER BY s1.[object_id]), '19700101')
  FROM sys.all_columns AS s1
  CROSS JOIN sys.all_objects AS s2
  ORDER BY s1.[object_id];

Isso me permitiu preencher as tabelas desta forma:
INSERT /* dt_comp_clus */ dbo.datetime_compression_clustered(dt) 
  SELECT CONVERT(DATETIME, d) FROM dbo.TenMillionDates;
CHECKPOINT;
INSERT /* dt2_comp_clus */ dbo.datetime2_compression_clustered(dt) 
  SELECT CONVERT(DATETIME2, d) FROM dbo.TenMillionDates;
CHECKPOINT;
INSERT /* sdt_comp_clus */ dbo.smalldatetime_compression_clustered(dt) 
  SELECT CONVERT(SMALLDATETIME, d) FROM dbo.TenMillionDates;
CHECKPOINT;

[Em seguida, repita novamente para os heaps e o índice clusterizado não compactado. Coloquei um CHECKPOINT entre cada inserção para garantir a reutilização do log (o modelo de recuperação é simples).]

INSERIR Tempos e Espaço Usado


Aqui estão os horários para cada inserção (conforme capturado com o Plan Explorer):





E aqui está a quantidade de espaço ocupado por cada mesa:
SELECT 
  [table] = OBJECT_NAME([object_id]), 
  row_count, 
  page_count = reserved_page_count,
  reserved_size_MB = reserved_page_count * 8/1024
FROM sys.dm_db_partition_stats 
WHERE OBJECT_NAME([object_id]) LIKE '%datetime%';


Desempenho do padrão de consulta


Em seguida, comecei a testar dois padrões de consulta diferentes para desempenho:
  • Contando as linhas de um dia específico, usando as sete abordagens acima, bem como o período em aberto
  • Converter todas as 10.000.000 linhas usando as sete abordagens acima, bem como apenas retornar os dados brutos (já que a formatação no lado do cliente pode ser melhor)

[Com exceção do FLOAT métodos e o DATETIME2 coluna, já que esta conversão não é legal.]

Para a primeira pergunta, as consultas ficam assim (repetidas para cada tipo de tabela):
SELECT /* C_CHAR10 - dt_comp_clus */ COUNT(*) 
    FROM dbo.datetime_compression_clustered 
    WHERE CONVERT(CHAR(10), dt, 120) = '19860301';
 
SELECT /* C_CHAR8  - dt_comp_clus */ COUNT(*) 
    FROM dbo.datetime_compression_clustered 
    WHERE CONVERT(CHAR(8),  dt, 112) = '19860301';
 
SELECT /* C_FLOOR_FLOAT - dt_comp_clus */ COUNT(*) 
    FROM dbo.datetime_compression_clustered 
    WHERE CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, dt))) = '19860301';
 
SELECT /* C_DATETIME  - dt_comp_clus */ COUNT(*) 
    FROM dbo.datetime_compression_clustered 
    WHERE CONVERT(DATETIME, DATEDIFF(DAY, '19000101', dt)) = '19860301';
 
SELECT /* C_DATE  - dt_comp_clus */ COUNT(*) 
    FROM dbo.datetime_compression_clustered 
    WHERE CONVERT(DATE, dt) = '19860301';
 
SELECT /* C_INT_FLOAT - dt_comp_clus */ COUNT(*) 
    FROM dbo.datetime_compression_clustered 
    WHERE CONVERT(DATETIME, CONVERT(INT, CONVERT(FLOAT, dt))) = '19860301';
 
SELECT /* DATEADD - dt_comp_clus */ COUNT(*) 
    FROM dbo.datetime_compression_clustered 
    WHERE DATEADD(DAY, DATEDIFF(DAY, '19000101', dt), '19000101') = '19860301';
 
SELECT /* RANGE - dt_comp_clus */ COUNT(*) 
    FROM dbo.datetime_compression_clustered 
    WHERE dt >= '19860301' AND dt < '19860302';

Os resultados em relação a um índice clusterizado são assim (clique para ampliar):



Aqui vemos que a conversão para data e o intervalo aberto usando um índice têm os melhores desempenhos. No entanto, em relação a um heap, a conversão para data na verdade leva algum tempo, tornando o intervalo aberto a escolha ideal (clique para ampliar):



E aqui está o segundo conjunto de consultas (novamente, repetindo para cada tipo de tabela):
SELECT /* C_CHAR10 - dt_comp_clus */ dt = CONVERT(CHAR(10), dt, 120) 
    FROM dbo.datetime_compression_clustered;
 
SELECT /* C_CHAR8 - dt_comp_clus */ dt = CONVERT(CHAR(8), dt, 112) 
    FROM dbo.datetime_compression_clustered;
 
SELECT /* C_FLOOR_FLOAT - dt_comp_clus */ dt = CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, dt))) 
    FROM dbo.datetime_compression_clustered;
 
SELECT /* C_DATETIME  - dt_comp_clus */ dt = CONVERT(DATETIME, DATEDIFF(DAY, '19000101', dt)) 
    FROM dbo.datetime_compression_clustered;
 
SELECT /* C_DATE  - dt_comp_clus */ dt = CONVERT(DATE, dt) 
    FROM dbo.datetime_compression_clustered;
 
SELECT /* C_INT_FLOAT - dt_comp_clus */ dt = CONVERT(DATETIME, CONVERT(INT, CONVERT(FLOAT, dt))) 
    FROM dbo.datetime_compression_clustered;
 
SELECT /* DATEADD - dt_comp_clus */ dt = DATEADD(DAY, DATEDIFF(DAY, '19000101', dt), '19000101') 
    FROM dbo.datetime_compression_clustered;
 
SELECT /* RAW - dt_comp_clus */ dt 
    FROM dbo.datetime_compression_clustered;

Focando nos resultados para tabelas com um índice clusterizado, fica claro que o convert to date teve um desempenho muito próximo de apenas selecionar os dados brutos (clique para ampliar):



(Para este conjunto de consultas, o heap mostrou resultados muito semelhantes – praticamente indistinguíveis.)

Conclusão


Caso você queira pular para a conclusão, esses resultados mostram que as conversões na memória não são importantes, mas se você estiver convertendo dados ao sair de uma tabela (ou como parte de um predicado de pesquisa), o método escolhido pode ter um impacto dramático no desempenho. Convertendo para um DATE (para um único dia) ou usar um intervalo de datas aberto em qualquer caso produzirá o melhor desempenho, enquanto o método mais popular disponível – converter para uma string – é absolutamente péssimo.

Também vemos que a compactação pode ter um efeito decente no espaço de armazenamento, com um impacto muito pequeno no desempenho da consulta. O efeito sobre o desempenho da inserção parece ser tão dependente se a tabela tem ou não um índice clusterizado, em vez de se a compactação está ou não habilitada. No entanto, com um índice clusterizado, houve um aumento notável na duração necessária para inserir 10 milhões de linhas. Algo para manter em mente e equilibrar com a economia de espaço em disco.

Claramente, poderia haver muito mais testes envolvidos, com cargas de trabalho mais substanciais e variadas, que posso explorar mais em um post futuro.