A medida que las bases de datos crecen, las consultas se vuelven más largas, más repetitivas y más difíciles de mantener. Una vista en SQL resuelve exactamente este problema: te permite guardar una consulta compleja con un nombre y reutilizarla como si fuera una tabla normal, sin tener que reescribirla cada vez ni duplicar lógica de negocio en distintos lugares del código.
En esta guía vas a entender qué es una vista, cómo crearlas, la diferencia entre vistas normales y materializadas, y los casos de uso reales donde aportan más valor.
El esquema de ejemplo
CREATE TABLE clientes (
id INT PRIMARY KEY AUTO_INCREMENT,
nombre VARCHAR(100) NOT NULL,
ciudad VARCHAR(50),
activo BOOLEAN DEFAULT TRUE
);
CREATE TABLE productos (
id INT PRIMARY KEY AUTO_INCREMENT,
nombre VARCHAR(100) NOT NULL,
categoria VARCHAR(50),
precio DECIMAL(10, 2) NOT NULL,
stock INT DEFAULT 0
);
CREATE TABLE pedidos (
id INT PRIMARY KEY AUTO_INCREMENT,
cliente_id INT REFERENCES clientes(id),
producto_id INT REFERENCES productos(id),
fecha DATE NOT NULL,
cantidad INT NOT NULL,
total DECIMAL(10, 2) NOT NULL,
estado VARCHAR(20) DEFAULT 'completado'
);
Qué es una vista
Una vista es una consulta SQL almacenada con un nombre que puedes usar como si fuera una tabla. No almacena datos por sí misma (con la excepción de las vistas materializadas, que veremos más adelante): cada vez que la consultas, el motor de base de datos ejecuta la consulta original detrás de escena y te devuelve el resultado actualizado.
-- Sin vista: hay que escribir esta consulta cada vez que la necesitas
SELECT
c.nombre,
c.ciudad,
COUNT(p.id) AS total_pedidos,
SUM(p.total) AS gasto_total
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
WHERE p.estado = 'completado'
GROUP BY c.id, c.nombre, c.ciudad
HAVING COUNT(p.id) > 0;
-- Con vista: defines la consulta una vez...
CREATE VIEW resumen_clientes AS
SELECT
c.nombre,
c.ciudad,
COUNT(p.id) AS total_pedidos,
SUM(p.total) AS gasto_total
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
WHERE p.estado = 'completado'
GROUP BY c.id, c.nombre, c.ciudad
HAVING COUNT(p.id) > 0;
-- ...y la usas como si fuera una tabla, cuantas veces quieras
SELECT * FROM resumen_clientes;
SELECT * FROM resumen_clientes WHERE ciudad = 'Madrid';
SELECT * FROM resumen_clientes ORDER BY gasto_total DESC LIMIT 10;
Cada vez que consultas resumen_clientes, el motor de base de datos ejecuta la consulta original con los datos más recientes. Si insertas un nuevo pedido, la vista lo refleja automáticamente la próxima vez que la consultes, sin que tengas que hacer nada.
Sintaxis básica: CREATE VIEW
CREATE VIEW nombre_vista AS
SELECT columnas
FROM tabla
WHERE condicion;
-- Ejemplo simple: productos disponibles con stock
CREATE VIEW productos_disponibles AS
SELECT id, nombre, categoria, precio, stock
FROM productos
WHERE stock > 0;
SELECT * FROM productos_disponibles;
SELECT * FROM productos_disponibles WHERE categoria = 'Electrónica';
SELECT COUNT(*) FROM productos_disponibles;
-- Vista con JOIN: pedidos con información del cliente y producto
CREATE VIEW detalle_pedidos AS
SELECT
p.id AS pedido_id,
c.nombre AS cliente,
pr.nombre AS producto,
pr.categoria,
p.cantidad,
p.total,
p.fecha,
p.estado
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
JOIN productos pr ON pr.id = p.producto_id;
-- Ahora cualquier consulta sobre pedidos es mucho más simple de escribir
SELECT * FROM detalle_pedidos WHERE estado = 'completado';
SELECT cliente, SUM(total) FROM detalle_pedidos GROUP BY cliente;
SELECT * FROM detalle_pedidos WHERE categoria = 'Electrónica' AND fecha >= '2026-01-01';
Renombrar columnas en una vista
-- Puedes renombrar columnas para que el resultado sea más claro
CREATE VIEW informe_ventas (
id_pedido, nombre_cliente, nombre_producto, ingresos, fecha_venta
) AS
SELECT
p.id,
c.nombre,
pr.nombre,
p.total,
p.fecha
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
JOIN productos pr ON pr.id = p.producto_id;
SELECT nombre_cliente, ingresos FROM informe_ventas ORDER BY ingresos DESC;
-- Alternativa: usar alias dentro del SELECT (más habitual en la práctica)
CREATE VIEW informe_ventas_v2 AS
SELECT
p.id AS id_pedido,
c.nombre AS nombre_cliente,
pr.nombre AS nombre_producto,
p.total AS ingresos,
p.fecha AS fecha_venta
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
JOIN productos pr ON pr.id = p.producto_id;
Modificar y eliminar vistas
-- Modificar una vista existente (reemplaza completamente la definición)
CREATE OR REPLACE VIEW productos_disponibles AS
SELECT id, nombre, categoria, precio, stock
FROM productos
WHERE stock > 0 AND precio > 0; -- ahora con una condición adicional
-- En MySQL, ALTER VIEW también funciona como alternativa a CREATE OR REPLACE
ALTER VIEW productos_disponibles AS
SELECT id, nombre, categoria, precio, stock
FROM productos
WHERE stock > 0;
-- Eliminar una vista
DROP VIEW productos_disponibles;
DROP VIEW IF EXISTS productos_disponibles; -- no falla si no existe
-- Ver la definición de una vista existente
-- MySQL:
SHOW CREATE VIEW resumen_clientes;
-- PostgreSQL:
SELECT pg_get_viewdef('resumen_clientes', true);
-- Listar todas las vistas de la base de datos
-- MySQL:
SHOW FULL TABLES WHERE table_type = 'VIEW';
-- PostgreSQL:
SELECT table_name FROM information_schema.views WHERE table_schema = 'public';
Vistas actualizables: INSERT, UPDATE y DELETE
En determinadas condiciones, puedes modificar datos a través de una vista y el cambio se aplica a la tabla subyacente. Una vista es actualizable si cumple ciertas restricciones: normalmente debe basarse en una sola tabla, no debe usar GROUP BY, DISTINCT, funciones de agregación ni UNION.
-- Esta vista SÍ es actualizable: una sola tabla, sin agregaciones
CREATE VIEW clientes_activos AS
SELECT id, nombre, ciudad
FROM clientes
WHERE activo = TRUE;
-- Insertar a través de la vista (se inserta en la tabla clientes)
INSERT INTO clientes_activos (nombre, ciudad) VALUES ('Laura Méndez', 'Bilbao');
-- Actualizar a través de la vista
UPDATE clientes_activos SET ciudad = 'Vitoria' WHERE nombre = 'Laura Méndez';
-- Verificar: el cambio está en la tabla real
SELECT * FROM clientes WHERE nombre = 'Laura Méndez';
-- Esta vista NO es actualizable: tiene JOIN y agregación
CREATE VIEW resumen_clientes AS
SELECT c.nombre, COUNT(p.id) AS total_pedidos
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.id, c.nombre;
-- INSERT INTO resumen_clientes ... ← ERROR: la vista no es actualizable
WITH CHECK OPTION: proteger la integridad de los datos
-- Sin CHECK OPTION: puedes insertar/actualizar datos que "desaparecen" de la vista
CREATE VIEW clientes_activos AS
SELECT id, nombre, ciudad, activo
FROM clientes
WHERE activo = TRUE;
-- Esto funciona pero es confuso: el registro deja de cumplir la condición de la vista
UPDATE clientes_activos SET activo = FALSE WHERE nombre = 'Laura Méndez';
SELECT * FROM clientes_activos WHERE nombre = 'Laura Méndez'; -- ya no aparece
-- Con CHECK OPTION: el motor de base de datos rechaza cambios que violen la condición
CREATE VIEW clientes_activos AS
SELECT id, nombre, ciudad, activo
FROM clientes
WHERE activo = TRUE
WITH CHECK OPTION;
-- Esto ahora falla: no puedes desactivar un cliente a través de esta vista
-- UPDATE clientes_activos SET activo = FALSE WHERE nombre = 'Laura Méndez';
-- ERROR: row violates check option for view "clientes_activos"
Vistas materializadas: cuando el rendimiento importa
Una vista normal ejecuta su consulta cada vez que se accede a ella. Si la consulta subyacente es muy costosa (agregaciones sobre millones de filas, múltiples joins complejos) y se consulta con mucha frecuencia, esto puede ser un problema de rendimiento real.
Una vista materializada almacena físicamente el resultado de la consulta en disco, como una tabla normal. Las consultas sobre ella son instantáneas porque no recalculan nada, pero los datos se quedan "congelados" en el momento en que se generó la vista hasta que se refresca explícitamente.
-- PostgreSQL: vistas materializadas son soporte nativo
CREATE MATERIALIZED VIEW resumen_ventas_mensuales AS
SELECT
DATE_TRUNC('month', fecha) AS mes,
COUNT(*) AS num_pedidos,
SUM(total) AS ingresos_totales,
AVG(total) AS ticket_medio
FROM pedidos
WHERE estado = 'completado'
GROUP BY DATE_TRUNC('month', fecha);
-- Consultarla es tan rápido como consultar una tabla normal
SELECT * FROM resumen_ventas_mensuales ORDER BY mes;
-- Pero los datos NO se actualizan automáticamente al insertar nuevos pedidos
-- Hay que refrescarla manualmente (o con un job programado)
REFRESH MATERIALIZED VIEW resumen_ventas_mensuales;
-- Refrescar sin bloquear las lecturas concurrentes (requiere un índice único)
CREATE UNIQUE INDEX idx_resumen_mes ON resumen_ventas_mensuales (mes);
REFRESH MATERIALIZED VIEW CONCURRENTLY resumen_ventas_mensuales;
-- MySQL NO tiene vistas materializadas nativas
-- La alternativa habitual es crear una tabla real y actualizarla con un evento programado
CREATE TABLE resumen_ventas_mensuales (
mes DATE PRIMARY KEY,
num_pedidos INT,
ingresos_totales DECIMAL(12, 2),
ticket_medio DECIMAL(10, 2)
);
-- Evento programado para refrescar la tabla cada hora
DELIMITER //
CREATE EVENT refrescar_resumen_ventas
ON SCHEDULE EVERY 1 HOUR
DO
BEGIN
DELETE FROM resumen_ventas_mensuales;
INSERT INTO resumen_ventas_mensuales
SELECT
DATE_FORMAT(fecha, '%Y-%m-01') AS mes,
COUNT(*),
SUM(total),
AVG(total)
FROM pedidos
WHERE estado = 'completado'
GROUP BY DATE_FORMAT(fecha, '%Y-%m-01');
END //
DELIMITER ;
Vista normal vs vista materializada
| Característica | Vista normal | Vista materializada |
|---|---|---|
| Almacenamiento de datos | No, solo la consulta | Sí, el resultado físico |
| Datos en tiempo real | Sí, siempre actualizados | No, hasta el siguiente refresh |
| Velocidad de lectura | Depende de la consulta subyacente | Muy rápida (datos precalculados) |
| Coste de escritura | Ninguno | El refresh tiene coste |
| Uso de espacio en disco | Mínimo | Igual que una tabla equivalente |
| Mejor para | Simplificar consultas, abstracción, seguridad | Informes y dashboards con datos pesados de calcular |
Casos de uso reales de las vistas
1. Simplificar consultas complejas y repetitivas
-- En lugar de repetir este JOIN complejo en 15 lugares distintos del código,
-- defínelo una vez como vista
CREATE VIEW pedidos_completos AS
SELECT
p.id,
c.nombre AS cliente,
c.ciudad,
pr.nombre AS producto,
pr.categoria,
p.cantidad,
p.total,
p.fecha
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
JOIN productos pr ON pr.id = p.producto_id
WHERE p.estado = 'completado';
-- Ahora cualquier desarrollador del equipo escribe consultas simples
SELECT * FROM pedidos_completos WHERE ciudad = 'Madrid';
SELECT categoria, SUM(total) FROM pedidos_completos GROUP BY categoria;
2. Seguridad: restringir el acceso a columnas o filas sensibles
-- Tabla con datos sensibles
CREATE TABLE empleados (
id INT PRIMARY KEY,
nombre VARCHAR(100),
departamento VARCHAR(50),
salario DECIMAL(10, 2),
dni VARCHAR(20)
);
-- Vista que oculta las columnas sensibles para usuarios sin permisos de RRHH
CREATE VIEW empleados_publico AS
SELECT id, nombre, departamento
FROM empleados;
-- Conceder permisos: el equipo general solo puede ver la vista, no la tabla completa
GRANT SELECT ON empleados_publico TO rol_empleado_general;
GRANT SELECT ON empleados TO rol_recursos_humanos;
-- Vista con filtrado por filas: cada gerente solo ve su departamento
CREATE VIEW mi_equipo AS
SELECT id, nombre, departamento
FROM empleados
WHERE departamento = CURRENT_USER(); -- ejemplo simplificado, en la práctica se usa
-- una función o tabla de mapeo usuario→departamento
3. Capa de compatibilidad al cambiar el esquema
-- Si renombras una tabla o cambias su estructura, una vista puede
-- mantener la compatibilidad con código antiguo que la usa
-- Supongamos que renombras 'usuarios' a 'cuentas_usuario'
ALTER TABLE usuarios RENAME TO cuentas_usuario;
-- Creas una vista con el nombre antiguo para no romper código existente
CREATE VIEW usuarios AS
SELECT * FROM cuentas_usuario;
-- El código legado sigue funcionando mientras se migra gradualmente
-- a usar el nuevo nombre de tabla
4. Reportes y dashboards
-- Vista para un dashboard de ventas
CREATE VIEW dashboard_ventas AS
SELECT
pr.categoria,
DATE_FORMAT(p.fecha, '%Y-%m') AS mes,
COUNT(*) AS num_ventas,
SUM(p.total) AS ingresos,
AVG(p.total) AS ticket_medio
FROM pedidos p
JOIN productos pr ON pr.id = p.producto_id
WHERE p.estado = 'completado'
GROUP BY pr.categoria, DATE_FORMAT(p.fecha, '%Y-%m');
-- La herramienta de BI (Power BI, Tableau, Metabase) se conecta
-- directamente a esta vista en lugar de tener que conocer
-- la estructura completa de las tablas subyacentes
5. Simplificar la lógica de negocio repetida
-- Vista que encapsula la definición de "cliente VIP" para todo el sistema
CREATE VIEW clientes_vip AS
SELECT c.id, c.nombre, c.ciudad, SUM(p.total) AS gasto_total
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
WHERE p.estado = 'completado'
GROUP BY c.id, c.nombre, c.ciudad
HAVING SUM(p.total) > 5000;
-- Si la definición de "VIP" cambia (el umbral pasa de 5000 a 10000),
-- solo hay que modificar la vista UNA VEZ
-- en lugar de buscar y cambiar la condición en cada consulta que la usa
SELECT * FROM clientes_vip;
Vistas anidadas: usar una vista dentro de otra
-- Una vista puede construirse sobre otra vista
CREATE VIEW pedidos_completos AS
SELECT p.id, c.nombre AS cliente, pr.categoria, p.total, p.fecha
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
JOIN productos pr ON pr.id = p.producto_id
WHERE p.estado = 'completado';
-- Esta segunda vista usa la primera como si fuera una tabla
CREATE VIEW resumen_categorias AS
SELECT categoria, COUNT(*) AS num_ventas, SUM(total) AS ingresos
FROM pedidos_completos
GROUP BY categoria;
SELECT * FROM resumen_categorias ORDER BY ingresos DESC;
Las vistas anidadas son cómodas pero hay que usarlas con moderación: cada nivel de anidación añade complejidad a la consulta que el optimizador de la base de datos tiene que resolver, y demasiados niveles pueden hacer que el rendimiento sea difícil de razonar y de optimizar.
Cuándo NO usar vistas
Cuando el rendimiento es crítico y la consulta es muy costosa. Una vista normal no acelera nada: simplemente encapsula la consulta. Si la consulta subyacente es lenta, la vista será igual de lenta. En esos casos, considera una vista materializada o una tabla de resumen actualizada periódicamente.
Cuando necesitas parámetros dinámicos complejos. Las vistas no aceptan parámetros como las funciones. Si necesitas lógica condicional compleja según el contexto de cada llamada, una función o procedimiento almacenado es más adecuado.
-- Las vistas no aceptan parámetros directamente
-- Esto NO es válido:
-- CREATE VIEW pedidos_por_cliente(id_cliente) AS SELECT * FROM pedidos WHERE cliente_id = id_cliente;
-- Para eso se usa una función con parámetros
CREATE FUNCTION pedidos_de_cliente(id_cliente INT)
RETURNS TABLE (id INT, total DECIMAL, fecha DATE) AS $$
SELECT id, total, fecha FROM pedidos WHERE cliente_id = id_cliente;
$$ LANGUAGE SQL;
SELECT * FROM pedidos_de_cliente(5);
Cuando se vuelven demasiado anidadas o complejas. Si tienes vistas que dependen de otras vistas que dependen de otras vistas, el sistema se vuelve difícil de depurar y de optimizar. En ese punto, considera si una tabla física con un proceso ETL programado sería más manejable.
Resumen
- Una vista es una consulta SQL guardada con un nombre que se comporta como una tabla. No almacena datos por sí misma: ejecuta la consulta original cada vez que se accede.
- Se crean con
CREATE VIEW nombre AS SELECT ...y se eliminan conDROP VIEW. - Las vistas simples (sobre una sola tabla, sin agregaciones) pueden ser actualizables: permiten
INSERT,UPDATEyDELETEque se reflejan en la tabla subyacente. WITH CHECK OPTIONevita que modificaciones a través de la vista produzcan filas que ya no cumplan su condición de filtrado.- Las vistas materializadas almacenan físicamente el resultado, lo que las hace mucho más rápidas de leer pero requieren refrescarse manualmente para reflejar cambios.
- Úsalas para simplificar consultas repetitivas, restringir el acceso a columnas o filas sensibles, mantener compatibilidad al cambiar esquemas, y alimentar dashboards de reporting.
- No esperes que una vista mejore el rendimiento por sí sola: si la consulta es lenta, sigue siendo lenta dentro de una vista. Para eso existen las vistas materializadas o las tablas de resumen.
No hay comentarios todavía. Sé el primero en compartir tu opinión.