Apuntes DAM
Volver al inicio

Banco: vistas, disparadores, UPSERT y transacciones

Ejercicio de SQLMuy difícilUnos 100 minutos

Ocho apartados sobre las cuentas de un banco pequeño que modifican la base de datos: una vista, un disparador que actualiza saldos, otro que ignora operaciones imposibles con RAISE(IGNORE), intereses con UPDATE, comisiones con INSERT … SELECT, un DELETE, un UPSERT y un traspaso en una transacción.

  • CREATE VIEW
  • CREATE TRIGGER (AFTER y BEFORE)
  • NEW y RAISE(IGNORE)
  • UPDATE con ROUND
  • INSERT … SELECT
  • DELETE con subconsulta
  • UPSERT (ON CONFLICT)
  • BEGIN y COMMIT

Enunciado

Un banco pequeño guarda sus clientes, sus cuentas (corrientes y de ahorro) y los movimientos de cada cuenta: ingresos en positivo y cargos en negativo. La tabla resumen guarda los ingresos y gastos de cada cuenta por mes, y se rellena con un proceso nocturno.

Todos los apartados de este ejercicio cambian algo: crean objetos (vistas y disparadores) o modifican datos. Como en una base de datos real no se puede ver el resultado de un UPDATE directamente, cada apartado dice qué consulta se usará para comprobar el estado final de la base de datos. Si tu sentencia da un error, el apartado no se corrige.

En SQLite los disparadores no pueden usar variables ni bloques como en MySQL o PL/SQL, pero tienen lo esencial: NEW y OLD para la fila afectada, WHEN para la condición y RAISE para detener o ignorar la operación.

Qué hay que hacer

  1. Escribe la sentencia o sentencias de cada apartado (puedes poner varias separadas por ;). Cada apartado indica la consulta con la que se comprueba.
  2. Cada apartado se corrige por separado sobre una copia nueva de la base de datos, con exactamente los datos de las tablas de arriba: lo que hagas en uno no afecta a los demás.
  3. Importan las filas, el número y el orden de las columnas y, cuando el apartado pide un orden, el orden de las filas; los nombres de las columnas no importan. Redondea con ROUND cuando se pidan decimales.

Las tablas y sus datos

Base de datos del ejercicio · 4 tablas

  • clientes
  • cuentas
  • movimientos
  • resumen

Subrayada, la clave primaria; con flecha, la columna a la que apunta cada clave ajena.

