OR en el WHERE: por qué penaliza y cómo usar UNION ALL para ganar rendimiento

OR en el WHERE: por qué penaliza y cómo usar UNION ALL para ganar rendimiento

0
(0)

Optimizar una consulta SQL no es una cuestión de estética sintáctica, sino de entender cómo el motor de base de datos decide acceder a las páginas de datos en el disco. En mi experiencia como consultor, uno de los cuellos de botella más recurrentes en proyectos de retail y sector público no es la complejidad del cálculo, sino el uso indiscriminado del operador OR en la cláusula WHERE. Lo que parece una instrucción lógica sencilla suele convertirse en una trampa para el optimizador de consultas de SQL Server.

El problema fundamental del OR aparece cuando filtramos por columnas distintas que no forman parte del mismo índice compuesto o que requieren búsquedas independientes. En lugar de realizar dos búsquedas rápidas (Index Seek), el optimizador a menudo se rinde y opta por un escaneo completo de la tabla (Table Scan o Clustered Index Scan). Para un analista que trabaja con Power BI en modo DirectQuery, esto se traduce en visuales que tardan segundos en cargar y una experiencia de usuario frustrante. Antes de intentar arreglarlo en el modelo, debemos solucionar la raíz en la fuente.

En este artículo, desglosaremos por qué ocurre esta degradación y cómo aplicar el patrón de UNION ALL para forzar al motor a utilizar los índices disponibles de forma eficiente. No se trata de escribir menos código, sino de escribir código que el optimizador pueda entender y ejecutar con el menor coste de I/O posible.

El problema del optimizador con los predicados OR

Para entender por qué el OR es problemático, debemos recordar cómo funciona un índice. Un índice es, simplificando mucho, una estructura ordenada. Si buscas por Apellido = 'García' OR Ciudad = 'Madrid', y tienes un índice por Apellido y otro por Ciudad, el motor se enfrenta a un dilema. Puede usar el índice de Apellidos para encontrar a los García, pero eso no le dice nada sobre quién vive en Madrid. De igual forma, el índice de Ciudad no le ayuda a encontrar a todos los García que viven en otras provincias.

SQL Server tiene un mecanismo llamado Index Intersection (o Index Union), donde intenta buscar en ambos índices y luego combinar los RowIDs resultantes. Sin embargo, este proceso tiene un coste de CPU y memoria elevado. En tablas con millones de filas, el optimizador suele decidir que es más «barato» (desde su perspectiva de estimación de costes) leer toda la tabla de principio a fin una sola vez que andar saltando entre índices y mezclando resultados en memoria. Es aquí donde el rendimiento cae en picado.

Este escenario es crítico cuando trabajamos con Conformed dimensions y bus matrix, donde las consultas suelen cruzar múltiples atributos para filtrar hechos. Si el motor no puede realizar un Index Seek, el escalado de la solución de BI está comprometido desde el primer día.

SARGability y el operador OR

En SQL, hablamos de consultas SARGable (Search ARGumentable) cuando el motor puede utilizar un índice para filtrar los datos. Un WHERE con OR sobre diferentes columnas suele romper la SARGability. El motor no puede saltar directamente a los datos; debe evaluar cada fila para ver si cumple la condición A o la condición B.

Consideremos una tabla de Ventas con 50 millones de registros. Queremos obtener las ventas de un cliente específico o las ventas gestionadas por un promotor concreto:

-- Consulta problemática: El optimizador probablemente hará un Scan
SELECT VentasID, Fecha, Importe
FROM Fact.Ventas
WHERE ClienteID = 450 
 OR PromotorID = 12;

Si tenemos un índice en ClienteID y otro en PromotorID, lo ideal sería que SQL Server buscara en ambos. Pero en la práctica, si la selectividad no es extremadamente alta, verás un Clustered Index Scan en el plan de ejecución. Para profundizar en cómo identificar estos operadores, te recomiendo leer sobre leer planes de ejecución en SQL Server: los 5 operadores que debes dominar.

La solución: Dividir para vencer con UNION ALL

La técnica más efectiva para resolver este problema es descomponer la consulta en dos ramas independientes unidas por un UNION ALL. Al hacerlo, transformamos una única consulta con un predicado complejo en dos consultas con predicados simples que SQL Server puede optimizar por separado.

-- Consulta optimizada: Forzamos el uso de índices independientes
SELECT VentasID, Fecha, Importe
FROM Fact.Ventas
WHERE ClienteID = 450

UNION ALL

SELECT VentasID, Fecha, Importe
FROM Fact.Ventas
WHERE PromotorID = 12 
 AND ClienteID <> 450; -- Evitamos duplicados si una fila cumple ambas

En este ejemplo, la primera parte de la consulta realizará un Index Seek sobre el índice de ClienteID. La segunda parte realizará otro Index Seek sobre el índice de PromotorID. El coste de unir ambos resultados es despreciable comparado con el ahorro de evitar un escaneo de 50 millones de filas. Es una estrategia vital cuando implementas las 50 Medidas y Patrones DAX que Destruyen el Rendimiento en DirectQuery, ya que Power BI a menudo genera SQL subóptimo que podemos mejorar mediante vistas indexadas o transformación previa en el DWH.

¿Por qué UNION ALL y no UNION?

