Bases de datos SQL

Agrupar, fechas y texto

Lección 7 de 8 · 16 min

Pasar del detalle al resumen

Hasta ahora las consultas devolvían filas: un cliente, un pedido. Pero la pregunta que hace un jefe casi nunca es esa, sino «¿cuánto vendimos por ciudad?» o «¿cuántos pedidos hizo cada cliente?».

Eso es GROUP BY: juntar filas en grupos y calcular algo de cada grupo.

GROUP BY con las funciones de resumen
-- Cuantos clientes y cuanto saldo por ciudad
SELECT ciudad,
       COUNT(*)     AS clientes,
       SUM(saldo)   AS saldo_total,
       AVG(saldo)   AS saldo_medio,
       MAX(saldo)   AS mayor_saldo
FROM clientes
GROUP BY ciudad
ORDER BY saldo_total DESC;
Resultado
ciudad     clientes  saldo_total  saldo_medio  mayor_saldo
---------  --------  -----------  -----------  -----------
Bogota            2       235000       117500       150000
Medellin          1            0            0            0
Cali              1       -20000       -20000       -20000

La regla que ordena todo GROUP BY: lo que aparece en el SELECT tiene que estar agrupado o estar dentro de una función de resumen. No puedes pedir nombre junto a un SUM() por ciudad, porque hay varios nombres en cada ciudad y la base de datos no sabe cuál devolver.

La trampa que produce informes con números inflados: COUNT(*) cuenta filas, y COUNT(email) cuenta valores que no son NULL.

Si haces un informe de «clientes con correo» usando COUNT(*), estás contando también a los que no tienen correo. El número sale más alto, es perfectamente creíble, y nadie da ningún error. Para contar cuántos tienen dato: COUNT(email).

La diferencia, en una consulta
SELECT COUNT(*)              AS filas,
       COUNT(email)          AS con_correo,
       COUNT(DISTINCT ciudad) AS ciudades
FROM clientes;
Resultado
 filas  con_correo  ciudades
------  ----------  --------
     4           3         3

WHERE y HAVING: antes o después de agrupar

Los dos filtran, y no son intercambiables. La diferencia es cuándo actúan.

WHERE filtra las filas, antes de agrupar. HAVING filtra los grupos ya calculados, y es el único que puede usar un SUM() o un COUNT() en su condición.

Los dos juntos, que es lo habitual
SELECT ciudad, COUNT(*) AS clientes, SUM(saldo) AS total
FROM clientes
WHERE activo = TRUE            -- 1. descarta filas: solo clientes activos
GROUP BY ciudad                -- 2. agrupa lo que quedo
HAVING SUM(saldo) > 100000     -- 3. descarta grupos con poco total
ORDER BY total DESC;

Ese orden -filtrar filas, agrupar, filtrar grupos, ordenar- no es solo la forma de escribirlo: es el orden en que la base de datos lo ejecuta. Entenderlo explica por qué no se puede poner SUM() en el WHERE: cuando el WHERE actúa, esa suma todavía no existe.

Fechas: lo menos portátil de todo SQL

Si hay una parte donde los tres motores se parecen poco, es esta. Cada uno tiene sus propias funciones y no coinciden casi en nada.

La misma pregunta, en los tres motores
-- La fecha de hoy
-- PostgreSQL:  CURRENT_DATE
-- MySQL:       CURDATE()
-- SQL Server:  CAST(GETDATE() AS DATE)

-- Sacar el año de una fecha
-- PostgreSQL:  EXTRACT(YEAR FROM fecha)
-- MySQL:       YEAR(fecha)
-- SQL Server:  YEAR(fecha)

-- Sumar 30 dias
-- PostgreSQL:  fecha + INTERVAL '30 days'
-- MySQL:       DATE_ADD(fecha, INTERVAL 30 DAY)
-- SQL Server:  DATEADD(day, 30, fecha)

-- Dias entre dos fechas
-- PostgreSQL:  fecha_fin - fecha_inicio
-- MySQL:       DATEDIFF(fecha_fin, fecha_inicio)
-- SQL Server:  DATEDIFF(day, fecha_inicio, fecha_fin)

Fíjate en el último: DATEDIFF existe en MySQL y en SQL Server, y no recibe los argumentos en el mismo orden ni con el mismo formato. Una consulta copiada de un motor al otro se ejecuta y devuelve un número equivocado o con el signo cambiado.

Es peor que un error de sintaxis: un error de sintaxis se ve, y un número equivocado se publica en un informe.

El patrón de fechas que funciona en los tres

La forma de no depender de funciones propietarias es filtrar por rango. Y hay una trampa concreta que evitar.

Rango bien hecho
-- Portatil y correcto: desde el dia 1 inclusive, hasta el 1 del mes siguiente EXCLUIDO
SELECT * FROM pedidos
WHERE fecha >= '2026-09-01' AND fecha < '2026-10-01';

-- PELIGROSO si la columna guarda tambien la hora:
SELECT * FROM pedidos
WHERE fecha BETWEEN '2026-09-01' AND '2026-09-30';

Por qué el segundo es peligroso: si la columna es de fecha y hora, '2026-09-30' significa las 00:00 del día 30. Todo lo ocurrido ese día a partir de las 00:01 queda fuera del informe.

Se pierde un día entero de ventas, el total sale menor y no hay ningún error que lo delate. El patrón >= inicio AND < principio_del_siguiente no tiene ese problema, y funciona igual en los tres motores.

Juntar textos

Otra diferencia visible entre motores, aunque más inofensiva… salvo por lo que hace con los NULL.

Concatenar, en los tres
-- PostgreSQL: doble barra vertical
SELECT nombre || ' (' || ciudad || ')' AS etiqueta FROM clientes;

-- MySQL: no tiene ||, usa la funcion
SELECT CONCAT(nombre, ' (', ciudad, ')') AS etiqueta FROM clientes;

-- SQL Server: el signo mas
SELECT nombre + ' (' + ciudad + ')' AS etiqueta FROM clientes;

-- CONCAT() funciona en los tres: es la forma portatil
SELECT CONCAT(nombre, ' (', ciudad, ')') AS etiqueta FROM clientes;

La trampa del NULL: concatenar algo con NULL usando || o + da NULL entero. El resultado no es «Julia Gomez ()», es nada: la fila aparece vacía.

Es lo que hace que un listado de nombres completos tenga huecos porque a esas personas les faltaba el segundo apellido. CONCAT() es más amable -trata el NULL como texto vacío-, y la solución explícita es COALESCE(columna, ''), que sustituye el NULL por lo que tú digas.

Repaso de las diferencias

Piensa cómo se hace en cada motor antes de voltear.

Compruébalo tú mismo

Un informe de «clientes con correo» usa COUNT(*) y da un número más alto de lo esperado. ¿Por qué?

Filtras un mes con BETWEEN '2026-09-01' AND '2026-09-30' y la columna guarda fecha y hora. ¿Qué pasa?

Concatenas nombre y segundo apellido con || y algunas filas salen vacías. ¿Qué ocurrió?

Lo que sigue

Última lección: hacer que una consulta lenta deje de serlo con índices, agrupar varios cambios para que se apliquen todos o ninguno con transacciones, y respaldar cada motor con su propia herramienta.

Siguiente: Índices, transacciones y copias

Continuar ← Relaciones y JOIN