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
- Redondear y truncar:
ROUND,TRUNC,CEIL,FLOOR - El redondeo del
.5:NUMERICfrente aDOUBLE PRECISION - Aritmética:
ABS,SIGN,MOD,POWER,SQRT, logaritmos GREATESTyLEASTno sonMAXyMIN- La división entera y sus tres arreglos
- Precisión: el céntimo perdido de TiendaVerde
- Casos reales: IVA, margen, descuentos y portes
RANDOMygenerate_series- Tabla comparativa por motor
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- 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)yTRUNC(x, n)solo existen paraNUMERIC. SixesDOUBLE PRECISION, PostgreSQL falla conERROR: function round(double precision, integer) does not exist. La solución esROUND(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 unDOUBLE PRECISIONa dos decimales no significa lo que crees. La sección 6 lo demuestra.
- El redondeo del
.5: NUMERIC frente a DOUBLE PRECISION
.5: NUMERIC frente a DOUBLE PRECISIONCuando 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 siempreNUMERIC, y así el.5sube. Si recibes unDOUBLE PRECISIONde fuera, conviértelo con::NUMERICantes 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.
- Aritmética:
ABS, SIGN, MOD, POWER, SQRT, logaritmos
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:
| 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.
GREATEST y LEAST no son MAX y MIN
GREATEST y LEAST no son MAX y MINLa 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)devuelve1en PostgreSQL (ignora los nulos) yNULLen MySQL y Oracle. Es una diferencia real y peligrosa al portar.
- La división entera y sus tres arreglos
| 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.
- 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 |
Sí | 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.
- 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.
RANDOM y generate_series
RANDOM y generate_seriesDos 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 |
| 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).
- 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 / 3es3. Convierte un operando aNUMERIC, o usaAVGcuando estés calculando una media. - Usar
ROUND(x, n)sobre unDOUBLE PRECISION. No existe esa función, y elERRORavisa de algo más profundo: no deberías tener dinero en coma flotante. - Suponer que
ROUND(2.5)da lo mismo siempre.3enNUMERIC,2enDOUBLE PRECISION. - Guardar dinero en
REALoDOUBLE 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/LEASTconMAX/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
TRUNCyFLOORcoincidan. Solo con positivos:TRUNC(-1.5)es-1yFLOOR(-1.5)es-2. YMOD(-10, 3)es-1, no2. - Sumar importes redondeados y esperar que cuadren con el total. No cuadran: hay que decidir dónde va el residuo.
- Consejo: pon el
::NUMERIClo antes posible y elROUNDlo 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 conTRUNCy aproximas conCEIL/FLOOR, sabiendo que con negativos las cuatro se separan. Conoces la trampa del.5:NUMERICredondea half-up yDOUBLE PRECISIONhalf-even, por lo queROUND(2.5)puede ser3o2según el tipo. YROUND(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,SQRTy los logaritmos, con el aviso de queLOGsin base no significa lo mismo en PostgreSQL que en MySQL. Y distinguesGREATEST/LEAST—escalares— deMAX/MIN—agregadas—. - Evitas la división entera con un literal decimal, un
::NUMERICo un1.0 *, y sabes reconocerla:SUM(cantidad) / COUNT(*)daba2donde la media es2,4043. Y has visto el céntimo perdido con datos reales: 727,95 € enNUMERICfrente 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
.95y 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
- ¿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
