Sqlserver
 sql >> Base de données >  >> RDS >> Sqlserver

DATEDIFF() renvoie des résultats erronés dans SQL Server ? Lis ça.

Si vous obtenez des résultats vraiment étranges lorsque vous utilisez le DATEDIFF() fonction dans SQL Server et que vous êtes convaincu que la fonction contient un bogue, ne vous arrachez pas les cheveux pour le moment. Ce n'est probablement pas un bug.

Il existe des scénarios où les résultats produits par cette fonction peuvent être assez loufoques. Et si vous ne comprenez pas comment la fonction fonctionne réellement, les résultats seront complètement faux.

Espérons que cet article puisse aider à clarifier comment le DATEDIFF() est conçue pour fonctionner et fournit des exemples de scénarios où vos résultats pourraient ne pas être ceux auxquels vous vous attendiez.

Exemple 1 :365 jours ne correspondent pas toujours à une année

Question : Quand est-ce que 365 jours ne sont pas un an ?

Réponse : Lors de l'utilisation de DATEDIFF() bien sûr !

Voici un exemple où j'utilise DATEDIFF() pour renvoyer le nombre de jours entre deux dates, puis le nombre d'années entre ces deux mêmes dates.

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;

Résultat :

+--------+---------+
| Days   | Years   |
|--------+---------|
| 365    | 0       |
+--------+---------+

Si vous pensez que ce résultat est faux, et que DATEDIFF() a évidemment un bogue, lisez la suite - tout n'est pas comme il semble.

Croyez-le ou non, c'est en fait le résultat attendu. Ce résultat est exactement conforme à la façon dont DATEDIFF() est conçu pour fonctionner.

Exemple 2 – 100 nanosecondes =1 an ?

Prenons les choses dans l'autre sens.

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;

Résultats (affichés avec une sortie verticale) :

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

Il n'y a qu'une centaine de nanosecondes (0,0000001 seconde) de différence entre les deux dates/heures, mais nous obtenons exactement le même résultat pour chaque partie de date, à l'exception des nanosecondes.

Comment cela peut-il arriver ? Comment peut-il y avoir 1 microseconde de différence et 1 an de différence en même temps ? Sans parler de toutes les parties de date entre les deux ?

Cela peut sembler fou, mais ce n'est pas non plus un bug. Ces résultats sont exactement conformes à la façon dont DATEDIFF() est censé fonctionner.

Et pour rendre les choses encore plus confuses, nous pourrions obtenir des résultats différents selon le type de données. Mais nous y reviendrons bientôt. Voyons d'abord comment le DATEDIFF() la fonction fonctionne réellement.

La définition réelle de DATEDIFF()

La raison pour laquelle nous obtenons les résultats que nous obtenons est que le DATEDIFF() fonction est définie comme suit :

Cette fonction renvoie le nombre (sous forme de valeur entière signée) des limites de partie de date spécifiées franchies entre la date de début spécifiée et date de fin .

Portez une attention particulière aux mots "limites de datepart franchies". C'est pourquoi nous obtenons les résultats que nous avons obtenus dans les exemples précédents. Il est facile de supposer que DATEDIFF() utilise le temps écoulé pour ses calculs, mais ce n'est pas le cas. Il utilise le nombre de limites de partie de date franchies.

Dans le premier exemple, les dates ne traversaient aucune limite de partie d'année. L'année du premier rendez-vous était exactement la même que l'année du deuxième rendez-vous. Aucune limite n'a été franchie.

Dans le deuxième exemple, nous avons eu le scénario inverse. Les dates ont traversé chaque limite de partie de date au moins une fois (100 fois pour les nanosecondes).

Exemple 3 - Un résultat différent pour la semaine

Maintenant, supposons qu'une année entière se soit écoulée. Et nous voici exactement un an plus tard avec les valeurs de date/heure, sauf que les valeurs d'année ont augmenté de un.

