Seleccionar el cuerpo de un artículo puede descartar un análisis solo del índice: PostgreSQL debe recuperar una columna que el índice no contiene. Esta guía muestra cómo diagnosticar ese cambio en un Mac y medir su efecto en Node.js. Requisitos para un análisis solo del índice.
¿Por qué cambia el plan al cambiar las columnas seleccionadas?
Considera una tabla de artículos con id, category y body, además de un índice B-tree sobre (category, id). Una consulta que selecciona identificadores por categoría puede obtener tanto el valor de filtro como el resultado de ese índice. Añadir body hace que el índice sea insuficiente para un análisis solo del índice. Los índices de PostgreSQL están separados del almacenamiento principal de la tabla, llamado heap; un análisis normal del índice también recupera datos de la tabla. Análisis solo del índice e índices de cobertura.
Ahora el planificador compara distintas formas de recuperar las filas requeridas de la tabla. Puede mantener un Index Scan, combinar un Bitmap Index Scan con un Bitmap Heap Scan o elegir un Seq Scan. Los análisis de mapa de bits agrupan los accesos al heap por ubicación física. Cuando se recuperan muchas filas dispersas, analizar la tabla puede ser más barato que buscarlas por separado. Un plan de mapa de bits sigue usando un índice, aunque el nodo de análisis superior mencione el heap. Uso de EXPLAIN.
Revisa también cuidadosamente el plan original. Un Index Only Scan puede indicar un valor distinto de cero en Heap Fetches: a veces PostgreSQL necesita visitar filas de la tabla para determinar su visibilidad. Evitar esas visitas depende del mapa de visibilidad de la tabla, además de que el índice contenga las columnas requeridas. Comprobaciones de visibilidad para análisis solo del índice.
Reproduce la comparación desde tu Mac
Usa una base de datos PostgreSQL local en ejecución y una versión instalada y actualmente compatible de Node.js con npm. El ejemplo de referencia usa funciones disponibles en PostgreSQL 14–18; el manual actual de PostgreSQL consultado se identifica como versión 18. El diagnóstico adicional de serialización que aparece a continuación requiere PostgreSQL 17 o posterior. La documentación de node-postgres describe su compatibilidad con Node.js y el comando de instalación. Documentación de PostgreSQL, Instalación y compatibilidad de node-postgres.
Abre Terminal y trabaja en una carpeta nueva. Estos comandos crean el proyecto e instalan el controlador de la base de datos:
mkdir article-plan-lab
cd article-plan-lab
npm init -y
npm install pg
El programa usa la configuración de conexión local existente mediante PGHOST, PGPORT, PGDATABASE y PGUSER. Configúralos para una base de datos a la que ya puedas conectarte. node-postgres lee estas variables de entorno al crear un Client sin opciones de conexión explícitas. Conexión mediante variables de entorno.
Guarda el siguiente programa completo como article-plan.cjs. Crea filas sintéticas en una tabla temporal, construye un índice, imprime ambos planes de ejecución y luego mide ambas consultas normales. Las dos comparaciones usan la misma conexión y el mismo valor de categoría. La configuración de consulta proporciona SQL mediante text y el parámetro mediante values, siguiendo la API documentada de node-postgres. Configuración de consultas.
const { Client } = require('pg');
const { performance } = require('node:perf_hooks');
async function main() {
const client = new Client();
try {
await client.connect();
const version = await client.query('SHOW server_version');
console.log(version.rows[0]);
await client.query(`CREATE TEMP TABLE article_plan_demo (
id integer PRIMARY KEY,
category integer NOT NULL,
body text NOT NULL
)`);
await client.query(`INSERT INTO article_plan_demo
SELECT g, g % 10, repeat('Synthetic article paragraph. ', 40)
FROM generate_series(1, 50000) AS s(g)`);
await client.query(`CREATE INDEX article_plan_demo_category_id
ON article_plan_demo (category, id)`);
await client.query('VACUUM (ANALYZE) article_plan_demo');
const queries = [
['IDs', 'SELECT id FROM article_plan_demo WHERE category = $1'],
['Bodies', 'SELECT id, body FROM article_plan_demo WHERE category = $1']
];
for (const [label, text] of queries) {
const plan = await client.query({
text: `EXPLAIN (ANALYZE, BUFFERS, VERBOSE) ${text}`,
values: [3]
});
console.log(label);
console.log(plan.rows.map(row => row['QUERY PLAN']).join('\n'));
const start = performance.now();
const result = await client.query({ text, values: [3] });
const elapsed = performance.now() - start;
const bodyBytes = result.rows.reduce((sum, row) =>
sum + (row.body === undefined ? 0 : Buffer.byteLength(row.body)), 0);
console.log({
rows: result.rows.length,
elapsedMs: elapsed.toFixed(2),
bodyBytes
});
}
} finally {
await client.end();
}
}
main().catch(error => {
console.error(error.message);
process.exitCode = 1;
});
Ejecútalo desde la misma carpeta:
node article-plan.cjs
La configuración envía VACUUM (ANALYZE) como comando independiente. Ese comando primero realiza la limpieza y luego recopila estadísticas, y no se puede ejecutar dentro de un bloque de transacción. Déjalo fuera de cualquier envoltorio de transacción que añadas al ejemplo. VACUUM.
Se comprobó la sintaxis de JavaScript y un cálculo determinista independiente confirmó el número de filas de prueba y el total de bytes de los cuerpos. PostgreSQL no estaba disponible en el entorno de investigación, así que las consultas de la base de datos se revisaron, pero no se ejecutaron aquí. No se presenta ningún plan o tiempo de ejecución específico como resultado observado.
Interpreta los planes en un orden útil
Empieza por los nombres de los análisis y luego compara las filas estimadas con las reales. Después, revisa el acceso a la tabla y la actividad de búferes. Esta secuencia permite distinguir un cambio en los métodos de acceso disponibles de una estimación inexacta o de un resultado costoso. VERBOSE también muestra las columnas de salida de cada nodo. Uso de EXPLAIN, Opciones de EXPLAIN.
| Observación | Interpretación | Siguiente comprobación |
|---|---|---|
Los identificadores usan Index Only Scan; los cuerpos usan Index Scan |
La consulta de cuerpos necesita datos de la tabla | Compara los búferes y el tiempo de la consulta normal |
Los cuerpos usan Bitmap Heap Scan |
Revisa si el nodo secundario usa un índice | Compara los bloques del heap y las filas devueltas |
Los cuerpos usan Seq Scan |
El planificador prefiere analizar la tabla | Compara las filas coincidentes estimadas con las reales |
Los identificadores muestran muchos Heap Fetches |
La ejecución solo del índice sigue visitando el heap | Comprueba si esto refleja cambios recientes en la tabla |
| Las estimaciones difieren mucho de las filas reales | Puede que las estadísticas no describan bien los datos | Recopila estadísticas y vuelve a comparar |
ANALYZE recopila las estadísticas que el planificador usa para elegir planes. Si cambias la distribución sintética de categorías, vuelve a recopilar estadísticas antes de interpretar la siguiente comparación. El muestreo puede hacer que las estimaciones varíen ligeramente entre ejecuciones. ANALYZE.
Interpreta width como bytes de salida estimados por fila, no como una medición de los bytes transferidos. Del mismo modo, los costos del planificador usan unidades relativas, no milisegundos. Los aciertos de búfer indican bloques encontrados en la caché de PostgreSQL; no significan que no se haya realizado ningún trabajo. Estimaciones del plan, Informes de búferes.
Para este conjunto de prueba, ambas consultas deberían devolver 5.000 filas. Cada cuerpo sintético contiene 1.160 bytes ASCII, así que la consulta de cuerpos devuelve 5.800.000 bytes de cuerpos, unos 5,53 MiB, sin contar los identificadores ni la sobrecarga del protocolo. Es un cálculo basado en las entradas de muestra, no una medición del rendimiento de la base de datos. El campo bodyBytes del programa permite verificar esa diferencia.
La consulta normal se ejecuta después de medir su plan, así que su tiempo transcurrido refleja ese orden de ejecución con la caché ya caliente. Repite las comparaciones y alterna el orden antes de sacar conclusiones de pequeñas diferencias de tiempo. Una sola ejecución sirve para diagnosticar, pero no permite establecer una proporción de rendimiento fiable.
¿Por qué recuperar los cuerpos puede tardar más de lo que sugiere EXPLAIN?
Los valores de texto grandes pueden comprimirse o almacenarse fuera de la tabla principal mediante el mecanismo TOAST de PostgreSQL. Por eso, recuperar la entrada principal de una fila y producir el cuerpo completo para el cliente pueden implicar cantidades de trabajo distintas. El conjunto de prueba breve y repetitivo de arriba está pensado para demostrar la cobertura de columnas; no representa todos los patrones de almacenamiento de cuerpos. Almacenamiento TOAST.
De forma predeterminada, EXPLAIN ANALYZE descarta los resultados de la consulta sin convertirlos para su entrega. En PostgreSQL 17 y versiones posteriores, añade SERIALIZE TEXT para investigar la conversión de salida, incluida la recuperación de valores almacenados fuera de línea cuando sea necesario. PostgreSQL 17 introdujo esta opción. Comportamiento de serialización de EXPLAIN, Notas de la versión de PostgreSQL 17.
Para probarlo, reemplaza la línea de plantilla EXPLAIN existente del programa por este fragmento y vuelve a ejecutar el programa completo:
text: `EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SERIALIZE TEXT) ${text}`,
En PostgreSQL 14–16, conserva la línea original y usa el temporizador de la consulta normal para medir la recepción del resultado. SERIALIZE sigue sin enviar datos del resultado al cliente, así que no puede medir la transmisión ni el procesamiento de JavaScript. Límites de EXPLAIN.
Por lo general, node-postgres recopila las filas del resultado en objetos JavaScript. Su opción rowMode: 'array' cambia la representación del resultado; no elimina los valores de cuerpo seleccionados del resultado SQL. Por consiguiente, no puede recuperar la cobertura de columnas que se pierde al añadir body a esta consulta. Modo de filas.
Elige un cambio que se ajuste a la tarea con los artículos
Si la tarea en el escritorio solo necesita una lista de identificadores, mantén la proyección acotada. Si necesita el texto completo de los artículos, evalúa el tiempo transcurrido real de la consulta de cuerpos y el tamaño del resultado. El objetivo del diagnóstico es identificar trabajo innecesario y conservar el resultado solicitado.
Si una pantalla solo muestra los primeros 50 artículos de una categoría, limitar el resultado solicitado es una alternativa práctica. En el arreglo queries del programa, reemplaza la cadena de consulta de cuerpos por este SQL:
SELECT id, body
FROM article_plan_demo
WHERE category = $1
ORDER BY id
LIMIT 50
Esto cambia deliberadamente la tarea: pasa de recuperar todos los artículos coincidentes a recuperar una página ordenada. Con la categoría fijada, el índice (category, id) existente ofrece una ruta candidata en orden de identificador. Un índice B-tree adecuado puede hacer eficiente ORDER BY con LIMIT, porque la ejecución puede detenerse después de encontrar suficientes filas. Aun así, hay que inspeccionar el plan elegido. Índices y ORDER BY.
Evita tratar INCLUDE (body) como una solución automática. Las columnas incluidas duplican datos de la tabla, amplían el índice y pueden hacer que una inserción falle si una tupla del índice supera su límite de tamaño. Las columnas de carga útil pequeñas pueden ser buenas candidatas para experimentos medidos. INCLUDE se introdujo en PostgreSQL 11; no está disponible en versiones anteriores. Precauciones sobre índices de cobertura, Notas de la versión de PostgreSQL 11.
Termina la investigación con tres conclusiones concretas: si seleccionar los cuerpos eliminó la posibilidad de un análisis solo del índice, si el nuevo análisis tiene un rendimiento aceptable y cuánto tarda el resultado completo en llegar a Node.js. Esas conclusiones te permitirán decidir si conservas el plan, solicitas menos artículos o pruebas otro índice para la consulta real.
Fuentes
- Análisis solo del índice e índices de cobertura — PostgreSQL Global Development Group.
- Uso de EXPLAIN — PostgreSQL Global Development Group.
- EXPLAIN — PostgreSQL Global Development Group.
- VACUUM — PostgreSQL Global Development Group.
- ANALYZE — PostgreSQL Global Development Group.
- TOAST — PostgreSQL Global Development Group.
- Índices y ORDER BY — PostgreSQL Global Development Group.
- Versión 17 — PostgreSQL Global Development Group.
- Versión 11 — PostgreSQL Global Development Group.
- node-postgres — node-postgres.
- Conexión — node-postgres.
- Consultas — node-postgres.