¿Por qué mi consulta de búsqueda de fecha y hora no coincide?


20
select * 
from A 
where posted_date >= '2015-07-27 00:00:00.000' 
  and posted_date  <= '2015-07-27 23:59:59.999'

Pero el resultado contiene un registro que ha publicado date_date hoy: 28/07/2015. Mi servidor de bases de datos no está en mi país. Cuál es el problema ?

Respuestas:


16

Como está utilizando el datetimetipo de datos, debe comprender cómo el servidor sql redondea los datos de fecha y hora.

╔═══════════╦═════╦═════════════════════════════╦═════════════════════════════╦══════════╦═══════════╗
   Name     sn          Minimum value                Maximum value         Accuracy   Storage  
╠═══════════╬═════╬═════════════════════════════╬═════════════════════════════╬══════════╬═══════════╣
 datetime   dt   1753-01-01 00:00:00.000      9999-12-31 23:59:59.997      3.33 ms   8 bytes   
 datetime2  dt2  0001-01-01 00:00:00.0000000  9999-12-31 23:59:59.9999999  100ns     6-8 bytes 
╚═══════════╩═════╩═════════════════════════════╩═════════════════════════════╩══════════╩═══════════╝

ingrese la descripción de la imagen aquí

Usando la consulta a continuación, puede ver fácilmente el problema de redondeo que hace el servidor sql cuando usa DATETIMEel tipo de datos.

select  '2015-07-27 00:00:00.000'                       as Original_startDateTime,
        convert(datetime ,'2015-07-27 00:00:00.000')    as startDateTime,
        '2015-07-27 23:59:59.999'                       as Original_endDateTime,
        convert(datetime ,'2015-07-27 23:59:59.999')    as endDateTime,
        '2015-07-27 00:00:00.000'                       as Original_startDateTime2,
        convert(datetime2 ,'2015-07-27 00:00:00.000')   as startDateTime2,  -- default precision is 7
        '2015-07-27 23:59:59.999'                       as Original_endDateTime2,
        convert(datetime2 ,'2015-07-27 23:59:59.999')   as endDateTime2     -- default precision is 7

ingrese la descripción de la imagen aquí Click para agrandar

DATETIME2ha existido desde SQL Server 2008, así que comience a usarlo en lugar de DATETIME. Para su situación, puede utilizar datetime2con precisión de 3 decimales, por ejemplo datetime2(3).

Beneficios de usar datetime2:

  • Admite hasta 7 lugares decimales para el componente de tiempo frente a datetimeadmitir solo 3 lugares decimales ... y, por lo tanto, ve el problema de redondeo ya que de forma predeterminada datetimeredondea el más cercano .003 secondscon incrementos de .000, .003o .007segundos.
  • datetime2es mucho más preciso datetimey datetime2le da control DATEy TIMEno datetime.

Referencia:


1
gives you control of DATE and TIME as opposed to datetime.¿Qué significa eso?
nurettin

Re. usando DateTime2vs DateTime.: a. Para la gran mayoría de los casos de uso en el mundo real , beneficios de DateTime2Mucho <costos. Ver: stackoverflow.com/questions/1334143/… b. Ese no es el problema raíz aquí. Ver siguiente comentario.
Tom

El problema raíz aquí (como apuesto a que la mayoría de los desarrolladores senior estarán de acuerdo) no es una precisión insuficiente en la fecha y hora de finalización inclusiva de una comparación de rango de fecha y hora, sino más bien el uso de un período inclusivo (versus exclusivo). Es como verificar la igualdad con Pi, siempre existe la posibilidad de que uno de los #s tenga> o <precisión (es decir, ¿qué pasa si se datetime3agregan 70 (vs. 7) dígitos de precisión?). La mejor práctica es usar un valor donde la precisión no importa, es decir, <el comienzo del siguiente segundo, minuto, hora o día vs. <= el final del segundo, minuto, hora o día anterior.
Tom

18

Como varios otros han mencionado en los comentarios y otras respuestas a su pregunta es el tema central 2015-07-27 23:59:59.999se redondea al 2015-07-28 00:00:00.000por SQL Server. Según la documentación para DATETIME:

Rango de tiempo: 00:00:00 a 23: 59: 59.997

Tenga en cuenta que el rango de tiempo nunca puede ser .999. Más abajo en la documentación, especifica las reglas de redondeo que SQL Server usa para el dígito menos significativo.

Tabla que muestra las reglas de redondeo

Observe que el dígito menos significativo solo puede tener uno de tres valores potenciales: "0", "3" o "7".

