Skip to content

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_idclienteestadototal
1Ana Lópezentregado1597.00
2Carlos Ruizentregado1847.00
3Ana Lópezenviado3499.00
4María Pérezpendiente1146.00
5Jorge Díazcancelado899.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;
ciudadpedidosventas
Ciudad de México25096.00
Guadalajara11847.00
Monterrey11146.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';
idnombrecategoriapreciodisponibilidad
4Libro Aprende SQLLibros349.00Disponible
5Cien años de soledadLibros289.00Disponible

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, UNION ni 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;
mespedidosventas
2024-05-0123444.00
2024-06-0124645.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 normalVista materializada
Guarda datosNoSí
DatosSiempre actualesDel último REFRESH
Velocidad de lecturaLa de la consultaMuy rápida
Admite índicesNoSí
Ideal paraSimplificar y seguridadReportes 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;
idnombreciudademail_ocultofecha_registro
1Ana LópezCiudad de Méxicoan***@correo.com2024-01-15
2Carlos RuizGuadalajaraca***@correo.com2024-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 ​

  1. Crea la vista v_detalle_completo con: pedido_id, fecha, cliente, producto, cantidad, precio_unitario y subtotal.
  2. Usando la vista del ejercicio 1, obtén los productos más vendidos en piezas.
  3. Crea la vista v_clientes_inactivos con los clientes que no tienen ningún pedido.
  4. ¿Se puede hacer UPDATE sobre v_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;
productopiezas
Taza térmica5
Audífonos Bluetooth2
Libro Aprende SQL2
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
  1. No. Usa GROUP BY y SUM sobre 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 OPTION evita 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 BY dentro de ellas.

Academia Web — aprende, copia y construye.