Cómo las ideas centrales de Jeffrey Ullman impulsan las bases de datos modernas: álgebra relacional, reglas de optimización, joins y planificación al estilo compilador que permiten escalar sistemas.

La mayoría de personas que escriben SQL, crean dashboards o afinan una consulta lenta se han beneficiado del trabajo de Jeffrey Ullman, aunque nunca hayan oído su nombre. Ullman es un científico de la computación y educador cuya investigación y libros de texto ayudaron a definir cómo las bases de datos describen datos, razonan sobre consultas y las ejecutan eficientemente.
Cuando un motor de base de datos convierte tu SQL en algo que puede ejecutar rápido, se apoya en ideas que deben ser precisas y adaptables. Ullman ayudó a formalizar el significado de las consultas (para que el sistema pueda reescribirlas con seguridad) y a conectar el pensamiento de bases de datos con el de compiladores (para que una consulta se analice, optimice y traduzca en pasos ejecutables).
Esa influencia es silenciosa porque no aparece como un botón en tu herramienta de BI ni como una opción visible en tu consola cloud. Aparece como:
JOINEste artículo usa las ideas centrales de Ullman como un tour guiado por los internals de bases de datos que importan en la práctica: cómo el álgebra relacional está debajo de SQL, cómo las reescrituras preservan significado, por qué los optimizadores basados en costes toman las decisiones que toman y cómo los algoritmos de unión suelen decidir si un trabajo termina en segundos o en horas.
También traeremos algunos conceptos parecidos a compiladores: parsing, reescritura y planificación, porque los motores de base de datos se comportan más como compiladores sofisticados de lo que muchos imaginan.
Una promesa rápida: mantendremos la discusión precisa, pero evitaremos pruebas pesadas de matemática. El objetivo es darte modelos mentales que puedas aplicar en el trabajo la próxima vez que aparezca un problema de rendimiento, escalado o comportamiento confuso de una consulta.
Si alguna vez escribiste una consulta SQL y esperaste que “signifique solo una cosa”, te apoyas en ideas que Jeffrey Ullman ayudó a popularizar y formalizar: un modelo limpio para los datos, más formas precisas de describir lo que pide una consulta.
En su núcleo, el modelo relacional trata los datos como tablas (relaciones). Cada tabla tiene filas (tuplas) y columnas (atributos). Eso suena obvio hoy, pero lo importante es la disciplina que crea:
Este encuadre permite razonar sobre corrección y rendimiento sin ambigüedad. Cuando sabes qué representa una tabla y cómo se identifican las filas, puedes predecir qué deberían hacer los joins, qué significan los duplicados y por qué ciertos filtros cambian los resultados.
La enseñanza de Ullman a menudo usa la álgebra relacional como una especie de calculadora de consultas: un pequeño conjunto de operaciones (select, project, join, union, difference) que puedes combinar para expresar lo que quieres.
Por qué importa para trabajar con SQL: las bases de datos traducen SQL a una forma algebraica y luego la reescriben a otra forma equivalente. Dos consultas que parecen diferentes pueden ser algebraicamente iguales—y así los optimizadores pueden reordenar joins, empujar filtros o eliminar trabajo redundante sin cambiar el resultado.
SQL es en gran medida “qué”, pero los motores suelen optimizar usando el “cómo” algebraico.
Los dialectos de SQL varían (Postgres vs. Snowflake vs. MySQL), pero los fundamentos no. Entender claves, relaciones y equivalencia algebraica te ayuda a detectar cuándo una consulta es lógicamente incorrecta, cuándo es simplemente lenta y qué cambios preservan el significado entre plataformas.
El álgebra relacional es la “matemática debajo” de SQL: un pequeño conjunto de operadores que describen el resultado que deseas. El trabajo de Jeffrey Ullman ayudó a que esta visión de operadores fuese clara y enseñable, y sigue siendo el modelo mental que usan la mayoría de optimizadores.
Una consulta de base de datos puede expresarse como una tubería de unos pocos bloques constructores:
WHERE en SQL)SELECT col1, col2)JOIN ... ON ...)UNION)EXCEPT en muchos dialectos)Porque el conjunto es pequeño, es más fácil razonar sobre corrección: si dos expresiones algebraicas son equivalentes, devuelven la misma tabla para cualquier estado válido de la base de datos.
Toma una consulta familiar:
SELECT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.total > 100;
Conceptualmente, esto es:
empezar con un join de customers y orders: customers ⋈ orders
select sólo las órdenes por encima de 100: σ(o.total > 100)(...)
project la columna que quieres: π(c.name)(...)
Eso no es la notación interna exacta usada por cada motor, pero es la idea correcta: SQL se convierte en un árbol de operadores.
Muchos árboles diferentes pueden significar lo mismo. Por ejemplo, los filtros a menudo pueden empujarse hacia etapas anteriores (aplicar σ antes de un gran join), y las proyecciones pueden eliminar columnas no usadas antes (aplicar π antes).
Esas reglas de equivalencia permiten que una base de datos reescriba tu consulta en un plan más barato sin cambiar el significado. Una vez que ves las consultas como álgebra, la “optimización” deja de ser magia y pasa a ser remodelado guiado por reglas seguras.
Cuando escribes SQL, la base de datos no lo ejecuta “tal cual”. Traduce tu sentencia a un plan de consulta: una representación estructurada del trabajo a realizar.
Un buen modelo mental es un árbol de operadores. Las hojas leen tablas o índices; los nodos internos transforman y combinan filas. Operadores comunes incluyen scan, filter (selección), project (elegir columnas), join, group/aggregate y sort.
Las bases de datos suelen separar la planificación en dos capas:
La influencia de Ullman aparece en el énfasis en las transformaciones que preservan el significado: reordena el plan lógico de muchas maneras sin cambiar la respuesta, y luego elige una estrategia física eficiente.
Antes de escoger el enfoque final de ejecución, los optimizadores aplican reglas algebraicas de “limpieza”. Estas reescrituras no cambian resultados; reducen trabajo innecesario.
Ejemplos comunes:
Supón que quieres órdenes de usuarios en un país:
SELECT o.order_id, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.country = 'CA';
Una interpretación ingenua podría unir todos los users con todas las orders y luego filtrar Canadá. Una reescritura que preserve el significado empuja el filtro hacia abajo para que el join toque menos filas:
country = 'CA'order_id y totalEn términos de plan, el optimizador intenta convertir:
Join(Users, Orders) → Filter(country='CA') → Project(order_id,total)
en algo más parecido a:
Filter(country='CA') on Users → Join(with Orders) → Project(order_id,total)
Mismo resultado. Menos trabajo.
Estas reescrituras son fáciles de pasar por alto porque nunca las tecleas—sin embargo son una gran razón por la que el mismo SQL puede ser rápido en una base de datos y lento en otra.
Cuando ejecutas una consulta SQL, la base de datos considera múltiples formas válidas de obtener la misma respuesta y elige la que espera que sea más barata. Ese proceso de decisión se llama optimización basada en costes—y es uno de los lugares más prácticos donde la teoría al estilo Ullman aparece en el rendimiento cotidiano.
Un modelo de costes es un sistema de puntuación que el optimizador usa para comparar planes alternativos. La mayoría de motores estiman el coste usando unos pocos recursos clave:
El modelo no tiene que ser perfecto; debe equivocarse en la dirección equivocada lo menos posible para escoger buenos planes.
Antes de puntuar planes, el optimizador se pregunta en cada paso: ¿cuántas filas producirá esto? Eso es la estimación de cardinalidad.
Si filtras WHERE country = 'CA', el motor estima qué fracción de la tabla coincide. Si unes customers con orders, estima cuántos pares coincidirán en la clave de unión. Estas suposiciones sobre conteos de filas determinan si prefiere un index scan sobre un full scan, un hash join sobre un nested loop, o si un ordenamiento será pequeño o enorme.
Las suposiciones del optimizador se basan en estadísticas: conteos, distribuciones de valores, tasas de nulos y a veces correlaciones entre columnas.
Cuando las estadísticas están obsoletas o faltan, el motor puede equivocarse en el conteo de filas por órdenes de magnitud. Un plan que parecía barato puede volverse caro en la práctica—síntomas clásicos incluyen desaceleraciones tras crecimiento de datos, cambios “aleatorios” de plan, o joins que inesperadamente derraman a disco.
Mejores estimaciones suelen requerir más trabajo: estadísticas más detalladas, muestreo o explorar más planes candidatos. Pero planificar también cuesta tiempo, especialmente para consultas complejas.
Así que los optimizadores balancean dos objetivos:
Entender ese trade-off te ayuda a interpretar EXPLAIN: el optimizador no intenta ser extremadamente ingenioso—intenta ser predeciblemente correcto con información limitada.
El trabajo de Ullman ayudó a popularizar una idea simple pero poderosa: SQL no se “ejecuta” tanto como se traduce a un plan de ejecución. Ningún lugar lo muestra mejor que los joins. Dos consultas que devuelven las mismas filas pueden diferir enormemente en tiempo de ejecución según el algoritmo de unión elegido y el orden de uniones.
Nested loop join es conceptualmente sencillo: por cada fila del lado izquierdo, busca filas coincidentes en el derecho. Puede ser rápido cuando el lado izquierdo es pequeño y el derecho tiene un índice útil.
Hash join construye una tabla hash a partir de una entrada (a menudo la más pequeña) y la consulta con la otra. Brilla para entradas grandes no ordenadas con condiciones de igualdad (por ejemplo, A.id = B.id), pero necesita memoria; el derrame a disco puede eliminar la ventaja.
Merge join recorre dos entradas en orden. Es muy útil cuando ambos lados ya están ordenados (o son baratos de ordenar), por ejemplo cuando índices entregan filas en el orden de la clave de unión.
Con tres o más tablas, el número de órdenes posibles de unión se dispara. Unir dos tablas grandes primero puede crear un resultado intermedio enorme que ralentice todo lo demás. Un mejor orden suele empezar con el filtro más selectivo (menos filas) y unirse hacia afuera, manteniendo los intermedios pequeños.
Los índices no solo aceleran búsquedas: hacen viable ciertas estrategias de unión. Un índice en la clave de unión puede convertir un nested loop costoso en un patrón de “seek por fila” rápido. En cambio, índices faltantes o no utilizables pueden empujar al motor hacia hash joins o grandes ordenamientos para un merge join.
Las bases de datos no solo “ejecutan SQL”. Lo compilan. La influencia de Ullman abarca teoría de bases de datos y pensamiento de compiladores, y esa conexión explica por qué los motores de consultas se comportan como toolchains de lenguajes: traducen, reescriben y optimizan antes de hacer cualquier trabajo.
Cuando envías una consulta, el primer paso se parece al front-end de un compilador. El motor tokeniza keywords e identificadores, verifica la gramática y construye un parse tree (a menudo simplificado en un árbol de sintaxis abstracto). Aquí se capturan errores básicos: comas faltantes, nombres de columna ambiguos, reglas de agrupamiento inválidas.
Un modelo mental útil: SQL es un lenguaje de programación cuyo “programa” describe relaciones de datos en lugar de bucles.
Los compiladores convierten sintaxis en una representación intermedia (IR). Las bases de datos hacen algo similar: traducen la sintaxis SQL en operadores lógicos como:
GROUP BY)Esa forma lógica está más cerca del álgebra relacional que del texto SQL, lo que facilita razonar sobre significado y equivalencia.
Las optimizaciones de compilador mantienen idénticos los resultados del programa mientras hacen la ejecución más barata. Los optimizadores de bases de datos hacen lo mismo, usando sistemas de reglas como:
Esta es la versión de bases de datos de la “eliminación de código muerto”: no son técnicas idénticas, pero comparten la misma filosofía—preservar la semántica, reducir coste.
Si tu consulta es lenta, no te quedes solo mirando el SQL. Observa el plan de consulta como inspeccionar la salida de un compilador. Un plan te dice lo que el motor realmente eligió: orden de joins, uso de índices y dónde se gasta el tiempo.
Conclusión práctica: aprende a leer EXPLAIN como un listado de ensamblador de rendimiento. Convierte la afinación en depuración basada en evidencia. Para más sobre convertir esto en hábito, ve a /blog/practical-query-optimization-habits.
Un buen rendimiento de consultas a menudo comienza antes de escribir SQL. La teoría de diseño de esquemas de Ullman (especialmente la normalización) trata de estructurar datos para que la base de datos pueda mantenerlos correctos, predecibles y eficientes a medida que crecen.
La normalización busca:
Esas ganancias de corrección se traducen en beneficios de rendimiento: menos campos duplicados, índices más pequeños y menos actualizaciones costosas.
No necesitas memorizar pruebas para usar las ideas:
La desnormalización puede ser una elección inteligente cuando:
La clave es desnormalizar deliberadamente, con un proceso para mantener duplicados sincronizados.
El diseño del esquema condiciona lo que el optimizador puede hacer. Claves y foreign keys claras habilitan mejores estrategias de join, reescrituras seguras y estimaciones de filas más precisas. Mientras tanto, duplicación excesiva puede inflar índices y ralentizar escrituras, y columnas con múltiples valores bloquean predicados eficientes. A medida que el volumen de datos crece, estas decisiones de modelado tempranas suelen importar más que micro-optimizaciones de una sola consulta.
Cuando un sistema “escala”, raramente es solo añadir máquinas más grandes. Con más frecuencia, lo difícil es que el mismo significado de consulta se preserve mientras el motor elige una estrategia física muy distinta para mantener los tiempos previsibles. El énfasis de Ullman en equivalencias formales es exactamente lo que permite esos cambios de estrategia sin alterar resultados.
A tamaños pequeños, muchos planes “funcionan”. A escala, la diferencia entre escanear una tabla, usar un índice o usar un resultado precomputado puede ser la diferencia entre segundos y horas. La teoría importa porque el optimizador necesita un conjunto seguro de reglas de reescritura (por ejemplo, empujar filtros antes, reordenar joins) que no alteren la respuesta—aunque cambien radicalmente el trabajo realizado.
El particionamiento (por fecha, cliente, región, etc.) convierte una tabla lógica en muchas piezas físicas. Eso afecta la planificación:
El texto SQL puede no cambiar, pero el mejor plan ahora depende de dónde viven las filas.
Las vistas materializadas son, esencialmente, “subexpresiones guardadas”. Si el motor puede probar que tu consulta coincide (o puede reescribirse para coincidir) con un resultado almacenado, puede reemplazar trabajo costoso—como joins y agregaciones repetidas—por una búsqueda rápida. Esto es pensar en álgebra relacional en la práctica: reconocer equivalencia y luego reutilizar.
El caching puede acelerar lecturas repetidas, pero no salvará una consulta que debe escanear demasiados datos, redistribuir enormes resultados intermedios o calcular un join gigantesco. Cuando aparecen problemas de escala, la solución suele ser: reducir la cantidad de datos tocados (layout/particionamiento), reducir cómputo repetido (vistas materializadas) o cambiar el plan—no solo “añadir cache”.
La influencia de Ullman aparece en una mentalidad simple: trata una consulta lenta como una declaración de intención que la base de datos puede reescribir, y luego verifica qué decidió hacer realmente. No necesitas ser teórico para beneficiarte—solo necesitas una rutina repetible.
Empieza por las partes que suelen dominar el tiempo de ejecución:
Si solo haces una cosa, identifica el primer operador donde explota el recuento de filas. Ese suele ser la causa raíz.
Son fáciles de escribir y sorprendentemente costosos:
WHERE LOWER(email) = ... puede impedir el uso del índice (usa una columna normalizada o un índice funcional si está soportado).El álgebra relacional sugiere dos movimientos prácticos:
WHERE antes de joins cuando sea posible para encoger entradas.Una buena hipótesis suena así: “Este join es caro porque estamos uniendo demasiadas filas; si filtramos orders a los últimos 30 días antes, la entrada al join cae.”
Usa una regla de decisión simple:
EXPLAIN muestre trabajo evitable (joins innecesarios, filtrado tardío, predicados no sargables).La meta no es “SQL ingenioso”. Es resultados intermedios más pequeños y predecibles—justo el tipo de mejora que las ideas de Ullman hacen más fácil de ver.
Estos conceptos no son solo para DBAs. Si estás lanzando una aplicación, estás tomando decisiones de base de datos y planificación de consultas aunque no lo notes: la forma del esquema, la elección de claves, los patrones de consulta y la capa de acceso a datos influyen en lo que el optimizador puede hacer.
Si usas un flujo de trabajo de desarrollo acelerado (por ejemplo, generar una app React + Go + PostgreSQL desde una interfaz conversacional en Koder.ai), los modelos mentales al estilo Ullman son una red de seguridad práctica: puedes revisar el esquema generado buscando claves y relaciones claras, inspeccionar las consultas que la app usa y validar rendimiento con EXPLAIN antes de que aparezcan problemas en producción. Cuanto más rápido puedas iterar “intención de consulta → plan → corrección”, más valor obtendrás del desarrollo acelerado.
No necesitas “estudiar teoría” como un hobby separado. La forma más rápida de beneficiarte de los fundamentos de Ullman es aprender lo suficiente para leer planes de consulta con confianza y practicar en tu propia base de datos.
Busca estos libros y temas de clase (sin afiliación—solo puntos de partida citados ampliamente):
Empieza pequeño y ata cada paso a algo que puedas observar:
Elige 2–3 consultas reales y itera:
IN por EXISTS, empuja predicados, elimina columnas innecesarias y compara resultados.Usa un lenguaje claro, basado en planes:
Ese es el beneficio práctico de los fundamentos de Ullman: consigues un vocabulario compartido para explicar rendimiento—sin adivinar.
Jeffrey Ullman ayudó a formalizar cómo las bases de datos representan el significado de una consulta y cómo pueden transformar consultas de forma segura para obtener equivalentes más rápidas. Esa base aparece cada vez que un motor reescribe una consulta, reordena joins o escoge un plan de ejecución distinto garantizando el mismo conjunto de resultados.
El álgebra relacional es un conjunto pequeño de operadores (select, project, join, union, difference) que describen con precisión los resultados de una consulta. Los motores suelen traducir SQL a un árbol de operadores semejante al álgebra para poder aplicar reglas de equivalencia (como empujar filtros hacia abajo) antes de elegir una estrategia de ejecución.
Porque la optimización depende de demostrar que una consulta reescrita devuelve los mismos resultados. Las reglas de equivalencia permiten al optimizador:
WHERE) antes de un JOINEsas transformaciones pueden reducir el trabajo drásticamente sin cambiar el significado.
Un plan lógico describe qué operaciones son necesarias (filtrar, unir, agregar) independientemente de detalles de almacenamiento. Un plan físico elige cómo ejecutarlas (index scan vs full scan, hash join vs nested loop, paralelismo, estrategias de ordenación). La mayoría de diferencias de rendimiento provienen de las decisiones físicas, que están habilitadas por reescrituras lógicas.
La optimización basada en costes evalúa múltiples planes válidos y elige el que estima más barato. Los costes suelen venir de factores prácticos como filas procesadas, I/O, CPU y memoria (incluyendo si un hash o un ordenamiento derrama a disco).
La estimación de cardinalidad es la suposición del optimizador sobre “cuántas filas saldrán de este paso”. Esas estimaciones determinan orden de joins, tipo de join y si vale la pena usar un index scan. Cuando las estimaciones fallan (a menudo por estadísticas obsoletas/missing), pueden aparecer desaceleraciones súbitas, grandes derrames a disco o cambios de plan inesperados.
Concéntrate en unas pocas pistas de alta señal:
Trata el plan como salida compilada: muestra lo que el motor decidió hacer.
La normalización reduce datos duplicados y anomalías de actualización, lo que suele traducirse en tablas e índices más pequeños y joins más fiables. La desnormalización puede tener sentido para workloads analíticos o patrones de lectura repetida, pero debe ser deliberada (reglas claras de actualización, redundancia controlada) para no degradar la corrección con el tiempo.
Escalar a menudo exige cambiar la estrategia física mientras se mantiene idéntico el significado lógico. Herramientas comunes:
El caching ayuda lecturas repetidas, pero no arregla una consulta que necesita tocar demasiados datos o produce grandes joins intermedios.