Hay varias soluciones / soluciones para esto que puede usar.

-- Option 1
SELECT 
    * 
FROM A 
WHERE posted_date >= '2015-07-27 00:00:00.000' 
  AND posted_date <  '2015-07-28 00:00:00.000' --Round up and remove equality

-- Option 2
SELECT 
    * 
FROM A 
WHERE posted_date >= '2015-07-27 00:00:00.000' 
  AND posted_date <=  '2015-07-27 23:59:59.997' --Round down and keep equality

-- Option 3
SELECT 
    * 
FROM A 
WHERE CAST(posted_date AS DATE) = '2015-07-27' -- Use different data type

-- Option 4
SELECT 
    * 
FROM A 
WHERE CONVERT(CHAR(8), DateColumn, 112) = '20150727' -- Cast to string stripping off time

-- Option 5
SELECT 
    * 
FROM A 
WHERE posted_date BETWEEN '2015-07-27 00:00:00.000' 
  AND '2015-07-27 23:59:59.997' --Use between

De las cinco opciones que he presentado anteriormente, consideraría las opciones 1 y 3 como las únicas opciones viables. Transmiten su intención claramente y no se romperán si actualiza los tipos de datos. Si está utilizando SQL Server 2008 o más reciente, creo que la opción 3 debería ser su enfoque preferido. Eso es especialmente cierto si puede cambiar el uso del DATETIMEtipo de datos a un DATEtipo de datos para su posted_datecolumna.

Con respecto a la opción 3, se puede encontrar una muy buena explicación sobre algunos problemas aquí: el reparto hasta la fecha es modificable, pero ¿es una buena idea?

No me gustan las opciones 2 y 5 porque los .997segundos fraccionarios serán solo otro número mágico que la gente querrá "arreglar". Por algunas razones más por las BETWEENque no se acepta ampliamente, es posible que desee ver esta publicación .

No me gusta la opción 4 porque convertir los tipos de datos en una cadena para fines de comparación me parece sucio. Una razón más cualitativa para evitarlo en SQL Server es que afecta la capacidad de rastreo, es decir , que no puede realizar una búsqueda de índice y que con frecuencia dará como resultado un rendimiento más deficiente.

Para obtener más información sobre la forma correcta y la manera incorrecta de manejar las consultas de rango de fechas, consulte esta publicación de Aaron Bertrand .

Al partir, podrá mantener su consulta original y se comportará como lo desee si cambia su posted_datecolumna de DATETIMEa a DATETIME2(3). Eso ahorraría espacio de almacenamiento en el servidor, le daría una mayor precisión con la misma precisión, sería más compatible con los estándares / portátil y le permitiría ajustar fácilmente la precisión / precisión si sus necesidades cambian en el futuro. Sin embargo, esta es solo una opción si está utilizando SQL Server 2008 o posterior.

Como un poco de trivia, la 1/300segunda precisión DATETIMEparece ser una retención de UNIX según esta respuesta de StackOverflow . Sybase que tiene una herencia compartida tiene un similares 1/300de un segundo de precisión en sus DATETIMEyTIME tipos de datos pero sus dígitos menos significativos son un toque diferente a "0", "3" y "6". En mi opinión, la 1/300precisión de un segundo y / o 3,33 ms es una decisión arquitectónica desafortunada ya que el bloque de 4 bytes por el momento en el DATETIMEtipo de datos de SQL Server podría haber admitido fácilmente la precisión de 1 ms.


Sí, pero el núcleo "tema central" no está utilizando la opción 1 (por ejemplo, utilizando cualquier intervalo de valores incluidos final (frente a exclusivo) donde la precisión de los tipos de datos pasados o futuros potenciales podría afectar los resultados). Es como verificar la igualdad con Pi, siempre es posible que un # tenga> o <precisión (a menos que ambos estén redondeados previamente a la precisión común más baja). ¿Qué sucede si se datetime3agregan 70 (vs. 7) dígitos de precisión? La mejor práctica es usar un valor donde la precisión no importa, es decir, <el comienzo del siguiente segundo, minuto, hora o día vs. <= el final del segundo, minuto, hora o día anterior.
Tom

9

Conversión Implícita

Supuse que el tipo de datos post_date es Datetime. Sin embargo, no importa si el tipo en el otro lado es Datetime, Datetime2 o simplemente Time porque la cadena (Varchar) se convertirá implícitamente en Datetime.

