14.2. Estadísticas utilizadas por el planificador #

14.2.1. Estadísticas de una sola columna
14.2.2. Estadísticas extendidas

14.2.1. Estadísticas de una sola columna #

Como vimos en la sección anterior, el planificador de consultas necesita estimar el número de filas recuperadas por una consulta para poder tomar buenas decisiones sobre los planes de consulta. Esta sección proporciona una vista rápida de las estadísticas que el sistema utiliza para estas estimaciones.

Un componente de las estadísticas es el número total de entradas en cada tabla e índice, así como el número de bloques de disco ocupados por cada tabla e índice. Esta información se mantiene en la tabla pg_class, en las columnas reltuples y relpages. Podemos consultarla con consultas similares a esta:

SELECT relname, relkind, reltuples, relpages
FROM pg_class
WHERE relname LIKE 'tenk1%';

       relname        | relkind | reltuples | relpages
----------------------+---------+-----------+----------
 tenk1                | r       |     10000 |      345
 tenk1_hundred        | i       |     10000 |       11
 tenk1_thous_tenthous | i       |     10000 |       30
 tenk1_unique1        | i       |     10000 |       30
 tenk1_unique2        | i       |     10000 |       30
(5 rows)

Aquí podemos ver que tenk1 contiene 10000 filas, al igual que sus índices, pero los índices son (como era de esperar) mucho más pequeños que la tabla.

Por razones de eficiencia, reltuples y relpages no se actualizan sobre la marcha y, por lo tanto, suelen contener valores algo desactualizados. Se actualizan mediante VACUUM, ANALYZE, y a few DDL commands tales como CREATE INDEX. Una operación VACUUM o ANALYZE que no escanea toda la tabla (lo cual es común) actualizará incrementalmente el recuento de reltuples basándose en la parte de la tabla que escaneó, lo que da como resultado un valor aproximado. En cualquier caso, el planificador escalará los valores que encuentra en pg_class para que coincidan con el tamaño físico actual de la tabla, obteniendo así una aproximación más cercana.

La mayoría de las consultas recuperan solo una fracción de las filas de una tabla, debido a las cláusulas WHERE que restringen las filas que se deben examinar. El planificador necesita, por tanto, realizar una estimación de la selectividad (selectivity) de las cláusulas WHERE, es decir, la fracción de filas que coinciden con cada condición en la cláusula WHERE. La información utilizada para esta tarea se almacena en el catálogo del sistema pg_statistic. Las entradas en pg_statistic se actualizan mediante los comandos ANALYZE y VACUUM ANALYZE, y siempre son aproximadas, incluso cuando están recién actualizadas.

En lugar de examinar pg_statistic directamente, es mejor mirar su vista pg_stats cuando se examinan las estadísticas manualmente. pg_stats está diseñada para ser más fácilmente legible. Además, pg_stats es legible por todos, mientras que pg_statistic solo es legible por un superusuario. (Esto evita que los usuarios sin privilegios obtengan información sobre el contenido de las tablas de otras personas a partir de las estadísticas. La vista pg_stats está restringida para mostrar solo filas sobre las tablas que el usuario actual puede leer). Por ejemplo, podríamos ejecutar:

SELECT attname, inherited, n_distinct,
       array_to_string(most_common_vals, E'\n') as most_common_vals
FROM pg_stats
WHERE tablename = 'road';

 attname | inherited | n_distinct |          most_common_vals
---------+-----------+------------+------------------------------------
 name    | f         | -0.5681108 | I- 580                        Ramp+
         |           |            | I- 880                        Ramp+
         |           |            | Sp Railroad                       +
         |           |            | I- 580                            +
         |           |            | I- 680                        Ramp+
         |           |            | I- 80                         Ramp+
         |           |            | 14th                          St  +
         |           |            | I- 880                            +
         |           |            | Mac Arthur                    Blvd+
         |           |            | Mission                       Blvd+
...
 name    | t         |    -0.5125 | I- 580                        Ramp+
         |           |            | I- 880                        Ramp+
         |           |            | I- 580                            +
         |           |            | I- 680                        Ramp+
         |           |            | I- 80                         Ramp+
         |           |            | Sp Railroad                       +
         |           |            | I- 880                            +
         |           |            | State Hwy 13                  Ramp+
         |           |            | I- 80                             +
         |           |            | State Hwy 24                  Ramp+
