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

  1. Conversión implícita frente a explícita
  2. CAST y el operador ::
  3. Las conversiones habituales y sus trampas
  4. to_number, to_char, to_date: conversión controlada
  5. COALESCE: el primer valor no nulo
  6. NULLIF y sus dos usos canónicos
  7. COALESCE frente a CASE
  8. COALESCE en agregaciones: la deuda de 04-04
  9. Tabla comparativa por motor
  10. Errores Comunes y Consejos
  11. Ejercicios
  12. Conclusión

  1. 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
Visible al leer el código No
Portable entre motores No: cada uno tiene sus reglas

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.

  1. 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.

  1. 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

SELECT 'abc'::INTEGER;
ERROR:  invalid input syntax for type integer: "abc"
LINE 1: SELECT 'abc'::INTEGER;
               ^

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

NUMERICINTEGER 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 ISO AAAA-MM-DD en 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 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).

  1. to_number, to_char, to_date: conversión controlada

Cuando 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.

  1. COALESCE: el primer valor no nulo

COALESCE(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: COALESCE es 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 un 0 dentro de un cálculo cambia el resultado, y eso es la sección 8.

  1. NULLIF y sus dos usos canónicos

NULLIF(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.

SELECT NULLIF(5, 5) AS iguales, NULLIF(5, 3) AS distintos, NULLIF('', '') AS cadena_vacia;
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), '').

  1. COALESCE frente a CASE

COALESCE es azúcar sintáctico sobre un CASE. Estas dos expresiones son exactamente equivalentes:

COALESCE(a, b)

CASE WHEN a IS NOT NULL THEN a ELSE b END
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.

  1. COALESCE en agregaciones: la deuda de 04-04

Aquí 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 COALESCE fuera 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.

  1. 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, CONVERT0 + aviso CAST0 en silencio CAST, CONVERTERROR 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' es false. Convierte tú y quedará escrito lo que quieres decir.
  • Esperar que NUMERIC::INTEGER trunque. Redondea: 12.9::INTEGER es 13. 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'::DATE decida por ti. Depende de DateStyle. Usa TO_DATE con patrón, o ISO en el origen.
  • Creer que COALESCE arregla las cadenas vacías. '' no es nulo: el idiom correcto es COALESCE(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. SUM sobre cero filas devuelve NULL, no 0, 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 CAST a una columna en el WHERE. 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 repartas REPLACE por cincuenta informes.
  • Consejo: COALESCE para presentar, nunca para calcular; y si calculas con él, escribe en un comentario por qué el cero es legítimo.
  • Consejo: usa CAST en 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' siendo false— de la explícita, que escribes tú y queda documentada. Conviertes con CAST(expr AS tipo) (estándar) o expr::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::INTEGER redondea en vez de truncar, '03/04/2025' depende de DateStyle y la conversión a VARCHAR(n) trunca en silencio. Y controlas el formato con TO_NUMBER, TO_CHAR y TO_DATE cuando el estándar no basta.
  • Sustituyes nulos con COALESCE, que es perezosa, admite varios argumentos y exige tipos compatibles; y fabricas nulos con NULLIF, 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 de CONCAT_WS que arrastrabas desde 02-02. Y sabes que COALESCE equivale a un CASE, y cuándo usar cada uno.
  • Y sobre todo, sabes que COALESCE dentro de un agregado cambia el resultado: 4,08 se convirtió en 2,13 por contar como ceros once productos que nadie había valorado. COALESCE fuera 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

Módulo 2: Consultas básicas de SQL

Módulo 3: Trabajando con múltiples tablas

Módulo 4: Filtrado avanzado de datos

Módulo 5: Manipulación de datos

Módulo 6: Funciones avanzadas de SQL

Módulo 7: Subconsultas y consultas anidadas

Módulo 8: Índices y optimización de rendimiento

Módulo 9: Transacciones y concurrencia

Módulo 10: Temas avanzados

Módulo 11: SQL en la práctica

Módulo 12: Proyecto final

© Copyright 2026. Todos los derechos reservados