clientes
cliente_idnombredni
1Ana Gil12345678Z
2Luis Mora23456789D
3Eva Ruiz34567890V
4Pedro Sanz45678901G
cuentas
cuenta_idcliente_idnumerotiposaldoabiertacerrada
11C-001corriente1250.42019-05-10NULL
21A-001ahorro83002020-01-15NULL
32C-002corriente340.752021-03-01NULL
43C-003corriente22102018-11-20NULL
53A-002ahorro9502022-06-30NULL
64C-004corriente02015-02-012020-12-31
movimientos
movimiento_idcuenta_idfechaconceptoimporte
112026-09-01Nómina1800
212026-09-03Alquiler-750
312026-09-15Supermercado-120.35
432026-09-01Nómina1200
532026-09-05Hipoteca-610
632026-09-20Luz-48.2
742026-09-02Transferencia recibida300
822026-09-30Intereses10.37
962019-06-01Recibo-25
1062020-11-15Recibo-30
1112026-10-01Nómina1800
1232026-10-02Supermercado-95.6
resumen
cuenta_idmesingresosgastos
12026-081800900
12026-0900
Ver el script SQL que crea la base de datos
sql
1CREATE TABLE clientes (
2  cliente_id INTEGER PRIMARY KEY,
3  nombre TEXT NOT NULL,
4  dni TEXT NOT NULL UNIQUE
5);
6CREATE TABLE cuentas (
7  cuenta_id INTEGER PRIMARY KEY,
8  cliente_id INTEGER NOT NULL REFERENCES clientes(cliente_id),
9  numero TEXT NOT NULL UNIQUE,
10  tipo TEXT NOT NULL CHECK (tipo IN ('corriente', 'ahorro')),
11  saldo REAL NOT NULL,
12  abierta DATE NOT NULL,
13  cerrada DATE
14);
15CREATE TABLE movimientos (
16  movimiento_id INTEGER PRIMARY KEY,
17  cuenta_id INTEGER NOT NULL REFERENCES cuentas(cuenta_id),
18  fecha DATE NOT NULL,
19  concepto TEXT NOT NULL,
20  importe REAL NOT NULL
21);
22CREATE TABLE resumen (
23  cuenta_id INTEGER NOT NULL REFERENCES cuentas(cuenta_id),
24  mes TEXT NOT NULL,
25  ingresos REAL NOT NULL,
26  gastos REAL NOT NULL,
27  PRIMARY KEY (cuenta_id, mes)
28);
29
30INSERT INTO clientes (cliente_id, nombre, dni) VALUES
31  (1, 'Ana Gil', '12345678Z'),
32  (2, 'Luis Mora', '23456789D'),
33  (3, 'Eva Ruiz', '34567890V'),
34  (4, 'Pedro Sanz', '45678901G');
35
36INSERT INTO cuentas (cuenta_id, cliente_id, numero, tipo, saldo, abierta, cerrada) VALUES
37  (1, 1, 'C-001', 'corriente', 1250.4, '2019-05-10', NULL),
38  (2, 1, 'A-001', 'ahorro', 8300, '2020-01-15', NULL),
39  (3, 2, 'C-002', 'corriente', 340.75, '2021-03-01', NULL),
40  (4, 3, 'C-003', 'corriente', 2210, '2018-11-20', NULL),
41  (5, 3, 'A-002', 'ahorro', 950, '2022-06-30', NULL),
42  (6, 4, 'C-004', 'corriente', 0, '2015-02-01', '2020-12-31');
43
44INSERT INTO movimientos (movimiento_id, cuenta_id, fecha, concepto, importe) VALUES
45  (1, 1, '2026-09-01', 'Nómina', 1800),
46  (2, 1, '2026-09-03', 'Alquiler', -750),
47  (3, 1, '2026-09-15', 'Supermercado', -120.35),
48  (4, 3, '2026-09-01', 'Nómina', 1200),
49  (5, 3, '2026-09-05', 'Hipoteca', -610),
50  (6, 3, '2026-09-20', 'Luz', -48.2),
51  (7, 4, '2026-09-02', 'Transferencia recibida', 300),
52  (8, 2, '2026-09-30', 'Intereses', 10.37),
53  (9, 6, '2019-06-01', 'Recibo', -25),
54  (10, 6, '2020-11-15', 'Recibo', -30),
55  (11, 1, '2026-10-01', 'Nómina', 1800),
56  (12, 3, '2026-10-02', 'Supermercado', -95.6);
57
58INSERT INTO resumen (cuenta_id, mes, ingresos, gastos) VALUES
59  (1, '2026-08', 1800, 900),
60  (1, '2026-09', 0, 0);

Guía paso a paso

Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.

1. Cada apartado parte de cero

Cada apartado se ejecuta sobre una copia nueva de la base de datos: el disparador del apartado 2 no existe en el apartado 8. Si un apartado necesita algo, se crea en ese mismo apartado.

2. Un disparador por dentro

Se dispara BEFORE o AFTER una operación sobre una tabla, opcionalmente solo WHEN se cumple una condición. Dentro, NEW es la fila que entra (y OLD la que sale, en UPDATE y DELETE).

sql
CREATE TRIGGER trg_saldo AFTER INSERT ON movimientos
BEGIN
  UPDATE cuentas SET saldo = ROUND(saldo + NEW.importe, 2)
  WHERE cuenta_id = NEW.cuenta_id;
END;
3. Insertar a partir de una consulta

INSERT INTO t (cols) SELECT … crea una fila por cada fila del SELECT, y el SELECT puede mezclar columnas con valores fijos. Es la forma de hacer operaciones masivas sin un bucle.

4. Insertar o actualizar (UPSERT)

ON CONFLICT (clave) DO UPDATE SET … evita tener que comprobar antes si la fila existe. En el SET, excluded.columna es el valor que se intentaba insertar.

5. Comprobar antes de modificar

Antes de un UPDATE o un DELETE, ejecuta un SELECT con el mismo WHERE. Y para ver qué ha hecho tu sentencia, ejecuta después la consulta de comprobación del apartado.

