Key Lookup y trabajo repetido

Un índice puede localizar las filas solicitadas sin contener todas las columnas que necesita la consulta. SQL Server utiliza entonces el localizador guardado en ese índice para volver a la fila completa. El plan representa ese segundo acceso como Key Lookup.

Qué es un Key Lookup

Key es la llave clustered que identifica la fila. Cuando una tabla tiene un índice clustered, sus índices nonclustered conservan esa llave como localizador aunque no aparezca escrita entre sus columnas incluidas.

Lookup es la recuperación de columnas que faltan en el índice utilizado para buscar. SQL Server toma el localizador encontrado en el índice nonclustered, entra al índice clustered y obtiene el resto de la fila.

El Key Lookup aparece normalmente debajo de Nested Loops. Nested Loops recibe una fila del primer acceso y solicita el segundo acceso con la llave de esa fila. Si el primer acceso entrega 90 localizadores, el Key Lookup puede ejecutarse 90 veces.

Qué contiene cada estructura

El ejemplo utiliza una tabla temporal con 100,000 pedidos y dos índices:

  • PK_PedidoLookup es el índice clustered por IdPedido. Su nivel hoja contiene las filas completas.
  • IX_PedidoLookup_IdCliente es un índice nonclustered por IdCliente. También conserva IdPedido como localizador, pero no incluye Estado ni Total.

La consulta filtra por IdCliente, pero solicita además Estado y Total.

IF OBJECT_ID(N'tempdb..#PedidoLookup', N'U') IS NOT NULL
    DROP TABLE #PedidoLookup;

CREATE TABLE #PedidoLookup
(
    IdPedido  INT NOT NULL,
    IdCliente INT NOT NULL,
    Estado    VARCHAR(20) NOT NULL,
    Total     DECIMAL(12, 2) NOT NULL,
    CONSTRAINT PK_PedidoLookup
        PRIMARY KEY CLUSTERED (IdPedido)
);

INSERT INTO #PedidoLookup
    (IdPedido, IdCliente, Estado, Total)
SELECT
    p.IdPedido,
    p.IdCliente,
    p.Estado,
    p.Total
FROM dbo.PedidoLaboratorio AS p;

CREATE INDEX IX_PedidoLookup_IdCliente
    ON #PedidoLookup (IdCliente);

DECLARE @ClienteInicial INT = 1;
DECLARE @ClienteFinal INT = 10;

SELECT
    p.IdPedido,
    p.IdCliente,
    p.Estado,
    p.Total
FROM #PedidoLookup AS p
WHERE p.IdCliente BETWEEN @ClienteInicial AND @ClienteFinal
ORDER BY
    p.IdCliente,
    p.IdPedido
OPTION (RECOMPILE);

DROP TABLE #PedidoLookup;

OPTION (RECOMPILE) permite observar el plan compilado para este intervalo. Se utiliza para esta comprobación; no constituye una recomendación general para consultas de aplicación. Consulta OPTION y los query hints para distinguir este recurso de prueba de una decisión que permanecerá en producción.

Ejecuta el bloque completo en una ventana nueva. Antes de hacerlo, activa Include Actual Execution Plan con Ctrl+M. Al terminar, abre Execution plan y localiza la cadena Index Seek → Nested Loops → Key Lookup. La tabla temporal solo existe durante esta conexión y el bloque la elimina al terminar.

Cómo se forma el plan

El plan se lee desde los accesos situados a la derecha hacia el resultado situado a la izquierda.

Index Seek, Key Lookup y Nested Loops señalados en un plan de ejecución

  1. 1
    INDEX SEEK

    Localiza 90 entradas de IdCliente y entrega el IdPedido que identifica cada fila.

    El índice es estrecho: todavía no contiene Estado ni Total.

  2. 2
    KEY LOOKUP

    Usa cada IdPedido para volver al índice clustered y recuperar Estado y Total.

    Una recuperación entrega una fila; en esta ejecución se repite 90 veces.

  3. 3
    NESTED LOOPS

    Coordina ambos accesos: por cada localizador recibido, solicita una recuperación.

    El resultado reúne las columnas encontradas en las dos estructuras.

Index Seek utiliza IX_PedidoLookup_IdCliente para localizar el intervalo de clientes. Entrega 90 filas con IdCliente e IdPedido.

Nested Loops toma cada IdPedido y solicita una recuperación. Key Lookup utiliza esa llave para entrar a PK_PedidoLookup y obtener Estado y Total. El resultado reúne las columnas provenientes de ambos accesos.

La operación no apareció porque SQL Server creara un índice durante la ejecución. Ambos índices ya existían al compilar la consulta. El optimizador combinó el índice estrecho que permite buscar por IdCliente con el índice clustered que contiene la fila completa.

Por qué importa el número de ejecuciones

Un solo Key Lookup recupera una fila. El trabajo acumulado depende de cuántas veces se solicita.

Propiedades de Key Lookup con filas, ejecuciones, objeto y columnas recuperadas

En la ejecución observada:

Propiedad Valor Qué describe
Actual Number of Rows for All Executions 90 Filas que entregó el operador entre todas sus ejecuciones.
Number of Executions 90 Veces que Nested Loops solicitó la recuperación.
Object PK_PedidoLookup Índice clustered al que regresó el operador.
Output List Estado, Total Columnas recuperadas porque no estaban en el primer índice.