Nous devrions obtenir les mêmes résultats, n'est-ce pas ?

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;

Résultats :

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

Faux.

La plupart d'entre eux sont identiques, mais cette fois, la semaine a renvoyé 0 .

Hein ?

Cela s'est produit parce que les dates d'entrée ont le même calendrier semaine valeurs. Il se trouve que les dates choisies par exemple 2 avaient des valeurs de semaines calendaires différentes.

Pour être plus précis, l'exemple 2 a traversé les limites des parties de semaine allant de "2016-12-31" à "2017-01-01". En effet, la dernière semaine de 2016 s'est terminée le 2016-12-31 et la première semaine de 2017 a commencé le 2017-01-01 (dimanche).

Mais dans l'exemple 3, la première semaine de 2018 a en fait commencé à notre date de début du 2017-12-31 (dimanche). Notre date de fin, étant le lendemain, tombait dans la même semaine. Par conséquent, aucune limite de semaine n'a été franchie.

Cela suppose évidemment que le dimanche est le premier jour de chaque semaine. Il s'avère que le DATEDIFF() la fonction fait supposons que le dimanche est le premier jour de la semaine. Il ignore même votre SET DATEFIRST paramètre (ce paramètre vous permet de spécifier explicitement quel jour est considéré comme le premier jour de la semaine). Le raisonnement de Microsoft pour ignorer SET DATEFIRST est qu'il assure le DATEDIFF() fonction est déterministe. Voici une solution de contournement si cela vous pose problème.

Donc, en un mot, vos résultats pourraient sembler "faux" pour n'importe quelle partie de date en fonction des dates/heures. Vos résultats peuvent sembler encore plus erronés lorsque vous utilisez la partie semaine. Et ils pourraient sembler encore plus faux si vous utilisez un SET DATEFIRST valeur autre que 7 (pour dimanche) et vous attendez DATEDIFF() pour honorer cela.

Mais les résultats ne sont pas faux, et ce n'est pas un bug. C'est juste plus un "gotcha" pour ceux qui ne savent pas comment la fonction fonctionne réellement.

Tous ces pièges s'appliquent également au DATEDIFF_BIG() une fonction. Cela fonctionne de la même manière que DATEDIFF() à l'exception qu'il renvoie le résultat sous la forme d'un bigint signé (par opposition à un int pour DATEDIFF() ).

Exemple 4 - Les résultats dépendent du type de données

Vous pouvez également obtenir des résultats inattendus en raison du type de données que vous utilisez pour vos dates d'entrée. Les résultats diffèrent souvent en fonction du type de données des dates d'entrée. Mais vous ne pouvez pas blâmer DATEDIFF() pour cela, car cela est uniquement dû aux capacités et aux limites des différents types de données. Vous ne pouvez pas vous attendre à obtenir des résultats de haute précision à partir d'une valeur d'entrée de faible précision.

Par exemple, chaque fois que la date de début ou la date de fin a un smalldatetime valeur, les secondes et les millisecondes renverront toujours 0. C'est parce que le smalldatetime le type de données n'est précis qu'à la minute près.

Voici ce qui se passe si nous changeons l'exemple 2 pour utiliser smalldatetime au lieu 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;

Résultat :

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

La raison pour laquelle elles sont toutes nulles est que les deux dates d'entrée sont en fait identiques :

DECLARE @startdate smalldatetime = '2016-12-31 23:59:59', 
  @enddate smalldatetime = '2017-01-01 00:00:00';
SELECT  
  @startdate 'Start Date',   
  @enddate 'End Date';

Résultat :

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

Les limites de smalldatetime Le type de données a provoqué l'arrondi des secondes, ce qui a ensuite provoqué un flux d'effet et tout a été arrondi. Même si vous ne vous retrouvez pas avec des valeurs d'entrée identiques, vous pouvez toujours obtenir un résultat inattendu car le type de données ne fournit pas la précision dont vous avez besoin.