Lección 9.6 · Tiempo de lectura: ~9 min
Una vista materializada es una vista cuyo resultado se guarda en disco como una tabla: la consulta se ejecuta una vez y las lecturas devuelven las filas guardadas en lugar de recalcularlas. Los informes pesados se vuelven rápidos, a cambio de que los datos solo estén al día hasta la última actualización. En esta lección crearás, consultarás y actualizarás vistas materializadas en PostgreSQL, verás qué bases de datos las admiten y construirás lo mismo en MySQL con una tabla de resumen.
Vistas materializadas en SQL
En la lección anterior creaste vistas: una vista solo guarda la consulta y cada lectura la ejecuta de nuevo. Es cómodo, pero un informe que une cinco tablas y agrega miles de filas repite todo ese trabajo en cada petición.
Una vista materializada lo resuelve guardando el resultado. Los ejemplos usan el esquema Sakila con la sintaxis de PostgreSQL; el comportamiento mostrado se comprobó en PostgreSQL 17, y la parte de MySQL en MySQL 8.0.
¿Qué es una vista materializada?
Una vista materializada es una consulta con nombre más una copia guardada de su resultado. Las lecturas salen de la copia, así que son tan rápidas como leer una tabla pequeña. La copia no se actualiza sola: cuando cambian las tablas de origen, la vista materializada conserva los datos antiguos hasta que la actualizas.
| Vista | Vista materializada | Tabla | |
|---|---|---|---|
| Guarda | la consulta | la consulta y su resultado | datos |
| Una lectura | ejecuta la consulta | lee el resultado guardado | lee los datos |
| Actualidad | siempre al día | de la última actualización | lo que escribiste |
| Admite índices | no | sí | sí |
INSERT / UPDATE | a veces | no | sí |
Crear una vista materializada
La sintaxis es la misma que la de una vista, con la palabra MATERIALIZED:
CREATE MATERIALIZED VIEW category_revenue AS
SELECT c.name AS category, COUNT(*) AS rentals, SUM(p.amount) AS revenue
FROM payment AS p
JOIN rental AS r ON r.rental_id = p.rental_id
JOIN inventory AS i ON i.inventory_id = r.inventory_id
JOIN film_category AS fc ON fc.film_id = i.film_id
JOIN category AS c ON c.category_id = fc.category_id
GROUP BY c.name;
Resultado: PostgreSQL ejecuta la consulta una vez y guarda 16 filas, una por categoría.
Después se consulta como una tabla:
SELECT category, rentals, revenue
FROM category_revenue
ORDER BY revenue DESC
LIMIT 5;
Resultado: Sports (1179 alquileres, 5314.21), Sci-Fi (4756.98), Animation (4656.30), Drama (4587.39), Comedy (4383.58). Los cinco joins no se vuelven a ejecutar: las filas salen del resultado guardado.
Para crear el objeto sin ejecutar la consulta, añade WITH NO DATA. Esa vista no se puede leer hasta su primera actualización: un SELECT falla con "materialized view has not been populated".
Una vista materializada es de solo lectura. INSERT INTO category_revenue … falla con "cannot change materialized view". Sus datos solo cambian al actualizarla.
Actualizar los datos
Cuando llegan pagos nuevos, la vista materializada sigue mostrando los totales antiguos. Para recalcularla:
REFRESH MATERIALIZED VIEW category_revenue;
Resultado: la consulta se ejecuta de nuevo y sustituye las filas guardadas. Antes de este comando, un pago nuevo no aparece en la vista en absoluto.
Un REFRESH simple bloquea la vista materializada: las consultas que la leen esperan a que termine la actualización. Para una vista que se lee constantemente, usa CONCURRENTLY. Así el nuevo resultado se construye junto al antiguo y solo se sustituyen las filas que cambiaron, sin bloquear las lecturas:
CREATE UNIQUE INDEX category_revenue_category_idx ON category_revenue (category);
REFRESH MATERIALIZED VIEW CONCURRENTLY category_revenue;
CONCURRENTLY exige un índice único en la vista materializada; sin él, el comando falla con "cannot refresh materialized view concurrently". Además es más lento que una actualización simple, así que úsalo solo donde el bloqueo de las lecturas sea un problema.
La frecuencia de actualización depende de la tarea: una vez por noche para un informe diario, cada pocos minutos para un dashboard. La actualización la lanza un planificador, por ejemplo cron o la extensión pg_cron.
Índices y eliminación
Una vista materializada se guarda como una tabla, así que admite índices normales, algo que una vista normal no puede tener. Estudiarás los índices en detalle en el módulo 10.
Para eliminarla:
DROP MATERIALIZED VIEW category_revenue;
La lista de vistas materializadas de PostgreSQL está en pg_matviews; la columna ispopulated indica si una vista se ha llenado.
Soporte en distintas bases de datos
| Base de datos | Vistas materializadas | Actualización |
|---|---|---|
| PostgreSQL | CREATE MATERIALIZED VIEW | manual: REFRESH MATERIALIZED VIEW |
| Oracle | CREATE MATERIALIZED VIEW | bajo demanda, programada o en cada commit; puede ser incremental |
| SQL Server | vistas indexadas (CREATE VIEW … WITH SCHEMABINDING + un índice agrupado) | automática, con cada cambio de las tablas de origen |
| MySQL, MariaDB | no se admiten | tabla de resumen (ver abajo) |
| SQLite | no se admiten | tabla de resumen |
En MySQL, CREATE MATERIALIZED VIEW es un error de sintaxis.
Una vista materializada en MySQL: la tabla de resumen
En MySQL, el mismo resultado se guarda en una tabla normal que reconstruyes tú mismo:
CREATE TABLE category_revenue AS
SELECT c.name AS category, COUNT(*) AS rentals, SUM(p.amount) AS revenue
FROM payment AS p
JOIN rental AS r ON r.rental_id = p.rental_id
JOIN inventory AS i ON i.inventory_id = r.inventory_id
JOIN film_category AS fc ON fc.film_id = i.film_id
JOIN category AS c ON c.category_id = fc.category_id
GROUP BY c.name;
Para actualizarla sin dejar a los lectores ante una tabla vacía, llena una copia nueva e intercambia las dos tablas en una sola sentencia:
CREATE TABLE category_revenue_new LIKE category_revenue;
INSERT INTO category_revenue_new
SELECT c.name, COUNT(*), SUM(p.amount)
FROM payment AS p
JOIN rental AS r ON r.rental_id = p.rental_id
JOIN inventory AS i ON i.inventory_id = r.inventory_id
JOIN film_category AS fc ON fc.film_id = i.film_id
JOIN category AS c ON c.category_id = fc.category_id
GROUP BY c.name;
RENAME TABLE category_revenue TO category_revenue_old,
category_revenue_new TO category_revenue;
DROP TABLE category_revenue_old;
Resultado: RENAME TABLE intercambia las dos tablas a la vez, así que las consultas ven los datos antiguos o los nuevos, nunca una tabla vacía.
Para una tabla pequeña también sirve DELETE seguido de INSERT … SELECT dentro de una transacción. No uses TRUNCATE aquí: en MySQL hace un commit implícito y los lectores verían la tabla vacía (consulta la lección 8.4).
La actualización se programa con el planificador de eventos de MySQL (CREATE EVENT … ON SCHEDULE EVERY 1 HOUR) o con una tarea externa de cron.
Cuándo usar una vista materializada
Buenos usos:
- informes y dashboards pesados que unen muchas tablas y agregan muchas filas;
- datos que pueden ir minutos u horas por detrás: ventas del día, estadísticas mensuales, rankings;
- resultados que leen muchos usuarios y que cambian poco.
Malos usos:
- datos que siempre deben estar al día, como un saldo o el stock disponible;
- tablas de origen que cambian todo el tiempo, cuando habría que actualizar la vista sin parar;
- consultas simples y rápidas: basta con una vista normal o con la propia consulta.
Conclusiones clave:
- Una vista materializada guarda el resultado de su consulta; las lecturas devuelven las filas guardadas sin ejecutar la consulta.
- Los datos son de la última
REFRESH MATERIALIZED VIEW; en PostgreSQL nada se actualiza automáticamente. REFRESH … CONCURRENTLYno bloquea las lecturas, pero exige un índice único en la vista materializada.- Una vista materializada es de solo lectura y puede tener sus propios índices.
- MySQL y MariaDB no tienen vistas materializadas: usa una tabla de resumen e intercámbiala con
RENAME TABLE. - Úsalas en informes pesados que toleran datos algo desactualizados, no en valores que deben estar al día.
Preguntas frecuentes
¿Qué diferencia hay entre una vista y una vista materializada?
Una vista solo guarda la consulta y la ejecuta en cada lectura, así que los datos están siempre al día. Una vista materializada guarda el resultado de la consulta: las lecturas son rápidas, pero los datos son de la última actualización.
¿Una vista materializada se actualiza sola?
En PostgreSQL no: ejecutas REFRESH MATERIALIZED VIEW tú mismo o de forma programada. Oracle puede actualizar en cada commit, y las vistas indexadas de SQL Server se mantienen automáticamente con cada cambio.
¿MySQL admite vistas materializadas?
No. MySQL y MariaDB solo tienen vistas normales. El sustituto habitual es una tabla de resumen que se llena con INSERT … SELECT y se reconstruye de forma programada.
¿Puedo crear un índice en una vista materializada?
Sí. Una vista materializada se guarda como una tabla, así que se puede indexar, y REFRESH … CONCURRENTLY exige un índice único.
Preguntas de entrevista
¿Cuándo usarías una vista materializada en lugar de una vista normal?
Cuando la consulta es costosa, su resultado se lee a menudo y los datos pueden ir algo por detrás: informes, dashboards, estadísticas agregadas. Una vista normal es mejor cuando los datos deben estar siempre al día o la consulta es barata.
¿Qué hace REFRESH MATERIALIZED VIEW CONCURRENTLY y qué necesita?
Recalcula el resultado sin bloquear a los lectores: el nuevo resultado se construye junto al antiguo y solo se aplican las diferencias. Necesita un índice único en la vista materializada y es más lento que una actualización simple.
¿Cómo implementarías una vista materializada en MySQL?
Crearía una tabla de resumen con CREATE TABLE … AS SELECT. Para actualizarla, llenaría una copia nueva e intercambiaría las tablas con RENAME TABLE, lanzando la actualización con el planificador de eventos o con cron.