69.1. Ejemplos de estimación de filas #

Los ejemplos que se muestran a continuación utilizan tablas de la base de datos de pruebas de regresión de PostgreSQL. Ten en cuenta también que, dado que ANALYZE utiliza un muestreo aleatorio al producir estadísticas, los resultados cambiarán ligeramente después de cualquier nuevo ANALYZE.

Comencemos con una consulta muy simple:

EXPLAIN SELECT * FROM tenk1;

                         QUERY PLAN
-------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..458.00 rows=10000 width=244)

Cómo determina el planificador la cardinalidad de tenk1 se cubre en la Section 14.2, pero se repite aquí para completitud. El número de páginas y filas se busca en pg_class:

SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1';

 relpages | reltuples
----------+-----------
      358 |     10000

Estos números están actualizados hasta el último VACUUM o ANALYZE en la tabla. El planificador luego obtiene el número actual real de páginas en la tabla (esta es una operación económica, que no requiere un escaneo de la tabla). Si es diferente de relpages, entonces reltuples se escala proporcionalmente para llegar a una estimación del número actual de filas. En el ejemplo anterior, el valor de relpages está al día, por lo que la estimación de filas es la misma que reltuples.

Pasemos a un ejemplo con una condición de rango en su cláusula WHERE:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 1000;

                                   QUERY PLAN
--------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=24.06..394.64 rows=1007 width=244)
   Recheck Cond: (unique1 < 1000)
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..23.80 rows=1007 width=0)
         Index Cond: (unique1 < 1000)

El planificador examina la condición de la cláusula WHERE y busca la función de selectividad para el operador < en pg_operator. Esta se encuentra en la columna oprrest, y la entrada en este caso es scalarltsel. La función scalarltsel recupera el histograma para unique1 de pg_statistic. Para consultas manuales es más conveniente buscar en la vista más simple pg_stats:

SELECT histogram_bounds FROM pg_stats
WHERE tablename='tenk1' AND attname='unique1';

                   histogram_bounds
------------------------------------------------------
 {0,993,1997,3050,4040,5036,5957,7057,8029,9016,9995}

A continuación, se calcula la fracción del histograma ocupada por < 1000. Esta es la selectividad. El histograma divide el rango en cubetas (buckets) de igual frecuencia, por lo que todo lo que tenemos que hacer es localizar la cubeta en la que se encuentra nuestro valor y contar una parte de ella y todas las anteriores. El valor 1000 está claramente en la segunda cubeta (993–1997). Asumiendo una distribución lineal de los valores dentro de cada cubeta, podemos calcular la selectividad como:

selectivity = (1 + (1000 - bucket[2].min)/(bucket[2].max - bucket[2].min))/num_buckets
            = (1 + (1000 - 993)/(1997 - 993))/10
            = 0.100697

es decir, una cubeta completa más una fracción lineal de la segunda, dividida por el número de cubetas. El número estimado de filas ahora se puede calcular como el producto de la selectividad y la cardinalidad de tenk1:

rows = rel_cardinality * selectivity
     = 10000 * 0.100697
     = 1007  (redondeando)

A continuación, consideremos un ejemplo con una condición de igualdad en su cláusula WHERE:

EXPLAIN SELECT * FROM tenk1 WHERE stringu1 = 'CRAAAA';

                         QUERY PLAN
----------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..483.00 rows=30 width=244)
   Filter: (stringu1 = 'CRAAAA'::name)

Nuevamente, el planificador examina la condición de la cláusula WHERE y busca la función de selectividad para =, que es eqsel. Para la estimación de igualdad, el histograma no es útil; en su lugar, se utiliza la lista de los valores más comunes (MCVs) para determinar la selectividad. Echemos un vistazo a los MCVs, con algunas columnas adicionales que serán útiles más adelante:

SELECT null_frac, n_distinct, most_common_vals, most_common_freqs FROM pg_stats
WHERE tablename='tenk1' AND attname='stringu1';

null_frac         | 0
n_distinct        | 676
most_common_vals  | {EJAAAA,BBAAAA,CRAAAA,FCAAAA,FEAAAA,GSAAAA,JOAAAA,MCAAAA,NAAAAA,WGAAAA}
most_common_freqs | {0.00333333,0.003,0.003,0.003,0.003,0.003,0.003,0.003,0.003,0.003}

Dado que CRAAAA aparece en la lista de MCVs, la selectividad es simplemente la entrada correspondiente en la lista de frecuencias más comunes (MCFs):

selectivity = mcf[3]
            = 0.003

Como antes, el número estimado de filas es simplemente el producto de esto con la cardinalidad de tenk1:

rows = 10000 * 0.003
     = 30

Ahora considera la misma consulta, pero con una constante que no está en la lista MCV:

EXPLAIN SELECT * FROM tenk1 WHERE stringu1 = 'xxx';

                         QUERY PLAN
