Too Cool for Internet Explorer

Multiple ‘GROUP BY’ en SQL

Hora y Fecha: Noviembre 30, 2006 @ 12:45 am Autor: Moisés Maciá
Categorías:
962 views

Os planteo una dudilla típica en el mundo de las bases de datos: tenemos una tabla con todos los empleados de una empresa, cada empleado tiene un único perfil asociado y además cada uno de ellos puede tener (o no) una serie de atributos.

Lo que quiero es obtener una tabla de frecuencias en la que diga cuantos empleados pertenecientes a un perfil y con una serie de atributos, empiezan con la letra A, con la B, con la C y así hasta la Z.

Es decir agrupar los empleados por perfil/atributo y después por letra alfabetica; algo parecido a una nube de tags o un histográma.

La instrucción GROUP BY de SQL sólo nos permite agrupar por un campo, entonces ¿Se puede sacar con una sola consulta sin utilizar programación ni tablas temporales? Si.

Veamos el esquema de datos sobre MySQL:

Tabla employees, almacena los datos relativos a los empleados:

  1.  
  2. CREATE TABLE employees (
  3.   id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  4.   name varchar(50),
  5.   profile_id int(10) UNSIGNED NOT NULL,
  6.   PRIMARY KEY (id)
  7. ) ENGINE=MyISAM;

Tabla profile, almacena el perfil de los empleados (uno por empleado):

  1.  
  2. CREATE TABLE profiles (
  3.   id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  4.   name varchar(50),
  5.   PRIMARY KEY (id)
  6. ) ENGINE=MyISAM;

Tabla attributes, almacena todos los atributos:

  1.  
  2. CREATE TABLE attributes (
  3.   id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  4.   name varchar(50),
  5.   PRIMARY KEY (id)
  6. ) ENGINE=MyISAM;

Tabla employee_attributes, almacena las relaciones entre empleados y atributos:

  1.  
  2. CREATE TABLE employee_attributes (
  3.   id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  4.   employee_id int(10) UNSIGNED NOT NULL,
  5.   attribute_id int(10) UNSIGNED NOT NULL,
  6.   PRIMARY KEY (id)
  7. ) ENGINE=MyISAM;

Insertamos algunos datos de prueba:

  1.  
  2. INSERT INTO profiles (name) VALUES(‘perfil de programador’);
  3. INSERT INTO profiles (name) VALUES(‘perfil de manager’);
  4. INSERT INTO profiles (name) VALUES(‘perfil de tester’);
  5.  
  6. INSERT INTO attributes (name) VALUES(‘jornada completa’);
  7. INSERT INTO attributes (name) VALUES(‘empleado del mes’);
  8. INSERT INTO attributes (name) VALUES(‘encargado de sección’);
  9. INSERT INTO attributes (name) VALUES(‘le toca guardia’);
  10.  
  11. INSERT INTO employees (name,profile_id) VALUES(‘pepito’, 1);
  12. INSERT INTO employees (name,profile_id) VALUES(‘juanito’, 1);
  13. INSERT INTO employees (name,profile_id) VALUES(‘manolito’, 1);
  14. INSERT INTO employees (name,profile_id) VALUES(‘menganito’, 1);
  15.  
  16. INSERT INTO employee_attributes (employee_id,attribute_id) VALUES(1,1);
  17. INSERT INTO employee_attributes (employee_id,attribute_id) VALUES(1,3);
  18. INSERT INTO employee_attributes (employee_id,attribute_id) VALUES(2,1);
  19. INSERT INTO employee_attributes (employee_id,attribute_id) VALUES(2,2);
  20. INSERT INTO employee_attributes (employee_id,attribute_id) VALUES(2,3);
  21. INSERT INTO employee_attributes (employee_id,attribute_id) VALUES(3,1);
  22. INSERT INTO employee_attributes (employee_id,attribute_id) VALUES(3,4);
  23. INSERT INTO employee_attributes (employee_id,attribute_id) VALUES(3,3);
  24. INSERT INTO employee_attributes (employee_id,attribute_id) VALUES(3,2);