...
 thepath | f         |          0 |
 thepath | t         |          0 |
(4 rows)

Ten en cuenta que se muestran dos filas para la misma columna: una correspondiente a la jerarquía de herencia completa que comienza en la tabla road (inherited=t), y otra que incluye solo la propia tabla road (inherited=f). (Por brevedad, solo hemos mostrado los primeros diez valores más comunes para la columna name).

La cantidad de información almacenada en pg_statistic por ANALYZE, en particular el número máximo de entradas en las matrices most_common_vals y histogram_bounds para cada columna, se puede establecer columna por columna utilizando el comando ALTER TABLE SET STATISTICS, o globalmente configurando la variable de configuración default_statistics_target. El límite por defecto es actualmente de 100 entradas. Elevar el límite podría permitir al planificador realizar estimaciones más precisas, especialmente para columnas con distribuciones de datos irregulares, a costa de consumir más espacio en pg_statistic y de requerir un poco más de tiempo para calcular las estimaciones. Por el contrario, un límite inferior podría ser suficiente para columnas con distribuciones de datos sencillas.

Se pueden encontrar más detalles sobre el uso de estadísticas por parte del planificador en la Chapter 69.

14.2.2. Estadísticas extendidas #

Es común ver consultas lentas que ejecutan planes de ejecución deficientes porque varias columnas utilizadas en las cláusulas de la consulta están correlacionadas. El planificador normalmente asume que múltiples condiciones son independientes entre sí, una suposición que no se cumple cuando los valores de las columnas están correlacionados. Las estadísticas regulares, debido a su naturaleza por columna individual, no pueden capturar ningún conocimiento sobre la correlación cruzada de columnas. Sin embargo, PostgreSQL tiene la capacidad de calcular estadísticas multivariantes, las cuales pueden capturar dicha información.

Debido a que el número de combinaciones de columnas posibles es muy grande, resulta poco práctico calcular estadísticas multivariantes automáticamente. En su lugar, se pueden crear objetos de estadísticas extendidas, más a menudo llamados simplemente objetos de estadísticas, para indicar al servidor que obtenga estadísticas sobre conjuntos de columnas interesantes.

Los objetos de estadísticas se crean utilizando el comando CREATE STATISTICS. La creación de dicho objeto simplemente crea una entrada en el catálogo que expresa el interés en las estadísticas. La recolección de datos real la realiza ANALYZE (ya sea mediante un comando manual o mediante el autoanálisis en segundo plano). Los valores recolectados se pueden examinar en el catálogo pg_statistic_ext_data.

ANALYZE calcula estadísticas extendidas basándose en la misma muestra de filas de la tabla que toma para calcular las estadísticas regulares de una sola columna. Dado que el tamaño de la muestra aumenta al incrementar el objetivo de estadísticas de la tabla o de cualquiera de sus columnas (así como se describe en la sección anterior), un objetivo de estadísticas mayor dará como resultado normalmente estadísticas extendidas más precisas, además de requerir más tiempo para calcularlas.

Las siguientes subsecciones describen los tipos de estadísticas extendidas que se admiten actualmente.

14.2.2.1. Dependencias funcionales #

El tipo más simple de estadísticas extendidas realiza un seguimiento de las dependencias funcionales, un concepto utilizado en las definiciones de las formas normales de las bases de datos. Decimos que la columna b depende funcionalmente de la columna a si el conocimiento del valor de a is suficiente para determinar el valor de b, es decir, no hay dos filas que tengan el mismo valor de a pero diferentes valores de b. En una base de datos totalmente normalizada, las dependencias funcionales deberían existir únicamente en las claves primarias y superclaves. Sin embargo, en la práctica muchos conjuntos de datos no están totalmente normalizados por diversas razones; la desnormalización intencionada por motivos de rendimiento es un ejemplo común. Incluso en una base de datos totalmente normalizada, puede haber una correlación parcial entre algunas columnas, lo cual se puede expresar como una dependencia funcional parcial.

