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

Estimación de cardinalidad para un predicado en una expresión COUNT

Este artículo analiza la estimación de selectividad y cardinalidad para predicados en COUNT(*) expresiones, como puede verse en HAVING cláusulas. Se espera que los detalles sean interesantes en sí mismos. También brindan información sobre algunos de los enfoques y algoritmos generales utilizados por el estimador de cardinalidad.

Un ejemplo simple usando la base de datos de muestra AdventureWorks:

SELECT A.City
  FROM Person.[Address] AS A
  GROUP BY A.City
  HAVING COUNT_BIG(*) = 1;

Estamos interesados ​​en ver cómo SQL Server deriva una estimación para el predicado en la expresión de conteo en HAVING cláusula.

Por supuesto el HAVING la cláusula es solo azúcar de sintaxis. Podríamos igualmente haber escrito la consulta utilizando una tabla derivada o una expresión de tabla común:

-- Derived table
SELECT SQ1.City
FROM
(
    SELECT A.City, Expr1001 = COUNT_BIG(*)
    FROM Person.[Address] AS A
    GROUP BY A.City
) AS SQ1
WHERE SQ1.Expr1001 = 1;
 
-- CTE
WITH Grouped AS
(
    SELECT A.City, Expr1001 = COUNT_BIG(*)
    FROM Person.[Address] AS A
    GROUP BY A.City
)
SELECT G.City
FROM Grouped AS G
WHERE G.Expr1001 = 1;

Los tres formularios de consulta producen el mismo plan de ejecución, con valores hash del plan de consulta idénticos.

El plan posterior a la ejecución (real) muestra una estimación perfecta para el agregado; sin embargo, la estimación de HAVING filtro de cláusula (o equivalente, en los otros formularios de consulta) es pobre:

Estadísticas sobre la City columna proporciona información precisa sobre el número de valores de ciudades distintas:

DBCC SHOW_STATISTICS ([Person.Address], City) WITH DENSITY_VECTOR;

La toda densidad cifra es el recíproco del número de valores únicos. Simplemente calculando (1 / 0.00173913) =575 da la cardinalidad estimada para el agregado. La agrupación por ciudad obviamente produce una fila para cada valor distinto.

Tenga en cuenta que toda la densidad proviene del vector densidad. Tenga cuidado de no usar accidentalmente la densidad valor de la salida del encabezado de estadísticas de DBCC SHOW_STATISTICS . La densidad del encabezado se mantiene solo por compatibilidad con versiones anteriores; el optimizador no lo usa durante la estimación de cardinalidad en estos días.

El problema

El agregado introduce una nueva columna calculada en el flujo de trabajo, denominada Expr1001 en el plan de ejecución. Contiene el valor de COUNT(*) en cada fila de salida agrupada:

Obviamente, no hay información estadística en la base de datos sobre esta nueva columna calculada. Si bien el optimizador sabe que habrá 575 filas, no sabe nada sobre la distribución de valores de conteo dentro de esas filas.

Bueno, no del todo:el optimizador es consciente de que los valores de conteo serán números enteros positivos (1, 2, 3…). Aún así, es la distribución de estos valores de conteo de enteros entre las 575 filas lo que sería necesario para estimar con precisión la selectividad del COUNT(*) = 1 predicado.

Uno podría pensar que algún tipo de información de distribución podría derivarse del histograma, pero el histograma solo proporciona información de conteo específica (en EQ_ROWS ) para valores de paso de histograma. Entre los pasos del histograma, todo lo que tenemos es un resumen:RANGE_ROWS las filas tienen DISTINCT_RANGE_ROWS valores distintos. Para las tablas que son lo suficientemente grandes como para que nos importe la calidad de la estimación de selectividad, es muy probable que la mayor parte de la tabla esté representada por estos resúmenes entre pasos.

Por ejemplo, las dos primeras filas de City histograma de columna son:

DBCC SHOW_STATISTICS ([Person.Address], City) WITH HISTOGRAM;

Esto nos dice que hay exactamente una fila para "Abingdon" y otras 29 filas después de "Abingdon" pero antes de "Ballard", con 19 valores distintos en ese rango de 29 filas. La siguiente consulta muestra la distribución real de filas entre valores únicos en ese rango intra-paso de 29 filas:

SELECT A.City, NumRows = COUNT_BIG(*)
FROM Person.[Address] AS A 
WHERE A.City > N'Abingdon' 
AND A.City < N'Ballard'
GROUP BY ROLLUP (A.City);