La solución Quick&Dirt empleada masivamente en estos casos es hacer una consulta para filtrar los empleados que nos interesan mediante la siguiente consulta:

  1.  
  2. SELECT Employee.id, Employee.name
  3. FROM employees AS Employee, employee_attributes AS EmployeeAttr
  4. WHERE Employee.id = EmployeeAttr.employee_id
  5.     AND EmployeeAttr.attribute_id IN (1,2,3)
  6.     AND Employee.profile_id = 1
  7. GROUP BY Employee.id

Resultado:

  id    name
----------------
  1    pepito
  2    juanito
  3    manolito
  4    menganito

… para después mediante programación, agruparlos y realizar el conteo.

¿Es óptimo? NO, hay que hacer un tratamiento a posteriori de los datos.

¿Es sencillo? NO, quizá sea más complicado y engorroso la programación del tratamiento posterior que hacerlo corréctamente.

¿Es elegante? NO, se mezcla la capa de negocio con la capa de datos.

¿Entonces cómo lo hago?

La mejor forma de hacer este tipo de consulta y mantener tu karma como programador es utilizar una Subquery. Una subquery es una manera especial de anidar sentencias SQL para componer consultas mas complejas que de otra forma no se podrían realizar.

Las subqueries se agrupan fundamentalmente en dos grandes grupos: escalares y correlacionadas, aunque la que voy a utilizar yo es un poco diferente puesto que el resultado de la subquery suplantará a una tabla; jugando con este efecto conseguimos el comportamiento de un GROUP BY multiple.

  • Más información en el apartado Subquery Syntax de la documentación de MySQL.
  • Más infromación sobre Subqueries correlacionadas y escalares (con muchos ejemplos) en MySQL Hispano.

Veamos la consulta:

  1.  
  2. SELECT LEFT(Employee.name,1) AS alphacrum, COUNT(LEFT(Employee.name,1)) AS total
  3. FROM (
  4.   SELECT Employee.id, Employee.name
  5.     FROM employees AS Employee, employee_attributes AS EmployeeAttr
  6.     WHERE Employee.id = EmployeeAttr.employee_id
  7.       AND EmployeeAttr.attribute_id IN (1,2,3)
  8.       AND Employee.profile_id = 1
  9.     GROUP BY Employee.id
  10.   ) AS Employee
  11. GROUP BY alphacrum
  12. ORDER BY alphacrum ASC;

Resultado:

  alphacrum    total
----------------------------
  j                    1
  m                  2
  p                   1

Como podeis observar, la tabla employees de la consulta principal no es la original; es el resultado del filtrado previo según los parametros que nos interesen.

Una vez filtrados es muy sencillo reordenar los datos a nuestro gusto, únicamente agrupamos por letra y realizamos el conteo de los resultados con COUNT, devolviendonos los datos tal y como nos hacen falta.

De esta manera se utiliza efectivamente el cache del servidor de bases de datos y la programación de la aplicación no se satura con un montón funciones de tratamiento de datos. Simple y elegante.





« Anterior post: Convertir audio APE/CUE en WAV/OGG/MP3 bajo Linux | Próximo post: Historia de una función »

2 Comentarios para “Multiple ‘GROUP BY’ en SQL”

Cheli
30 de Noviembre de 2006 a las 2:32 am    

Muy bueno, me gusta mucho tu bitácora pero cuando haces un apunte técnico y de calidad como este simplemente me encanta.

Un saludo, Cheli.

Moisés Maciá
30 de Noviembre de 2006 a las 11:08 am    

Bufff .. pues tengo millones de apuntes como este rondandome por la cabeza. Me alegro que te gusten!


Bad Behavior has blocked 691 access attempts in the last 7 days.