----------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..483.00 rows=15 width=244)
   Filter: (stringu1 = 'xxx'::name)

Este es un problema bastante diferente: cómo estimar la selectividad cuando el valor no está en la lista MCV. El enfoque consiste en utilizar el hecho de que el valor no está en la lista, combinado con el conocimiento de las frecuencias de todos los MCVs:

selectivity = (1 - sum(mcv_freqs))/(num_distinct - num_mcv)
            = (1 - (0.00333333 + 0.003 + 0.003 + 0.003 + 0.003 + 0.003 +
                    0.003 + 0.003 + 0.003 + 0.003))/(676 - 10)
            = 0.0014559

Es decir, suma todas las frecuencias de los MCVs y réstalas de uno, luego divide por el número de los otros valores distintos. Esto equivale a asumir que la fracción de la columna que no es ninguno de los MCVs se distribuye uniformemente entre todos los demás valores distintos. Ten en cuenta que no hay valores nulos, por lo que no tenemos que preocuparnos por ellos (de lo contrario, también restaríamos la fracción de nulos del numerador). El número estimado de filas se calcula de la manera habitual:

rows = 10000 * 0.0014559
     = 15  (redondeando)

El ejemplo anterior con unique1 < 1000 era una simplificación excesiva de lo que scalarltsel realmente hace; ahora que hemos visto un ejemplo del uso de MCVs, podemos agregar algunos detalles más. El ejemplo era correcto en la medida en que avanzó, porque dado que unique1 es una columna única, no tiene MCVs (obviamente, ningún valor es más común que cualquier otro valor). Para una columna que no es única, normalmente habrá tanto un histograma como una lista de MCVs, y el histograma no incluye la parte de la población de la columna representada por los MCVs. Hacemos las cosas de esta manera porque permite una estimación más precisa. En esta situación, scalarltsel aplica directamente la condición (por ejemplo, < 1000) a cada valor de la lista de MCVs, y suma las frecuencias de los MCVs para los cuales la condición es verdadera. Esto da una estimación exacta de la selectividad dentro de la parte de la tabla que son MCVs. El histograma luego se usa de la misma manera que arriba para estimar la selectividad en la parte de la tabla que no son MCVs, y luego los dos números se combinan para estimar la selectividad general. Por ejemplo, considera:

EXPLAIN SELECT * FROM tenk1 WHERE stringu1 < 'IAAAAA';

                          QUERY PLAN
------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..483.00 rows=3077 width=244)
   Filter: (stringu1 < 'IAAAAA'::name)

Ya vimos la información de MCVs para stringu1, y aquí está su histograma:

SELECT histogram_bounds FROM pg_stats
WHERE tablename='tenk1' AND attname='stringu1';

                                histogram_bounds
--------------------------------------------------------------------------------
 {AAAAAA,CQAAAA,FRAAAA,IBAAAA,KRAAAA,NFAAAA,PSAAAA,SGAAAA,VAAAAA,XLAAAA,ZZAAAA}

Revisando la lista de MCVs, encontramos que la condición stringu1 < 'IAAAAA' se satisface por las primeras seis entradas y no por las últimas cuatro, por lo que la selectividad dentro de la parte MCV de la población es:

selectivity = sum(relevant mvfs)
            = 0.00333333 + 0.003 + 0.003 + 0.003 + 0.003 + 0.003
            = 0.01833333

Sumar todas las MCFs también nos dice que la fracción total de la población representada por MCVs es 0.03033333, y por lo tanto la fracción representada por el histograma es 0.96966667 (nuevamente, no hay nulos, de lo contrario tendríamos que excluirlos aquí). Podemos ver que el valor IAAAAA cae casi al final de la tercera cubeta del histograma. Utilizando algunas suposiciones bastante imprecisas sobre la frecuencia de los diferentes caracteres, el planificador llega a la estimación de 0.298387 para la parte de la población del histograma que es menor que IAAAAA. Luego combinamos las estimaciones para las poblaciones MCV y no MCV:

selectivity = mcv_selectivity + histogram_selectivity * histogram_fraction
            = 0.01833333 + 0.298387 * 0.96966667
            = 0.307669

rows        = 10000 * 0.307669
            = 3077  (redondeando)

En este ejemplo particular, la corrección de la lista de MCVs es bastante pequeña, porque la distribución de la columna es en realidad bastante plana (las estadísticas que muestran estos valores particulares como más comunes que otros se deben principalmente a errores de muestreo). En un caso más típico donde algunos valores son significativamente más comunes que otros, este proceso complicado proporciona una mejora útil en la precisión porque la selectividad para los valores más comunes se determina exactamente.

Ahora consideremos un caso con más de una condición en la cláusula WHERE:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 1000 AND stringu1 = 'xxx';

                                   QUERY PLAN
