Las funciones de cadena servían para presentar. Las numéricas sirven para calcular, y ahí el margen de error deja de ser estético: un redondeo mal hecho no queda feo, cuesta dinero. Esta lección cubre las funciones escalares que trabajan con números y, sobre todo, las dos trampas que separan un informe correcto de uno que descuadra por céntimos: la división entera y el redondeo del .5. Ya has usado ROUND desde el módulo 2 sin explicación; aquí toca entenderlo de verdad, incluida la parte incómoda: en PostgreSQL, ROUND(2.5) y ROUND(2.5::DOUBLE PRECISION) no devuelven lo mismo.

Contenido

  1. Redondear y truncar: ROUND, TRUNC, CEIL, FLOOR
  2. El redondeo del .5: NUMERIC frente a DOUBLE PRECISION
  3. Aritmética: ABS, SIGN, MOD, POWER, SQRT, logaritmos
  4. GREATEST y LEAST no son MAX y MIN
  5. La división entera y sus tres arreglos
  6. Precisión: el céntimo perdido de TiendaVerde
  7. Casos reales: IVA, margen, descuentos y portes
  8. RANDOM y generate_series
  9. Tabla comparativa por motor
  10. Errores Comunes y Consejos
  11. Ejercicios
  12. Conclusión

  1. Redondear y truncar

Función Qué hace Ejemplo Resultado
ROUND(x) Redondea al entero más próximo ROUND(12.789) 13
ROUND(x, n) Redondea a n decimales ROUND(12.789, 2) 12.79
TRUNC(x) Corta hacia cero, sin redondear TRUNC(12.789) 12
TRUNC(x, n) Corta a n decimales TRUNC(12.789, 2) 12.78
CEIL(x) / CEILING(x) Entero igual o superior CEIL(12.001) 13
FLOOR(x) Entero igual o inferior FLOOR(12.999) 12

Con números negativos las cuatro se separan, y ahí es donde la gente se equivoca:

SELECT TRUNC(-12.789) AS trunc, ROUND(-12.789) AS round,
       CEIL(-12.789)  AS ceil,  FLOOR(-12.789) AS floor;
trunc round ceil floor
-12 -13 -12 -13

TRUNC corta hacia cero (-12.789-12), así que coincide con CEIL en los negativos y con FLOOR en los positivos; FLOOR va siempre hacia abajo, hacia -∞ (-13); y ROUND va al más próximo, que aquí también es -13.

Trampa de tipos. ROUND(x, n) y TRUNC(x, n) solo existen para NUMERIC. Si x es DOUBLE PRECISION, PostgreSQL falla con ERROR: function round(double precision, integer) does not exist. La solución es ROUND(x::NUMERIC, 2), y que la versión de dos argumentos no exista para coma flotante no es un descuido: es el motor diciéndote que redondear un DOUBLE PRECISION a dos decimales no significa lo que crees. La sección 6 lo demuestra.

  1. El redondeo del .5: NUMERIC frente a DOUBLE PRECISION

Cuando el valor cae exactamente en la mitad, hay que decidir hacia dónde. Y PostgreSQL decide distinto según el tipo:

SELECT ROUND(0.5)          AS num_05, ROUND(2.5)          AS num_25,
       ROUND(0.5::FLOAT8)  AS flt_05, ROUND(2.5::FLOAT8)  AS flt_25;
num_05 num_25 flt_05 flt_25
1 3 0 2

Dos respuestas distintas para la misma pregunta. Los literales 0.5 y 2.5 son NUMERIC, y NUMERIC redondea half-up (medio hacia arriba, alejándose del cero). Al convertirlos a coma flotante, PostgreSQL delega en la función rint() del sistema, que redondea half-even: al par más cercano.

Estrategia Nombre habitual 0.5 1.5 2.5 3.5 -2.5
Half-up (NUMERIC) Redondeo comercial 1 2 3 4 -3
Half-even (DOUBLE PRECISION) Redondeo del banquero 0 2 2 4 -2

Ninguna es "la correcta". El half-even no sesga las sumas —la mitad de los empates sube y la mitad baja— y el half-up es el que enseñan en el colegio y el que la mayoría de normativas contables exigen. El problema no es elegir mal, es elegir sin saberlo: si la mitad de tu informe usa NUMERIC y la otra DOUBLE PRECISION, tus totales no cuadrarán entre sí y nadie sabrá por qué.