Es una distinción técnica fundamental. UNION realiza una operación de Distinct implícita para eliminar duplicados entre los dos conjuntos de resultados. Esto implica una operación de Sort (ordenación) en TempDB que consume mucha memoria y tiempo de CPU. UNION ALL, por el contrario, simplemente concatena los resultados. Si sabemos que los conjuntos son disjuntos o si manejamos la lógica de duplicados en el WHERE (como en el ejemplo anterior con el AND ClienteID <> 450), UNION ALL siempre será más rápido.

Esta lógica es idéntica a la que aplicamos en procesos de integración de datos. De hecho, en Power Query también debemos tener cuidado al combinar tablas, como explico en el artículo sobre unir y anexar consultas en Power Query: la diferencia que casi nadie usa bien.

Comparativa de rendimiento y escenarios

TécnicaTipo de AccesoCoste de CPURiesgo de DuplicadosRecomendación
OR en WHEREScan (habitualmente)Bajo/MedioNuloSolo en tablas pequeñas
UNION ALLSeek + SeekBajoSí (requiere lógica)Óptimo para grandes volúmenes
UNION (Simple)Seek + Seek + SortAltoNo (los elimina)Evitar si el volumen es alto
Índice CompuestoSeek parcialMínimoNuloSolo si las columnas son fijas

Estrategias de indexación complementarias

No siempre UNION ALL es la respuesta. A veces, el problema radica en que no tenemos los índices adecuados para soportar el OR. Si las consultas suelen filtrar por dos columnas específicas con mucha frecuencia, un índice compuesto puede ayudar, pero solo si el orden de las columnas coincide con la búsqueda. Sin embargo, para el OR, los índices compuestos no suelen ser la solución definitiva porque el motor solo puede buscar por la primera columna del índice de forma eficiente.

Otra opción avanzada son los Índices Filtrados. Si el OR se utiliza para buscar estados específicos (por ejemplo, Estado = 'Pendiente' OR Estado = 'Error'), un índice filtrado que solo incluya esas filas reducirá drásticamente el tamaño de la estructura y acelerará la consulta. No obstante, en modelos de BI dinámicos donde el usuario puede elegir cualquier valor, los índices filtrados pierden utilidad frente a una buena estrategia de particionamiento o el uso de UNION ALL en vistas.

Errores frecuentes al intentar optimizar OR

  • No controlar los duplicados: Al usar UNION ALL, si una fila cumple ambas condiciones del OR original, aparecerá dos veces en el informe. Esto falsea los totales de ventas y unidades. Siempre hay que añadir una negación en la segunda consulta.
  • Abusar de funciones en el WHERE: Usar OR junto con funciones como ISNULL() o LEFT(). Esto garantiza un Scan y anula cualquier beneficio de los índices.
  • Olvidar las columnas incluidas (INCLUDE): De nada sirve hacer un Index Seek si luego el motor tiene que ir a la tabla principal (Key Lookup) a buscar el resto de columnas. Asegúrate de que tus índices cubran la consulta.
  • Confiar ciegamente en el motor: El optimizador es inteligente, pero trabaja con estadísticas. Si las estadísticas están desactualizadas, elegirá un plan de ejecución basado en premisas falsas.

Checklist de optimización para consultas con OR

  1. Comprueba el plan de ejecución: ¿Hay un Clustered Index Scan? Si es así, tienes un problema de rendimiento.
  2. Verifica la selectividad de las columnas: Si el filtro devuelve el 90% de la tabla, el Scan es inevitable y correcto. Si devuelve el 1%, el Seek es obligatorio.
  3. Evalúa transformar el OR en un UNION ALL, especialmente si las columnas pertenecen a índices diferentes.
  4. Si usas UNION ALL, asegúrate de que la lógica de exclusión en la segunda rama sea correcta para no duplicar datos.
  5. Actualiza estadísticas (UPDATE STATISTICS) antes de sacar conclusiones sobre si el cambio ha funcionado.

Preguntas frecuentes

¿Cuándo es preferible mantener el OR en lugar de cambiar a UNION ALL?

Cuando trabajas con tablas pequeñas (dimensiones con pocos miles de filas) o cuando el optimizador ya es capaz de realizar un «Index Union» de forma eficiente. El coste de mantener y leer dos ramas de consulta puede ser mayor que un simple escaneo si el volumen de datos es reducido.

¿El uso de UNION ALL afecta a los parámetros de Power BI?

No directamente, pero si estás usando parámetros de consulta para generar SQL dinámico, asegúrate de que la lógica de duplicados se mantenga consistente independientemente de los valores seleccionados por el usuario para evitar errores de cálculo en el modelo.

¿Es mejor usar IN que OR para mejorar el rendimiento?

El operador IN es sintácticamente equivalente a múltiples OR sobre la misma columna. SQL Server lo gestiona muy bien mediante una operación de Constant Scan e Inner Join interna. El problema de rendimiento real del OR es cuando involucra columnas diferentes.


¿De cuánta utilidad te ha parecido este contenido?

¡Haz clic en una estrella para puntuarlo!

Puntuación media 0 / 5. Recuento de votos: 0

Hasta ahora, ¡no hay votos!. Sé el primero en puntuar este contenido.

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *