sql >> Base de Datos >  >> RDS >> Database

Por qué el Optimizer no utiliza el conocimiento del grupo de almacenamiento intermedio

SQL Server tiene un optimizador basado en costos que utiliza el conocimiento sobre las diversas tablas involucradas en una consulta para producir lo que decide que es el plan más óptimo en el tiempo disponible durante la compilación. Este conocimiento incluye los índices que existen y sus tamaños y las estadísticas de las columnas que existen. Parte de lo que implica encontrar el plan de consulta óptimo es tratar de minimizar la cantidad de lecturas físicas necesarias durante la ejecución del plan.

Una cosa que me han preguntado varias veces es por qué el optimizador no considera lo que hay en el grupo de búfer de SQL Server al compilar un plan de consulta, ya que seguramente eso podría hacer que una consulta se ejecute más rápido. En esta publicación, explicaré por qué.

Descubrir el contenido de la agrupación de almacenamiento intermedio

La primera razón por la que el optimizador ignora el grupo de búfer es que no es un problema trivial averiguar qué hay en el grupo de búfer debido a la forma en que está organizado el grupo de búfer. Las páginas de archivos de datos están controladas en el grupo de búferes por pequeñas estructuras de datos llamadas búferes, que rastrean cosas como (lista no exhaustiva):

  • El ID de la página (número de archivo:número de página en el archivo)
  • La última vez que se hizo referencia a la página (usada por el escritor perezoso para ayudar a implementar el algoritmo usado menos recientemente que crea espacio libre cuando es necesario)
  • La ubicación de memoria de la página de 8 KB en el grupo de búfer
  • Si la página está sucia o no (una página sucia tiene cambios que aún no se han vuelto a escribir en el almacenamiento duradero)
  • La unidad de asignación a la que pertenece la página (explicada aquí) y el ID de la unidad de asignación se pueden usar para averiguar de qué tabla e índice forma parte la página

Para cada base de datos que tiene páginas en el grupo de búfer, hay una lista hash de páginas, en orden de ID de página, que se puede buscar rápidamente para determinar si una página ya está en la memoria o si se debe realizar una lectura física. Sin embargo, nada permite que SQL Server determine fácilmente qué porcentaje del nivel de hoja para cada índice de una tabla ya está en la memoria. El código tendría que escanear la lista completa de búferes para la base de datos, buscando búferes que mapeen páginas para la unidad de asignación en cuestión. Y cuantas más páginas haya en la memoria de una base de datos, más tardará la exploración. Sería prohibitivamente costoso hacerlo como parte de la compilación de consultas.

Si está interesado, escribí una publicación hace un tiempo con un código T-SQL que escanea el grupo de búfer y brinda algunas métricas, usando DMV sys.dm_os_buffer_descriptors .

Por qué sería peligroso usar el contenido de la agrupación de almacenamiento intermedio

Supongamos que *existe* un mecanismo altamente eficiente para determinar el contenido del grupo de búferes que el optimizador puede usar para ayudarlo a elegir qué índice usar en un plan de consulta. La hipótesis que voy a explorar es que si el optimizador sabe lo suficiente sobre un índice menos eficiente (más grande) que ya está en la memoria, en comparación con el índice más eficiente (más pequeño) a usar, debería elegir el índice en memoria porque reduzca la cantidad de lecturas físicas requeridas y la consulta se ejecutará más rápido.

El escenario que voy a usar es el siguiente:una tabla BigTable tiene dos índices no agrupados, Index_A e Index_B, que cubren completamente una consulta en particular. La consulta requiere una exploración completa del nivel de hoja del índice para recuperar los resultados de la consulta. La tabla tiene 1 millón de filas. Index_A tiene 200 000 páginas en su nivel de hoja e Index_B tiene 1 millón de páginas en su nivel de hoja, por lo que un escaneo completo de Index_B requiere procesar cinco veces más páginas.

Creé este ejemplo artificial en una computadora portátil que ejecuta SQL Server 2019 con 8 núcleos de procesador, 32 GB de memoria y discos de estado sólido. El código es el siguiente:

CREATE TABLE BigTable (
  	c1 BIGINT IDENTITY,
  	c2 AS (c1 * 2),
  	c3 CHAR (1500) DEFAULT 'a',
  	c4 CHAR (5000) DEFAULT 'b'
);
GO
 
INSERT INTO BigTable DEFAULT VALUES;
GO 1000000
 
CREATE NONCLUSTERED INDEX Index_A ON BigTable (c2) INCLUDE (c3);
-- 5 records per page = 200,000 pages
GO
 
CREATE NONCLUSTERED INDEX Index_B ON BigTable (c2) INCLUDE (c4);
-- 1 record per page = 1 million pages
GO
 
CHECKPOINT;
GO

Y luego cronometré las consultas artificiales:

DBCC DROPCLEANBUFFERS;
GO
 
-- Index_A not in memory
SELECT SUM (c2) FROM BigTable WITH (INDEX (Index_A));
GO
-- CPU time = 796 ms, elapsed time = 764 ms
 
-- Index_A in memory
SELECT SUM (c2) FROM BigTable WITH (INDEX (Index_A));
GO
-- CPU time = 312 ms, elapsed time = 52 ms
 
DBCC DROPCLEANBUFFERS;
GO
 
-- Index_B not in memory
SELECT SUM (c2) FROM BigTable WITH (INDEX (Index_B));
GO
-- CPU time = 2952 ms, elapsed time = 2761 ms
 
-- Index_B in memory
SELECT SUM (c2) FROM BigTable WITH (INDEX (Index_B));
GO
-- CPU time = 1219 ms, elapsed time = 149 ms

Puede ver cuando ninguno de los índices está en la memoria, Index_A es fácilmente el índice más eficiente para usar, con un tiempo de consulta transcurrido de 764 ms contra 2761 ms usando Index_B, y lo mismo ocurre cuando ambos índices están en la memoria. Sin embargo, si Index_B está en la memoria e Index_A no, si la consulta usa Index_B (149 ms) se ejecutará más rápido que si usa Index_A (764 ms).

Ahora permitamos que el optimizador base la elección del plan en lo que hay en el grupo de búfer...

Si Index_A no está mayormente en la memoria e Index_B está mayormente en la memoria, sería más eficiente compilar el plan de consulta para usar Index_B, para una consulta que se ejecuta en ese instante. Aunque Index_B es más grande y necesitaría más ciclos de CPU para escanear, las lecturas físicas son mucho más lentas que los ciclos de CPU adicionales, por lo que un plan de consulta más eficiente minimiza la cantidad de lecturas físicas.

Este argumento solo se mantiene, y un plan de consulta "usar Index_B" solo es más eficiente que un plan de consulta "usar Index_A", si Index_B permanece principalmente en la memoria e Index_A permanece mayormente fuera de la memoria. Tan pronto como la mayor parte de Index_A esté en la memoria, el plan de consulta "usar Index_A" será más eficiente, y el plan de consulta "usar Index_B" es la elección incorrecta.

Las situaciones en las que el plan compilado "usar Index_B" es menos eficiente que el plan "usar Index_A" basado en costos son (generalizando):

  • Index_A e Index_B están ambos en la memoria:el plan compilado tardará casi tres veces más
  • Ninguno de los índices reside en la memoria:el plan compilado tarda 3,5 veces más
  • Index_A es residente en memoria e Index_B no:todas las lecturas físicas realizadas por el plan son extrañas, Y tomará 53 veces más

Resumen

Aunque en nuestro ejercicio de pensamiento, el optimizador puede usar el conocimiento del grupo de búfer para compilar la consulta más eficiente en un solo instante, sería una forma peligrosa de impulsar la compilación del plan debido a la volatilidad potencial del contenido del grupo de búfer, haciendo que la eficiencia futura de el plan almacenado en caché es muy poco fiable.

Recuerde, el trabajo del optimizador es encontrar un buen plan rápido, no necesariamente el mejor plan para el 100% de todas las situaciones. En mi opinión, el optimizador de SQL Server hace lo correcto al ignorar el contenido real del grupo de búfer de SQL Server y, en cambio, se basa en las diversas reglas de costos para producir un plan de consulta que probablemente sea el más eficiente la mayor parte del tiempo. .