La regla del curso: el dinero es NUMERIC (01-04). Redondea siempre NUMERIC, y así el .5 sube. Si recibes un DOUBLE PRECISION de fuera, conviértelo con ::NUMERIC antes de redondear, no después.

Y una precisión importante: el famoso ROUND(1.005, 2) se comporta bien con NUMERIC —devuelve 1.01— porque 1.005 se almacena exacto. En DOUBLE PRECISION ese literal es realmente 1.00499999999999989… y redondearía a 1.00. No es un fallo de ROUND: es que el número que le das ya no es 1.005.

  1. Aritmética: ABS, SIGN, MOD, POWER, SQRT, logaritmos

Función Qué hace Ejemplo Resultado
ABS(x) Valor absoluto ABS(-12.789) 12.789
SIGN(x) -1, 0 o 1 según el signo SIGN(-12.789) -1
MOD(a, b), a % b Resto de la división entera MOD(10, 3) 1
POWER(a, b) a elevado a b POWER(2, 10) 1024
SQRT(x) Raíz cuadrada SQRT(16) 4
EXP(x) / LN(x) Exponencial / logaritmo natural EXP(0) 1
LOG(x) Logaritmo en base 10 LOG(100) 2
LOG(b, x) Logaritmo en base b LOG(2, 8) 3

Dos avisos sobre esta tabla. Los resultados que ves son los valores matemáticos: PostgreSQL calcula estas funciones sobre NUMERIC con 16 dígitos significativos, así que SELECT SQRT(16); muestra en realidad 4.0000000000000000. No es un error, es la precisión de trabajo; envuélvelo en ROUND al presentar. Y LOG sin base es base 10 en PostgreSQL pero es el logaritmo natural en MySQL: el mismo SQL da resultados distintos y ninguno de los dos motores avisa.

MOD con negativos merece una nota: en SQL el resto hereda el signo del dividendo, así que MOD(-10, 3) es -1, no 2 (coincide con C y Java, y no con Python). Su uso práctico es partir un conjunto en lotes:

SELECT id, MOD(id, 4) AS lote, fecha_pedido, estado
FROM pedidos WHERE MOD(id, 4) = 0 ORDER BY id;
id lote fecha_pedido estado
4 0 2025-04-19 entregado
8 0 2025-06-28 entregado
12 0 2025-10-01 entregado
16 0 2025-12-19 enviado
20 0 2026-02-21 pendiente

Es el patrón de los backfills por lotes que 05-06 recomendaba: cuatro procesos concurrentes, cada uno con su resto, sin solaparse.

  1. GREATEST y LEAST no son MAX y MIN

La confusión clásica del módulo, que se resuelve con la distinción de 06-01:

GREATEST / LEAST MAX / MIN
Familia Escalar Agregada
Compara Varias columnas de la misma fila Una columna en muchas filas
Filas del resultado Las mismas que había Una por grupo
SELECT id, precio, coste,
       GREATEST(precio, coste) AS mayor_de_los_dos,
       LEAST(precio, coste)    AS menor_de_los_dos
FROM productos WHERE id IN (1, 5, 15) ORDER BY id;
id precio coste mayor_de_los_dos menor_de_los_dos
1 12.50 7.80 12.50 7.80
5 1.95 0.90 1.95 0.90
15 22.00 12.50 22.00 12.50

Tres filas. La misma consulta con MAX y MIN devolvería una, con el precio más caro y el más barato de todo el catálogo (22.00 y 1.95).

Su uso más frecuente es acotar un valor: GREATEST(stock - 5, 0) nunca baja de cero y LEAST(descuento, 0.30) nunca supera el 30 %. Un CASE haría lo mismo con cuatro líneas más (06-05).

Nota de dialecto: ante un nulo, GREATEST(1, NULL) devuelve 1 en PostgreSQL (ignora los nulos) y NULL en MySQL y Oracle. Es una diferencia real y peligrosa al portar.

  1. La división entera y sus tres arreglos

SELECT 10 / 3 AS division, 10 % 3 AS resto;
division resto
3 1

3, no 3.33. Si los dos operandos son enteros, SQL hace división entera y descarta la parte decimal: no redondea, trunca. No es un capricho de PostgreSQL sino el estándar —el tipo del resultado se deriva del de los operandos, y INTEGER / INTEGER es INTEGER—. Las tres formas de evitarlo:

SELECT 10.0 / 3                  AS con_literal_decimal,
       10::NUMERIC / 3           AS con_cast,
       ROUND(10.0 / 3, 4)        AS redondeado;
con_literal_decimal con_cast redondeado
3.3333333333333333 3.3333333333333333 3.3333

(1) Un operando con decimales: 10.0 / 3 — el literal 10.0 es NUMERIC y arrastra al otro. (2) Conversión explícita: 10::NUMERIC / 3 o CAST(10 AS NUMERIC) / 3, la única que funciona cuando los dos operandos son columnas enteras, donde no puedes "escribir un punto". (3) Multiplicar por 1.0 antes de dividir: 1.0 * cantidad / total, truco viejo, portable y legítimo.

Los dieciséis decimales de las dos primeras columnas son la precisión de trabajo de la división NUMERIC: al menos 16 dígitos significativos. Calcula con esa precisión y redondea solo al presentar.

El caso real que muerde

"¿Cuántas unidades tiene de media una línea de pedido?" La respuesta son 113 / 47 unidades:

SELECT SUM(cantidad)            AS unidades,  COUNT(*)                AS lineas,
       SUM(cantidad) / COUNT(*) AS media_mal, ROUND(AVG(cantidad), 4) AS media_bien
FROM lineas_pedido;
unidades lineas media_mal media_bien
113 47 2 2.4043

SUM(cantidad) es BIGINT y COUNT(*) también, así que la división es entera y devuelve 2 en lugar de 2,4043: un 17 % de error, sin aviso, en una consulta que parece obviamente correcta. AVG no cae en la trampa porque devuelve NUMERIC cuando la entrada es entera; ese es el motivo de que exista.

  1. Precisión: el céntimo perdido de TiendaVerde

En 01-04 se dijo que el dinero va en NUMERIC y nunca en coma flotante. Toca demostrarlo.

SELECT 0.1 + 0.2 AS en_numeric, 0.1::FLOAT8 + 0.2::FLOAT8 AS en_float8,
       (0.1::FLOAT8 + 0.2::FLOAT8) = 0.3::FLOAT8 AS son_iguales;
en_numeric en_float8 son_iguales
0.3 0.30000000000000004 false

REAL y DOUBLE PRECISION almacenan los números en base 2, y 0,1 no tiene representación exacta en base 2, igual que 1/3 no la tiene en base 10. El error es minúsculo… hasta que se acumula sobre datos reales. La facturación de producto de TiendaVerde —la cifra canónica del curso— calculada de las dos maneras:

SELECT SUM(cantidad * precio_unitario * (1 - descuento))                     AS suma_numeric,
       ROUND(SUM(cantidad * precio_unitario * (1 - descuento)), 2)           AS total_numeric,
       SUM(cantidad * precio_unitario::FLOAT8 * (1 - descuento::FLOAT8))     AS suma_float8,
       ROUND(SUM(cantidad * precio_unitario::FLOAT8
                          * (1 - descuento::FLOAT8))::NUMERIC, 2)            AS total_float8
FROM lineas_pedido;
suma_numeric total_numeric suma_float8 total_float8
727.9450 727.95 727.9449999999998 727.94

727,95 € frente a 727,94 €. Un céntimo, sobre 47 líneas y 20 pedidos: la suma exacta es 727,9450, que en NUMERIC redondea hacia arriba, mientras que el acumulado en coma flotante se queda en 727,9449999999998 y redondea hacia abajo. Y hay algo peor que el céntimo: los últimos dígitos de suma_float8 dependen del orden en que el motor sume las filas. Un plan paralelo, un índice distinto o simplemente más datos pueden cambiarlos, así que la misma consulta sobre los mismos datos puede dar dos resultados distintos. Con NUMERIC eso no pasa nunca.

NUMERIC(10,2) REAL / DOUBLE PRECISION
Representación Decimal exacta Binaria aproximada
Suma de las 47 líneas 727.9450 727.9449999999998
¿Resultado reproducible? Siempre Depende del orden de suma
= fiable No: compara con tolerancia
Velocidad / uso Más lenta · dinero y cantidades exactas Más rápida · medidas físicas, estadística

La regla del curso, ahora justificada: dinero en NUMERIC; calcular con precisión completa y redondear solo al presentar. Redondear en cada paso intermedio introduce error de acumulación; calcular en coma flotante introduce otro peor, porque es impredecible.

  1. Casos reales: IVA, margen, descuentos y portes

