Llevas tres lecciones escribiendo ::TEXT, ::NUMERIC y ::DATE sin que nadie te haya explicado qué son. Y llevas dos módulos arrastrando una promesa: 04-03 cerró el tema de los nulos diciendo que COALESCE y NULLIF —las herramientas que sustituyen un nulo por algo presentable— se explicarían aquí. Los dos temas van juntos porque son lo mismo visto desde dos ángulos: qué hacer cuando un valor no tiene la forma que necesitas. Unas veces tiene el tipo equivocado y hay que convertirlo; otras no hay valor en absoluto y hay que sustituirlo. Son el pegamento de todo lo anterior: sin ellos, las funciones de cadena rompen ante un nulo, las numéricas fallan al dividir entre cero y las de fecha se niegan a leer un texto.
Contenido
- Conversión implícita frente a explícita
CASTy el operador::- Las conversiones habituales y sus trampas
to_number,to_char,to_date: conversión controladaCOALESCE: el primer valor no nuloNULLIFy sus dos usos canónicosCOALESCEfrente aCASECOALESCEen agregaciones: la deuda de 04-04- Tabla comparativa por motor
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Conversión implícita frente a explícita
Una conversión implícita la hace el motor por su cuenta, sin que se lo pidas. Una explícita la escribes tú.
SELECT 1 + '2' AS suma_implicita, 10 > '9' AS numero_contra_literal,
'10' > '9' AS literal_contra_literal;| suma_implicita | numero_contra_literal | literal_contra_literal |
|---|---|---|
| 3 | true | false |
Las tres columnas usan el mismo '9' o '2' y se comportan de tres maneras. 1 + '2' da 3 porque PostgreSQL ve un entero a la izquierda y resuelve el literal como entero; 10 > '9' da true por el mismo mecanismo; y '10' > '9' da false, porque ahí no hay ningún tipo que dé la pista, los dos literales se resuelven como texto y en orden alfabético '10' va antes que '9'.
Ese último es el ejemplo de por qué la conversión implícita es peligrosa: no falla, devuelve otra cosa. Un WHERE codigo > '9' sobre una columna de texto que contiene números filtra al revés de lo que crees, y nadie te avisa.
| Conversión implícita | Conversión explícita | |
|---|---|---|
| Quién decide | El motor, según el contexto | Tú |
| Visible al leer el código | No | Sí |
| Portable entre motores | No: cada uno tiene sus reglas | Sí |
La regla: cuando dos tipos se mezclan, conviértelos tú. Que el motor sepa adivinar no significa que adivine lo que quieres, y desde luego no significa que el siguiente motor adivine igual.
CAST y el operador ::
CAST y el operador ::Hay dos sintaxis para lo mismo:
SELECT CAST('123' AS INTEGER) AS forma_estandar, '123'::INTEGER AS forma_postgresql,
CAST(12.9 AS INTEGER) AS numerico_a_entero, CAST(42 AS TEXT) AS numero_a_texto;| forma_estandar | forma_postgresql | numerico_a_entero | numero_a_texto |
|---|---|---|---|
| 123 | 123 | 13 | 42 |
CAST(expr AS tipo) es SQL estándar y funciona en todos los motores; expr::tipo es la abreviatura de PostgreSQL, más corta y cómoda al encadenar pero no portable. El consejo práctico: :: en consultas de análisis, CAST en código que pueda acabar en otro motor. Y ojo con la precedencia: :: se aplica antes que los operadores aritméticos, así que a + b::NUMERIC convierte solo b; para convertir la suma, (a + b)::NUMERIC.
Fíjate ya en la tercera columna: CAST(12.9 AS INTEGER) devuelve 13, no 12. Es la primera trampa.
- Las conversiones habituales y sus trampas
| Conversión | Ejemplo | Resultado | Trampa |
|---|---|---|---|
| Texto → entero | '123'::INTEGER |
123 |
Falla con cualquier carácter no numérico |
| Texto inválido → entero | 'abc'::INTEGER |
ERROR |
Ver abajo |
| Texto con coma → numérico | '12,50'::NUMERIC |
ERROR |
El separador decimal debe ser . |
| Numérico → entero | 12.9::INTEGER / 12.4::INTEGER |
13 / 12 |
Redondea, no trunca |
| Número → texto, texto ISO → fecha | 42::TEXT, '2025-03-04'::DATE |
'42', 2025-03-04 |
Sin problema |
| Texto ambiguo → fecha | '03/04/2025'::DATE |
Depende de DateStyle |
Ver abajo |
Texto → VARCHAR(n) |
'Aceite de oliva'::VARCHAR(6) |
'Aceite' |
Trunca en silencio |
La conversión que falla
Y esto es una buena noticia: el error detiene la consulta y te obliga a mirar el dato. Compáralo con SQLite, que para la misma expresión devuelve 0 sin decir nada, de modo que un SUM sobre esa columna daría un total plausible y equivocado. Que PostgreSQL sea estricto es una característica, no una molestia.
El caso real aparece al importar una columna de texto con '12,50' en formato español. '12,50'::NUMERIC falla, y la solución es normalizar antes con las funciones de 06-01:
SELECT REPLACE('12,50', ',', '.')::NUMERIC AS importe,
REPLACE(REPLACE('1.234,56', '.', ''), ',', '.')::NUMERIC AS importe_con_miles;| importe | importe_con_miles |
|---|---|
| 12.50 | 1234.56 |
NUMERIC → INTEGER redondea
12.4::INTEGER es 12, pero 12.5::INTEGER y 12.9::INTEGER son los dos 13. Es coherente con 06-02 —NUMERIC redondea half-up— y contradice la intuición de quien viene de un lenguaje de programación, donde convertir a entero trunca. Si quieres truncar, escribe TRUNC(12.9)::INTEGER, que sí da 12.
La fecha ambigua
'03/04/2025'::DATE no tiene una respuesta única: depende del parámetro de sesión DateStyle, que por omisión vale ISO, MDY (salida ISO, entrada mes-día-año) y se consulta con SHOW DateStyle;. Con ese valor, '03/04/2025'::DATE es el 4 de marzo; con SET DateStyle = 'ISO, DMY'; pasa a ser el 3 de abril. La misma cadena, dos fechas, según una variable de sesión que casi nadie mira.
La regla, ya vista en 06-03: para fechas ambiguas usa siempre
TO_DATE(texto, patron)con el patrón explícito, o exige el formato ISOAAAA-MM-DDen el origen. Nunca dejes que la interpretación dependa de la configuración del servidor.
El truncamiento silencioso
SELECT 'Aceite de oliva virgen extra'::VARCHAR(6); devuelve Aceite. Sin error y sin aviso. Y aquí está la incoherencia que hay que conocer: si intentas insertar esa misma cadena en una columna VARCHAR(6), PostgreSQL sí falla con value too long for type character varying(6). La conversión explícita trunca; la inserción, no. Es la única conversión de PostgreSQL que pierde datos en silencio. Por último, el aviso de siempre: un WHERE fecha_pedido::TEXT LIKE '2025%' envuelve la columna en una conversión y no puede usar el índice, igual que las funciones de 06-01 (08-03).
to_number, to_char, to_date: conversión controlada
to_number, to_char, to_date: conversión controladaCuando el formato no es el estándar, las funciones to_* reciben un patrón y hacen la conversión según tus reglas, no según las del motor.
| Función | Qué hace | Ejemplo | Resultado |
|---|---|---|---|
TO_CHAR(valor, patron) |
Número o fecha → texto formateado | TO_CHAR(1234.5, 'FM999G999D00') |
1.234,50 (con lc_numeric español) |
TO_NUMBER(texto, patron) |
Texto → NUMERIC |
TO_NUMBER('12500', '99999') |
12500 |
TO_DATE(texto, patron) |
Texto → DATE |
TO_DATE('04/03/2025', 'DD/MM/YYYY') |
2025-03-04 |
TO_TIMESTAMP(texto, patron) |
Texto → TIMESTAMPTZ |
TO_TIMESTAMP('04/03/2025 18:30', 'DD/MM/YYYY HH24:MI') |
2025-03-04 18:30:00+01 |
Los símbolos G (miles) y D (decimal) de los patrones numéricos dependen de lc_numeric: en un servidor en español producen 1.234,50 y en uno en inglés, 1,234.50. Si necesitas un resultado independiente del servidor, usa los literales , y . en el patrón o normaliza con REPLACE. TO_CHAR sobre números completa la pareja con el TO_CHAR sobre fechas de 06-03: la misma función, patrones distintos.
COALESCE: el primer valor no nulo
COALESCE: el primer valor no nuloCOALESCE(a, b, c, …) devuelve el primero de sus argumentos que no sea nulo, y NULL solo si todos lo son. Admite cualquier número de argumentos.
SELECT COALESCE(NULL, NULL, 'tercero', 'cuarto') AS primero_no_nulo,
COALESCE(NULL, NULL, NULL) AS todos_nulos, COALESCE(1, 1/0) AS perezosa;| primero_no_nulo | todos_nulos | perezosa |
|---|---|---|
| tercero | (null) | 1 |
La tercera columna merece atención: 1/0 provocaría un error de división por cero… si llegara a evaluarse. COALESCE es perezosa: en cuanto encuentra un argumento no nulo deja de mirar los siguientes, lo que permite poner cálculos caros o peligrosos como último recurso. Y un requisito que sorprende: todos los argumentos deben ser de tipos compatibles, así que COALESCE(empleado_id, 'Venta web') falla porque empleado_id es entero.
El patrón de presentación
Esto es lo que 04-03 dejó pendiente: los diez pedidos web con empleado_id IS NULL.
SELECT pe.id, pe.fecha_pedido, pe.estado,
CONCAT_WS(' ', e.nombre, e.apellidos) AS comercial_crudo,
COALESCE(CONCAT_WS(' ', e.nombre, e.apellidos), 'Venta web') AS canal_mal,
COALESCE(NULLIF(CONCAT_WS(' ', e.nombre, e.apellidos), ''),
'Venta web') AS canal_bien
FROM pedidos AS pe
LEFT JOIN empleados AS e ON e.id = pe.empleado_id
WHERE pe.id <= 4 ORDER BY pe.id;| id | fecha_pedido | estado | comercial_crudo | canal_mal | canal_bien |
|---|---|---|---|---|---|
| 1 | 2025-03-04 | entregado | Venta web | ||
| 2 | 2025-03-12 | entregado | Óscar Peris Blasco | Óscar Peris Blasco | Óscar Peris Blasco |
| 3 | 2025-04-02 | entregado | Venta web | ||
| 4 | 2025-04-19 | entregado | Laia Puig Sanchis | Laia Puig Sanchis | Laia Puig Sanchis |
Aquí se cierra el círculo de 06-01. CONCAT_WS ignora los nulos, así que para los pedidos web devuelve la cadena vacía y no NULL; y COALESCE no la sustituye, porque '' no es nulo, de modo que canal_mal sale vacía. La combinación COALESCE(NULLIF(x, ''), 'Venta web') es la forma correcta y el idiom que hay que memorizar: NULLIF convierte la cadena vacía en nulo y entonces COALESCE puede hacer su trabajo. Si concatenas con || en lugar de CONCAT_WS el nulo se propaga y COALESCE(e.nombre || ' ' || e.apellidos, 'Venta web') funciona directamente; las dos vías son válidas, lo que no vale es mezclarlas sin darse cuenta.
El mismo patrón con los siete clientes sin recomendador (04-03), esta vez con ||:
SELECT c.id, CONCAT_WS(' ', c.nombre, c.apellidos) AS cliente,
COALESCE(ref.nombre || ' ' || ref.apellidos, 'Registro directo') AS origen
FROM clientes AS c
LEFT JOIN clientes AS ref ON ref.id = c.referido_por_id
WHERE c.id <= 5 ORDER BY c.id;| id | cliente | origen |
|---|---|---|
| 1 | Lucía Martínez Soler | Registro directo |
| 2 | Carlos Ferrer Ibáñez | Lucía Martínez Soler |
| 3 | Marta Sanchis Gil | Lucía Martínez Soler |
| 4 | Javier Ortega Ruiz | Registro directo |
| 5 | Ana Belmonte Roca | Carlos Ferrer Ibáñez |
Importante:
COALESCEes una herramienta de presentación, no de análisis. Sustituir un nulo por un texto está bien para un informe que lee una persona; sustituirlo por un0dentro de un cálculo cambia el resultado, y eso es la sección 8.
NULLIF y sus dos usos canónicos
NULLIF y sus dos usos canónicosNULLIF(a, b) devuelve NULL si a = b, y a en caso contrario. Es exactamente lo contrario de COALESCE: en lugar de quitar nulos, los fabrica.
| iguales | distintos | cadena_vacia |
|---|---|---|
| (null) | 5 | (null) |
Parece inútil hasta que ves para qué se usa. Tiene dos casos canónicos y prácticamente ningún otro.
Uso 1: evitar la división por cero
Recuerda el aviso de 04-03: no puedes protegerte con AND stock <> 0, porque el planificador reordena las condiciones. NULLIF sí protege, porque actúa dentro de la expresión:
SELECT p.id, p.nombre, p.stock,
SUM(lp.cantidad) AS vendidas,
ROUND(SUM(lp.cantidad) * 100.0 / NULLIF(p.stock, 0), 2) AS rotacion_pct
FROM productos AS p
LEFT JOIN lineas_pedido AS lp ON lp.producto_id = p.id
WHERE p.id IN (2, 13, 15, 18)
GROUP BY p.id, p.nombre, p.stock ORDER BY p.id;| id | nombre | stock | vendidas | rotacion_pct |
|---|---|---|---|---|
| 2 | Arroz integral ecológico 1 kg | 200 | 14 | 7.00 |
| 13 | Velas de cera de soja (pack 2) | 0 | (null) | (null) |
| 15 | Té verde matcha ceremonial 30 g | 40 | 4 | 10.00 |
| 18 | Cepillo de dientes de bambú | 240 | 9 | 3.75 |
El producto 13 tiene stock 0: sin NULLIF, esa fila provocaría ERROR: division by zero y la consulta entera fallaría. Con NULLIF(p.stock, 0) el denominador se vuelve nulo, la división devuelve NULL y el informe dice honestamente "no se puede calcular", que es la verdad: la rotación de un producto sin stock no es cero, es indefinida.
Uso 2: tratar la cadena vacía como nulo
Es el problema del segundo apellido que quedó abierto en 06-01:
SELECT id, apellidos,
SPLIT_PART(apellidos, ' ', 2) AS segundo_crudo,
NULLIF(SPLIT_PART(apellidos, ' ', 2), '') AS segundo_limpio,
COALESCE(NULLIF(SPLIT_PART(apellidos, ' ', 2), ''), '—') AS segundo_presentable
FROM clientes WHERE id IN (1, 9, 10) ORDER BY id;| id | apellidos | segundo_crudo | segundo_limpio | segundo_presentable |
|---|---|---|---|---|
| 1 | Martínez Soler | Soler | Soler | Soler |
| 9 | Dubois | (null) | — | |
| 10 | Moreau | (null) | — |
Muchos sistemas heredados guardan '' donde deberían guardar NULL —formularios web con campos vacíos, importaciones de CSV—. NULLIF(columna, '') es la limpieza estándar; en un UPDATE de saneamiento sería SET ciudad = NULLIF(TRIM(ciudad), '').
COALESCE frente a CASE
COALESCE frente a CASECOALESCE es azúcar sintáctico sobre un CASE. Estas dos expresiones son exactamente equivalentes:
COALESCE |
CASE |
|
|---|---|---|
| Legibilidad | Muy alta para "sustituir el nulo" | Verbosa para ese caso |
| Condición | Solo "¿es nulo?" | Cualquier condición |
| Cuándo usarlo | Sustituir nulos | Clasificar, comparar rangos, decidir por valor |
La regla es simple: si la pregunta es "¿es nulo?", COALESCE; si es cualquier otra, CASE — la lección siguiente. Escribir CASE WHEN x IS NULL THEN 0 ELSE x END no está mal, pero es cuatro veces más largo que COALESCE(x, 0) y esconde la intención.
COALESCE en agregaciones: la deuda de 04-04
COALESCE en agregaciones: la deuda de 04-04Aquí COALESCE deja de ser cosmética y cambia el resultado. En 04-04 aprendiste que las funciones de agregación ignoran los nulos; veamos qué pasa si se los das convertidos en ceros. La puntuación media de los productos, uniendo las 20 filas del catálogo con las 12 reseñas:
SELECT COUNT(*) AS filas,
COUNT(r.puntuacion) AS con_resena,
SUM(r.puntuacion) AS suma,
ROUND(AVG(r.puntuacion), 4) AS avg_ignorando_nulos,
ROUND(AVG(COALESCE(r.puntuacion, 0)), 4) AS avg_contando_ceros
FROM productos AS p
LEFT JOIN resenas AS r ON r.producto_id = p.id;| filas | con_resena | suma | avg_ignorando_nulos | avg_contando_ceros |
|---|---|---|---|---|
| 23 | 12 | 49 | 4.0833 | 2.1304 |
4,08 frente a 2,13. El mismo dato, dos respuestas que no se parecen en nada. AVG(r.puntuacion) divide 49 entre 12: la media de las reseñas que existen, la respuesta a "¿qué opinan los clientes que han opinado?". AVG(COALESCE(r.puntuacion, 0)) divide 49 entre 23, contando los 11 productos sin reseña como si les hubieran puesto un cero.
La primera es casi siempre la correcta, porque "nadie ha opinado" no es "todos han opinado mal". La segunda es exactamente el error que 04-03 describía al hablar de valores centinela: inventar un dato para no tener que manejar su ausencia.
El mismo efecto con salarios, uniendo cada pedido con su comercial:
| Expresión | Divide entre | Resultado | Qué significa |
|---|---|---|---|
AVG(e.salario) |
10 (los no nulos) | 27 420,00 € | Salario medio de quien gestiona pedidos con comercial |
AVG(COALESCE(e.salario, 0)) |
20 (todas las filas) | 13 710,00 € | Como si los pedidos web los gestionara alguien que cobra 0 € |
La regla: usa
COALESCEfuera del agregado para presentar (COALESCE(SUM(x), 0)) y dentro solo cuando el cero sea un valor real, no un hueco. La pregunta que hay que hacerse es: "¿ese hueco significa cero, o significa que no hay dato?".
COALESCE fuera del agregado: el conjunto vacío
Justo el caso contrario. SUM sobre cero filas no devuelve 0, devuelve NULL:
SELECT cat.id, cat.nombre,
ROUND(SUM(lp.cantidad * lp.precio_unitario * (1 - lp.descuento)), 2) AS facturacion,
COALESCE(ROUND(SUM(lp.cantidad * lp.precio_unitario
* (1 - lp.descuento)), 2), 0) AS facturacion_presentable
FROM categorias AS cat
LEFT JOIN productos AS p ON p.categoria_id = cat.id
LEFT JOIN lineas_pedido AS lp ON lp.producto_id = p.id
GROUP BY cat.id, cat.nombre ORDER BY cat.id;| id | nombre | facturacion | facturacion_presentable |
|---|---|---|---|
| 1 | Alimentación | 256.27 | 256.27 |
| 2 | Cosmética natural | 156.32 | 156.32 |
| 3 | Hogar sostenible | 88.58 | 88.58 |
| 4 | Bebidas | 195.28 | 195.28 |
| 5 | Higiene personal | 31.50 | 31.50 |
| 6 | Complementos | (null) | 0.00 |
Las cinco primeras suman 727,95 €, la cifra canónica. La categoría Complementos solo contiene el producto 20, descatalogado y nunca vendido: SUM sobre un conjunto vacío da NULL. Y aquí COALESCE(…, 0) sí es correcto, porque "no se ha vendido nada" es exactamente cero euros. La diferencia con el caso de las reseñas es de significado, no de sintaxis.
- Tabla comparativa por motor
| Tarea | PostgreSQL 16 | MySQL 8 | SQLite | SQL Server | Oracle |
|---|---|---|---|---|---|
| Primer no nulo (n argumentos) | COALESCE |
COALESCE |
COALESCE |
COALESCE |
COALESCE |
| Versión de dos argumentos | COALESCE(a,b) |
IFNULL(a,b) |
IFNULL(a,b) |
ISNULL(a,b) |
NVL(a,b) |
| "Si no es nulo X, si lo es Y" | CASE |
IF(a IS NULL, y, x) |
IIF(...) |
IIF(...) |
NVL2(a, x, y) |
| Convertir en nulo si son iguales | NULLIF |
NULLIF |
NULLIF |
NULLIF |
NULLIF |
| Conversión / la que falla | CAST, :: → ERROR |
CAST, CONVERT → 0 + aviso |
CAST → 0 en silencio |
CAST, CONVERT → ERROR |
CAST, TO_* → ERROR |
| Conversión tolerante | — (no existe TRY_CAST) |
— | — | TRY_CAST, TRY_CONVERT |
CAST(… DEFAULT … ON CONVERSION ERROR) |
'' frente a NULL |
Son distintos | Son distintos | Son distintos | Son distintos | '' ES NULL |
Tres avisos. ISNULL de SQL Server no es IS NULL: son cosas distintas y se escriben casi igual. En Oracle, '' es NULL, así que el uso 2 de NULLIF sobra allí… y todo el código que dependa de distinguirlos deja de funcionar al migrar. Y PostgreSQL no tiene TRY_CAST: para una conversión tolerante hay que validar antes con una expresión regular (texto ~ '^[0-9]+$', de 04-01) o escribir una función propia (módulo 10).
Errores Comunes y Consejos
- Confiar en la conversión implícita.
'10' > '9'esfalse. Convierte tú y quedará escrito lo que quieres decir. - Esperar que
NUMERIC::INTEGERtrunque. Redondea:12.9::INTEGERes13. Para truncar,TRUNC. - Convertir a
VARCHAR(n)sin contar los caracteres. Es la única conversión de PostgreSQL que pierde datos en silencio. - Dejar que
'03/04/2025'::DATEdecida por ti. Depende deDateStyle. UsaTO_DATEcon patrón, o ISO en el origen. - Creer que
COALESCEarregla las cadenas vacías.''no es nulo: el idiom correcto esCOALESCE(NULLIF(x, ''), 'valor'). Y todos los argumentos deben ser de tipos compatibles. - Meter
COALESCE(col, 0)dentro de un agregado sin pensarlo. Cambia el denominador: 4,08 pasó a 2,13. Solo si el cero es un valor real. - Olvidar
COALESCE(SUM(x), 0)en informes.SUMsobre cero filas devuelveNULL, no0, y la celda sale vacía. - Protegerse de la división por cero con un
AND. El planificador reordena.NULLIF(divisor, 0)sí protege. - Aplicar un
CASTa una columna en elWHERE. Igual que una función: impide usar el índice (08-03). - Consejo: normaliza en la entrada, no en cada consulta. Si el CSV trae
'12,50', arréglalo al cargar; no repartasREPLACEpor cincuenta informes. - Consejo:
COALESCEpara presentar, nunca para calcular; y si calculas con él, escribe en un comentario por qué el cero es legítimo. - Consejo: usa
CASTen lugar de::en el código que vayas a publicar o portar. Cuesta cinco caracteres más y funciona en todas partes.
Ejercicios
Ejercicio 1
Prepara el listado de pedidos para dirección: id, fecha, estado, nombre del cliente y una columna gestionado_por con el nombre completo del comercial o el texto 'Canal web' cuando no lo haya. Añade metodo con el método de pago en mayúsculas, muestra los pedidos 15 a 20 y comprueba que ninguna celda queda vacía.
Ejercicio 2
Sobre productos, calcula el margen porcentual defensivo —(precio - coste) / precio * 100 a dos decimales— de forma que la consulta no falle nunca aunque algún día precio valga 0 o coste sea NULL. Muestra el coste con COALESCE a 0.00 y explica por qué esa sustitución concreta es discutible.
Ejercicio 3
Un compañero presenta este informe de satisfacción y concluye que "la valoración media del catálogo es de 2,13 sobre 5, un desastre":
SELECT ROUND(AVG(COALESCE(r.puntuacion, 0)), 2) AS media
FROM productos AS p
LEFT JOIN resenas AS r ON r.producto_id = p.id;(1) ¿De dónde sale el 2,13 y por qué es engañoso? (2) Escribe la consulta correcta y da la cifra. (3) Escribe una tercera que dé las dos métricas útiles a la vez: la media de los productos valorados y cuántos productos no tienen ninguna reseña.
Soluciones
Solución 1
SELECT pe.id, pe.fecha_pedido, pe.estado,
CONCAT_WS(' ', c.nombre, c.apellidos) AS cliente,
COALESCE(NULLIF(CONCAT_WS(' ', e.nombre, e.apellidos), ''), 'Canal web') AS gestionado_por,
UPPER(pe.metodo_pago) AS metodo
FROM pedidos AS pe
JOIN clientes AS c ON c.id = pe.cliente_id
LEFT JOIN empleados AS e ON e.id = pe.empleado_id
WHERE pe.id BETWEEN 15 AND 20 ORDER BY pe.id;| id | fecha_pedido | estado | cliente | gestionado_por | metodo |
|---|---|---|---|---|---|
| 15 | 2025-12-02 | entregado | Lucía Martínez Soler | Canal web | TARJETA |
| 16 | 2025-12-19 | enviado | Javier Ortega Ruiz | Óscar Peris Blasco | TARJETA |
| 17 | 2026-01-13 | enviado | Sofia Moreira Costa | Canal web | PAYPAL |
| 18 | 2026-01-27 | pagado | Ana Belmonte Roca | Laia Puig Sanchis | TRANSFERENCIA |
| 19 | 2026-02-09 | pagado | Pau Llorens Vidal | Canal web | TARJETA |
| 20 | 2026-02-21 | pendiente | Camille Dubois | Marc Estévez Roig | CONTRAREEMBOLSO |
El JOIN con clientes puede ser interno porque cliente_id es NOT NULL; el de empleados tiene que ser LEFT JOIN, o perderías la mitad de los pedidos (03-03). Y el NULLIF es imprescindible: sin él, las tres filas web mostrarían una celda vacía en vez de 'Canal web'.
Solución 2
SELECT id, nombre, precio,
COALESCE(coste, 0.00) AS coste_presentado,
ROUND((precio - COALESCE(coste, 0)) * 100.0 / NULLIF(precio, 0), 2) AS margen_pct
FROM productos WHERE id IN (1, 5, 15, 18) ORDER BY id;| id | nombre | precio | coste_presentado | margen_pct |
|---|---|---|---|---|
| 1 | Aceite de oliva virgen extra 500 ml | 12.50 | 7.80 | 37.60 |
| 5 | Tomate triturado ecológico 400 g | 1.95 | 0.90 | 53.85 |
| 15 | Té verde matcha ceremonial 30 g | 22.00 | 12.50 | 43.18 |
| 18 | Cepillo de dientes de bambú | 3.50 | 1.20 | 65.71 |
NULLIF(precio, 0) blinda la división y 100.0 evita la división entera de 06-02. Pero el COALESCE(coste, 0) es discutible, y mucho: un coste desconocido no es un coste de cero euros. Con esa sustitución, un producto cuyo proveedor aún no ha comunicado el precio de compra aparecería con un margen del 100 %, la cifra más optimista posible y la más falsa. Lo honesto es dejar que el margen salga NULL —(precio - coste) ya lo hace solo— y que el informe muestre "sin datos".
Solución 3
1. El LEFT JOIN produce 23 filas: las 12 reseñas más una fila fabricada por cada uno de los 11 productos sin reseña. COALESCE(r.puntuacion, 0) convierte esas 11 ausencias en once ceros, así que la media es 49 / 23 = 2,13. Es engañoso porque un producto sin reseñas no ha recibido un cero: no ha recibido nada. El informe mide la falta de reseñas, no la satisfacción.
2. La consulta correcta es la que deja que AVG haga lo que hace desde 04-04 —ignorar los nulos— y devuelve 4,08 sobre 5, la cifra canónica del módulo 4. No es un desastre: es una valoración buena. 3. Y las dos métricas separadas, cada una respondiendo a su pregunta:
SELECT ROUND(AVG(r.puntuacion), 4) AS media_valorados,
COUNT(r.id) AS resenas,
COUNT(DISTINCT r.producto_id) AS productos_valorados,
COUNT(DISTINCT p.id) - COUNT(DISTINCT r.producto_id) AS productos_sin_resena
FROM productos AS p
LEFT JOIN resenas AS r ON r.producto_id = p.id;| media_valorados | resenas | productos_valorados | productos_sin_resena |
|---|---|---|---|
| 4.0833 | 12 | 9 | 11 |
Ahora el informe dice la verdad completa: los productos valorados sacan 4,08 de media, pero 11 de los 20 no tienen ninguna reseña. Ese segundo número es el problema real de TiendaVerde, y estaba escondido dentro del 2,13. Cuando un COALESCE mezcla dos preguntas en una cifra, la solución no es afinar la cifra: es separar las preguntas.
Conclusión
Ya tienes el pegamento:
- Distingues la conversión implícita —que el motor hace sin avisar y puede devolver otra cosa, como
'10' > '9'siendofalse— de la explícita, que escribes tú y queda documentada. Conviertes conCAST(expr AS tipo)(estándar) oexpr::tipo(PostgreSQL), sabiendo que::se aplica antes que la aritmética. - Conoces las trampas: texto no numérico produce
ERROR(y eso es bueno),NUMERIC::INTEGERredondea en vez de truncar,'03/04/2025'depende deDateStyley la conversión aVARCHAR(n)trunca en silencio. Y controlas el formato conTO_NUMBER,TO_CHARyTO_DATEcuando el estándar no basta. - Sustituyes nulos con
COALESCE, que es perezosa, admite varios argumentos y exige tipos compatibles; y fabricas nulos conNULLIF, cuyos dos usos son evitar la división por cero y tratar''como nulo. - Tienes memorizado el idiom
COALESCE(NULLIF(x, ''), 'valor'), que cierra definitivamente el problema del||y deCONCAT_WSque arrastrabas desde 02-02. Y sabes queCOALESCEequivale a unCASE, y cuándo usar cada uno. - Y sobre todo, sabes que
COALESCEdentro de un agregado cambia el resultado: 4,08 se convirtió en 2,13 por contar como ceros once productos que nadie había valorado.COALESCEfuera del agregado presenta; dentro, decide.
Queda una última pieza. COALESCE solo sabe responder a una pregunta —"¿es nulo?"— y todas las demás siguen fuera de tu alcance: clasificar un producto como "barato", "medio" o "caro" según su precio; poner un semáforo de stock; ordenar los estados de un pedido por su orden de flujo y no alfabéticamente; o convertir las filas de un GROUP BY en columnas de un informe. Para eso hace falta lógica condicional dentro de la consulta, y es lo que trae la última lección del módulo: CASE, expresiones condicionales.
Curso de SQL
Módulo 1: Introducción a SQL
- ¿Qué es SQL?
- Configurando tu entorno SQL
- Sintaxis básica de SQL
- Entendiendo bases de datos y tablas
- El modelo relacional: claves primarias y foráneas
- La base de datos del curso: TiendaVerde
Módulo 2: Consultas básicas de SQL
- Instrucción SELECT
- Alias, expresiones y columnas calculadas
- Filtrando datos con WHERE
- DISTINCT y eliminación de duplicados
- Ordenando datos con ORDER BY
- Limitando resultados con LIMIT
Módulo 3: Trabajando con múltiples tablas
- Operaciones JOIN
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN y CROSS JOIN
- Uniones de conjuntos: UNION, INTERSECT y EXCEPT
Módulo 4: Filtrado avanzado de datos
- Usando LIKE para coincidencia de patrones
- Operadores IN y BETWEEN
- Valores NULL y IS NULL
- Funciones de agregación: COUNT, SUM, AVG, MIN y MAX
- Agregando datos con GROUP BY
- Cláusula HAVING
Módulo 5: Manipulación de datos
- Creando tablas y restricciones con CREATE TABLE
- Instrucción INSERT
- Instrucción UPDATE
- Instrucción DELETE
- Instrucción UPSERT (MERGE)
- Modificando el esquema: ALTER TABLE y migraciones seguras
Módulo 6: Funciones avanzadas de SQL
- Funciones de cadena
- Funciones numéricas
- Funciones de fecha y hora
- Conversión de tipos y manejo de NULL: CAST y COALESCE
- Expresiones condicionales
Módulo 7: Subconsultas y consultas anidadas
- Introducción a subconsultas
- Subconsultas correlacionadas
- EXISTS y NOT EXISTS
- Usando subconsultas en cláusulas SELECT, FROM y WHERE
- Subconsultas o JOIN: cuál elegir
Módulo 8: Índices y optimización de rendimiento
- Entendiendo los índices
- Creación y gestión de índices
- Tipos de índice y cuándo no indexar
- Técnicas de optimización de consultas
- Análisis del rendimiento de consultas
Módulo 9: Transacciones y concurrencia
- Introducción a las transacciones
- Propiedades ACID
- Instrucciones de control de transacciones
- Niveles de aislamiento y anomalías de concurrencia
- Manejo de concurrencia: bloqueos e interbloqueos
Módulo 10: Temas avanzados
- Vistas
- Expresiones de tabla comunes (CTE)
- Funciones de ventana
- Procedimientos almacenados
- Triggers
- JSON y datos semiestructurados
Módulo 11: SQL en la práctica
- Casos de uso en el mundo real
- Mejores prácticas
- Seguridad: inyección SQL, permisos y roles
- SQL para análisis de datos
- SQL en desarrollo web