Resuélvelo aquí

Cada apartado se corrige por separado contra la base de datos de arriba: escribe la consulta y pulsa «Comprobar».

🗄️SQLApartado 1Muy difícil

Crea la vista v_saldos con el nombre del cliente, el número de cuenta, el tipo y el saldo de las cuentas abiertas (las que no tienen fecha de cierre). Se comprueba con SELECT * FROM v_saldos ORDER BY 2.

🗄️SQLApartado 2Muy difícil

Crea el disparador trg_saldo que, después de insertar un movimiento, sume su importe al saldo de su cuenta (redondeando el saldo a dos decimales). Después inserta estos dos movimientos: cuenta 3, 2026-10-03, 'Gimnasio', -39.90; y cuenta 5, 2026-10-03, 'Ingreso', 200. Se comprueba con SELECT cuenta_id, saldo FROM cuentas ORDER BY cuenta_id.

🗄️SQLApartado 3Muy difícil

Las cuentas de ahorro no pueden quedarse en negativo. Crea el disparador trg_ahorro que, antes de insertar un movimiento en una cuenta de ahorro, ignore la inserción (sin dar error) si el saldo actual más el importe sería menor que 0. Después inserta: cuenta 2, 2026-10-04, 'Retirada', -9000; cuenta 5, 2026-10-04, 'Retirada', -500; y cuenta 1, 2026-10-04, 'Recibo', -2000. Se comprueba con SELECT cuenta_id, concepto, importe FROM movimientos WHERE fecha = '2026-10-04' ORDER BY cuenta_id.

🗄️SQLApartado 4Muy difícil

Aplica los intereses: las cuentas de ahorro abiertas con un saldo de más de 1.000 € ganan un 1,5 %, con el saldo redondeado al céntimo. Se comprueba con SELECT cuenta_id, saldo FROM cuentas ORDER BY cuenta_id.

🗄️SQLApartado 5Muy difícil

Cobra la comisión de mantenimiento: inserta un movimiento de -2 € con fecha 2026-10-31 y concepto 'Comisión de mantenimiento' en cada cuenta corriente abierta con un saldo de menos de 1.500 € (no hace falta actualizar el saldo). Se comprueba con SELECT cuenta_id, fecha, concepto, importe FROM movimientos WHERE fecha = '2026-10-31' ORDER BY cuenta_id.

🗄️SQLApartado 6Muy difícil

Borra los movimientos de las cuentas cerradas antes del 1 de octubre de 2021. Se comprueba con SELECT movimiento_id FROM movimientos ORDER BY movimiento_id.

🗄️SQLApartado 7Muy difícil

Calcula el resumen de septiembre de 2026 de cada cuenta con movimientos en ese mes (ingresos: suma de los importes positivos; gastos: suma de los negativos, en positivo; ambos redondeados a dos decimales) y guárdalo en resumen: inserta las cuentas que no tienen fila de ese mes y actualiza la que ya la tiene. Se comprueba con SELECT * FROM resumen ORDER BY cuenta_id, mes.

🗄️SQLApartado 8Muy difícil

Traspasa 150 € de la cuenta C-001 a la A-002 dentro de una transacción: resta el importe de una, súmalo a la otra y registra los dos movimientos con fecha 2026-10-05 y los conceptos 'Traspaso enviado' (-150) y 'Traspaso recibido' (150). Se comprueba con el saldo y el número de movimientos de ese día de las dos cuentas.

Solución explicada

Ver las soluciones de todos los apartados

Apartado 1

sql
CREATE VIEW v_saldos AS
SELECT cl.nombre, c.numero, c.tipo, c.saldo
FROM cuentas c
JOIN clientes cl ON cl.cliente_id = c.cliente_id
WHERE c.cerrada IS NULL;

Apartado 2

sql
CREATE TRIGGER trg_saldo AFTER INSERT ON movimientos
BEGIN
  UPDATE cuentas SET saldo = ROUND(saldo + NEW.importe, 2) WHERE cuenta_id = NEW.cuenta_id;
END;

INSERT INTO movimientos (cuenta_id, fecha, concepto, importe) VALUES
  (3, '2026-10-03', 'Gimnasio', -39.90),
  (5, '2026-10-03', 'Ingreso', 200);