7.1. Precio con IVA y margen

SELECT id, nombre, precio, coste,
       ROUND(precio * 1.21, 2)                   AS precio_con_iva,
       ROUND(precio - coste, 2)                  AS margen,
       ROUND((precio - coste) / precio * 100, 1) AS margen_pct
FROM productos WHERE id IN (1, 5, 15, 18) ORDER BY id;
id nombre precio coste precio_con_iva margen margen_pct
1 Aceite de oliva virgen extra 500 ml 12.50 7.80 15.13 4.70 37.6
5 Tomate triturado ecológico 400 g 1.95 0.90 2.36 1.05 53.8
15 Té verde matcha ceremonial 30 g 22.00 12.50 26.62 9.50 43.2
18 Cepillo de dientes de bambú 3.50 1.20 4.24 2.30 65.7

Fíjate en (precio - coste) / precio * 100: los operandos son NUMERIC, así que no hay división entera. Si precio y coste fueran INTEGER (céntimos, por ejemplo), esa expresión daría 0 para los veinte productos: es el error del apartado 5 disfrazado de fórmula de negocio.

7.2. Descuentos aplicados

SELECT id, pedido_id, cantidad, precio_unitario, descuento,
       ROUND(cantidad * precio_unitario, 2)                   AS bruto,
       ROUND(cantidad * precio_unitario * descuento, 2)       AS ahorro,
       ROUND(cantidad * precio_unitario * (1 - descuento), 2) AS importe
FROM lineas_pedido WHERE descuento > 0 ORDER BY id;
id pedido_id cantidad precio_unitario descuento bruto ahorro importe
6 3 6 1.95 0.10 11.70 1.17 10.53
18 8 3 12.50 0.05 37.50 1.88 35.63
24 10 2 18.90 0.10 37.80 3.78 34.02
27 11 8 1.95 0.15 15.60 2.34 13.26
39 16 2 11.20 0.05 22.40 1.12 21.28
45 19 6 4.95 0.10 29.70 2.97 26.73

Seis líneas con descuento de las 47, con un ahorro total de 13,26 €. Observa la línea 18: el ahorro exacto es 1,875 €, que ROUND(…, 2) sube a 1,88 porque NUMERIC redondea half-up; en coma flotante habría dado 1,87.

7.3. Redondeo comercial a .95

Marketing quiere subir un 10 % y dejar los precios terminados en .95:

SELECT id, nombre,
       precio                      AS precio_actual,
       ROUND(precio * 1.10, 2)     AS subida_bruta,
       FLOOR(precio * 1.10) + 0.95 AS precio_comercial
FROM productos WHERE id IN (2, 6, 15, 18) ORDER BY id;
id nombre precio_actual subida_bruta precio_comercial
2 Arroz integral ecológico 1 kg 3.90 4.29 4.95
6 Crema facial de aloe vera 50 ml 18.90 20.79 20.95
15 Té verde matcha ceremonial 30 g 22.00 24.20 24.95
18 Cepillo de dientes de bambú 3.50 3.85 3.95

FLOOR(x) + 0.95 es el patrón canónico: parte entera más los céntimos que quieras. Ojo con el efecto: el arroz sube de 4,29 a 4,95 (un 15 % extra) y el matcha de 24,20 a 24,95 (un 3 %). El redondeo comercial no es neutro; mídelo antes de aplicarlo.

7.4. Reparto de los gastos de envío por línea

El pedido 1 tiene tres líneas (42,10 € de producto) y 4,95 € de portes. ¿Cuánto envío le toca a cada línea, en proporción a su importe?

SELECT lp.id AS linea,
       ROUND(lp.cantidad * lp.precio_unitario * (1 - lp.descuento), 2)               AS importe,
       ROUND(lp.cantidad * lp.precio_unitario * (1 - lp.descuento) / 42.10 * 100, 2) AS peso_pct,
       ROUND(pe.gastos_envio * lp.cantidad * lp.precio_unitario
             * (1 - lp.descuento) / 42.10, 2)                                        AS envio_imputado
FROM lineas_pedido AS lp
JOIN pedidos       AS pe ON pe.id = lp.pedido_id
WHERE lp.pedido_id = 1 ORDER BY lp.id;
linea importe peso_pct envio_imputado
1 23.90 56.77 2.81
2 11.70 27.79 1.38
3 6.50 15.44 0.76

