Mihael Costa/índice
← índice
RENDIMIENTO

SQL Performance Lab

Ejecuta la consulta lenta y la optimizada lado a lado, contra una base de datos real, y lee los dos planes.

  • .NET 10
  • Dapper
  • EF Core
  • PostgreSQL
  • React

Qué es

Cinco pares de consultas. Cada par le hace la misma pregunta a la misma base de datos y devuelve las mismas filas: solo cambia cómo está escrita. Pulsas ejecutar, las dos corren contra 2,65 millones de filas de pedidos, clientes y líneas de pedido, y la página muestra dos tiempos y dos planes de ejecución reales, capturados con EXPLAIN (ANALYZE, BUFFERS).

Nada aquí está grabado: el texto de los escenarios, los recuentos de filas y los parámetros se leen de la base de datos, y los números llegan cuando haces clic. Si el servicio está dormido, la página lo dice en vez de recurrir a una cifra escrita antes.

Los cinco

Paginación profunda. OFFSET 500000 no salta filas, las produce y las tira. La versión con keyset lleva la última fila de la página anterior como cursor. Mismo índice, mismas veinte filas, y el recorrido baja de 500.020 a 20.

Agregación sin índice de cobertura. date_trunc('month', placed_at) = … es cierto para las mismas filas que un predicado de rango, pero el planificador no puede invertir una función sin índice. El arreglo es el rango más INCLUDE; el recuento de páginas cae dos órdenes de magnitud.

Join por la columna sin índice. order_items lleva una copia desnormalizada de order_id sin índice, como quedan las tablas reales tras una importación. Unir por ella fuerza un hash join sobre la tabla entera; por la columna indexada da un nested loop sobre la clave primaria.

Fila a agónica fila. Un bucle en plpgsql que calcula un total acumulado vuelve a sumar el trimestre en cada una de sus noventa iteraciones: rápido sentencia por sentencia, por eso nunca aparece en un log de consultas lentas. Una función de ventana lo hace en una sola pasada.

El N+1 de EF Core. Cuarenta clientes, cuarenta y un viajes a la base; el arreglo es un Include filtrado. Pero pide los cinco pedidos más recientes por cliente y EF Core 8 traduce el Take en un ROW_NUMBER() sobre toda la tabla de pedidos, acabando más lento que el bucle al que sustituyó. “Usa Include” no es la lección; leer el SQL que produjo tu ORM sí lo es.

Sobre el motor

Mi trabajo en producción es SQL Server; esto corre sobre Postgres porque la demo tenía que costar cero y el plan gratuito de Neon no pide tarjeta.

Cada escenario lleva un bloque equivalente de SQL Server: cómo se llama allí el operador, qué cambia en la sintaxis, y dónde el diagnóstico deja de trasladarse. INCLUDE, por ejemplo, vino de SQL Server y Postgres lo tomó prestado en la v11; SQL Server, en cambio, no tiene constructor de valor de fila (un predicado de keyset se vuelve una disyunción), su marco por defecto es RANGE (que vuelca a disco donde ROWS no lo hace), y la causa cotidiana de un recorrido inesperado es CONVERT_IMPLICIT, no un índice que falta.

Conocer los dos planificadores vale más que solo ejecutar el que encaja con mi currículum.

Lo que cuesta dejarlo abierto

Es un endpoint público que ejecuta SQL: las consultas son constantes de compilación en el servidor y la petición solo lleva un id de escenario; el rol de la base es de solo lectura, acotado a un esquema, con statement_timeout de cinco segundos; las ejecuciones tienen rate limit por IP, y el servicio está limitado a cuatro consultas concurrentes porque el cómputo del plan gratuito es un cuarto de vCPU.

El conjunto de datos está dimensionado a esa restricción también: 273MB de tablas e índices, medidos, contra una cuota de 0,5GB.

Demo en vivo

p95 en vivo -uptime -