Fecha: 2026-06-11 Estado: Aceptado Bloque: P5d (sub-tarea de P5 — planner-as-optimizer) Antecede: P5e (algorithm choice JOIN) Relaciona: Issue #6 (2026-05-27) — hash join inicial

Contexto

exec_select_joined ejecuta los JOINs left-deep: current (acumulado de joins previos) JOIN right_rows (próxima tabla). Para INNER JOIN con equi-predicado, Issue #6 (2026-05-27) introdujo hash join construyendo el HashMap siempre sobre right_rows.

Esto sub-óptimo cuando current es mucho más chico que right_rows. Ejemplo: tras un JOIN previo y un WHERE muy selectivo, current.len() = 10 filas. La próxima tabla big tiene 100k. Build hash sobre 100k → memoria + tiempo gastados en hashear filas que ningún row de current va a probear.

Path óptimo: build hash sobre las 10 de current, probear con las 100k. Mismo resultado, build trivial.

P5d implementa el swap automático cuando hay ventaja clara.

Decisión

Threshold: 2×

let swap_build_side = current.len() > right_rows.len() * 2;

Solo invertimos cuando current es más del doble que right_rows. Por qué 2×:

Sin stats, sin estimación. Las cardinalidades current.len() y right_rows.len() son conocidas exactamente al momento del JOIN (ya fueron materializadas). No hay estimación: es cost-based real.

Mecánica del swap

Cuando swap_build_side:

Cuando NO swap_build_side: comportamiento idéntico a Issue #6.

Sin impacto en LEFT/RIGHT/FULL JOIN semánticamente

El swap NO cambia qué rows aparecen ni con qué NULLs. Las arrays left_matched y right_matched siguen siendo la fuente de verdad para el post-JOIN null-fill. El resultado set es idéntico módulo orden — y SQL sin ORDER BY no garantiza orden de todos modos.

Verificado en p5d_left_join_swap_preserva_null_fill: LEFT JOIN con swap activo, las filas sin match siguen apareciendo con NULL.

NO afecta

Alternativas consideradas

  1. Reorder de toda la chain de JOINs (smallest-base-first).
    • Considerado pero descartado por scope: requiere reescribir build_join_scope, re-resolver predicados, y solo es seguro para INNER JOIN. Riesgo correctness alto vs ganancia incremental. Roadmap para P5d+1.
  2. Threshold dinámico basado en memoria disponible.
    • Descartado: agrega tuning runtime. 2× es robusto y no-paramétrico. Si en producción se ve que 2× no es óptimo, ajustamos la constante.
  3. Swap en cada hash join sin threshold (siempre lado menor).
    • Descartado por ordering: tests sin ORDER BY asumen estabilidad del orden actual. Cambiar el iterador exterior cambia el orden. Threshold 2× minimiza casos donde el swap NO ayuda pero sí cambia orden.
  4. Anotar la decisión en EXPLAIN.
    • Diferido: requiere refactor de explain_select_joined para simular el swap. No hay un path EXPLAIN-natural hoy. Lo dejamos para P5e que ya planea anotar choice de algoritmo.

Tests

3 tests nuevos en tests/integration_test.rs (suite p5d_*):

Suite total: 777 passing (774 → +3 P5d). Verificado vía Docker rust:1.94-bookworm.

Consecuencias

Positivas

Negativas / Limitaciones honestas

Limitaciones / Trabajo futuro