Síntoma
Quieres listar todas las entidades de la izquierda y obtener cero cuando no existe ninguna fila relacionada. Sin embargo, una consulta con LEFT JOIN y COUNT(*) devuelve 1 para el grupo sin coincidencias.
Consulta problemática
WITH categorias(id, nombre) AS (
VALUES (1, 'Portátiles'), (2, 'Audio'), (3, 'Monitores')
),
productos(id, categoria_id) AS (
VALUES (101, 1), (102, 1), (103, 2)
)
SELECT c.nombre, COUNT(*) AS productos
FROM categorias AS c
LEFT JOIN productos AS p
ON p.categoria_id = c.id
GROUP BY c.id, c.nombre
ORDER BY c.id;| nombre | productos |
|---|---|
| Portátiles | 2 |
| Audio | 1 |
| Monitores | 1 |
Por qué ocurre
LEFT JOIN conserva la fila de Monitores aunque no encuentre un producto. Las columnas de p quedan en NULL, pero la fila del resultado existe. COUNT(*) cuenta filas del grupo y por eso devuelve 1.
En cambio, COUNT(expresión) cuenta únicamente las filas donde esa expresión no es NULL. Si eliges una columna de la tabla derecha que sea NOT NULL cuando existe una coincidencia —normalmente su clave primaria—, la fila artificial del LEFT JOIN no se cuenta.
Corrección: cuenta una columna no nula de la tabla derecha
WITH categorias(id, nombre) AS (
VALUES (1, 'Portátiles'), (2, 'Audio'), (3, 'Monitores')
),
productos(id, categoria_id) AS (
VALUES (101, 1), (102, 1), (103, 2)
)
SELECT c.nombre, COUNT(p.id) AS productos
FROM categorias AS c
LEFT JOIN productos AS p
ON p.categoria_id = c.id
GROUP BY c.id, c.nombre
ORDER BY c.id;| nombre | productos |
|---|---|
| Portátiles | 2 |
| Audio | 1 |
| Monitores | 0 |
Regla práctica: después de un LEFT JOIN, usa COUNT(*) si de verdad quieres contar filas preservadas del resultado; usa COUNT(tabla_derecha.columna_no_nula) si quieres contar coincidencias reales.
Otra trampa: filtrar la tabla derecha en WHERE
Si además necesitas contar solo productos que cumplan una condición, colocarla en WHERE puede eliminar a Monitores y convertir el efecto del LEFT JOIN en uno equivalente a un INNER JOIN para ese filtro. Cuando quieres conservar el cero, suele ser más claro mover la condición relacionada al ON y continuar contando una columna derecha no nula.
Práctica correctiva
Cuenta ventas cerradas, incluido cero
Conserva las tres sucursales y cuenta solo sus ventas cerradas. La sucursal Sur no tiene ventas y debe devolver 0.
v.estado = 'cerrada' dentro de ON.v.id, no *.Solución razonada
WITH sucursales(id, nombre) AS (
VALUES (1, 'Centro'), (2, 'Norte'), (3, 'Sur')
),
ventas(id, sucursal_id, estado) AS (
VALUES (10, 1, 'cerrada'), (11, 1, 'abierta'), (12, 2, 'cerrada')
)
SELECT s.nombre, COUNT(v.id) AS ventas_cerradas
FROM sucursales AS s
LEFT JOIN ventas AS v
ON v.sucursal_id = s.id
AND v.estado = 'cerrada'
GROUP BY s.id, s.nombre
ORDER BY s.id;El resultado es Centro 1, Norte 1 y Sur 0. La condición en ON mantiene a todas las sucursales y COUNT(v.id) ignora la fila derecha nula.
Qué debes recordar
COUNT(*)cuenta filas, incluida la fila preservada por unLEFT JOIN.COUNT(columna)ignoraNULL.- Cuenta una columna derecha no nula para medir coincidencias reales.
- Si necesitas conservar grupos con cero, revisa también si un filtro derecho debería estar en
ONy no enWHERE.