¿Es posible aumentar el rendimiento de la consulta en una tabla estrecha con millones de filas?

14

Tengo una consulta que actualmente tarda un promedio de 2500 ms en completarse. Mi mesa es muy estrecha, pero hay 44 millones de filas. ¿Qué opciones tengo para mejorar el rendimiento, o es tan bueno como es posible?

La consulta

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31'; 

La mesa

CREATE TABLE [dbo].[Heartbeats](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [DeviceID] [int] NOT NULL,
    [IsPUp] [bit] NOT NULL,
    [IsWebUp] [bit] NOT NULL,
    [IsPingUp] [bit] NOT NULL,
    [DateEntered] [datetime] NOT NULL,
 CONSTRAINT [PK_Heartbeats] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

El índice

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

¿Sería útil agregar índices adicionales? Si es así, ¿cómo se verían? El rendimiento actual es aceptable, porque la consulta solo se ejecuta ocasionalmente, pero me pregunto como ejercicio de aprendizaje, ¿hay algo que pueda hacer para que esto sea más rápido?

ACTUALIZAR

Cuando cambio la consulta para usar una pista de índice de fuerza, la consulta se ejecuta en 50 ms:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats] WITH(INDEX(CommonQueryIndex))
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 

Agregar una cláusula DeviceID correctamente selectiva también alcanza el rango de 50 ms:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' AND DeviceID = 4;

Si agrego ORDER BY [DateEntered], [DeviceID]a la consulta original, estoy en el rango de 50 ms:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 
ORDER BY [DateEntered], [DeviceID];

Todos usan el índice que esperaba (CommonQueryIndex), así que supongo que mi pregunta es ahora, ¿hay alguna manera de forzar este índice para que se use en consultas como esta? ¿O es que el tamaño de mi mesa arroja demasiado el optimizador y debo usar una ORDER BYo una pista?

Nate
fuente
Supongo que podría agregar un índice no agrupado más en "DateEntered" que aumentaría el rendimiento en mayor medida
Praveen
@Praveen ¿Sería básicamente lo mismo que mi índice existente? ¿Debo hacer algo especial ya que habrá dos índices en el mismo campo?
Nate
@Nate, dado que la tabla se llama latido y hay 44 millones de registros involucrados, ¿supongo que tiene inserciones pesadas en esta tabla? Con la indexación, solo puede agregar un índice de cobertura para acelerar. Pero como mencionó, solo usa esta consulta de vez en cuando, lo desaconsejaría si realiza inserciones pesadas. Básicamente duplica su carga de inserción. ¿Estás ejecutando la edición Enterprise?
Edward Dortland el
Me di cuenta de que tienes deviceID en tu índice NC. ¿Es posible incluir eso en su cláusula where? ¿Y eso reduciría el conjunto de resultados por debajo del umbral? <35k registros (sin la cláusula 1000 superior).
Edward Dortland el
1
última pregunta, ¿siempre está insertando en orden de fecha? O pueden estar fuera de servicio ya que los dispositivos pueden insertar asíncronos entre sí. Puede intentar cambiar el índice agrupado a la columna DateEntered. Sus páginas de licencia de su índice agrupado ahora son 445 páginas. Eso se duplicaría si pasaras de int a datetime. Pero en este caso, eso podría no ser tan malo.
Edward Dortland el

Respuestas:

13

Por qué el optimizador no va para tu primer índice:

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

Es una cuestión de selectividad de la columna [DateEntered].

Nos dijo que su mesa tiene 44 millones de filas. el tamaño de la fila es:

4 bytes, para la ID, 4 bytes para la ID del dispositivo, 8 bytes para la fecha y 1 byte para las columnas de 4 bits. eso es 17 bytes + 7 bytes de sobrecarga para (etiquetas, mapa de bits nulo, desplazamiento de col variable, recuento de col) totaliza 24 bytes por fila.

Eso se traduciría a 140k páginas. Para almacenar esos 44 millones de filas.

Ahora el optimizador puede hacer dos cosas:

  1. Podría escanear la tabla (escaneo de índice agrupado)
  2. O podría usar su índice. Para cada fila en su índice, entonces necesitaría hacer una búsqueda de marcadores en el índice agrupado.