Hay 29 filas con 19 valores distintos, tal como dice el histograma. Aún así, está claro que no tenemos ninguna base para evaluar la selectividad de un predicado en la columna de recuento en esa consulta. Por ejemplo, HAVING COUNT_BIG(*) = 2 devolvería 5 filas (para Alexandria, Altadena, Atlanta, Augsburg y Austin) pero no tenemos forma de determinar eso a partir del histograma.

Una conjetura fundamentada

El enfoque que adopta SQL Server es asumir que cada grupo es lo más probable para contener la media general (promedio) del número de filas. Esto es simplemente la cardinalidad dividida por el número de valores únicos. Por ejemplo, para 1000 filas con 20 valores únicos, SQL Server asumiría que (1000/20) =50 filas por grupo es el valor más probable.

Volviendo a nuestro ejemplo original, esto significa que es "muy probable" que la columna de conteo calculado contenga un valor alrededor de (19614/575) ~=34.1113 . Desde densidad es el recíproco del número de valores únicos, también podemos expresarlo como cardinalidad * densidad =(19614 * 0.00173913), dando un resultado muy similar.

Distribución

Decir que el valor medio es muy probable solo nos lleva hasta cierto punto. También necesitamos establecer exactamente qué tan probable es eso; y cómo cambia la probabilidad a medida que nos alejamos del valor medio. ¡Asumir que todos los grupos tienen exactamente 34.113 filas en nuestro ejemplo no sería una conjetura muy "educada"!

SQL Server maneja esto asumiendo una distribución normal. Esto tiene la forma de campana característica con la que quizás ya esté familiarizado (imagen de la entrada de Wikipedia vinculada):

La forma exacta de la distribución normal depende de dos parámetros :la media (µ ) y la desviación estándar (σ ). La media determina la ubicación del pico. La desviación estándar especifica cuán "aplanada" es la curva de campana. Cuanto más plana es la curva, más bajo es el pico y más se distribuye la densidad de probabilidad entre otros valores.

SQL Server puede derivar la media de la información estadística como ya se indicó. La desviación estándar de los valores de columna de conteo calculados es desconocido. SQL Server lo estima como la raíz cuadrada de la media (con un ligero ajuste detallado más adelante). En nuestro ejemplo, esto significa que los dos parámetros de la distribución normal son aproximadamente 34,1113 y 5,84 (la raíz cuadrada).

El estándar La distribución normal (la curva roja en el diagrama de arriba) es un caso especial digno de mención. Esto ocurre cuando la media es cero y la desviación estándar es 1. Cualquier distribución normal se puede transformar en la distribución normal estándar restando la media y dividiendo por la desviación estándar.

Áreas e Intervalos

Estamos interesados ​​en estimar la selectividad, por lo que buscamos la probabilidad de que la columna calculada de conteo tenga un cierto valor (x). Esta probabilidad no viene dada por el valor del eje y anterior, sino por el área bajo la curva a la izquierda de x.

Para la distribución normal con media 34,1113 y desviación estándar 5,84, el área bajo la curva a la izquierda de x =30 es de aproximadamente 0,2406:

Esto corresponde a la probabilidad de que la columna de conteo calculado sea menor o igual que 30 para nuestra consulta de ejemplo.

Esto lleva muy bien a la idea de que, en general, no buscamos la probabilidad de un valor específico, sino un intervalo . Para encontrar la probabilidad de que la cuenta es igual un valor entero, debemos tener en cuenta el hecho de que los enteros abarcan un intervalo de tamaño 1. La forma en que convertimos un entero en un intervalo es algo arbitraria. SQL Server maneja esto sumando y restando 0.5 para dar los límites inferior y superior del intervalo.