2,81 + 1,38 + 0,76 = 4,95. Cuadra. Pero no siempre cuadra: repite el cálculo para el pedido 3 (4,95 € de portes sobre 29,53 € de producto) y las tres partes redondeadas suman 4,96 €; para el pedido 8, 9,91 € en lugar de 9,90 €. Ese céntimo sobrante es inevitable, porque redondear tres números a dos decimales y sumarlos no tiene por qué dar la suma redondeada. La solución estándar es imputar el residuo a una línea, normalmente la mayor: se calculan n − 1 partes redondeadas y la última se obtiene restando. Aquí necesitarías comparar cada línea con el máximo de su pedido, que es una subconsulta correlacionada y llega en 07-02.

La lección que hay detrás: el reparto proporcional con redondeo siempre produce residuos. Si un informe financiero suma partidas redondeadas, alguien tiene que decidir dónde va el céntimo. Que lo decida tu consulta, no el azar.

  1. RANDOM y generate_series

Dos utilidades que reaparecerán más adelante.

Función Qué hace
RANDOM() Un DOUBLE PRECISION aleatorio en [0, 1)
generate_series(a, b [, paso]) Genera las filas a, a+1, …, b
SELECT n, n * n AS cuadrado FROM generate_series(1, 4) AS g(n) ORDER BY n;
n cuadrado
1 1
2 4
3 9
4 16

generate_series no lee ninguna tabla: fabrica filas de la nada. Eso la convierte en la herramienta para construir informes sin huecos —una fila por mes aunque ese mes no tenga ventas—, que es lo que 03-06 anunció y lo que verás en 06-03 con fechas. RANDOM() sirve para muestreo (ORDER BY RANDOM() LIMIT 10 devuelve diez filas al azar) y para datos de prueba; no muestro su resultado porque cambia en cada ejecución, y por eso mismo nunca la uses en un DEFAULT de columna sin pensarlo (05-06: un DEFAULT volátil reescribe la tabla entera).

  1. Tabla comparativa por motor

Tarea PostgreSQL 16 MySQL 8 SQLite SQL Server Oracle
INTEGER / INTEGER Entera (3) Decimal (3.3333) Entera (3) Entera (3) Decimal (no hay INTEGER)
División entera explícita a / b con enteros a DIV b a / b a / b TRUNC(a/b)
Redondeo del .5 half-up en NUMERIC, half-even en DOUBLE half-up en DECIMAL, depende del sistema en DOUBLE half-up (todo es REAL) half-up half-up
Truncar a n decimales TRUNC(x, n) TRUNCATE(x, n) — (usar CAST) ROUND(x, n, 1) TRUNC(x, n)
Techo / suelo CEIL, CEILING, FLOOR CEIL, CEILING, FLOOR CEIL, FLOOR (3.35+) CEILING, FLOOR CEIL, FLOOR
Módulo MOD(a,b), a % b MOD(a,b), a % b a % b a % b MOD(a,b) (no hay %)
Logaritmo sin base LOG(x) = base 10 LOG(x) = natural LOG(x) = base 10 LOG(x) = natural LOG(b,x) obligatorio
GREATEST / LEAST Sí, ignoran nulos Sí, devuelven NULL con un nulo MAX(a,b) / MIN(a,b) escalares Sí (2022+) Sí, NULL con un nulo
Aleatorio en [0,1) RANDOM() RAND() RANDOM() devuelve un entero de 64 bits RAND() DBMS_RANDOM.VALUE
Serie de enteros generate_series(a,b) CTE recursiva CTE recursiva GENERATE_SERIES (2022+) CONNECT BY LEVEL

Las tres filas que más código rompen al migrar: la división entera (una consulta que funciona en MySQL devuelve ceros en PostgreSQL), el logaritmo sin base (mismo SQL, resultado distinto, sin error) y el RANDOM() de SQLite, que devuelve un entero enorme con signo y no un decimal entre 0 y 1.