La existencia de dependencias funcionales afecta directamente a la precisión de las estimaciones en ciertas consultas. Si una consulta contiene condiciones tanto en la columna independiente como en la(s) dependiente(s), las condiciones en las columnas dependientes no reducen aún más el tamaño del resultado; pero sin el conocimiento de la dependencia funcional, el planificador de consultas asumirá que las condiciones son independientes, lo que dará como resultado una subestimación del tamaño del resultado.

Para informar al planificador sobre las dependencias funcionales, ANALYZE puede recolectar mediciones de la dependencia entre columnas. Evaluar el grado de dependencia entre todos los conjuntos de columnas sería prohibitivamente costoso, por lo que la recolección de datos se limita a aquellos grupos de columnas que aparecen juntos en un objeto de estadísticas definido con la opción dependencies. Se aconseja crear estadísticas de tipo dependencies solo para grupos de columnas que estén fuertemente correlacionados, para evitar una sobrecarga innecesaria tanto en ANALYZE como en la posterior planificación de consultas.

Aquí tienes un ejemplo de recolección de estadísticas de dependencia funcional:

CREATE STATISTICS stts (dependencies) ON city, zip FROM zipcodes;

ANALYZE zipcodes;

SELECT stxname, stxkeys, stxddependencies
  FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid)
  WHERE stxname = 'stts';
  stxname | stxkeys |             stxddependencies
 ---------+---------+------------------------------------------
  stts    | 1 5     | {"1 => 5": 1.000000, "5 => 1": 0.423130}
 (1 row)

Aquí se puede ver que la columna 1 (código ZIP) determina por completo la columna 5 (ciudad), por lo que el coeficiente es 1.0, mientras que la ciudad solo determina el código ZIP alrededor del 42% de las veces, lo que significa que hay muchas ciudades (58%) que están representadas por más de un código ZIP único.

Al calcular la selectividad para una consulta que involucra columnas con dependencia funcional, el planificador ajusta las estimaciones de selectividad por condición utilizando los coeficientes de dependencia para no producir una subestimación.

14.2.2.1.1. Limitaciones de las dependencias funcionales #

Las dependencias funcionales actualmente solo se aplican cuando se consideran condiciones de igualdad simples que comparan columnas con valores constantes, y cláusulas IN con valores constantes. No se utilizan para mejorar las estimaciones de condiciones de igualdad que comparan dos columnas o que comparan una columna con una expresión, ni tampoco para cláusulas de rango, LIKE o cualquier otro tipo de condición.

Al realizar estimaciones con dependencias funcionales, el planificador asume que las condiciones en las columnas implicadas son compatibles y, por lo tanto, redundantes. Si son incompatibles, la estimación correcta sería de cero filas, pero esa posibilidad no se considera. Por ejemplo, dada una consulta como:

SELECT * FROM zipcodes WHERE city = 'San Francisco' AND zip = '94105';

el planificador ignorará la cláusula city considerando que no cambia la selectividad, lo cual es correcto. Sin embargo, hará la misma suposición sobre:

SELECT * FROM zipcodes WHERE city = 'San Francisco' AND zip = '90210';

aunque en realidad habrá cero filas que satisfagan esta consulta. Las estadísticas de dependencia funcional no proporcionan suficiente información para concluir eso, sin embargo.

En muchas situaciones prácticas, esta suposición suele cumplirse; por ejemplo, podría haber una interfaz gráfica de usuario en la aplicación que solo permita seleccionar valores compatibles de ciudad y código ZIP para utilizarlos en una consulta. Pero si ese no es el caso, las dependencias funcionales podrían no ser una opción viable.

14.2.2.2. Recuentos de N-distintos multivariantes #

Las estadísticas de una sola columna almacenan el número de valores distintos en cada columna. Las estimaciones del número de valores distintos al combinar más de una columna (por ejemplo, para GROUP BY a, b) son frecuentemente erróneas cuando el planificador solo dispone de datos estadísticos de una sola columna, lo que le lleva a seleccionar planes deficientes.

Para mejorar tales estimaciones, ANALYZE puede recolectar estadísticas de n-distintos para grupos de columnas. Como antes, resulta poco práctico hacer esto para cada agrupación de columnas posible, por lo que los datos se recolectan solo para aquellos grupos de columnas que aparecen juntos en un objeto de estadísticas definido con la opción ndistinct. Se recolectarán datos para cada combinación posible de dos o más columnas del conjunto de columnas listadas.