Apartado 3

sql
CREATE TRIGGER trg_ahorro BEFORE INSERT ON movimientos
WHEN (SELECT tipo FROM cuentas WHERE cuenta_id = NEW.cuenta_id) = 'ahorro'
 AND (SELECT saldo FROM cuentas WHERE cuenta_id = NEW.cuenta_id) + NEW.importe < 0
BEGIN
  SELECT RAISE(IGNORE);
END;

INSERT INTO movimientos (cuenta_id, fecha, concepto, importe) VALUES (2, '2026-10-04', 'Retirada', -9000);
INSERT INTO movimientos (cuenta_id, fecha, concepto, importe) VALUES (5, '2026-10-04', 'Retirada', -500);
INSERT INTO movimientos (cuenta_id, fecha, concepto, importe) VALUES (1, '2026-10-04', 'Recibo', -2000);

Apartado 4

sql
UPDATE cuentas
SET saldo = ROUND(saldo * 1.015, 2)
WHERE tipo = 'ahorro' AND cerrada IS NULL AND saldo > 1000;

Apartado 5

sql
INSERT INTO movimientos (cuenta_id, fecha, concepto, importe)
SELECT cuenta_id, '2026-10-31', 'Comisión de mantenimiento', -2
FROM cuentas
WHERE tipo = 'corriente' AND cerrada IS NULL AND saldo < 1500;

Apartado 6

sql
DELETE FROM movimientos
WHERE cuenta_id IN (SELECT cuenta_id FROM cuentas WHERE cerrada < '2021-10-01');

Apartado 7

sql
INSERT INTO resumen (cuenta_id, mes, ingresos, gastos)
SELECT cuenta_id, '2026-09',
       ROUND(SUM(CASE WHEN importe > 0 THEN importe ELSE 0 END), 2),
       ROUND(SUM(CASE WHEN importe < 0 THEN -importe ELSE 0 END), 2)
FROM movimientos
WHERE fecha LIKE '2026-09-%'
GROUP BY cuenta_id
ON CONFLICT (cuenta_id, mes) DO UPDATE SET ingresos = excluded.ingresos, gastos = excluded.gastos;

Apartado 8

sql
BEGIN;
UPDATE cuentas SET saldo = saldo - 150 WHERE numero = 'C-001';
UPDATE cuentas SET saldo = saldo + 150 WHERE numero = 'A-002';
INSERT INTO movimientos (cuenta_id, fecha, concepto, importe)
  SELECT cuenta_id, '2026-10-05', 'Traspaso enviado', -150 FROM cuentas WHERE numero = 'C-001';
INSERT INTO movimientos (cuenta_id, fecha, concepto, importe)
  SELECT cuenta_id, '2026-10-05', 'Traspaso recibido', 150 FROM cuentas WHERE numero = 'A-002';
COMMIT;

Las vistas guardan consultas, no datos: v_saldos siempre muestra los saldos actuales. Los disparadores mueven reglas de negocio a la base de datos, de modo que se cumplen aunque los datos lleguen desde varias aplicaciones distintas.

RAISE(IGNORE) y RAISE(ABORT, …) son las dos formas de frenar una operación desde un disparador: ignorarla en silencio o rechazarla con un error. En una aplicación real se prefiere el error, para que el usuario sepa por qué no se ha hecho la retirada.

INSERT … SELECT, UPDATE y DELETE con subconsultas, y el UPSERT, operan sobre conjuntos de filas en una sola sentencia: es mucho más rápido y seguro que traer los datos a un programa y modificarlos fila a fila.

El traspaso son cuatro sentencias que solo tienen sentido juntas. La transacción garantiza la atomicidad (todo o nada): sin ella, un fallo después del primer UPDATE haría desaparecer 150 €.

Para ir más allá

  • Cambia trg_ahorro para que dé un error con RAISE(ABORT, 'Saldo insuficiente') y prueba qué pasa con el resto de sentencias del script.
  • Añade un disparador AFTER DELETE que guarde en una tabla de auditoría los movimientos borrados con la fecha del borrado.
  • Escribe el traspaso como un procedimiento almacenado en MySQL con un manejador de errores que haga ROLLBACK.

Dónde se explica