Errores Comunes y Consejos

  • Dividir dos enteros y esperar decimales. 10 / 3 es 3. Convierte un operando a NUMERIC, o usa AVG cuando estés calculando una media.
  • Usar ROUND(x, n) sobre un DOUBLE PRECISION. No existe esa función, y el ERROR avisa de algo más profundo: no deberías tener dinero en coma flotante.
  • Suponer que ROUND(2.5) da lo mismo siempre. 3 en NUMERIC, 2 en DOUBLE PRECISION.
  • Guardar dinero en REAL o DOUBLE PRECISION. El error se acumula, los totales dejan de cuadrar y el resultado ni siquiera es reproducible. Y no redondees en cada paso intermedio: calcula con precisión completa y redondea solo al presentar.
  • Confundir GREATEST/LEAST con MAX/MIN. Los primeros comparan columnas de una fila; los segundos, filas de una columna. Y con nulos se comportan distinto según el motor.
  • Esperar que TRUNC y FLOOR coincidan. Solo con positivos: TRUNC(-1.5) es -1 y FLOOR(-1.5) es -2. Y MOD(-10, 3) es -1, no 2.
  • Sumar importes redondeados y esperar que cuadren con el total. No cuadran: hay que decidir dónde va el residuo.
  • Consejo: pon el ::NUMERIC lo antes posible y el ROUND lo más tarde posible, y comprueba siempre que la suma de las partes es el total. Es el equivalente numérico de "el filtro y su negación suman el total" de 04-03.
  • Consejo: usa MOD(id, n) para partir un proceso en lotes disjuntos. Simple, determinista y sin tabla de control.

Ejercicios

Ejercicio 1

Dirección quiere un análisis de rentabilidad del catálogo. Para cada producto activo, devuelve precio, coste, margen absoluto, margen porcentual con un decimal y valor del inventario (stock * coste) a dos decimales. Ordena por margen porcentual descendente y muestra los cinco primeros. ¿Qué tipo de producto domina el ranking y por qué?

Ejercicio 2

Sobre resenas.puntuacion: (1) calcula la media global con 4 decimales y con 0 decimales; (2) calcúlala otra vez escribiendo SUM(puntuacion) / COUNT(*) — ¿qué obtienes y por qué?; (3) calcula la media por producto redondeada a 2 decimales, de peor a mejor. ¿Cuántos productos aparecen y por qué no son 20?

Ejercicio 3

Un compañero afirma que estas dos consultas sobre lineas_pedido son equivalentes:

-- A
SELECT ROUND(SUM(cantidad * precio_unitario * (1 - descuento)), 2) AS total FROM lineas_pedido;
-- B
SELECT SUM(ROUND(cantidad * precio_unitario * (1 - descuento), 2)) AS total FROM lineas_pedido;

(1) Predice si darán el mismo número y ejecútalas. (2) Explica en qué se diferencian conceptualmente. (3) ¿Cuál coincide con lo que factura de verdad la tienda, si cada factura imprime el importe de cada línea redondeado a dos decimales?

Soluciones

Solución 1

SELECT id, nombre, precio, coste,
       ROUND(precio - coste, 2)                  AS margen,
       ROUND((precio - coste) / precio * 100, 1) AS margen_pct,
       ROUND(stock * coste, 2)                   AS valor_inventario
FROM productos WHERE activo = TRUE
ORDER BY margen_pct DESC, id LIMIT 5;
id nombre precio coste margen margen_pct valor_inventario
18 Cepillo de dientes de bambú 3.50 1.20 2.30 65.7 288.00
9 Bálsamo labial de caléndula 15 ml 4.60 1.80 2.80 60.9 234.00
11 Estropajo vegetal de luffa (pack 3) 5.50 2.20 3.30 60.0 242.00
19 Desodorante natural en barra 50 g 7.80 3.30 4.50 57.7 247.50
7 Champú sólido de romero 80 g 8.40 3.60 4.80 57.1 342.00

(El margen porcentual medio del catálogo es 52,03 %.) Dominan los productos baratos de higiene y hogar: sobre un precio pequeño, un coste pequeño deja un porcentaje alto aunque el margen absoluto sean dos euros. Margen porcentual y margen absoluto ordenan de forma distinta: el matcha, con 9,50 € de margen, es el que más dinero deja por unidad y no aparece aquí. Y el producto 20 no puede salir nunca: está descatalogado y el WHERE activo = TRUE lo excluye.

Solución 2

SELECT ROUND(AVG(puntuacion), 4) AS media_4dec, ROUND(AVG(puntuacion), 0) AS media_0dec,
       SUM(puntuacion) AS suma, COUNT(*) AS resenas,
       SUM(puntuacion) / COUNT(*) AS media_entera
FROM resenas;
media_4dec media_0dec suma resenas media_entera
4.0833 4 49 12 4

