9.1 KiB
Informe de optimizacion query_02
Generado: 2026-07-12T22:54:28.283Z
1. Descripcion de query_02
query_02 corresponde a la primera pagina general del catalogo INVIMA:
SELECT
source_dataset_key,
source_dataset_id,
source_dataset_name,
categoria_linea,
categoria_canonica,
categoria_slug,
expediente,
registro_sanitario,
producto,
titular,
estado_registro,
fecha_expedicion,
fecha_vencimiento,
modalidad,
grupo,
marca,
principio_activo,
forma_farmaceutica,
presentacion_comercial,
atc,
rol_nombre,
rol_tipo,
ciudad_titular,
pais_titular,
fabricante,
importador,
vigencia
FROM invima_api_catalog
ORDER BY
fecha_vencimiento DESC NULLS LAST,
producto ASC NULLS LAST,
source_dataset_key ASC,
source_uid ASC
LIMIT 25 OFFSET 0;
El endpoint real asociado es GET /api/invima/catalogo?limit=25&offset=0. En Angular se invoca desde InvimaService.searchCatalog() y CatalogPageComponent.loadData(). En Express pasa por backend/src/routes/invima.routes.js y termina en getInvimaCatalog() / runCatalogQuery() dentro de backend/invima/service.js.
La solicitud real del endpoint ejecuta un COUNT(*) exacto y luego la consulta paginada. El benchmark historico de query_02 mide la consulta paginada SQL.
2. Causa raiz
Antes de optimizar, PostgreSQL recorria aproximadamente 1.407.694 filas de una tabla de 1772 MB y aplicaba top-N heapsort para devolver solo 25 registros. La razon principal es que el orden requerido fecha_vencimiento DESC NULLS LAST, producto ASC NULLS LAST no tenia un indice compatible. El indice existente sobre fecha_vencimiento es ascendente y no satisface directamente DESC NULLS LAST.
Un indice compuesto completo con producto no es recomendable en esta base: se observaron valores de producto extremadamente largos, y btree puede superar el limite de tamano de tupla. Por eso la optimizacion usa un indice estrecho por fecha descendente y deja el orden secundario sobre producto para un conjunto reducido.
3. Plan anterior
- Nodos: Limit, Gather Merge, Sort, Seq Scan.
- Seq Scan: 1; Index Scan: 0; Incremental Sort: 0; Gather Merge: 1.
- Filas estimadas/real raiz: 25 / 25.
- Ancho estimado de fila: 596 bytes.
- Bloques hit/read: 63.428 / 844.108.
- Temporales read/write: 0 / 0.
- Metodo de ordenamiento: top-N heapsort 44Memory.
- Planeacion/ejecucion: 7,218 ms / 2.380,218 ms.
4. Cambios implementados
Se agrego una migracion reversible:
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_query02_catalog_vencimiento_desc_nullslast
ON invima_api_catalog (fecha_vencimiento DESC NULLS LAST);
ALTER TABLE invima_api_catalog ALTER COLUMN fecha_vencimiento SET STATISTICS 1000;
ALTER TABLE invima_api_catalog ALTER COLUMN producto SET STATISTICS 1000;
No se modificaron columnas de respuesta, filtros, universo consultado, conteo exacto ni registros regulatorios. Se agrego un desempate estable al ORDER BY con source_dataset_key, source_uid, que son la clave primaria, para evitar duplicados u omisiones entre paginas cuando hay empates en fecha_vencimiento y producto.
5. Indices relevantes
| Indice | Tamano | Valido | Listo | Definicion |
|---|---|---|---|---|
idx_invima_api_catalog_producto_prefix |
71 MB | true | true | CREATE INDEX idx_invima_api_catalog_producto_prefix ON public.invima_api_catalog USING btree ("left"(producto, 512)) |
idx_invima_api_catalog_vencimiento |
15 MB | true | true | CREATE INDEX idx_invima_api_catalog_vencimiento ON public.invima_api_catalog USING btree (fecha_vencimiento) |
idx_query02_catalog_vencimiento_desc_nullslast |
9808 kB | true | true | CREATE INDEX idx_query02_catalog_vencimiento_desc_nullslast ON public.invima_api_catalog USING btree (fecha_vencimiento DESC NULLS LAST) |
invima_api_catalog_pk |
131 MB | true | true | CREATE UNIQUE INDEX invima_api_catalog_pk ON public.invima_api_catalog USING btree (source_dataset_key, source_uid) |
6. Plan posterior
- Nodos: Limit, Incremental Sort, Index Scan.
- Seq Scan: 0; Index Scan: 1; Incremental Sort: 1; Gather Merge: 0.
- Filas estimadas/real raiz: 25 / 25.
- Ancho estimado de fila: 596 bytes.
- Bloques hit/read: 12 / 53.
- Temporales read/write: 0 / 0.
- Metodo de ordenamiento: N/D.
- Planeacion/ejecucion: 7,893 ms / 0,835 ms.
7. Validacion funcional
- Total antes/despues: 1.407.694 / 1.407.694.
- Resultado global: VALIDO.
- Offset 0: valido=true; faltantes=0; adicionales=0; cambio orden=false; cambio claves ORDER BY=false.
- Offset 25: valido=true; faltantes=0; adicionales=0; cambio orden=false; cambio claves ORDER BY=false.
- Offset 250.000: valido=true; faltantes=0; adicionales=0; cambio orden=false; cambio claves ORDER BY=false.
- Offset 1.000.000: valido=true; faltantes=0; adicionales=0; cambio orden=false; cambio claves ORDER BY=false.
La validacion before se ejecuto con el plan antiguo forzado mediante GUCs de sesion, y la validacion after con el nuevo indice habilitado. En ambos casos se uso la misma consulta determinista. La validacion exige que no cambien IDs, orden, claves ORDER BY, estructura publica ni valores publicos.
8. Resultados secuenciales
- Promedio: 1.076,2853 ms -> 0,6162 ms; mejora 1.075,6691 ms (99,9427 %).
- Mediana: 1.085,2339 ms -> 0,5799 ms.
- p95: 1.248,0925 ms -> 0,9834 ms; mejora 1.247,1091 ms (99,9212 %).
- p99: 1.270,9296 ms -> 1,0935 ms; mejora 1.269,8361 ms (99,914 %).
- Fallos: 0; tasa de fallos: 0 %.
- Throughput: 0,9352 ops/s -> 1.515,1515 ops/s.
9. Resultados concurrentes
- 1 usuario(s): promedio 1.078,747 ms -> 51,8153 ms; p95 1.400,5967 ms -> 152,1045 ms; throughput 0,4465 req/s -> 17,5193 req/s.
- 5 usuario(s): promedio 5.157,4488 ms -> 57,3871 ms; p95 11.830,5646 ms -> 147,1289 ms; throughput 0,8296 req/s -> 82,9876 req/s.
- 10 usuario(s): promedio 13.306,701 ms -> 70,3586 ms; p95 22.564,3625 ms -> 163,5365 ms; throughput 0,7737 req/s -> 131,2336 req/s.
- 25 usuario(s): promedio 21.160,2308 ms -> 143,9608 ms; p95 24.849,9474 ms -> 248,9395 ms; throughput 3,0272 req/s -> 158,2278 req/s.
- 50 usuario(s): promedio 26.465,4267 ms -> 303,4708 ms; p95 28.761,2915 ms -> 712,3709 ms; throughput 4,404 req/s -> 183,4862 req/s.
10. Criterios
- Promedio individual: 0,6162 ms frente a objetivo 300 ms => cumple.
- p95 individual: 0,9834 ms frente a objetivo 500 ms => cumple.
- p99 individual: 1,0935 ms frente a objetivo 750 ms => cumple.
- p95 con 5 usuarios: 147,1289 ms frente a objetivo 1.000 ms => cumple.
- p95 con 10 usuarios: 163,5365 ms frente a objetivo 2.000 ms => cumple.
- p95 con 25 usuarios: 248,9395 ms frente a objetivo 5.000 ms => cumple.
11. Paginacion, conteo y concurrencia
La consulta usa LIMIT 25 OFFSET n. Para OFFSET 0, el costo dominante era encontrar el orden inicial sin indice compatible. Las paginas profundas siguen pagando el costo natural de OFFSET; una siguiente iteracion compatible seria exponer paginacion keyset con cursor basado en fecha_vencimiento, producto y la clave primaria source_dataset_key/source_uid, manteniendo la ruta actual como compatibilidad.
El endpoint real conserva COUNT(*) exacto en cada solicitud. No se reemplazo por estimaciones ni cache como unica solucion. Si el conteo se convierte en cuello de botella, se recomienda medir y separar el total exacto en una ruta o tabla de estadisticas invalidada durante sincronizacion.
Perfil del servicio actualizado: getInvimaCatalog(pool, { limit: 25, offset: 0 }) devolvio 25 filas, total exacto 1.407.694, tiempo logico 178,4872 ms, serializacion 0,3199 ms y respuesta de 20.456 bytes.
El pool actual usa PGPOOL_MAX desde entorno y PostgreSQL tiene max_connections estandar. En una maquina de 2 vCPU y 4 GB, aumentar conexiones sin reducir el costo de query empeora la contencion; la mejora principal provino del plan SQL.
12. Riesgos
- El nuevo indice aumenta espacio en disco y costo de mantenimiento durante inserciones/sincronizaciones.
- No cubrir
productoevita un indice enorme, pero todavia requiere ordenar grupos con la misma fecha. - El desempate por
source_dataset_key, source_uidestabiliza la paginacion, pero cambia el orden interno previamente no garantizado de filas empatadas. ANALYZEautomatico se omitio porque esta base local tiene bytes con codificacion invalida que hacen fallar el escaneo completo.
13. Redaccion academica
Antes de la optimizacion, la consulta general paginada presento una latencia promedio de 1.076,2853 ms y un p95 de 1.248,0925 ms. Despues de implementar el indice idx_query02_catalog_vencimiento_desc_nullslast, la latencia promedio se redujo a 0,6162 ms y el p95 a 0,9834 ms, equivalente a una mejora de 1.075,6691 ms (99,9427 %). Bajo 50 usuarios concurrentes, la latencia promedio paso de aproximadamente 26.465,4267 ms a 303,4708 ms.
14. Reversion y repeticion
Revertir:
npm.cmd run benchmark:query02:rollback
Repetir:
npm.cmd run benchmark:query02:validate:before
npm.cmd run benchmark:query02:explain:before
npm.cmd run benchmark:query02:optimize
npm.cmd run benchmark:query02:explain:after
npm.cmd run benchmark:query02:validate
npm.cmd run benchmark:query02:optimized
npm.cmd run benchmark:query02:concurrency
npm.cmd run benchmark:query02:report