PostgreSQL
Views y materialized views
Views para encapsular consultas y materialized views para persistir resultados, incluyendo refresh, permisos, dependencias y trade-offs de frescura.
- Última actualización
- Actualizada
- Nivel
- Aplicación
PostgreSQL
Views para encapsular consultas y materialized views para persistir resultados, incluyendo refresh, permisos, dependencias y trade-offs de frescura.
Una view guarda una consulta como contrato lógico. Una materialized view guarda filas derivadas y exige decidir cuándo refrescarlas, cuánto retraso aceptar y cómo recuperarlas si quedan obsoletas.
PostgreSQL ofrece dos mecanismos que suelen confundirse:
VIEW
→ almacena definición SQL
→ consulta datos actuales al ejecutarse
MATERIALIZED VIEW
→ almacena resultado físico
→ permanece igual hasta REFRESHAmbas exponen una relación consultable, pero tienen ciclos de vida, costes y garantías diferentes.
Las views ayudan a:
Las materialized views ayudan cuando una consulta es costosa y el consumidor acepta datos con cierto retraso.
CREATE VIEW api.active_products AS
SELECT
id,
business_id,
name,
price
FROM app.products
WHERE deleted_at IS NULL;PostgreSQL guarda la definición en el catálogo. Al consultar:
SELECT *
FROM api.active_products
WHERE business_id = $1;El rewrite system integra la definición en la consulta. El planner puede empujar filtros y elegir índices de las tablas base cuando la semántica lo permite.
La view no mantiene una copia de esas filas.
consulta del cliente
↓
definición de view se expande
↓
planner optimiza conjunto completo
↓
executor consulta tablas realesNo es una cache. Si las tablas cambian, la próxima consulta puede ver los nuevos datos según snapshot.
Supón que una aplicación consume:
SELECT order_id, customer_name, total
FROM api.order_summary;El esquema interno puede evolucionar mientras la view conserva nombres y tipos.
Sin embargo:
SELECT * dentro de una view fija las columnas al crearla; no se actualiza mágicamente con columnas nuevas.Trata la view como una API versionada.
Una tabla con filtros o aliases.
Incluye joins, aggregates, CTEs, functions o set operations.
Una view compleja puede ser útil, pero esconder demasiado produce:
Abstracción no significa invisibilidad operativa.
Una view simple puede permitir:
INSERT INTO api.active_products (...);
UPDATE api.active_products SET ...;
DELETE FROM api.active_products WHERE ...;PostgreSQL puede traducir estas operaciones a la tabla base si cumple condiciones de actualizabilidad.
Views con joins, aggregates o DISTINCT normalmente no son actualizables automáticamente.
CREATE VIEW api.active_products AS
SELECT *
FROM app.products
WHERE deleted_at IS NULL
WITH CHECK OPTION;Impide que una escritura realizada a través de la view produzca una fila que deje de ser visible por su filtro.
Sin CHECK OPTION, un update podría “desaparecer” de la view inmediatamente.
Esto no reemplaza authorization ni constraints de la tabla.
Una view compleja puede aceptar escrituras mediante trigger:
CREATE TRIGGER order_view_insert
INSTEAD OF INSERT ON api.order_view
FOR EACH ROW
EXECUTE FUNCTION api.insert_order_from_view();Riesgos:
Úsalo cuando la view es realmente un boundary de escritura estable; no para ocultar una arquitectura confusa.
Una view puede permitir:
GRANT SELECT ON api.customer_public TO reporting_role;sin otorgar SELECT directo sobre la tabla base.
Pero debes comprender quién ejecuta las funciones y qué privilegios se usan.
Históricamente, las views utilizan privilegios del owner para acceder a tablas subyacentes. En versiones modernas, opciones como security_invoker permiten evaluar permisos del invoker en contextos compatibles.
Verifica la major version y documentación oficial antes de diseñar seguridad alrededor de esta opción.
CREATE VIEW api.safe_customers
WITH (security_barrier = true) AS
SELECT id, name
FROM customers
WHERE tenant_id = current_setting('app.tenant_id')::bigint;security_barrier limita ciertas reordenaciones que podrían ejecutar funciones del usuario antes de filtros de seguridad.
No convierte cualquier view en una política completa. RLS suele ser una capa más fuerte para acceso por fila.
Una función utilizada por el consumidor puede filtrar información según orden de evaluación, errores o timing.
Para views de seguridad:
LEAKPROOF, que solo superusers deben asignar.search_path.CREATE MATERIALIZED VIEW analytics.daily_sales AS
SELECT
date_trunc('day', paid_at) AS day,
branch_id,
count(*) AS order_count,
sum(total_amount) AS revenue
FROM app.orders
WHERE status = 'paid'
GROUP BY 1, 2;Esto ejecuta la consulta y almacena las filas en un objeto físico similar a una tabla consultable.
Puedes crear índices:
CREATE UNIQUE INDEX daily_sales_key
ON analytics.daily_sales(day, branch_id);REFRESH MATERIALIZED VIEW analytics.daily_sales;Reemplaza el contenido con un resultado nuevo. El refresh normal bloquea lecturas de forma más fuerte durante la operación.
REFRESH MATERIALIZED VIEW CONCURRENTLY analytics.daily_sales;Permite lecturas concurrentes, pero requiere:
No significa refresh incremental; PostgreSQL core vuelve a ejecutar la consulta y reconcilia resultados.
Una materialized view es una copia derivada. Debes definir:
frecuencia de refresh
+ duración del refresh
+ atraso máximo aceptable
+ comportamiento si falla
+ señal de frescura para consumidoresEjemplo:
CREATE TABLE analytics.refresh_status (
object_name text PRIMARY KEY,
refreshed_at timestamptz NOT NULL,
source_max_timestamp timestamptz
);El dashboard puede mostrar “datos actualizados hasta…”.
Opciones:
pg_cron si está disponible.No dependas de que alguien ejecute manualmente REFRESH.
La elección depende de coste del refresh y SLA de frescura.
No elijas solo por “más rápido”. Define patrones de lectura y recuperación.
Tablas internas:
app.orders
app.customers
app.order_itemsView:
CREATE VIEW api.v1_orders AS
SELECT
o.public_id AS id,
o.created_at,
o.status,
c.public_id AS customer_id,
c.name AS customer_name,
o.total_amount
FROM app.orders o
JOIN app.customers c ON c.id = o.customer_id;Ventajas:
Costes:
CREATE MATERIALIZED VIEW analytics.monthly_sales AS
SELECT
date_trunc('month', paid_at) AS month,
business_id,
branch_id,
sum(total_amount) AS revenue
FROM app.orders
WHERE status = 'paid'
GROUP BY 1, 2, 3;Refresh diario puede ser suficiente para reporting, pero no para mostrar caja en tiempo real.
Una view depende de tablas, columnas, funciones y tipos.
Cambios de esquema pueden fallar porque la view referencia una columna. Estrategias:
CREATE OR REPLACE VIEW cuando la forma compatible lo permite.CREATE OR REPLACE VIEW no permite cualquier cambio arbitrario de columnas existentes.
view A → view B → view C → tablasPuede crear SQL expandido enorme y planes difíciles.
Señales de problema:
Prefiere límites claros y documentación.
Una view guarda referencias resueltas, pero funciones utilizadas y SECURITY DEFINER pueden depender de search_path.
Califica objetos y configura paths seguros en funciones sensibles.
La materialized view conserva el contenido anterior. Debes alertar que está stale.
Se acumulan ejecuciones. Evita overlap y mide duración.
La view materializada queda vacía correctamente; distingue esto de fallo de refresh.
Agrupaciones por día/mes pueden cambiar semántica. Define zona explícita.
CONCURRENTLY no puede usarse hasta que exista una key única válida.
El refresh ve un snapshot coherente según la transacción, no una mezcla arbitraria.
El owner y configuración pueden afectar qué filas alimentan la view/materialized view. Verifica con roles reales.
Confunde coste y frescura.
Ownership y funciones pueden filtrar.
Entrega información stale.
El resultado almacenado puede seguir siendo lento.
Los consumidores ven datos viejos silenciosamente.
Ocultan complejidad en vez de reducirla.
Acopla consumidores y dificulta evolución.
CONCURRENTLY necesita unique index y más recursos.WITH CHECK OPTION?REFRESH ... CONCURRENTLY?Sequences e identity columns explica cómo PostgreSQL genera identificadores concurrentes y por qué los valores pueden tener huecos.