--------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=23.80..396.91 rows=1 width=244)
   Recheck Cond: (unique1 < 1000)
   Filter: (stringu1 = 'xxx'::name)
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..23.80 rows=1007 width=0)
         Index Cond: (unique1 < 1000)

El planificador asume que las dos condiciones son independientes, por lo que las selectividades individuales de las cláusulas se pueden multiplicar:

selectivity = selectivity(unique1 < 1000) * selectivity(stringu1 = 'xxx')
            = 0.100697 * 0.0014559
            = 0.0001466

rows        = 10000 * 0.0001466
            = 1  (redondeando)

Ten en cuenta que el número de filas estimado a devolver por el escaneo de índice de mapa de bits (bitmap index scan) refleja solo la condición utilizada con el índice; esto es importante ya que afecta la estimación del costo para las búsquedas posteriores en el heap (heap fetches).

Finalmente examinaremos una consulta que involucra una unión (join):

EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 50 AND t1.unique2 = t2.unique2;

                                       QUERY PLAN
--------------------------------------------------------------------------------------
 Nested Loop  (cost=4.64..456.23 rows=50 width=488)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.64..142.17 rows=50 width=244)
         Recheck Cond: (unique1 < 50)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.63 rows=50 width=0)
               Index Cond: (unique1 < 50)
   ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.00..6.27 rows=1 width=244)
         Index Cond: (unique2 = t1.unique2)

La restricción en tenk1, unique1 < 50, se evalúa antes de la unión por bucle anidado (nested-loop join). Esto se maneja de manera análoga al ejemplo de rango anterior. Esta vez, el valor 50 cae en la primera cubeta del histograma de unique1:

selectivity = (0 + (50 - bucket[1].min)/(bucket[1].max - bucket[1].min))/num_buckets
            = (0 + (50 - 0)/(993 - 0))/10
            = 0.005035

rows        = 10000 * 0.005035
            = 50  (redondeando)

La restricción para la unión es t2.unique2 = t1.unique2. El operador es simplemente nuestro conocido =, sin embargo, la función de selectividad se obtiene de la columna oprjoin de pg_operator, y es eqjoinsel. eqjoinsel busca la información estadística tanto para tenk2 como para tenk1:

SELECT tablename, null_frac,n_distinct, most_common_vals FROM pg_stats
WHERE tablename IN ('tenk1', 'tenk2') AND attname='unique2';

tablename  | null_frac | n_distinct | most_common_vals
-----------+-----------+------------+------------------
 tenk1     |         0 |         -1 | 
 tenk2     |         0 |         -1 | 

En este caso no hay información de MCV para unique2 y todos los valores seem ser únicos (n_distinct = -1), por lo que usamos un algoritmo que se basa en las estimaciones del recuento de filas para ambas relaciones (num_rows, no mostrado, pero es «tenk») junto con las fracciones de nulos de la columna (cero para ambas):

selectivity = (1 - null_frac1) * (1 - null_frac2) / max(num_rows1, num_rows2)
            = (1 - 0) * (1 - 0) / max(10000, 10000)
            = 0.0001

Esto es, restar la fracción de nulos de uno para cada una de las relaciones, y dividir por el recuento de filas de la relación más grande (este valor se escala en el caso no único). El número de filas que la unión probablemente emitirá se calcula como la cardinalidad del producto cartesiano de las dos entradas, multiplicado por la selectividad:

rows = (outer_cardinality * inner_cardinality) * selectivity
     = (50 * 10000) * 0.0001
     = 50

Si hubiera habido listas de MCVs para las dos columnas, eqjoinsel habría utilizado la comparación directa de las listas de MCVs para determinar la selectividad de la unión dentro de la parte de las poblaciones de columnas representadas por los MCVs. La estimación para el resto de las poblaciones sigue el mismo enfoque que se muestra aquí.

Ten en cuenta que mostramos la inner_cardinality como 10000, es decir, el tamaño no modificado de tenk2. Podría parecer, a partir de la inspección de la salida de EXPLAIN, que la estimación de las filas de la unión proviene de 50 * 1, es decir, el número de filas externas por el número estimado de filas obtenido por cada escaneo de índice interno en tenk2. Pero este no es el caso: el tamaño de la relación de unión se estima antes de que se haya considerado cualquier plan de unión particular. Si todo funciona bien, las dos formas de estimar el tamaño de la unión producirán aproximadamente la misma respuesta, pero debido al error de redondeo y otros factores, a veces divergen significativamente.

Para aquellos interesados en más detalles, la estimación del tamaño de una tabla (antes de cualquier cláusula WHERE) se realiza en src/backend/optimizer/util/plancat.c. La lógica genérica para las selectividades de las cláusulas está en src/backend/optimizer/path/clausesel.c. Las funciones de selectividad específicas de los operadores se encuentran principalmente en src/backend/utils/adt/selfuncs.c.