Con post_date declarado como Datetime2 (u Time), la posted_date <= '2015-07-27 23:59:59.99999'cláusula where falla porque aunque 23:59:59.99999es un valor válido de Datetime2, este no es un valor válido de Datetime:

 Conversion failed when converting date and/or time from character string.

Rango de tiempo para fecha y hora

El rango de tiempo de Fecha y hora es de 00:00:00 a 23: 59: 59.997. Por lo tanto, 23: 59: 59.999 está fuera de rango y debe redondearse hacia arriba o hacia abajo al valor más cercano.

Exactitud

Además, los valores de fecha y hora se redondean en incrementos de .000, .003 o .007 segundos. (es decir, 000, 003, 007, 010, 013, 017, 020, ..., 997)

Este no es el caso con el valor 2015-07-27 23:59:59.999que está dentro de este rango: 2015-07-27 23:59:59.997y 2015-07-28 0:00:00.000.

Este rango corresponde a las opciones anteriores y siguientes más cercanas, ambas terminando con .000, .003 o .007.

Redondeando hacia arriba o hacia abajo ?

Debido a que es más cercano a 2015-07-28 0:00:00.000(1 frente a -2) que 2015-07-27 23:59:59.997, la cadena se redondea y se convierte este valor de fecha y hora: 2015-07-28 0:00:00.000.

Con un límite superior como 2015-07-27 23:59:59.998(o .995, .996, .997, .998), se habría redondeado hacia abajo 2015-07-27 23:59:59.997y su consulta habría funcionado como se esperaba. Sin embargo, no habría sido una solución sino un valor afortunado.

Datetime2 o tipos de hora

Datetime2 y rangos de tiempo de tiempo son 00:00:00.0000000a través de 23:59:59.9999999con una precisión de 100 ns (el último dígito, cuando se usa con una precisión de 7 dígitos).

Sin embargo, un rango de fecha y hora (3) no es similar al rango de fecha y hora:

  • Datetime 0:0:00.000a23:59:59.997
  • Datetime2 0:0:00.000000000a23:59:59.999

Solución

Al final, es más seguro buscar fechas por debajo del día siguiente que las fechas por debajo o iguales a lo que crees que es el último fragmento de la hora del día. Esto se debe principalmente a que sabe que el día siguiente siempre comienza a las 0: 00: 00,000 pero que los diferentes tipos de datos pueden no tener la misma hora al final del día:

Datetime `0:0:00.000` to `23:59:59.997`
Datetime2 `0:0:00.000000000` to `23:59:59.999-999-900`
Time2 `0:0:00.000000000` to `23:59:59.999-999-900`
  • < 2015-07-28 0:00:00.000le dará resultados precisos y es la mejor opción
  • <= 2015-07-27 23:59:59.xxx puede devolver valores inesperados cuando no se redondea a lo que cree que debería ser.
  • Se debe evitar la conversión a la fecha y el uso de la función porque limita el uso de índices

Podríamos pensar que cambiar [date_date] a Datetime2 y su mayor precisión podría solucionar este problema, pero no ayudará porque la cadena todavía se convierte a Datetime. Sin embargo, si se agrega un elenco cast(2015-07-27 23:59:59.999' as datetime2), esto funciona bien

Moldear y Convertir

Cast puede convertir un valor de hasta 3 dígitos a Datetime o con hasta 9 dígitos a Datetime2 u Time y redondearlo a la precisión correcta.

Cabe señalar que Cast of Datetime2 y Time2 pueden dar resultados diferentes:

  • select cast('20150101 23:59:59.999999999' as datetime2(7)) es redondeado 2015-05-03 00: 00: 00.0000000 (para un valor superior a 999999949)
  • select cast('23:59:59.999999999' as time(7)) => 23: 59: 59.9999999

En cierto modo, soluciona el problema que la fecha y hora tiene con los incrementos de 0, 3 y 7, aunque siempre es mejor buscar fechas antes del primer nano segundo del día siguiente (siempre 0: 00: 00.000).

Fuente MSDN: fecha y hora (Transact-SQL)


6

Esta redondeando

 select cast('2015-07-27 23:59:59.999' as datetime) 
 returns 2015-07-28 00:00:00.000

.998, .997, .996, .995 todos emitidos / redondos a .997

Debería usar

select * 
from A 
where posted_date >= '2015-07-27 00:00:00.000' 
  and posted_date <  '2015-07-28 00:00:00.000'

o

where cast(posted_date as date) = '2015-07-27'

Vea la precisión en este enlace
Siempre informado como .000, .003, .007


Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.