1 y 2. AVG(puntuacion) devuelve 4.0833333333333333: puntuacion es SMALLINT, pero AVG sobre enteros devuelve NUMERIC. En cambio SUM(puntuacion) / COUNT(*) divide 49 entre 12 como enteros y devuelve 4. La coincidencia con el redondeo a 0 decimales es casual: si la media fuera 4,9, la división entera seguiría dando 4 y el redondeo daría 5. 3.

SELECT producto_id, COUNT(*) AS resenas, ROUND(AVG(puntuacion), 2) AS media
FROM resenas GROUP BY producto_id ORDER BY media, producto_id;
producto_id resenas media
16 1 2.00
5 1 3.00
12 1 3.00
10 1 4.00
18 1 4.00
2 2 4.50
6 2 4.50
1 2 5.00
15 1 5.00

9 productos, no 20: resenas solo tiene 12 filas sobre 9 productos distintos, y un GROUP BY sobre esa tabla no puede inventar los 11 que nadie ha valorado. Para que aparecieran haría falta partir de productos con un LEFT JOIN (03-03), y su media sería NULL — que no es 0, y que se presenta con COALESCE (06-04).

Solución 3

1. Las dos devuelven 727.95, pero por suerte: los 47 importes de línea de TiendaVerde tienen a lo sumo tres decimales y los residuos de redondeo se compensan. Con otros datos no coincidirían. 2. No son la misma pregunta. A suma primero y redondea al final: es el importe matemáticamente exacto, redondeado una sola vez, con un error máximo de medio céntimo siempre. B redondea cada línea y luego suma: introduce hasta medio céntimo de error por línea, así que con 47 líneas el error acumulado puede llegar a unos 24 céntimos y con un millón de líneas, a 5.000 €.

3. Y aquí el enunciado invierte la respuesta: si la factura que recibe el cliente imprime cada línea redondeada a dos decimales, lo que la tienda cobra de verdad es la suma de esas líneas redondeadas, es decir B. En ese caso B no es un error, es la definición del importe facturado, y A sería la cifra que no cuadra con los papeles. La moraleja no es "redondea al final siempre" sino "redondea donde lo hace el negocio": la regla del curso vale para informes analíticos, mientras que en facturación el redondeo por línea es un requisito legal en muchos países. Lo que nunca es aceptable es no saber cuál de las dos estás calculando.

Conclusión

Ya calculas con criterio:

  • Redondeas con ROUND, cortas con TRUNC y aproximas con CEIL/FLOOR, sabiendo que con negativos las cuatro se separan. Conoces la trampa del .5: NUMERIC redondea half-up y DOUBLE PRECISION half-even, por lo que ROUND(2.5) puede ser 3 o 2 según el tipo. Y ROUND(x, n) no existe para coma flotante, lo cual es una advertencia, no una limitación.
  • Manejas ABS, SIGN, MOD (cuyo resto hereda el signo del dividendo), POWER, SQRT y los logaritmos, con el aviso de que LOG sin base no significa lo mismo en PostgreSQL que en MySQL. Y distingues GREATEST/LEAST —escalares— de MAX/MIN —agregadas—.
  • Evitas la división entera con un literal decimal, un ::NUMERIC o un 1.0 *, y sabes reconocerla: SUM(cantidad) / COUNT(*) daba 2 donde la media es 2,4043. Y has visto el céntimo perdido con datos reales: 727,95 € en NUMERIC frente a 727,94 € en coma flotante, con la agravante de que el resultado en coma flotante ni siquiera es reproducible.
  • Aplicas todo eso a TiendaVerde: IVA, márgenes, descuentos, redondeo comercial a .95 y reparto proporcional de portes, con la advertencia de que el reparto redondeado no siempre cuadra y alguien debe decidir dónde va el residuo.

En la lección siguiente, funciones de fecha y hora, llega el tipo de dato que más preguntas de negocio responde y más errores esconde. Verás por qué TiendaVerde guarda todo como DATE y qué cambiaría con TIMESTAMP y zonas horarias, la diferencia entre NOW() y CLOCK_TIMESTAMP(), la aritmética con INTERVAL, y por fin EXTRACT y DATE_TRUNC, que son la deuda que 04-05 dejó pendiente: agrupar la facturación por mes y por trimestre sin escribir veinte condiciones a mano. Y volverá generate_series, esta vez para que un informe mensual muestre los meses en los que no se vendió nada.

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