# 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: ```sql 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: ```sql 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 `producto` evita un indice enorme, pero todavia requiere ordenar grupos con la misma fecha. - El desempate por `source_dataset_key, source_uid` estabiliza la paginacion, pero cambia el orden interno previamente no garantizado de filas empatadas. - `ANALYZE` automatico 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: ```powershell npm.cmd run benchmark:query02:rollback ``` Repetir: ```powershell 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 ```