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.
-- 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;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).
SELECT COUNT(*) AS filas,
COUNT(email) AS con_correo,
COUNT(DISTINCT ciudad) AS ciudades
FROM clientes; filas con_correo ciudades
------ ---------- --------
4 3 3WHERE 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.
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 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.
-- 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.
-- 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é?
COUNT(columna) descarta los NULL. El número
inflado es creíble y no da ningún error, que es lo que lo hace peligroso.Filtras un mes con BETWEEN '2026-09-01' AND '2026-09-30' y la columna guarda fecha y hora. ¿Qué pasa?
>= el inicio y < el principio del
mes siguiente.Concatenas nombre y segundo apellido con || y algunas filas salen vacías. ¿Qué ocurrió?
CONCAT(), que trata el NULL como
texto vacío, o con COALESCE(columna, '').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