Usuarios y permisos
Tu base de datos no se accede con una sola cuenta todopoderosa. Creas usuarios por rol (la app, el lector de informes, el administrador) y repartes privilegios con el principio del mínimo privilegio: cada cuenta, lo justo que necesita y nada más.
1.1 · Crear y dar permisos
CREATE USER 'app_tienda'@'localhost'
IDENTIFIED BY 'clave_fuerte_mayus9!';
GRANT SELECT, INSERT, UPDATE, DELETE
ON tw_ecommerce.* TO 'app_tienda'@'localhost';
tw_ecommerce.* significa «todas las tablas de esa base». Los privilegios y tablas pueden
precisarse más: GRANT SELECT ON tw_ecommerce.pagos TO ….
1.2 · Retirar y comprobar
REVOKE DELETE ON tw_ecommerce.* FROM 'app_tienda'@'localhost';
SHOW GRANTS FOR 'app_tienda'@'localhost';
ON tw_ecommerce.*
se revoca con ON tw_ecommerce.*. Si quisieras retirar un permiso SOLO de una tabla concreta
(p. ej. pagos), tendrías que haberlo concedido de forma explícita para esa tabla antes;
un revoke a nivel de tabla no resta lo concedido con .*. Este comportamiento es el mismo en
MySQL y MariaDB.GRANT OPTION de gratis y auditar con SHOW GRANTS antes y después de tocar
permisos.Transacciones (ACID)
Una transacción agrupa varias operaciones en una sola unidad: o se aplican todas o ninguna. De ahí cuatro propiedades que todos citan con siglas: Atomicity (indivisible), Consistency (estado coherente), Isolation (aisladas entre sí) e Durability (si queda hecha, queda hecha).
2.1 · COMMIT y ROLLBACK
START TRANSACTION;
INSERT INTO pedidos (usuario_id, estado) VALUES (2, 'creado');
INSERT INTO pedido_detalle (pedido_id, producto_id, cantidad, precio_unitario)
VALUES (LAST_INSERT_ID(), 1, 1, 449.00);
COMMIT;
START TRANSACTION;
UPDATE productos SET stock = stock - 2 WHERE id = 1;
-- ups, el pago no llega…
ROLLBACK;
2.2 · SAVEPOINT: quedarte a mitad
No siempre quieres deshacerlo todo: SAVEPOINT marca un punto intermedio y
ROLLBACK TO vuelve solo hasta ese punto.
START TRANSACTION;
INSERT INTO usuarios (nombre, email, password_hash) VALUES
('Ana Polo', 'ana@tienda.es', '$2y$11$hash');
SAVEPOINT antes_linea;
INSERT INTO direcciones (usuario_id, calle, ciudad, cp) VALUES
(LAST_INSERT_ID(), 'C/ Sur 4', 'Zaragoza', '50001');
-- CP no válido… deshacemos solo la dirección
ROLLBACK TO antes_linea;
COMMIT;
START TRANSACTION,
cada sentencia se confirma sola (autocommit). La transacción explícita es para bloques donde «o todo, o nada».Copias de seguridad
El dato vale más que el servidor. La herramienta clásica de MySQL es mysqldump: vuelca
esquema y datos a un fichero .sql que luego se restaura con un simple import.
3.1 · Hacer y restaurar un backup
# volcar la base completa
mysqldump -u root -p tw_ecommerce > backups/tw_ecommerce_2026.sql
# restaurar meses después en un servidor nuevo
mysql -u root -p < backups/tw_ecommerce_2026.sql
3.2 · Variantes útiles
mysqldump --single-transaction — volcado sin bloquear escrituras (InnoDB)--no-data — solo esquema (CREATE TABLE), sin filas--no-create-info — solo datos, para sumarlos a un esquema existentemysqldump … | gzip > backup.sql.gzSeguridad de datos
Tres frentes de seguridad trabajan en tu esquema: contraseñas no legibles, consulta sin inyección y tipos de dato modernos (como JSON).
4.1 · Ni una contraseña en claro
El campo password_hash VARCHAR(255) guarda un hash (bcrypt aquí): nunca la
contraseña original. En tu código solo guardas password_hash() y verificas con
password_verify().
INSERT INTO usuarios (nombre, email, password_hash)
VALUES ('Ana', 'ana@tienda.es', 'clara12');
4.2 · SQL injection y consultas paramétricas
Si pegas $_POST['email'] en una consulta, un atacante la reescribe. La defensa es no pegar
nunca texto en SQL: usar consultas con parámetros (prepared statements). Antes de escribir
cualquier WHERE, pregúntate si el valor viene del usuario.
-- versión insegura (nunca)
SELECT * FROM usuarios
WHERE email = 'clara@tienda.es' OR 1=1';
-- versión segura con placeholder
SELECT * FROM usuarios WHERE email = ?;
4.3 · Tipos modernos: JSON
Las columnas JSON guardan atributos flexibles sin explotar en tablas: metadatos de producto,
preferencias… Se consultan con JSON_VALUE o ->>:
SELECT nombre,
JSON_VALUE(datos_extra, '$.autonomia_km') AS autonomia_km
FROM productos
WHERE categoria_id = 1;
Proyecto final e-commerce
Este bloque se cierra con un proyecto final que integra los 8 bloques: administras la
base tw_ecommerce (normalizada en el bloque 5, poblada aquí con datos reales) como si fuera una
tienda de producción. Operaciones típicas de tu «trabajo»:
5.1 · El arranque de un día de tienda
SELECT u.nombre, p.fecha, p.estado
FROM pedidos p
INNER JOIN usuarios u ON u.id = p.usuario_id
WHERE p.estado = 'creado'
ORDER BY p.fecha DESC;
5.2 · Tu guion de despliegue
CREATE USER 'app_tienda'@'localhost' IDENTIFIED BY 'clave_fuerte';
GRANT SELECT, INSERT, UPDATE ON tw_ecommerce.* TO 'app_tienda'@'localhost';
-- nada de borrar desde la app
REVOKE DELETE ON tw_ecommerce.* FROM 'app_tienda'@'localhost';
-- mysqldump -u root -p tw_ecommerce > backup.sql
SELECT * FROM pagos;
✍️ Los 15 ejercicios del bloque
Abre MySQL Workbench y ejecuta sobre la base tw_ecommerce. Usa root para
los ejercicios de administración (usuarios, permisos) y el usuario app_tienda solo para
consulta. Añade un comentario -- con el número de cada ejercicio.
mysql -u root -p < sql/sql-bloque8_ecommerce.sql.Sección A · Usuarios y permisos
1 Crear el usuario de la app
Crea el usuario 'app_tienda'@'localhost' con CREATE USER y contraseña
'clave_fuerte_mayus9!'.
Ver solución
-- Ejercicio 1: usuario de la aplicación
CREATE USER 'app_tienda'@'localhost'
IDENTIFIED BY 'clave_fuerte_mayus9!';
2 Dar permisos a la app
Con GRANT da a 'app_tienda'@'localhost' los privilegios SELECT, INSERT y
UPDATE (no DELETE) sobre tw_ecommerce.*.
Ver solución
-- Ejercicio 2: mínimo privilegio para la app
GRANT SELECT, INSERT, UPDATE
ON tw_ecommerce.* TO 'app_tienda'@'localhost';
3 Quitar un privilegio
La app ya no debe poder borrar nada: REVOKE DELETE ON tw_ecommerce.* de
'app_tienda'@'localhost' (mantiene SELECT, INSERT y UPDATE intactos).
Ver solución
-- Ejercicio 3: quitar DELETE a nivel de base
REVOKE DELETE ON tw_ecommerce.* FROM 'app_tienda'@'localhost';
4 Comprobar los permisos
Muestra con SHOW GRANTS todos los permisos de 'app_tienda'@'localhost'.
Ver solución
-- Ejercicio 4: auditoría de permisos
SHOW GRANTS FOR 'app_tienda'@'localhost';
Sección B · Transacciones
5 Deshacer una escritura
Abre una transacción (START TRANSACTION), actualiza el stock del producto 1,
comprueba el cambio y deshazlo con ROLLBACK.
Ver solución
-- Ejercicio 5: transacción deshecha
START TRANSACTION;
UPDATE productos SET stock = stock - 2 WHERE id = 1;
-- SELECT stock FROM productos WHERE id = 1; (cambia)
ROLLBACK;
6 Pedido completo y confirmado
En una transacción añade un pedido para el usuario 2, su línea de pedido_detalle con el
producto 1, y termina con COMMIT para quedarlo.
Ver solución
-- Ejercicio 6: pedido que llega al final
START TRANSACTION;
INSERT INTO pedidos (usuario_id, estado) VALUES (2, 'creado');
INSERT INTO pedido_detalle (pedido_id, producto_id, cantidad, precio_unitario)
VALUES (LAST_INSERT_ID(), 1, 1, 449.00);
COMMIT;
7 SAVEPOINT: a mitad del camino
Con START TRANSACTION, inserta un usuario, marca SAVEPOINT s1, inserta una
dirección que luego deshaces con ROLLBACK TO s1, y cierra con COMMIT.
Ver solución
-- Ejercicio 7: punto intermedio
START TRANSACTION;
INSERT INTO usuarios (nombre, email, password_hash)
VALUES ('Zoe Ruiz', 'zoe@tienda.es', '$2y$11$hashfake');
SAVEPOINT s1;
INSERT INTO direcciones (usuario_id, calle, ciudad, cp)
VALUES (LAST_INSERT_ID(), 'C/ Norte 2', 'Bilbao', '000');
ROLLBACK TO s1;
COMMIT;
Sección C · Copias de seguridad
8 Volcar la base (terminal)
Desde la terminal, haz una copia completa de tw_ecommerce a un fichero con mysqldump
(no hace falta escribirlo en el fichero SQL).
Ver solución
-- Ejercicio 8: volcado completo (terminal)
mysqldump -u root -p tw_ecommerce > backups/tw_ecommerce_dia.sql
9 Restaurar la copia (terminal)
Simula la restauración del backup tw_ecommerce_dia.sql en el servidor con el cliente mysql.
Ver solución
-- Ejercicio 9: restauración (terminal)
mysql -u root -p < backups/tw_ecommerce_dia.sql
10 Solo esquema (terminal)
Vuelca únicamente el esquema (sin datos) de tw_ecommerce con mysqldump --no-data.
Ver solución
-- Ejercicio 10: solo la estructura
mysqldump -u root -p --no-data tw_ecommerce > backups/tw_ecommerce_schema.sql
Sección D · Seguridad de datos
11 Leer un JSON
Muestra nombre y la autonomía ($.autonomia_km) con
JSON_VALUE(datos_extra, …) de los productos de la categoría 1.
Ver solución
-- Ejercicio 11: consultar un campo JSON
SELECT nombre,
JSON_VALUE(datos_extra, '$.autonomia_km') AS autonomia_km
FROM productos
WHERE categoria_id = 1;
12 Login nunca en claro
Consulta el usuario de Clara solo por email, mostrando id, nombre y
password_hash (¡el hash, nunca la contraseña real!).
Ver solución
-- Ejercicio 12: el hash, jamás el texto plano
SELECT id, nombre, password_hash
FROM usuarios
WHERE email = 'clara@tienda.es';
13 Pago con su importe esperado
Muestra los pagos con pedido_id, metodo e importe cuyo importe
sea mayor o igual a 100 y ordena por importe descendente.
Ver solución
-- Ejercicio 13: importes relevantes
SELECT pedido_id, metodo, importe
FROM pagos
WHERE importe >= 100.00
ORDER BY importe DESC;
Sección E · Consultas de la tienda
14 La factura de un pedido
Junta pedidos, usuarios, pedido_detalle y productos
para mostrar u.nombre, pr.nombre, d.cantidad y
d.precio_unitario del pedido 1.
Ver solución
-- Ejercicio 14: factura completa
SELECT u.nombre, pr.nombre AS producto,
d.cantidad, d.precio_unitario
FROM pedidos p
INNER JOIN usuarios u ON u.id = p.usuario_id
INNER JOIN pedido_detalle d ON d.pedido_id = p.id
INNER JOIN productos pr ON pr.id = d.producto_id
WHERE p.id = 1;
15 Reponer el stock
Consulta nombre y stock de los productos con stock menor que 10,
ordenados por stock (menores primero).
Ver solución
-- Ejercicio 15: bajo de stock
SELECT nombre, stock
FROM productos
WHERE stock < 10
ORDER BY stock;
16 🏆 Proyecto final: guion de despliegue seguro
Crea el fichero despliegue_seguro.sql que prepare el e-commerce de producción. Requisitos:
- Crea la cuenta
'app_tienda'@'localhost'conCREATE USER. - Dale solo lo necesario con
GRANT(SELECT, INSERT, UPDATE sobretw_ecommerce.*). - Quítale DELETE a nivel de base (
ON tw_ecommerce.*) conREVOKE. - Abre una transacción con
START TRANSACTION. - Inserta un pedido del usuario 2 (con
INSERT) y una línea de detalle conLAST_INSERT_ID(). - Confirma con
COMMIT. - Verifica con un
SELECTque una pedidos con usuarios (JOIN) y filtraWHERE estado = 'creado'. - Ordena la verificación con
ORDER BY fecha DESC.
Ver una solución posible
-- Proyecto final: despliegue seguro del e-commerce
CREATE USER 'app_tienda'@'localhost' IDENTIFIED BY 'clave_fuerte_mayus9!';
GRANT SELECT, INSERT, UPDATE ON tw_ecommerce.* TO 'app_tienda'@'localhost';
REVOKE DELETE ON tw_ecommerce.* FROM 'app_tienda'@'localhost';
START TRANSACTION;
INSERT INTO pedidos (usuario_id, estado) VALUES (2, 'creado');
INSERT INTO pedido_detalle (pedido_id, producto_id, cantidad, precio_unitario)
VALUES (LAST_INSERT_ID(), 1, 1, 449.00);
COMMIT;
-- verificación final
SELECT u.nombre, p.id AS pedido_id, p.estado, p.fecha
FROM pedidos p
INNER JOIN usuarios u ON u.id = p.usuario_id
WHERE p.estado = 'creado'
ORDER BY p.fecha DESC;
📝 Examen del Bloque 8
Dos partes: un test teórico autoevaluable (mínimo 70 %) y el reto práctico final de autocorrección con laboratorio. Supera ambas y el curso de SQL es tuyo. 🎓
Parte B · Reto «despliegue seguro»
Crea despliegue_seguro.sql: usuario de la app, permisos con mínimos privilegios,
transacción para un pedido y verificación con JOIN. Tu SQL no se ejecuta en el laboratorio: se valida con
reglas automáticas. Mínimo para aprobar: 6 de 8 casillas.
Cumplidos: 0 / 8