Continuando con el ejemplo anterior, los recuentos de n-distintos en una tabla de códigos ZIP podrían verse de la siguiente manera:

CREATE STATISTICS stts2 (ndistinct) ON city, state, zip FROM zipcodes;

ANALYZE zipcodes;

SELECT stxkeys AS k, stxdndistinct AS nd
  FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid)
  WHERE stxname = 'stts2';
-[ RECORD 1 ]--------------------------------------------------------
k  | 1 2 5
nd | {"1, 2": 33178, "1, 5": 33178, "2, 5": 27435, "1, 2, 5": 33178}
(1 row)

Esto indica que hay tres combinaciones de columnas que tienen 33178 valores distintos: código ZIP y estado; código ZIP y ciudad; y código ZIP, ciudad y estado (el hecho de que sean todos iguales es de esperar dado que el código ZIP por sí solo es único en esta tabla). Por otro lado, la combinación de ciudad y estado tiene solo 27435 valores distintos.

Se aconseja crear objetos de estadísticas de tipo ndistinct únicamente en combinaciones de columnas que realmente se utilicen para agrupar, y para las cuales una estimación errónea del número de grupos esté dando como resultado planes deficientes. De lo contrario, los ciclos de ANALYZE simplemente se desperdician.

14.2.2.3. Listas de MCV multivariantes #

Otro tipo de estadística almacenada para cada columna son las listas de valores más comunes (MCV, most-common value). Esto permite realizar estimaciones muy precisas para columnas individuales, pero puede dar lugar a estimaciones significativamente erróneas para consultas con condiciones en varias columnas.

Para mejorar tales estimaciones, ANALYZE puede recolectar listas de MCV sobre combinaciones de columnas. Al igual que con las dependencias funcionales y los coeficientes de n-distintos, es poco práctico hacer esto para cada agrupación de columnas posible. Aún más en este caso, ya que la lista de MCV (a diferencia de las dependencias funcionales y los coeficientes de n-distintos) sí almacena los valores comunes de las columnas. Así que los datos se recolectan únicamente para aquellos grupos de columnas que aparecen juntos en un objeto de estadísticas definido con la opción mcv.

Continuando con el ejemplo anterior, la lista de MCV para una tabla de códigos ZIP podría verse de la siguiente manera (a diferencia de los tipos de estadísticas más sencillos, se requiere una función para la inspección del contenido de la MCV):

CREATE STATISTICS stts3 (mcv) ON city, state FROM zipcodes;

ANALYZE zipcodes;

SELECT m.* FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid),
                pg_mcv_list_items(stxdmcv) m WHERE stxname = 'stts3';

 index |         values         | nulls | frequency | base_frequency
-------+------------------------+-------+-----------+----------------
     0 | {Washington, DC}       | {f,f} |  0.003467 |        2.7e-05
     1 | {Apo, AE}              | {f,f} |  0.003067 |        1.9e-05
     2 | {Houston, TX}          | {f,f} |  0.002167 |       0.000133
     3 | {El Paso, TX}          | {f,f} |     0.002 |       0.000113
     4 | {New York, NY}         | {f,f} |  0.001967 |       0.000114
     5 | {Atlanta, GA}          | {f,f} |  0.001633 |        3.3e-05
     6 | {Sacramento, CA}       | {f,f} |  0.001433 |        7.8e-05
     7 | {Miami, FL}            | {f,f} |    0.0014 |          6e-05
     8 | {Dallas, TX}           | {f,f} |  0.001367 |        8.8e-05
     9 | {Chicago, IL}          | {f,f} |  0.001333 |        5.1e-05
   ...
(99 rows)

This indicates that the most common combination of city and state is Washington in DC, con una frecuencia real (en la muestra) de alrededor del 0.35%. La frecuencia base de la combinación (calculada a partir de las frecuencias simples por columna) es de solo 0.0027%, lo que da como resultado subestimaciones de dos órdenes de magnitud.

Se aconseja crear objetos de estadísticas de tipo MCV únicamente en combinaciones de columnas que realmente se utilicen juntas en condiciones, y para las cuales una estimación errónea del número de grupos esté dando como resultado planes deficientes. De lo contrario, los ciclos de ANALYZE y de planificación simplemente se desperdician.