La combinación de hash entre tablas maestras / detalladas produce una estimación de cardinalidad demasiado baja

9

Al unir una tabla maestra a una tabla de detalles, ¿cómo puedo alentar a SQL Server 2014 para que use la estimación de cardinalidad de la tabla más grande (detalle) como la estimación de cardinalidad de la salida de unión?

Por ejemplo, al unir filas maestras de 10K a filas de detalle de 100K, quiero que SQL Server estime la unión en filas de 100K, lo mismo que el número estimado de filas de detalle. ¿Cómo debo estructurar mis consultas y / o tablas y / o índices para ayudar al estimador de SQL Server a aprovechar el hecho de que cada fila de detalles siempre tiene una fila maestra correspondiente? (Lo que significa que una unión entre ellos nunca debería reducir la estimación de cardinalidad).

Aquí hay más detalles. Nuestra base de datos tiene un par de tablas maestro / detalle: VisitTargettiene una fila para cada transacción de ventas y VisitSaleuna fila para cada producto en cada transacción. Es una relación de uno a muchos: una fila VisitTarget para un promedio de 10 filas VisitSale.

Las tablas se ven así: (Estoy simplificando solo las columnas relevantes para esta pregunta)

-- "master" table
CREATE TABLE VisitTarget
(
  VisitTargetId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  SaleDate date NOT NULL,
  StoreId int NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitTarget_SaleDate 
    ON VisitTarget (SaleDate) INCLUDE (StoreId /*, ...more columns */);

-- "detail" table
CREATE TABLE VisitSale
(
  VisitSaleId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  VisitTargetId int NOT NULL,
  SaleDate date NOT NULL, -- denormalized; copied from VisitTarget
  StoreId int NOT NULL, -- denormalized; copied from VisitTarget
  ItemId int NOT NULL,
  SaleQty int NOT NULL,
  SalePrice decimal(9,2) NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitSale_SaleDate 
  ON VisitSale (SaleDate)
  INCLUDE (VisitTargetId, StoreId, ItemId, SaleQty, TotalSalePrice decimal(9,2) /*, ...more columns */
);
ALTER TABLE VisitSale 
  WITH CHECK ADD CONSTRAINT FK_VisitSale_VisitTargetId 
  FOREIGN KEY (VisitTargetId)
  REFERENCES VisitTarget (VisitTargetId);
ALTER TABLE VisitSale
  CHECK CONSTRAINT FK_VisitSale_VisitTargetId;

Por razones de rendimiento, hemos desnormalizado parcialmente copiando las columnas de filtrado más comunes (p SaleDate. Ej. ) De la tabla maestra en las filas de cada tabla de detalles, y luego agregamos índices de cobertura en ambas tablas para admitir mejor las consultas filtradas por fecha. Esto funciona muy bien para reducir la E / S cuando se ejecutan consultas con fecha filtrada, pero creo que este enfoque está causando problemas de estimación de cardinalidad al unir las tablas maestra y de detalle.

Cuando unimos estas dos tablas, las consultas se ven así:

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitTarget vt 
    JOIN VisitSale vs on vt.VisitTargetId = vs.VisitTargetId
WHERE
    vs.SaleDate BETWEEN '20170101' and '20171231'
    and vt.SaleDate BETWEEN '20170101' and '20171231'
    -- more filtering goes here, e.g. by store, by product, etc. 

El filtro de fecha en la tabla de detalles ( VisitSale) es redundante. Está allí para habilitar E / S secuenciales (también conocido como operador Index Seek) en la tabla de detalles para consultas que se filtran por un rango de fechas.

El plan para este tipo de consultas se ve así:

ingrese la descripción de la imagen aquí

Aquí se puede encontrar un plan real de una consulta con el mismo problema .

Como puede ver, la estimación de cardinalidad para la unión (la información sobre herramientas en la esquina inferior izquierda de la imagen) es 4 veces demasiado baja: 2.1M real frente a 0.5M estimado. Esto causa problemas de rendimiento (por ejemplo, derramar a tempdb), especialmente cuando esta consulta es una subconsulta que se utiliza en una consulta más compleja.

Pero las estimaciones de recuento de filas para cada rama de la unión están cerca de los recuentos de filas reales. La mitad superior de la unión es de 100K reales frente a 164K estimados. La mitad inferior de la unión es 2,1 millones de filas reales frente a 3,7 millones estimados. La distribución del cubo de hash también se ve bien. Estas observaciones me sugieren que las estadísticas están bien para cada tabla, y que el problema es la estimación de la cardinalidad de unión.

Al principio pensé que el problema era que SQL Server esperaba que las columnas SaleDate en cada tabla fueran independientes, mientras que en realidad son idénticas. Así que intenté agregar una comparación de igualdad para las fechas de Venta a la condición de unión o la cláusula WHERE, por ejemplo

ON vt.VisitTargetId = vs.VisitTargetId and vt.SaleDate = vs.SaleDate

o

WHERE vt.SaleDate = vs.SaleDate

Esto no funcionó. ¡Incluso empeoró las estimaciones de cardinalidad! Por lo tanto, SQL Server no está utilizando esa sugerencia de igualdad o algo más es la causa raíz del problema.

¿Tienes alguna idea sobre cómo solucionar problemas y, con suerte, solucionar este problema de estimación de cardinalidad? Mi objetivo es que la cardinalidad de la unión maestro / detalle se estime igual que la estimación para la entrada más grande ("tabla de detalles") de la unión.

Si es importante, estamos ejecutando SQL Server 2014 Enterprise SP2 CU8 build 12.0.5557.0 en Windows Server. No hay marcas de seguimiento habilitadas. El nivel de compatibilidad de la base de datos es SQL Server 2014. Vemos el mismo comportamiento en varios servidores SQL diferentes, por lo que parece poco probable que sea un problema específico del servidor.

Hay una optimización en el Estimador de cardinalidad de SQL Server 2014 que es exactamente el comportamiento que estoy buscando:

El nuevo CE, sin embargo, utiliza un algoritmo más simple que supone que hay una asociación de unión de uno a muchos entre una tabla grande y una tabla pequeña. Esto supone que cada fila de la tabla grande coincide exactamente con una fila de la tabla pequeña. Este algoritmo devuelve el tamaño estimado de la entrada más grande como cardinalidad de unión.

Idealmente, podría obtener este comportamiento, donde la estimación de cardinalidad para la unión sería la misma que la estimación para la tabla grande, ¡aunque mi tabla "pequeña" aún arrojará más de 100K filas!

Justin Grant
fuente

Respuestas:

6

Suponiendo que no se puede obtener ninguna mejora haciendo algo a las estadísticas o utilizando el CE heredado, la forma más directa de solucionar su problema es cambiar su INNER JOIN a LEFT OUTER JOIN:

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitSale vs
    LEFT OUTER JOIN VisitTarget vt on vt.VisitTargetId = vs.VisitTargetId
            AND vt.SaleDate BETWEEN '20170101' and '20171231'
WHERE vs.SaleDate BETWEEN '20170101' and '20171231'

Si tiene una clave foránea entre tablas, siempre filtra en el mismo SaleDaterango para ambas tablas, ySaleDate siempre coincide entre tablas, entonces los resultados de su consulta no deberían cambiar. Puede parecer extraño usar una combinación externa como esta, pero piense que informa al optimizador de consultas que la combinación a la VisitTargettabla nunca reducirá el número de filas devueltas por la consulta. Desafortunadamente, las claves externas no cambian las estimaciones de cardinalidad, por lo que a veces es necesario recurrir a trucos como este. (Sugerencia de Microsoft Connect: haga que las estimaciones del optimizador sean más precisas mediante el uso de metadatos ).

Es posible que escribir la consulta en este formulario no funcione bien dependiendo de qué más suceda en la consulta después de la unión. Puede intentar usar una tabla temporal para mantener los resultados intermedios del conjunto de resultados con la estimación de cardinalidad más importante.

Joe Obbish
fuente