Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

DATEDIFF() retorna resultados errados no SQL Server? Leia isso.


Se você está obtendo alguns resultados realmente estranhos ao usar o DATEDIFF() função no SQL Server, e você está convencido de que a função contém um bug, não arranque seus cabelos ainda. Provavelmente não é um bug.

Existem cenários em que os resultados produzidos por essa função podem ser bem malucos. E se você não entender como a função realmente funciona, os resultados parecerão completamente errados.

Espero que este artigo possa ajudar a esclarecer como o DATEDIFF() A função foi projetada para funcionar e fornece alguns cenários de exemplo de onde seus resultados podem não ser os esperados.


Exemplo 1 – 365 dias nem sempre é um ano


Pergunta: Quando é 365 dias não um ano?

Resposta: Ao usar DATEDIFF() é claro!

Aqui está um exemplo onde eu uso DATEDIFF() para retornar o número de dias entre duas datas e, em seguida, o número de anos entre as mesmas duas datas.
DECLARE 
  @startdate datetime2 = '2016-01-01  00:00:00.0000000', 
  @enddate datetime2 = '2016-12-31 23:59:59.9999999';
SELECT 
  DATEDIFF(day, @startdate, @enddate) Days,
  DATEDIFF(year, @startdate, @enddate) Years;

Resultado:
+--------+---------+
| Days   | Years   |
|--------+---------|
| 365    | 0       |
+--------+---------+

Se você acha que este resultado está errado e que DATEDIFF() obviamente tem um bug, continue lendo – nem tudo é o que parece.

Acredite ou não, este é realmente o resultado esperado. Este resultado está exatamente de acordo com como DATEDIFF() é projetado para funcionar.

Exemplo 2 – 100 nanossegundos =1 ano?


Vamos levá-lo de outra maneira.
DECLARE @startdate datetime2 = '2016-12-31 23:59:59.9999999', 
  @enddate datetime2 = '2017-01-01 00:00:00.0000000';

SELECT DATEDIFF(year,     @startdate,   @enddate) Year,
  DATEDIFF(quarter,       @startdate,   @enddate) Quarter,
  DATEDIFF(month,         @startdate,   @enddate) Month,
  DATEDIFF(dayofyear,     @startdate,   @enddate) DOY,
  DATEDIFF(day,           @startdate,   @enddate) Day,
  DATEDIFF(week,          @startdate,   @enddate) Week,
  DATEDIFF(hour,          @startdate,   @enddate) Hour,
  DATEDIFF(minute,        @startdate,   @enddate) Minute,
  DATEDIFF(second,        @startdate,   @enddate) Second,
  DATEDIFF(millisecond,   @startdate,   @enddate) Millisecond,
  DATEDIFF(microsecond,   @startdate,   @enddate) Microsecond,
  DATEDIFF(nanosecond,    @startdate,   @enddate) Nanosecond;

Resultados (mostrados com saída vertical):
Year        | 1
Quarter     | 1
Month       | 1
DOY         | 1
Day         | 1
Week        | 1
Hour        | 1
Minute      | 1
Second      | 1
Millisecond | 1
Microsecond | 1
Nanosecond  | 100

Há apenas uma diferença de cem nanossegundos (0,0000001 segundo) entre as duas datas/horas, mas obtemos exatamente o mesmo resultado para cada parte de data, exceto nanossegundos.

Como isso pode acontecer? Como pode haver 1 microssegundo de diferença e 1 ano de diferença ao mesmo tempo? Sem mencionar todos os dateparts no meio?

Pode parecer loucura, mas isso também não é um bug. Esses resultados estão exatamente de acordo com como DATEDIFF() é suposto funcionar.

E para tornar as coisas ainda mais confusas, poderíamos obter resultados diferentes dependendo do tipo de dados. Mas chegaremos a isso em breve. Primeiro vamos ver como o DATEDIFF() função realmente funciona.

A definição real de DATEDIFF()


A razão pela qual obtemos os resultados que obtemos é porque o DATEDIFF() função é definida da seguinte forma:

Esta função retorna a contagem (como um valor inteiro assinado) dos limites especificados da parte de data cruzados entre a data de início especificada e data de término .

Preste atenção especial às palavras “limites datepart cruzados”. É por isso que obtemos os resultados que obtivemos nos exemplos anteriores. É fácil supor que DATEDIFF() usa o tempo decorrido para seus cálculos, mas não. Ele usa o número de limites de datepart cruzados.

No primeiro exemplo, as datas não cruzaram nenhum limite anual. O ano da primeira data era exatamente igual ao ano da segunda data. Nenhum limite foi ultrapassado.

No segundo exemplo, tivemos o cenário oposto. As datas cruzaram todos os limites da parte de data pelo menos uma vez (100 vezes por nanossegundos).

Exemplo 3 - Um resultado diferente para a semana


Agora, vamos fingir que um ano inteiro se passou. E aqui estamos exatamente um ano depois com os valores de data/hora, exceto que os valores de ano foram incrementados em um.

Devemos obter os mesmos resultados, certo?
DECLARE @startdate datetime2 = '2017-12-31 23:59:59.9999999', 
  @enddate datetime2 = '2018-01-01 00:00:00.0000000';

SELECT DATEDIFF(year,     @startdate,   @enddate) Year,
  DATEDIFF(quarter,       @startdate,   @enddate) Quarter,
  DATEDIFF(month,         @startdate,   @enddate) Month,
  DATEDIFF(dayofyear,     @startdate,   @enddate) DOY,
  DATEDIFF(day,           @startdate,   @enddate) Day,
  DATEDIFF(week,          @startdate,   @enddate) Week,
  DATEDIFF(hour,          @startdate,   @enddate) Hour,
  DATEDIFF(minute,        @startdate,   @enddate) Minute,
  DATEDIFF(second,        @startdate,   @enddate) Second,
  DATEDIFF(millisecond,   @startdate,   @enddate) Millisecond,
  DATEDIFF(microsecond,   @startdate,   @enddate) Microsecond,
  DATEDIFF(nanosecond,    @startdate,   @enddate) Nanosecond;