Por ejemplo, para encontrar la probabilidad de que el valor de conteo calculado sea igual a 30, necesitamos restar el área bajo la curva de distribución normal para (x =29,5) del área para (x =30,5). El resultado corresponde al segmento para (29,5

El área de la rebanada roja es aproximadamente 0.0533 . En una buena primera aproximación, esta es la selectividad de un predicado count =30 en nuestra consulta de prueba.

La función de distribución acumulativa

Calcular el área bajo una distribución normal a la izquierda de un valor dado no es sencillo. La fórmula general viene dada por la función de distribución acumulada (CDF). El problema es que la CDF no se puede expresar en términos de funciones matemáticas elementales, por lo que en su lugar se deben utilizar métodos de aproximación numérica.

Dado que todas las distribuciones normales se pueden transformar fácilmente a la distribución normal estándar (media =0, desviación estándar =1), todas las aproximaciones funcionan para estimar la normal estándar. Esto significa que necesitamos transformar nuestros límites de intervalo desde la distribución normal particular apropiada para la consulta, hasta la distribución normal estándar. Esto se hace, como se mencionó anteriormente, restando la media y dividiendo por la desviación estándar.

Si está familiarizado con Excel, es posible que conozca las funciones DISTR.NORM.DIST y DISTR.NORM.ESTÁNDAR que pueden calcular CDF (usando métodos de aproximación numérica) para una distribución normal particular o la distribución normal estándar.

No hay una calculadora CDF integrada en SQL Server, pero podemos crear una fácilmente. Dado que la CDF para la distribución normal estándar es:

…donde erf es la función de error:

A continuación se muestra una implementación de T-SQL para obtener la CDF para la distribución normal estándar. Utiliza una aproximación numérica para la función de error que es muy parecido al que SQL Server usa internamente:

CREATE PROCEDURE dbo.GetStandardNormalCDF
(
    @x float,
    @cdf float OUTPUT
)
AS
BEGIN
    SET NOCOUNT, XACT_ABORT ON;
    DECLARE
        @sign float,
        @erf float;
 
    SET @sign = SIGN(@x);
    SET @x = ABS(@x) / SQRT(2);
    SET @erf = 1;
    SET @erf = @erf + (0.0705230784 * @x);
    SET @erf = @erf + (0.0422820123 * POWER(@x, 2));
    SET @erf = @erf + (0.0092705272 * POWER(@x, 3));
    SET @erf = @erf + (0.0001520143 * POWER(@x, 4));
    SET @erf = @erf + (0.0002765672 * POWER(@x, 5)); 
    SET @erf = @erf + (0.0000430638 * POWER(@x, 6));
    SET @erf = POWER(@erf, -16);
    SET @erf = 1 - @erf;
    SET @erf = @erf * @sign;
    SET @cdf = 0.5 * (1 + @erf);
END;

Un ejemplo, para calcular la CDF para x =30 usando la distribución normal para nuestra consulta de prueba:

DECLARE @cdf float;
DECLARE @x float;
-- HAVING COUNT_BIG(*) = x
SET @x = 30;
-- Normalize 30 by subtracting the mean
-- and dividing by the standard deviation
SET @x = (@x - 34.1113) / 5.84;
EXECUTE dbo.GetStandardNormalCDF
    @x = @x,
    @cdf = @cdf OUTPUT;
SELECT CDF = @cdf;

Tenga en cuenta el paso de normalización para convertir a la distribución normal estándar. El procedimiento devuelve el valor 0,2407196…, que coincide con el resultado de Excel correspondiente con siete decimales.

Detalles finales y ejemplos

El siguiente código modifica nuestra consulta de ejemplo para producir una estimación mayor para el filtro (la comparación ahora es con el valor 32, que está mucho más cerca de la media que antes):

SELECT A.City
FROM Person.[Address] AS A
GROUP BY A.City
HAVING COUNT_BIG(*) = 32;

La estimación del optimizador ahora es 36,7807 .

Para calcular la estimación manualmente, primero debemos abordar algunos detalles finales:

  • La media utilizada para derivar la desviación estándar (a través de la raíz cuadrada) se escala por un factor de ((valores distintos – 1) / (valores distintos) . En el ejemplo, el número de valores distintos es 575, por lo que el factor de escala es (574/575) ~=0,99826.
  • Si el límite inferior del intervalo (entero) es 1, SQL Server trata el intervalo como ilimitado en el lado inferior. La selectividad es igual a la CDF del límite superior del intervalo (1.5) solo. El límite inferior (que sería 0,5) no se utiliza.
  • El estimador de cardinalidad heredado (CE) tiene una lógica compleja para COUNT(*) = 1 , que no se detalla aquí.
  • Aparte del COUNT(*) = 1 caso, el CE heredado usa la misma lógica que el nuevo CE (disponible en SQL Server 2014 en adelante).

El siguiente procedimiento incorpora todos los detalles de este artículo. Requiere el procedimiento CDF dado anteriormente:

CREATE PROCEDURE dbo.GetCountPredicateEstimate
(
    @From           integer,
    @To             integer,
    @Cardinality    float,
    @Density        float,
    @Selectivity    float OUTPUT,
    @Estimate       float OUTPUT
)
AS
BEGIN
    SET NOCOUNT, XACT_ABORT ON;
    BEGIN TRY
    DECLARE
        @Start          float,
        @End            float,
        @Distinct       float,
        @Mean           float,
        @MeanAdj        float,
        @Stdev          float,
        @NormStart      float,
        @NormEnd        float,
        @CDFStart       float,
        @CDFEnd         float;
    -- Validate input and apply defaults
    IF ISNULL(@From, 0) = 0 SET @From = 1;
    IF @From < 1 RAISERROR ('@From must be >= 1', 16, 1);
    IF ISNULL(@Cardinality, -1) <= 0 RAISERROR('@Cardinality must be positive', 16, 1);
    IF ISNULL(@Density, -1) <= 0 RAISERROR('@Density must be positive', 16, 1);
    IF ISNULL(@To, 0) = 0 SET @To = CEILING(1 / @Density);
    IF @To < @From RAISERROR('@To must be >= @From', 16, 1);
    -- Convert integer range to interval
    SET @Start = @From - 0.5;
    SET @End = @To + 0.5;
    -- Get number of distinct values
    SET @Distinct = 1 / @Density;
    -- Calculate mean
    SET @Mean = @Cardinality * @Density;
    -- Adjust mean;
    SET @MeanAdj = @Mean * ((@Distinct - 1) / @Distinct);
    -- Get standard deviation (guess)
    SET @Stdev = SQRT(@MeanAdj);
    -- Normalize interval
    SET @NormStart = (@Start - @Mean) / @Stdev;
    SET @NormEnd = (@End - @Mean) / @Stdev;
    -- Calculate CDFs
    EXECUTE dbo.GetStandardNormalCDF
        @x = @NormStart,
        @cdf = @CDFStart OUTPUT;
 
    EXECUTE dbo.GetStandardNormalCDF
        @x = @NormEnd,
        @cdf = @CDFEnd OUTPUT;
    -- Selectivity
    SET @Selectivity =
        CASE
            -- Unbounded start
            WHEN @From = 1 THEN @CDFEnd
            -- Unbounded end
            WHEN @To >= @Distinct THEN 1 - @CDFStart
            -- Normal interval
            ELSE @CDFEnd - @CDFStart
        END;
    -- Return row estimate
    SET @Estimate = @Selectivity * @Distinct;
    END TRY
    BEGIN CATCH
        DECLARE @EM nvarchar(4000) = ERROR_MESSAGE();
        IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
        RAISERROR (@EM, 16, 1);
        RETURN;
    END CATCH;
END;

Ahora podemos usar este procedimiento para generar una estimación para nuestra nueva consulta de prueba:

DECLARE 
    @Selectivity float,
    @Estimate float;
EXECUTE dbo.GetCountPredicateEstimate
    @From = 32,
    @To = 32,
    @Cardinality = 19614,
    @Density = 0.00173913,
    @Selectivity = @Selectivity OUTPUT,
    @Estimate = @Estimate OUTPUT;
SELECT
    Selectivity = @Selectivity,
    Estimate = @Estimate,
    Rounded = ROUND(@Estimate, 4);

La salida es:

Esto se compara muy bien con la estimación de cardinalidad del optimizador de 36,7807.

Ejemplos de intervalos de desigualdad

El procedimiento se puede utilizar para otros intervalos de conteo además de las pruebas de igualdad. Todo lo que se requiere es establecer el @From y @To parámetros a los límites del intervalo entero. Para especificar ilimitado, pase cero o NULL como prefieras.

SELECT A.City
FROM Person.[Address] AS A
GROUP BY A.City
HAVING COUNT_BIG(*) < 50;

Para usar esto con nuestro procedimiento, establecemos @From = NULL y @To = 49 (porque 50 está excluido por menos de):

DECLARE 
    @Selectivity float,
    @Estimate float;
EXECUTE dbo.GetCountPredicateEstimate
    @From = NULL,
    @To = 49,
    @Cardinality = 19614,
    @Density = 0.00173913,
    @Selectivity = @Selectivity OUTPUT,
    @Estimate = @Estimate OUTPUT;
SELECT
    Selectivity = @Selectivity,
    Estimate = @Estimate,
    Rounded = ROUND(@Estimate, 4);

El resultado es 572.5964:

Un último ejemplo usando BETWEEN :

SELECT A.City
FROM Person.[Address] AS A
GROUP BY A.City
HAVING COUNT_BIG(*) BETWEEN 25 AND 30;

La estimación del optimizador es

Desde BETWEEN es inclusivo, pasamos el procedimiento @From = 25 y @To = 30 . El resultado es:

Nuevamente, esto concuerda con la estimación del optimizador.