Ahora, en cierto punto, se vuelve más costoso hacer todas estas búsquedas individuales en el índice agrupado para cada entrada de índice encontrada en su índice no agrupado. El umbral para eso es generalmente el recuento total de búsquedas debe exceder del 25% al ​​33% del recuento total de páginas de la tabla.

Entonces, en este caso: 140k / 25% = 35000 filas 140k / 33% = 46666 filas.

(@RBarryYoung, 35k es 0.08% del total de filas y 46666 es 0.10%, así que creo que ahí es donde estaba la confusión)

Entonces, si su cláusula where resultará en algún lugar entre las filas 35000 y 46666 (¡esto está debajo de la cláusula superior!) Es muy probable que su no agrupado no se use y que se use el escaneo de índice agrupado.

Las únicas dos formas de cambiar esto son:

  1. Haga que su cláusula where sea más selectiva. (si es posible)
  2. Suelte el * y seleccione solo unas pocas columnas para que pueda usar un índice de cobertura.

ahora seguro de que puede crear un índice de cobertura incluso cuando usa un select *. Sin embargo, eso solo crea una sobrecarga masiva para sus inserciones / actualizaciones / eliminaciones. Tendríamos que saber más sobre su carga de trabajo (lectura vs escritura) para asegurarnos de que esa sea la mejor solución.

Cambiar de datetime a smalldatetime es una reducción del tamaño del 16% en el índice agrupado y una reducción del tamaño del 24% en el índice no agrupado.

Edward Dortland
fuente
el umbral de exploración es normalmente mucho más bajo que eso (10% o incluso más bajo), sin embargo, dado que el rango es de un solo día desde hace más de un año, no debería alcanzar ese umbral. Y una exploración de índice agrupado no es un hecho dado que se agregó un índice de cobertura. Dado que ese índice hace que la cláusula WHERE sea compatible con SARG, debería preferirse.
RBarryYoung
@RBarryYoung Estaba tratando de explicar por qué el índice no agrupado en [EnteredDate], [DeviceID] no se estaba utilizando en primer lugar. Con respecto al Umbral, creo que ambos estamos de acuerdo, solo estoy hablando desde la perspectiva de una página. Alteraré mi respuesta para que quede más claro.
Edward Dortland el
Modifiqué la respuesta para dejar más claro lo que estaba respondiendo. No puedo explicar por qué no se usa el índice de cobertura que sugirió @RBarryYoung. Lo probé en un millón de filas justo aquí, y lo optimicé usando el índice de cobertura.
Edward Dortland el
Gracias por una respuesta muy completa, tiene mucho sentido. Con respecto a la carga de trabajo, la tabla tiene 150-300 inserciones por período de 5 minutos y algunas lecturas por día con fines informativos.
Nate
El encabezado del índice de cobertura no es realmente significativo dado que es una tabla estrecha y la "cobertura" es solo una adición al índice preexistente que ya incluía la mayor parte de la fila.
RBarryYoung
8

¿Hay alguna razón particular por la que su PK esté agrupado? Muchas personas hacen esto porque su valor predeterminado es ese, o piensan que las PK deben agruparse. No es asi. Los índices agrupados suelen ser mejores para consultas de rango (como este) o en la clave externa de una tabla secundaria.

Un efecto de un índice de agrupación es que agrupa todos los datos juntos porque los datos se almacenan en los nodos hoja del árbol de agrupación b. Entonces, suponiendo que no está pidiendo un rango 'demasiado amplio', el optimizador sabrá exactamente qué parte del árbol b contiene los datos y no tendrá que encontrar un identificador de fila y luego saltar a donde los datos es (como lo hace cuando se trata de un índice NC). ¿Qué es "demasiado amplio" de un rango? Un ejemplo ridículo sería pedir 11 meses de datos de una tabla que solo tiene un año de registros. Obtener un día de datos no debería ser un problema, suponiendo que sus estadísticas estén actualizadas. (Sin embargo, el optimizador puede meterse en problemas si está buscando los datos de ayer y no ha actualizado las estadísticas durante tres días).

Como está ejecutando una consulta "SELECCIONAR *", el motor deberá devolver todas las columnas de la tabla (incluso si alguien agrega una nueva que su aplicación no necesita en ese momento), de modo que un índice de cobertura o un índice con columnas incluidas no ayudará mucho, si es que lo hace. (Si incluye todas las columnas de la tabla en un índice, está haciendo algo mal). El optimizador probablemente ignorará esos índices NC.