Resultados:
Year        | 1
Quarter     | 1
Month       | 1
DOY         | 1
Day         | 1
Week        | 0
Hour        | 1
Minute      | 1
Second      | 1
Millisecond | 1
Microsecond | 1
Nanosecond  | 100

Errado.

A maioria deles são os mesmos, mas desta vez a semana retornou 0 .

Huh?

Isso aconteceu porque as datas de entrada têm o mesmo calendário semana valores. Aconteceu que as datas escolhidas para o exemplo 2 tinham valores de semanas de calendário diferentes.

Para ser mais específico, o exemplo 2 cruzou os limites da parte da semana indo de '2016-12-31' a '2017-01-01'. Isso ocorre porque a última semana de 2016 terminou em 31-12-2016, e a primeira semana de 2017 começou em 01-01-2017 (domingo).

Mas no exemplo 3, a primeira semana de 2018 realmente começou em nossa data de início de 31/12/2017 (domingo). Nossa data final, sendo o dia seguinte, caiu na mesma semana. Portanto, nenhum limite de parte da semana foi ultrapassado.

Isso obviamente pressupõe que o domingo é o primeiro dia de cada semana. Como se vê, o DATEDIFF() função faz suponha que domingo é o primeiro dia da semana. Ele até ignora seu SET DATEFIRST configuração (esta configuração permite especificar explicitamente qual dia é considerado o primeiro dia da semana). O raciocínio da Microsoft para ignorar SET DATEFIRST é que garante o DATEDIFF() função é determinística. Aqui está uma solução alternativa se isso for um problema para você.

Portanto, em poucas palavras, seus resultados podem parecer “errados” para qualquer parte da data, dependendo das datas/horários. Seus resultados podem parecer muito errados ao usar a parte da semana. E eles podem parecer ainda mais errados se você usar um SET DATEFIRST valor diferente de 7 (para domingo) e você está esperando DATEDIFF() para honrar isso.

Mas os resultados não estão errados e não é um bug. É apenas mais uma “pegadinha” para quem não sabe como a função realmente funciona.

Todas essas pegadinhas também se aplicam ao DATEDIFF_BIG() função. Funciona da mesma forma que DATEDIFF() com a exceção de que ele retorna o resultado como um bigint assinado (em oposição a um int para DATEDIFF() ).

Exemplo 4 – Os resultados dependem do tipo de dados


Você também pode obter resultados inesperados devido ao tipo de dados usado para as datas de entrada. Os resultados geralmente diferem dependendo do tipo de dados das datas de entrada. Mas você não pode culpar DATEDIFF() para isso, pois é puramente devido aos recursos e limitações dos vários tipos de dados. Você não pode esperar obter resultados de alta precisão a partir de um valor de entrada de baixa precisão.

Por exemplo, sempre que a data de início ou de término tiver um smalldatetime valor, os segundos e milissegundos sempre retornarão 0. Isso ocorre porque o smalldatetime tipo de dados é preciso apenas ao minuto.

Veja o que acontece se mudarmos o exemplo 2 para usar smalldatetime em vez de datetime2 :
DECLARE @startdate smalldatetime = '2016-12-31 23:59:59', 
  @enddate smalldatetime = '2017-01-01 00:00:00';

SELECT DATEDIFF(year,     @startdate,   @enddate) Year,
  DATEDIFF(quarter,       @startdate,   @enddate) Quarter,
  DATEDIFF(month,         @startdate,   @enddate) Month,
  DATEDIFF(dayofyear,     @startdate,   @enddate) DOY,
  DATEDIFF(day,           @startdate,   @enddate) Day,
  DATEDIFF(week,          @startdate,   @enddate) Week,
  DATEDIFF(hour,          @startdate,   @enddate) Hour,
  DATEDIFF(minute,        @startdate,   @enddate) Minute,
  DATEDIFF(second,        @startdate,   @enddate) Second,
  DATEDIFF(millisecond,   @startdate,   @enddate) Millisecond,
  DATEDIFF(microsecond,   @startdate,   @enddate) Microsecond,
  DATEDIFF(nanosecond,    @startdate,   @enddate) Nanosecond;

Resultado:
Year        | 0
Quarter     | 0
Month       | 0
DOY         | 0
Day         | 0
Week        | 0
Hour        | 0
Minute      | 0
Second      | 0
Millisecond | 0
Microsecond | 0
Nanosecond  | 0

A razão pela qual todos são zero é porque ambas as datas de entrada são realmente idênticas:
DECLARE @startdate smalldatetime = '2016-12-31 23:59:59', 
  @enddate smalldatetime = '2017-01-01 00:00:00';
SELECT  
  @startdate 'Start Date',   
  @enddate 'End Date';

Resultado:
+---------------------+---------------------+
| Start Date          | End Date            |
|---------------------+---------------------|
| 2017-01-01 00:00:00 | 2017-01-01 00:00:00 |
+---------------------+---------------------+

As limitações do smalldatetime tipo de dados fez com que os segundos fossem arredondados, o que causou um efeito de fluxo e tudo foi arredondado. Mesmo se você não obtiver valores de entrada idênticos, ainda poderá obter um resultado inesperado devido ao tipo de dados não fornecer a precisão necessária.