Tema
Vistas
Ya escribiste consultas largas con varios JOINs para obtener el total de cada pedido. ¿Vas a copiar y pegar esas 15 líneas cada vez que las necesites? Una vista guarda una consulta con nombre para usarla como si fuera una tabla. En esta lección aprenderás a crearlas, cuándo se pueden actualizar, qué son las vistas materializadas y cómo usarlas para seguridad.
Concepto
Una vista es una tabla virtual: no guarda datos propios, guarda la consulta. Cada vez que la consultas, la BD ejecuta la consulta original con los datos actuales.
Es como un acceso directo en tu escritorio: no es una copia del archivo, solo apunta a él. Si el archivo cambia, el acceso directo muestra la versión nueva.
txt
┌──────────────────────────┐
│ vista: v_pedidos_resumen│ ← SELECT * FROM v_pedidos_resumen
│ (solo guarda la consulta)│
└─────────────┬────────────┘
│ al consultarla ejecuta:
┌──────────┬───────┴──────┬────────────────┐
│ pedidos │ clientes │ detalle_pedido │ ← tablas reales con los datos
└──────────┴──────────────┴────────────────┘Beneficios:
- Reutilización: escribes la lógica una vez.
- Simplicidad: el equipo (o el sistema de reportes) consulta una "tabla" sencilla sin saber de JOINs.
- Seguridad: das acceso a la vista y no a las tablas, ocultando columnas sensibles.
- Consistencia: todos calculan "total del pedido" de la misma forma.
Ejemplo: vista de resumen de pedidos
sql
CREATE VIEW v_pedidos_resumen AS
SELECT
pe.id AS pedido_id,
pe.fecha,
pe.estado,
cl.id AS cliente_id,
cl.nombre AS cliente,
cl.ciudad,
SUM(d.cantidad) AS piezas,
SUM(d.cantidad * d.precio_unitario) AS total
FROM pedidos pe
JOIN clientes cl ON cl.id = pe.cliente_id
JOIN detalle_pedido d ON d.pedido_id = pe.id
GROUP BY pe.id, pe.fecha, pe.estado, cl.id, cl.nombre, cl.ciudad;Ahora se consulta como una tabla normal:
sql
SELECT pedido_id, cliente, estado, total
FROM v_pedidos_resumen
ORDER BY pedido_id;| pedido_id | cliente | estado | total |
|---|---|---|---|
| 1 | Ana López | entregado | 1597.00 |
| 2 | Carlos Ruiz | entregado | 1847.00 |
| 3 | Ana López | enviado | 3499.00 |
| 4 | María Pérez | pendiente | 1146.00 |
| 5 | Jorge Díaz | cancelado | 899.00 |
Y puedes filtrar, agrupar y unirla con otras tablas:
sql
-- Ventas por ciudad sin cancelados, usando la vista
SELECT ciudad, COUNT(*) AS pedidos, SUM(total) AS ventas
FROM v_pedidos_resumen
WHERE estado <> 'cancelado'
GROUP BY ciudad
ORDER BY ventas DESC;| ciudad | pedidos | ventas |
|---|---|---|
| Ciudad de México | 2 | 5096.00 |
| Guadalajara | 1 | 1847.00 |
| Monterrey | 1 | 1146.00 |
¡Mira qué corta quedó! Toda la complejidad vive en la vista.
Otra vista útil: catálogo público
sql
CREATE VIEW v_catalogo AS
SELECT
p.id,
p.nombre,
COALESCE(c.nombre, 'Sin categoría') AS categoria,
p.precio,
CASE WHEN p.stock > 0 THEN 'Disponible' ELSE 'Agotado' END AS disponibilidad
FROM productos p
LEFT JOIN categorias c ON c.id = p.categoria_id;
SELECT * FROM v_catalogo WHERE categoria = 'Libros';| id | nombre | categoria | precio | disponibilidad |
|---|---|---|---|---|
| 4 | Libro Aprende SQL | Libros | 349.00 | Disponible |
| 5 | Cien años de soledad | Libros | 289.00 | Disponible |
La vista no muestra el stock exacto: el sitio público solo necesita saber si hay o no.
Administrar vistas
sql
-- Modificar (o crear si no existe)
-- MySQL y PostgreSQL:
CREATE OR REPLACE VIEW v_catalogo AS
SELECT p.id, p.nombre, p.precio FROM productos p;
-- Eliminar
DROP VIEW IF EXISTS v_catalogo;
-- Listar vistas
-- MySQL: SHOW FULL TABLES WHERE Table_type = 'VIEW';
-- PostgreSQL: \dv (en psql)
-- SQLite: SELECT name FROM sqlite_master WHERE type = 'view';PostgreSQL y CREATE OR REPLACE
En PostgreSQL, CREATE OR REPLACE VIEW solo permite agregar columnas al final; no puedes quitar ni renombrar columnas existentes. En ese caso haz DROP VIEW y vuelve a crearla. SQLite no tiene CREATE OR REPLACE: usa DROP VIEW IF EXISTS + CREATE VIEW.
Buena práctica
Usa un prefijo como v_ para distinguir vistas de tablas, y guarda sus definiciones en tus scripts o migraciones igual que las tablas.
Vistas actualizables
Si una vista es "simple", puedes hacer INSERT, UPDATE y DELETE a través de ella y el cambio se aplica a la tabla base. Una vista es actualizable si (en general):
- Se basa en una sola tabla.
- No usa
GROUP BY,DISTINCT, funciones de agregación,UNIONni funciones de ventana. - Incluye las columnas obligatorias si quieres insertar.
sql
CREATE VIEW v_productos_electronica AS
SELECT id, nombre, categoria_id, precio, stock
FROM productos
WHERE categoria_id = 1;
-- Actualiza la tabla productos a través de la vista
UPDATE v_productos_electronica SET stock = stock + 5 WHERE id = 3;v_pedidos_resumen no es actualizable (tiene GROUP BY y SUM): ¿qué significaría cambiar un "total"?
WITH CHECK OPTION
Sin protección, podrías insertar un producto de Libros a través de la vista de Electrónica, y "desaparecería" de la vista. WITH CHECK OPTION lo impide:
sql
CREATE OR REPLACE VIEW v_productos_electronica AS
SELECT id, nombre, categoria_id, precio, stock
FROM productos
WHERE categoria_id = 1
WITH CHECK OPTION;
-- Falla: categoria_id = 2 no cumple el WHERE de la vista
INSERT INTO v_productos_electronica (id, nombre, categoria_id, precio, stock)
VALUES (20, 'Libro intruso', 2, 100.00, 1);Vistas materializadas (PostgreSQL)
Una vista normal ejecuta su consulta cada vez. Si la consulta tarda 20 segundos (reporte sobre millones de pedidos), cada consulta a la vista tarda 20 segundos.
Una vista materializada guarda físicamente el resultado, como una "foto" de los datos. Consultarla es instantáneo, pero hay que refrescarla para ver datos nuevos.
sql
-- PostgreSQL
CREATE MATERIALIZED VIEW mv_ventas_mensuales AS
SELECT
DATE_TRUNC('month', pe.fecha)::date AS mes,
COUNT(DISTINCT pe.id) AS pedidos,
SUM(d.cantidad * d.precio_unitario) AS ventas
FROM pedidos pe
JOIN detalle_pedido d ON d.pedido_id = pe.id
WHERE pe.estado <> 'cancelado'
GROUP BY DATE_TRUNC('month', pe.fecha);
SELECT * FROM mv_ventas_mensuales ORDER BY mes;| mes | pedidos | ventas |
|---|---|---|
| 2024-05-01 | 2 | 3444.00 |
| 2024-06-01 | 2 | 4645.00 |
sql
-- Actualizar la "foto" (por ejemplo, cada noche con un cron)
REFRESH MATERIALIZED VIEW mv_ventas_mensuales;
-- Sin bloquear lecturas mientras se refresca (requiere un índice único)
CREATE UNIQUE INDEX ON mv_ventas_mensuales (mes);
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_ventas_mensuales;| Vista normal | Vista materializada | |
|---|---|---|
| Guarda datos | No | Sí |
| Datos | Siempre actuales | Del último REFRESH |
| Velocidad de lectura | La de la consulta | Muy rápida |
| Admite índices | No | Sí |
| Ideal para | Simplificar y seguridad | Reportes y dashboards pesados |
MySQL no tiene vistas materializadas; se simulan con una tabla de resumen que se recalcula con un evento programado o un cron.
Vistas para seguridad
Imagina que el equipo de marketing necesita los clientes, pero no debe ver los correos completos:
sql
CREATE VIEW v_clientes_marketing AS
SELECT
id,
nombre,
ciudad,
CONCAT(LEFT(email, 2), '***@', SUBSTRING(email, POSITION('@' IN email) + 1)) AS email_oculto,
fecha_registro
FROM clientes;
-- PostgreSQL / MySQL: el usuario de marketing solo puede leer la vista
-- GRANT SELECT ON v_clientes_marketing TO marketing;| id | nombre | ciudad | email_oculto | fecha_registro |
|---|---|---|---|---|
| 1 | Ana López | Ciudad de México | an***@correo.com | 2024-01-15 |
| 2 | Carlos Ruiz | Guadalajara | ca***@correo.com | 2024-02-03 |
Los permisos los verás en Usuarios y permisos de MySQL y Esquemas y roles de PostgreSQL.
Error común: vistas sobre vistas sobre vistas
Encadenar muchas vistas (una vista que usa otra que usa otra...) esconde consultas enormes y lentas. Si una consulta a una vista es lenta, revisa la definición completa con EXPLAIN. Mantén las vistas de uno o dos niveles.
Error común: ORDER BY dentro de la vista
No confíes en un ORDER BY dentro de la definición de una vista: el orden de las filas no está garantizado cuando la consultas. Ordena en la consulta que usa la vista.
Ejercicios
- Crea la vista
v_detalle_completocon: pedido_id, fecha, cliente, producto, cantidad, precio_unitario y subtotal. - Usando la vista del ejercicio 1, obtén los productos más vendidos en piezas.
- Crea la vista
v_clientes_inactivoscon los clientes que no tienen ningún pedido. - ¿Se puede hacer
UPDATEsobrev_pedidos_resumen? ¿Por qué?
Ver solución
sql
-- 1
CREATE VIEW v_detalle_completo AS
SELECT
pe.id AS pedido_id,
pe.fecha,
cl.nombre AS cliente,
pr.nombre AS producto,
d.cantidad,
d.precio_unitario,
d.cantidad * d.precio_unitario AS subtotal
FROM detalle_pedido d
JOIN pedidos pe ON pe.id = d.pedido_id
JOIN clientes cl ON cl.id = pe.cliente_id
JOIN productos pr ON pr.id = d.producto_id;
-- 2
SELECT producto, SUM(cantidad) AS piezas
FROM v_detalle_completo
GROUP BY producto
ORDER BY piezas DESC
LIMIT 3;| producto | piezas |
|---|---|
| Taza térmica | 5 |
| Audífonos Bluetooth | 2 |
| Libro Aprende SQL | 2 |
sql
-- 3
CREATE VIEW v_clientes_inactivos AS
SELECT cl.id, cl.nombre, cl.email
FROM clientes cl
WHERE NOT EXISTS (SELECT 1 FROM pedidos pe WHERE pe.cliente_id = cl.id);
SELECT * FROM v_clientes_inactivos; -- Lucía Gómez- No. Usa
GROUP BYySUMsobre varias tablas; la BD no puede saber qué fila de qué tabla debería cambiar.
Resumen
- Una vista es una consulta guardada con nombre que se usa como tabla; no almacena datos.
- Sirven para reutilizar lógica, simplificar consultas y restringir acceso a columnas sensibles.
- Las vistas simples de una tabla son actualizables;
WITH CHECK OPTIONevita insertar filas fuera del filtro. - Las vistas materializadas (PostgreSQL) guardan el resultado y se actualizan con
REFRESH. - Evita cadenas largas de vistas y no dependas de
ORDER BYdentro de ellas.