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: VisitTarget
tiene una fila para cada transacción de ventas y VisitSale
una 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í:
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!