Utilización de subconsultas para realizar cálculos sobre “N” cantidad de columnas sin utilizar sentencias CASE
Hay ocasiones en que debemos realizar cálculos que parecen simples en papel, sin embargo al intentar hacer la consulta en SQL nos damos cuenta que son más complejas de lo que pensamos, o que requieren de mucha codificación para llevarla a cabo.
El presente documento resuelve de forma elegante y escalable un problema común de SQL para realizar cálculos de agrupación sobre columnas.
En la parte final del documento, hay un anexo que describe la solución completa del problema con indicaciones especiales acerca de las sentencias UNION, UNION ALL y cálculo de las funciones de agregación cuando existen valores nulos en los campos.
Método SQL para Calcular el valor máximo de un conjunto de columnas de una Tabla
1. Método SQL para Calcular el valor máximo de un
conjunto de columnas de una Tabla.
Utilización de subconsultas para realizar cálculos sobre “N” cantidad de
columnas sin utilizar sentencias CASE
Fecha: Mayo del 2013.
Versión: 1.0
Autor: Sebastián Rodríguez Robotham
e-mail: srodriguez@easybi.cl
Nombre Puntaje 1 Puntaje 2 Puntaje 3 Puntaje 4 Puntaje 5 Puntaje 6 Maxima Mínima
Nicolas 735 158 399 473 378 147 735 147
Cesar 578 252 557 410 441 504 578 252
Hector 578 NULL 168 NULL 189 714 714 168
Mario 578 105 168 105 189 714 714 105
Daniela 683 473 399 326 347 294 683 294
Jorge 105 672 672 735 693 305 735 105
2. Método SQL para Calcular el valor máximo de un conjunto de columnas en una Tabla
Sebastián Rodríguez Robotham. www.EasyBI.cl 2 | P a g e
Introducción.
Hay ocasiones en que debemos realizar cálculos que parecen simples en papel, sin embargo al
intentar hacer la consulta en SQL nos damos cuenta que son más complejas de lo que pensamos, o
que requieren de mucha codificación para llevarla a cabo.
El presente documento resuelve de forma elegante y escalable un problema común de SQL para
realizar cálculos de agrupación sobre columnas.
En la parte final del documento, hay un anexo que describe la solución completa del problema con
indicaciones especiales acerca de las sentencias UNION, UNION ALL y cálculo de las funciones de
agregación cuando existen valores nulos en los campos.
Problema.
Supongamos que contamos con un listado de clientes, y cada cliente tiene diversos puntajes
otorgados por un sistema de segmentación mensual, el requerimiento es calcular el puntaje
promedio y máximo de los últimos 6 meses, tal como lo muestra la siguiente figura:
Figura 1 – Listado de Puntajes de Clientes
Solución Clásica.
Primero generaremos una tabla con la estructura necesaria para almacenar la información base:
CREATE TABLE #TempRowToColumn ( Nombre VARCHAR(100)
,Puntaje_P1 INT
,Puntaje_P2 INT
,Puntaje_P3 INT
,Puntaje_P4 INT
,Puntaje_P5 INT
,Puntaje_P6 INT)
Luego, poblaremos la tabla con la información publicada en la Figura 1.
INSERT INTO #TempRowToColumn VALUES ('Nicolas',735,158,399,473,378,147),
('Cesar',578,252,557,410,441,504),
('Hector',578,NULL,168,NULL,189,714),
('Mario',578,105,168,105,189,714),
('Daniela',683,473,399,326,347,294),
Nombre Puntaje 1 Puntaje 2 Puntaje 3 Puntaje 4 Puntaje 5 Puntaje 6
Nicolas 735 158 399 473 378 147
Cesar 578 252 557 410 441 504
Hector 578 NULL 168 NULL 189 714
Mario 578 105 168 105 189 714
Daniela 683 473 399 326 347 294
Jorge 105 672 672 735 693 305
3. Método SQL para Calcular el valor máximo de un conjunto de columnas en una Tabla
Sebastián Rodríguez Robotham. www.EasyBI.cl 3 | P a g e
('Jorge',105,672,672,735,693,305)
Para calcular el promedio es bastante simple, ya que debemos sumar los 6 puntajes y dividirla en
6, sin embargo presenta un inconveniente, hay datos nulos que entregarán un resultado diferente
al que esperábamos inicialmente (los valores nulos hacen que el resultado final sea nulo en el
cliente “Héctor”), la figura 2 muestra el resultado de esta operación.
SELECT *
, (Puntaje_P1+ Puntaje_P2 + Puntaje_P3 +
Puntaje_P4 + Puntaje_P5 + Puntaje_P6) / 6 as Promedio
FROM #TempRowToColumn
Figura 2 – Calculo del Promedio
Y finalmente, si queremos calcular el valor máximo, tendremos que realizar una sentencia CASE
que evalúe cada alternativa, de la siguiente forma:
CASE WHEN Puntaje_P1 > Puntaje_P2 AND Puntaje_P1 > Puntaje_P3 AND
Puntaje_P1 > Puntaje_P4 AND Puntaje_P1 > Puntaje_P5 AND
Puntaje_P1 > Puntaje_P6 THEN Puntaje_P1
WHEN Puntaje_P2 > Puntaje_P3 AND Puntaje_P2 > Puntaje_P4 AND
Puntaje_P2 > Puntaje_P5 AND Puntaje_P2 > Puntaje_P6 THEN Puntaje_P2
WHEN Puntaje_P3 > Puntaje_P4 AND Puntaje_P3 > Puntaje_P5 AND
Puntaje_P3 > Puntaje_P6 THEN Puntaje_P3
WHEN Puntaje_P4 > Puntaje_P5 AND Puntaje_P4 > Puntaje_P6 THEN Puntaje_P4
WHEN Puntaje_P5 > Puntaje_P6 THEN Puntaje_P4
ELSE Puntaje_P6
END AS maximo
Figura 3 – Calculo del Promedio y Valor Máximo.
Nombre Puntaje 1 Puntaje 2 Puntaje 3 Puntaje 4 Puntaje 5 Puntaje 6 Promedio
Nicolas 735 158 399 473 378 147 381
Cesar 578 252 557 410 441 504 457
Hector 578 NULL 168 NULL 189 714 NULL
Mario 578 105 168 105 189 714 309
Daniela 683 473 399 326 347 294 420
Jorge 105 672 672 735 693 305 530
Nombre Puntaje 1 Puntaje 2 Puntaje 3 Puntaje 4 Puntaje 5 Puntaje 6 Promedio Maximo
Nicolas 735 158 399 473 378 147 381 735
Cesar 578 252 557 410 441 504 457 578
Hector 578 NULL 168 NULL 189 714 NULL 714
Mario 578 105 168 105 189 714 309 714
Daniela 683 473 399 326 347 294 420 683
Jorge 105 672 672 735 693 305 530 735
4. Método SQL para Calcular el valor máximo de un conjunto de columnas en una Tabla
Sebastián Rodríguez Robotham. www.EasyBI.cl 4 | P a g e
El problema de utilizar esta solución es que no es escalable, es decir cuando tenemos 10 o 20
columnas que comparar, la codificación se hace muy pesada y riesgosa, ya que es fácil cometer
errores en las comparaciones CASE.
Solución Óptima.
Para solucionar este tipo de requerimientos, lo que debemos hacer es convertir las columnas de
cada fila en una nueva subquery que podemos trabajar a nivel de la misma fila, y utilizar las
funciones de agrupamiento que provee SQL Server, a continuación se presenta la solución
propuesta.
SELECT T1.*
,(SELECT MAX(Puntaje)
FROM ( SELECT Puntaje_P1 AS Puntaje
UNION ALL SELECT Puntaje_P2
UNION ALL SELECT Puntaje_P3
UNION ALL SELECT Puntaje_P4
UNION ALL SELECT Puntaje_P5
UNION ALL SELECT Puntaje_P6) AliasTabla
) AS PuntajeMaximo
FROM #TempRowToColumn T1
Figura 4 – Resultado Máximo con SubQuery a Nivel de Fila.
Lo que hace esta consulta es bastante sencillo: crea una subconsulta con los valores de cada
columna, similar a la siguiente sentencia:
SELECT 1 AS Puntaje
UNION ALL SELECT 2
UNION ALL SELECT 3
UNION ALL SELECT 4
UNION ALL SELECT 5
Donde el resultado es una lista de números del uno al cinco, luego al utilizar esta sentencia como
subquery y aplicar una función de agregado, por ejemplo máximo, obtendremos el resultado
deseado (valor 5 como resultado de la función MAX):
Nombre Puntaje_P1 Puntaje_P2 Puntaje_P3 Puntaje_P4 Puntaje_P5 Puntaje_P6
Puntaje
Maximo
Nicolas 735 158 399 473 378 147 735
Cesar 578 252 557 410 441 504 578
Hector 578 NULL 168 NULL 189 714 714
Mario 578 105 168 105 189 714 714
Daniela 683 473 399 326 347 294 683
Jorge 105 672 672 735 693 305 735
Nueva
Columna
5. Método SQL para Calcular el valor máximo de un conjunto de columnas en una Tabla
Sebastián Rodríguez Robotham. www.EasyBI.cl 5 | P a g e
SELECT MAX(Puntaje)
FROM (
SELECT 1 AS Puntaje
UNION ALL SELECT 2
UNION ALL SELECT 3
UNION ALL SELECT 4
UNION ALL SELECT 5
) As Alias
El mismo concepto se aplica en la solución óptima, pero ahora en vez de utilizar valores
constantes, utilizamos los campos que provee la tabla #TempRowToColumn.
Rendimiento.
La siguiente figura muestra la comparación en rendimiento de ambas consultas, siendo la solución
tradicional ligeramente más eficiente que la solución propuesta en este documento, sin embargo
se observa que la diferencia es mínima.
Figura 5 – Comparación Plan de Ejecución: Consulta Tradicional vs Consulta Óptima.
Conclusiones Finales
Este método para resolver los requerimientos relacionados a funciones de agregación sobre
columnas es elegante en términos de codificación y fácilmente escalable, ya que permite incluir en
forma simple “N” columnas a la query, y el impacto en términos de posibles errores de digitación
es mínimo en comparación al método tradicional, por otro lado permite comprender fácilmente el
código implementado. Como desventaja podemos indicar que es ligeramente menos eficiente que
el método tradicional.
6. Método SQL para Calcular el valor máximo de un conjunto de columnas en una Tabla
Sebastián Rodríguez Robotham. www.EasyBI.cl 6 | P a g e
Anexo 1. Código de Ejemplo y Consideraciones en la cláusula UNION
A continuación se muestra el código a implementar para calcular los máximos, mínimos y
promedios de los clientes. Este ejemplo, además, pretende mostrar la diferencia que existe al
utilizar UNION, UNION ALL y AVG con valores NULL.
UNION vs UNION ALL
Como se muestra en la figura 6, el cliente “Mario” tiene valores repetidos (el valor 105 está 2
veces), por tanto al aplicar “UNION” considera solo un valor 105 y esto afecta el resultado del
promedio, en cambio cuando se aplica “UNION ALL” consideran todos los valores de las columnas,
aunque estos estén repetidos, a continuación se calcula manualmente esta fila:
Promedio con UNION : 578 + 105 + 168 + 189 + 714 = 1754 / 5 = 350 (Considera solo 5 valores)
Promedio con UNION ALL : 578 + 105 + 168 + 105 + 189 + 714 = 1859 / 6 = 309 (Valor correcto)
Función AVG vs Promedio Calculado Manualmente
Las columnas Promedio_01 calcula el promedio con AVG, mientras que Promedio_02 realiza la
suma de todas las columnas y divide en la cantidad de períodos. Como vemos en la figura 6, la
mayoría de los casos son idénticos, con excepción del cliente “Héctor”, dado que esta fila tiene
valores NULL. A diferencia del error por UNION, en este caso es necesario especificar el
comportamiento esperado con los puntajes, ¿se promedia con los puntajes reales o se promedia
por todos los períodos?, ¿Qué valor asignar en caso que no exista medición para ese cliente?, por
tanto en este caso no existe error de cálculo, sino que falta en la definición del criterio, por ese
motivo se presentan ambos resultados como válido en este ejemplo.
SELECT T1.*
,(SELECT MAX(Nota)
FROM (SELECT Puntaje_P1 AS Nota
UNION ALL SELECT Puntaje_P2
UNION ALL SELECT Puntaje_P3
UNION ALL SELECT Puntaje_P4
UNION ALL SELECT Puntaje_P5
UNION ALL SELECT Puntaje_P6 ) Alias) AS PuntajeMaximo
,(SELECT MIN(Nota)
FROM (SELECT Puntaje_P1 AS Nota
UNION ALL SELECT Puntaje_P2
UNION ALL SELECT Puntaje_P3
UNION ALL SELECT Puntaje_P4
UNION ALL SELECT Puntaje_P5
UNION ALL SELECT Puntaje_P6 ) Alias) AS PuntajeMinimo
,(SELECT AVG(Nota)
FROM (SELECT Puntaje_P1 AS Nota
UNION ALL SELECT Puntaje_P2
UNION ALL SELECT Puntaje_P3
UNION ALL SELECT Puntaje_P4
UNION ALL SELECT Puntaje_P5
UNION ALL SELECT Puntaje_P6 ) Alias) AS Promedio_01_UnionAll
7. Método SQL para Calcular el valor máximo de un conjunto de columnas en una Tabla
Sebastián Rodríguez Robotham. www.EasyBI.cl 7 | P a g e
,(SELECT SUM(Nota)
FROM (SELECT Puntaje_P1 AS Nota
UNION ALL SELECT Puntaje_P2
UNION ALL SELECT Puntaje_P3
UNION ALL SELECT Puntaje_P4
UNION ALL SELECT Puntaje_P5
UNION ALL SELECT Puntaje_P6 ) Alias) / 6 AS Promedio_02_UnionAll
,(SELECT AVG(Nota)
FROM (SELECT Puntaje_P1 AS Nota
UNION SELECT Puntaje_P2
UNION SELECT Puntaje_P3
UNION SELECT Puntaje_P4
UNION SELECT Puntaje_P5
UNION SELECT Puntaje_P6 ) Alias) AS Promedio_01_UnionSimple
,(SELECT SUM(Nota)
FROM (SELECT Puntaje_P1 AS Nota
UNION SELECT Puntaje_P2
UNION SELECT Puntaje_P3
UNION SELECT Puntaje_P4
UNION SELECT Puntaje_P5
UNION SELECT Puntaje_P6 ) Alias) / 6 AS Promedio_02_UnionSimple
FROM #TempRowToColumn T1
Figura 6 – Resultados de Query Final.
Nombre
Puntaje
_P1
Puntaje
_P2
Puntaje
_P3
Puntaje
_P4
Puntaje
_P5
Puntaje
_P6
Puntaje
Maximo
Puntaje
Minimo
Promedio_01
_UnionAll
Promedio_02
_UnionAll
Promedio_01_
UnionSimple
Promedio_02_
UnionSimple
Nicolas 735 158 399 473 378 147 735 147 381 381 381 381
Cesar 578 252 557 410 441 504 578 252 457 457 457 457
Hector 578 NULL 168 NULL 189 714 714 168 412 274 412 274
Mario 578 105 168 105 189 714 714 105 309 309 350 292
Daniela 683 473 399 326 347 294 683 294 420 420 420 420
Jorge 105 672 672 735 693 305 735 105 530 530 502 418
Cálculo Erróneo por Union Simple (considera solo una instancia del valor)
Diferencia de cálculo por valores NULL