La relación es directa en este caso: 90 localizadores producen 90 ejecuciones y cada ejecución entrega una fila. En otro plan, los valores pueden diferir; las propiedades deben comprobarse en lugar de inferirse por el dibujo.

Cuándo puede ser razonable

Un Key Lookup puede ser una forma eficiente de completar unas cuantas filas. Mantener un índice nonclustered estrecho reduce su tamaño y puede facilitar la búsqueda inicial. Si la consulta aparece poco y devuelve pocos localizadores, el segundo acceso puede costar menos que mantener otra estructura más amplia.

Su presencia no demuestra por sí sola un problema. El nombre del operador describe cómo se obtuvieron las columnas faltantes.

Cuándo merece revisión

El acceso merece una revisión más cercana cuando reúne varias condiciones:

  • el primer operador entrega muchas filas;
  • Number of Executions muestra muchas recuperaciones;
  • la consulta se ejecuta con frecuencia;
  • las lecturas, la duración o la CPU aumentan junto con ese patrón;
  • la cantidad real de filas supera ampliamente la estimada.

El porcentaje de costo visible en el plan es una estimación relativa dentro de ese plan. No sustituye las mediciones de una ejecución ni permite comparar por sí solo consultas distintas.

Qué significa cubrir la consulta

Un índice cubre una consulta cuando contiene las llaves necesarias para buscar y todas las columnas necesarias para filtrar, relacionar y devolver el resultado. En ese caso, SQL Server puede completar la consulta sin regresar a la fila clustered.

Las columnas agregadas mediante INCLUDE permanecen en el nivel hoja de un índice nonclustered y pueden completar su cobertura sin formar parte de la llave ordenada. Incluir Estado y Total en este índice podría eliminar el Key Lookup para esta forma concreta de la consulta.

Eso no convierte a INCLUDE en una corrección automática. Cada columna amplía el índice, consume almacenamiento y aumenta el trabajo de escritura y mantenimiento. Antes de cambiarlo conviene revisar índices semejantes, frecuencia de la consulta, columnas solicitadas, cantidad de filas y costo de las modificaciones.

Qué comprobar antes de modificar un índice

  • Confirma en el plan real cuántas veces se ejecutó el Key Lookup.
  • Revisa qué columnas aparecen en Output List.
  • Mide lecturas, CPU y duración con una carga representativa.
  • Comprueba si ya existe un índice que contiene una llave o cobertura equivalente.
  • Considera las operaciones INSERT, UPDATE y DELETE que deberán mantener una estructura más amplia.
  • Repite la medición después del cambio y conserva una ruta de reversión.

También puede ser razonable reducir las columnas solicitadas o revisar por qué la consulta recupera tantas filas. La alternativa correcta depende de la necesidad que produjo el plan.

Qué conclusión todavía no permite obtener

El caso demuestra cómo un índice nonclustered estrecho y un índice clustered pueden combinarse mediante Nested Loops. No demuestra que todo Key Lookup deba eliminarse ni que agregar columnas a un índice siempre mejore el sistema.

Para evaluar su impacto necesitamos relacionar ejecuciones, lecturas y frecuencia con la carga completa. Una recuperación repetida puede dominar una consulta; una recuperación ocasional puede ser la opción de menor costo.

Siguiente comprobación razonable

Continúa con Filter, Compute Scalar y Top para reconocer operadores que transforman, limitan o descartan filas antes de seguir el flujo.

Índice del recorrido

Planes de ejecución

30 artículos de Planes de ejecución
  1. 01Para qué sirve un plan de ejecución
  2. 02Plan estimado, plan real y estadísticas en vivo
  3. 03Cómo se organiza el árbol de un plan de ejecución
  4. 04Flechas, propiedades y número de ejecuciones
  5. 05Mapa de operadores: qué papel cumple cada nodo
  6. 06Filas estimadas y filas reales
  7. 07Index Seek, Index Scan y Table Scan
  8. 08Key Lookup y trabajo repetido
  9. 09Filter, Compute Scalar y Top
  10. 10Nested Loops, Hash Match y Merge Join
  11. 11Sort, Segment y Sequence Project
  12. 12Stream Aggregate y Hash Match (Aggregate)
  13. 13Spool: materialización y reutilización
  14. 14Parallelism: distribuir, repartir y reunir filas
  15. 15Advertencias del plan que requieren investigación
  16. 16Acciones disponibles en el plan gráfico de SSMS
  17. 17Cómo se organiza el XML Showplan
  18. 18Recorrer Analyze Actual Execution Plan
  19. 19Lecturas con SET STATISTICS IO
  20. 20CPU y duración con SET STATISTICS TIME
  21. 21Construir una línea base repetible
  22. 22Guardar y comparar dos planes
  23. 23Historial y regresiones con Query Store
  24. 24Convertir un escenario de Analyze Actual Execution Plan en una hipótesis
  25. 25Leer una sugerencia Missing Index en el plan
  26. 26Entender el CREATE INDEX generado por SSMS
  27. 27Decidir, crear y comprobar un índice
  28. 28De una consulta lenta a una hipótesis comprobable
  29. 29Evaluar un cambio sin optimizar solo una ejecución
  30. 30Análisis guiado de una consulta empresarial reproducible