¿Entonces lo que hay que hacer?

Mi sugerencia sería eliminar el índice NC, cambiar el PK agrupado a no agrupado y crear un índice agrupado en [DateEntered]. Más simple es mejor, hasta que se demuestre lo contrario.

estrecho de Darin
fuente
Suponiendo que las filas se insertan en orden creciente, esta es la respuesta más simple, pero la inserción en orden no lineal causará fragmentación.
Kirk Broadhurst
Agregar datos a cualquier estructura b-tree hará que pierda el equilibrio. Incluso si agrega filas en orden de clúster, los índices perderán el equilibrio. Volver a indexar las tablas elimina la fragmentación, y cualquier DBA le dirá que las tablas deben volver a indexarse ​​después de que se hayan agregado "suficientes" datos a una tabla. (La definición de "suficiente" podría debatirse, o "cuándo" podría ser una discusión.) No veo nada en la pregunta que diga que no se puede volver a indexar por alguna razón.
Darin estrecho
4

Mientras tenga ese "*" allí, lo único que podría imaginar que marcaría una gran diferencia sería cambiar su definición de índice a esto:

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)INCLUDE (ID, IsWebUp, IsPingUp, IsPUp)
 WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

Como señalé en los comentarios, debería usar ese índice, pero si no lo hace, puede persuadirlo con ORDER BY o una pista de índice.

RBarryYoung
fuente
Acabo de probar esto y todavía estoy en el mismo lugar, 2500ms de espera para la respuesta del servidor y 10ms de tiempo de proceso del cliente.
Nate
Publique el plan de consulta.
RBarryYoung
Parece que está usando el índice agrupado. (SELECCIONAR Costo: 0% <- Costo máximo: 20% <- Escaneo de índice agrupado PK_Heartbeats Costo: 80%)
Nate
Sí, eso no está bien, algunas cosas arrojan las estadísticas / optimizador. Agregue una pista para forzarlo a usar el nuevo índice.
RBarryYoung
@Max Vernon: Tal vez, pero eso debería haber sido marcado en el plan de consulta.
RBarryYoung
3

Vería esto un poco diferente.

  • Sí, sé que es un hilo viejo pero estoy intrigado.

Volcaría la columna de fecha y hora; cámbiela por una int. Tener una tabla de búsqueda o hacer una conversión para su fecha.

Volcar el índice agrupado: déjelo como un montón y cree un índice no agrupado en la nueva columna INT que representa la fecha. es decir, hoy sería 20121015. Ese orden es importante. Según la frecuencia con la que cargue la tabla, busque crear ese índice en orden DESC. El costo de mantenimiento será más alto y querrá introducir un factor de relleno o partición. Particionar también ayudaría a disminuir el tiempo de ejecución.

Por último, si puede usar SQL 2012, intente usar SECUENCIA: superará la identidad () para las inserciones.

Jeremy Lowell
fuente
Solución interesante Si bien no es obvio por mi pregunta, la parte de tiempo de DateTime es muy importante. Generalmente consulto en función de la fecha, para revisar tiempos específicos durante ese período. ¿Cómo ajustarías esta solución para dar cuenta de eso?
Nate
En ese caso, mantenga la columna de fecha y hora, agregue la columna int para la fecha (ya que su rango se basa en el elemento de fecha y no en el elemento de hora). También podría considerar usar el tipo de datos TIME y luego dividir efectivamente el tiempo aparte de la fecha. De esa manera, su huella de datos es más pequeña y aún tiene el elemento Tiempo de la columna.
Jeremy Lowell
1
No estoy seguro de por qué me perdí esto antes, pero también utilizo la compresión de filas en el índice agrupado y en el índice no agrupado. Acabo de hacer una prueba rápida con su tabla y esto es lo que encontré: creé un conjunto de datos (5.8 millones de filas) en la tabla definida anteriormente. Comprimí (fila) el índice agrupado y no agrupado. las lecturas lógicas, basadas en su consulta exacta, disminuyeron de 2,074 a 1,433. Esa es una disminución significativa y estoy seguro de que solo lo ayudaría, y es de muy bajo riesgo.
Jeremy Lowell el