Un índice parcial es un índice construido sobre un subconjunto de una tabla; el subconjunto está definido por una expresión condicional (llamada el predicado del índice parcial). El índice contiene entradas solo para aquellas filas de la tabla que satisfacen el predicado. Los índices parciales son una característica especializada, pero hay varias situaciones en las que resultan útiles.
Una de las razones principales para usar un índice parcial es evitar indexar valores comunes. Dado que una consulta que busca un valor común (uno que representa más de un pequeño porcentaje de todas las filas de la tabla) no usará el índice de todos modos, no tiene sentido mantener esas filas en el índice en absoluto. Esto reduce el tamaño del índice, lo que acelerará aquellas consultas que sí usan el índice. También acelerará muchas operaciones de actualización de la tabla porque el índice no necesita actualizarse en todos los casos. Example 11.1 muestra una posible aplicación de esta idea.
Example 11.1. Configuración de un índice parcial para excluir valores comunes
Supongamos que almacenas los registros de acceso del servidor web en una base de datos. La mayoría de los accesos provienen del rango de direcciones IP de tu organización, pero algunos son de otros lugares (por ejemplo, empleados con conexiones de marcado telefónico). Si tus búsquedas por IP son principalmente para accesos externos, probablemente no necesites indexar el rango de IP que corresponde a la subred de tu organización.
Asume una tabla como esta:
CREATE TABLE access_log (
url varchar,
client_ip inet,
...
);
Para crear un índice parcial que se adapte a nuestro ejemplo, usa un comando como este:
CREATE INDEX access_log_client_ip_ix ON access_log (client_ip)
WHERE NOT (client_ip > inet '192.168.100.0' AND
client_ip < inet '192.168.100.255');
Una consulta típica que puede usar este índice sería:
SELECT * FROM access_log WHERE url = '/index.html' AND client_ip = inet '212.78.10.32';
Aquí la dirección IP de la consulta está cubierta por el índice parcial. La siguiente consulta no puede usar el índice parcial, ya que utiliza una dirección IP que está excluida del índice:
SELECT * FROM access_log WHERE url = '/index.html' AND client_ip = inet '192.168.100.23';
Observa que este tipo de índice parcial requiere que los valores comunes estén predeterminados, por lo que tales índices parciales se utilizan mejor para distribuciones de datos que no cambian. Estos índices se pueden volver a crear ocasionalmente para ajustarse a las nuevas distribuciones de datos, pero esto añade un esfuerzo de mantenimiento.
Otro posible uso de un índice parcial es excluir valores del índice en los que la carga de trabajo de consultas típica no está interesada; esto se muestra en Example 11.2. Esto produce las mismas ventajas enumeradas anteriormente, pero evita que se acceda a los valores “no interesantes” a través de ese índice, incluso si un escaneo del índice pudiera ser rentable en ese caso. Obviamente, configurar índices parciales para este tipo de escenario requerirá mucho cuidado y experimentación.
Example 11.2. Configuración de un índice parcial para excluir valores no interesantes
Si tienes una tabla que contiene tanto pedidos facturados como no facturados, donde los pedidos no facturados ocupan una pequeña fracción del total de la tabla y, sin embargo, esas son las filas a las que más se accede, puedes mejorar el rendimiento creando un índice solo en las filas no facturadas. El comando para crear el índice se vería así:
CREATE INDEX orders_unbilled_index ON orders (order_nr)
WHERE billed is not true;
Una posible consulta para usar este índice sería:
SELECT * FROM orders WHERE billed is not true AND order_nr < 10000;
Sin embargo, el índice también se puede usar en consultas que no involucren
a order_nr en absoluto, por ejemplo:
SELECT * FROM orders WHERE billed is not true AND amount > 5000.00;
Esto no es tan eficiente como lo sería un índice parcial en la
columna amount, ya que el sistema tiene que
escanear todo el índice. Sin embargo, si hay relativamente pocos pedidos
no facturados, usar este índice parcial solo para encontrar los pedidos no facturados
podría ser una ganancia.
Ten en cuenta que esta consulta no puede usar este índice:
SELECT * FROM orders WHERE order_nr = 3501;
El pedido 3501 podría estar entre los pedidos facturados o no facturados.
El Example 11.2 también ilustra que la
columna indexada y la columna utilizada en el predicado no necesitan
coincidir. PostgreSQL admite índices parciales
con predicados arbitrarios, siempre que solo intervengan columnas de la
tabla que se está indexando. Sin embargo, ten en cuenta que el
predicado debe coincidir con las condiciones utilizadas en las consultas que
se supone que se beneficiarán del índice. Para ser precisos, un índice parcial
se puede usar en una consulta solo si el sistema puede reconocer que la
condición WHERE de la consulta implica matemáticamente
el predicado del índice.
PostgreSQL no dispone de un demostrador de teoremas sofisticado que pueda reconocer expresiones matemáticamente equivalentes
que estén escritas de formas diferentes. (No
solo es extremadamente difícil crear un demostrador de teoremas general de este tipo, sino que probablemente sería demasiado lento para ser de alguna utilidad real).
El sistema puede reconocer implicaciones de desigualdad simples, por ejemplo,
“x < 1” implica “x < 2”; de lo contrario,
la condición del predicado debe coincidir exactamente con parte de la condición WHERE de la consulta,
o el índice no se reconocerá como utilizable. La coincidencia se produce en el momento de la planificación de la consulta, no en el momento de la ejecución. Como resultado,
las cláusulas de consulta parametrizadas no funcionan con un índice parcial. Por
ejemplo, una consulta preparada con un parámetro podría especificar
“x < ?”, lo que nunca implicará
“x < 2” para todos los valores posibles del parámetro.
Un tercer uso posible para los índices parciales no requiere que el índice se use en las consultas en absoluto. La idea aquí es crear un índice único sobre un subconjunto de una tabla, como en el Example 11.3. Esto hace cumplir la unicidad entre las filas que satisfacen el predicado del índice, sin restringir a las que no lo hacen.
Example 11.3. Configuración de un índice parcial único
Supongamos que tenemos una tabla que describe los resultados de las pruebas. Queremos asegurarnos de que solo haya una entrada “exitosa” para una combinación dada de sujeto y objetivo, pero puede haber cualquier número de entradas “no exitosas”. Aquí hay una manera de hacerlo:
CREATE TABLE tests (
subject text,
target text,
success boolean,
...
);
CREATE UNIQUE INDEX tests_success_constraint ON tests (subject, target)
WHERE success;
Este es un enfoque particularmente eficiente cuando hay pocas
pruebas exitosas y muchas no exitosas. También es posible
permitir solo un nulo en una columna creando un índice parcial único
con una restricción IS NULL.
Por último, un índice parcial también se puede usar para anular las elecciones de planes de consulta del sistema. Además, los conjuntos de datos con distribuciones particulares podrían hacer que el sistema use un índice cuando realmente no debería hacerlo. En ese caso, el índice se puede configurar de modo que no esté disponible para la consulta infractora. Normalmente, PostgreSQL toma decisiones razonables sobre el uso de índices (por ejemplo, los evita al recuperar valores comunes, por lo que el ejemplo anterior solo ahorra tamaño de índice, no es necesario para evitar el uso del índice), y las elecciones de planes extremadamente incorrectas son motivo para un informe de error (bug report).
Ten en cuenta que configurar un índice parcial indica que sabes al menos tanto como sabe el planificador de consultas, en particular, sabes cuándo un índice puede ser rentable. Formar este conocimiento requiere experiencia y comprensión de cómo funcionan los índices en PostgreSQL. En la mayoría de los casos, la ventaja de un índice parcial sobre un índice regular será mínima. Hay casos en los que son bastante contraproducentes, como en el Example 11.4.
Example 11.4. No uses índices parciales como sustituto de la partición
Podrías sentirte tentado a crear un gran conjunto de índices parciales que no se solapen, por ejemplo:
CREATE INDEX mytable_cat_1 ON mytable (data) WHERE category = 1; CREATE INDEX mytable_cat_2 ON mytable (data) WHERE category = 2; CREATE INDEX mytable_cat_3 ON mytable (data) WHERE category = 3; ... CREATE INDEX mytable_cat_NON mytable (data) WHERE category =N;
¡Esta es una mala idea! Casi con seguridad, te irá mejor con un solo índice no parcial, declarado como:
CREATE INDEX mytable_cat_data ON mytable (category, data);
(Coloca la columna de la categoría primero, por las razones descritas en Section 11.3). Aunque una búsqueda en este índice más grande tenga que descender a través de un par de niveles de árbol más que una búsqueda en un índice más pequeño, casi con seguridad eso será más barato que el esfuerzo del planificador necesario para seleccionar el índice parcial adecuado. El núcleo del problema es que el sistema no entiende la relación entre los índices parciales y probará laboriosamente cada uno para ver si es aplicable a la consulta actual.
Si tu tabla es lo suficientemente grande como para que un solo índice sea realmente una mala idea, deberías considerar usar particionamiento en su lugar (consulta Section 5.12). Con ese mecanismo, el sistema sí entiende que las tablas y los índices no se solapan, por lo que es posible un rendimiento mucho mejor.
Se puede encontrar más información sobre los índices parciales en [ston89b], [olson93] y [seshadri95].