09-05 cerraba prometiendo "el arsenal que hace mantenible todo lo anterior", y la primera herramienta de ese arsenal es la más sencilla de todas: dar nombre a una consulta. Una vista es exactamente eso —una consulta guardada en la base de datos con un nombre— y no parece gran cosa hasta que cuentas cuántas veces has copiado en este curso el mismo JOIN de cuatro tablas y la misma expresión cantidad * precio_unitario * (1 - descuento).

En esta lección verás qué es y qué no es una vista (no almacena datos: almacena la definición), cómo crearla, reemplazarla y borrarla con sus reglas exactas, los cinco motivos reales por los que se usan —con un caso de TiendaVerde para cada uno, incluido el borrado lógico que 05-04 dejó pendiente—, cuándo PostgreSQL deja escribir a través de una vista y qué es WITH CHECK OPTION, por qué una vista no es una caché y qué pasa al anidarlas, y las vistas materializadas que 08-04 remitió aquí: las que sí guardan datos, con su REFRESH y su precio.

Contenido

  1. Qué es una vista y qué no es
  2. CREATE VIEW, CREATE OR REPLACE VIEW, DROP VIEW
  3. Los cinco motivos, con un caso cada uno
  4. Vistas actualizables y WITH CHECK OPTION
  5. Rendimiento: la vista se expande, no se cachea
  6. Vistas materializadas
  7. Metadatos: dónde viven las vistas
  8. Errores Comunes y Consejos
  9. Ejercicios
  10. Conclusión

  1. Qué es una vista y qué no es

Una vista es una consulta SELECT almacenada en el esquema con un nombre. Cuando la consultas, el motor sustituye el nombre por su definición y ejecuta la consulta resultante contra las tablas reales.

Afirmación ¿Cierta? Por qué
Una vista guarda filas No Guarda el texto del SELECT. Ocupa unos bytes, no megas
Una vista devuelve datos al día Se ejecuta en el momento de consultarla, contra las tablas actuales
Una vista acelera una consulta lenta No Ejecuta el mismo trabajo. Lo que acelera es una vista materializada (apartado 6)
Se consulta como una tabla SELECT, WHERE, JOIN, GROUP BY... todo lo del curso funciona sobre ella
flowchart LR
    A["SELECT * FROM v_detalle_ventas<br/>WHERE pais = 'Francia'"] --> B["el planificador<br/><b>expande</b> la vista"]
    B --> C["SELECT ... FROM lineas_pedido lp<br/>JOIN pedidos pe ... JOIN clientes c ...<br/>WHERE c.pais = 'Francia'"]
    C --> D["tablas reales"]

Ese diagrama es toda la lección en una imagen: la vista desaparece antes de ejecutarse. Es azúcar sintáctico con nombre y permisos propios, no una capa de almacenamiento.

  1. CREATE VIEW, CREATE OR REPLACE VIEW, DROP VIEW

CREATE VIEW v_productos_activos AS
SELECT p.id, p.nombre, p.categoria_id, p.precio, p.stock
FROM   productos AS p
WHERE  p.activo = TRUE;

(Todas las vistas de esta lección —v_productos_activos, v_detalle_ventas, v_ventas_por_categoria, v_clientes_publico, mv_ventas_mensuales— son objetos de ejemplo de esta lección: no forman parte del esquema canónico de TiendaVerde de 01-06.)

Las tres operaciones y sus reglas:

Sentencia Qué hace La regla que sorprende
CREATE VIEW v AS SELECT ... La crea Falla si ya existe
CREATE OR REPLACE VIEW v AS SELECT ... La crea o redefine Solo puede añadir columnas al final. No puede quitarlas, renombrarlas, reordenarlas ni cambiarles el tipo
DROP VIEW v La borra Falla si otra vista depende de ella, salvo con CASCADE

Esa restricción de OR REPLACE es la que más tiempo hace perder:

-- ⚠️ INCORRECTA: intenta renombrar una columna existente
CREATE OR REPLACE VIEW v_productos_activos AS
SELECT p.id, p.nombre AS producto, p.categoria_id, p.precio, p.stock
FROM productos AS p WHERE p.activo = TRUE;
ERROR:  cannot change name of view column "nombre" to "producto"
HINT:  Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.

En cambio, añadir al final sí se puede: SELECT p.id, p.nombre, p.categoria_id, p.precio, p.stock, p.proveedor_id funciona sin protestar.

La razón es la misma que hacía delicado un ALTER TABLE en 05-06: otras consultas y otras vistas dependen de la posición y el tipo de cada columna. Para cambiar la forma de una vista hay que hacer DROP VIEW + CREATE VIEW, y eso obliga a recrear también todo lo que dependía de ella. ALTER VIEW existe, pero solo para lo periférico: renombrar la vista o una columna, cambiar de propietario o de esquema.

  1. Los cinco motivos, con un caso cada uno

3.1. Simplificar: la consulta canónica de detalle

Llevas desde el módulo 3 escribiendo estas cuatro líneas de JOIN. Se escriben una vez:

CREATE VIEW v_detalle_ventas AS
SELECT lp.id AS linea_id, pe.id AS pedido_id, pe.fecha_pedido, pe.estado,
       c.id  AS cliente_id, c.nombre || ' ' || c.apellidos AS cliente, c.pais,
       p.id  AS producto_id, p.nombre AS producto, p.categoria_id,
       lp.cantidad, lp.precio_unitario, lp.descuento,
       ROUND(lp.cantidad * lp.precio_unitario * (1 - lp.descuento), 2) AS importe
FROM   lineas_pedido AS lp
JOIN   pedidos       AS pe ON lp.pedido_id  = pe.id
JOIN   clientes      AS c  ON pe.cliente_id = c.id
JOIN   productos     AS p  ON lp.producto_id = p.id;

Y a partir de ahí, una consulta de negocio se lee como una frase:

SELECT linea_id, pedido_id, fecha_pedido, cliente, producto, cantidad, importe
FROM   v_detalle_ventas
ORDER  BY linea_id
LIMIT  5;
linea_id pedido_id fecha_pedido cliente producto cantidad importe
1 1 2025-03-04 Lucía Martínez Soler Aceite de oliva virgen extra 500 ml 2 23.90
2 1 2025-03-04 Lucía Martínez Soler Arroz integral ecológico 1 kg 3 11.70
3 1 2025-03-04 Lucía Martínez Soler Infusión de manzanilla ecológica 20 uds 2 6.50
4 2 2025-03-12 Carlos Ferrer Ibáñez Crema facial de aloe vera 50 ml 1 17.50
5 2 2025-03-12 Carlos Ferrer Ibáñez Bálsamo labial de caléndula 15 ml 2 9.20

(5 primeras de 47 filas.) Y SELECT COUNT(*), ROUND(SUM(importe), 2) FROM v_detalle_ventas devuelve 47 y 727.95: las cifras canónicas del curso, ahora a un SELECT de distancia.

3.2. Estandarizar una métrica

Este motivo es más importante que el anterior y se ve peor. En la vista hay una decisión de negocio escrita una sola vez: el importe de una línea es cantidad * precio_unitario * (1 - descuento), redondeado a dos decimales, sin gastos de envío. Si esa fórmula vive copiada en catorce informes, tarde o temprano tres de ellos olvidarán el (1 - descuento) y la dirección recibirá tres cifras distintas de facturación en la misma reunión.

Una vista es el sitio donde una métrica se define. La versión agregada, apoyada en la anterior:

CREATE VIEW v_ventas_por_categoria AS
SELECT cat.id AS categoria_id, cat.nombre AS categoria,
       COUNT(DISTINCT dv.pedido_id) AS pedidos,
       SUM(dv.cantidad)             AS unidades,
       ROUND(SUM(dv.importe), 2)    AS facturacion
FROM   v_detalle_ventas AS dv
JOIN   categorias       AS cat ON cat.id = dv.categoria_id
GROUP  BY cat.id, cat.nombre;

SELECT * FROM v_ventas_por_categoria ORDER BY facturacion DESC;
categoria_id categoria pedidos unidades facturacion
1 Alimentación 10 49 256.27
4 Bebidas 8 29 195.28
2 Cosmética natural 6 16 156.32
3 Hogar sostenible 4 10 88.58
5 Higiene personal 3 9 31.50

Las cinco categorías con ventas y sus cifras canónicas. Complementos no aparece porque el JOIN sobre las ventas la descarta (04-05); para verla con 0 haría falta partir de categorias con un LEFT JOIN. Fíjate además en que esta vista está construida sobre otra vista: es legal y muy cómodo, con el matiz del apartado 5.

3.3. Encapsular el borrado lógico

Aquí se cierra la promesa de 05-04. El borrado lógico con activo = FALSE tenía un inconveniente enorme: hay que acordarse de escribir WHERE activo en todas partes, y el día que alguien lo olvide, un producto descatalogado aparecerá en la tienda.

La vista v_productos_activos del apartado 2 devuelve 19 de las 20 filas de productos: las Cápsulas de espirulina (producto 20, activo = FALSE) no están, y no hay forma de que se cuelen. La regla operativa es sencilla: la aplicación consulta la vista; solo el mantenimiento del catálogo toca la tabla. El filtro deja de ser algo que recordar y pasa a ser algo que está.

3.4. Desacoplar la aplicación del esquema físico

En 05-06 viste el patrón expand/contract: para renombrar una columna sin parar el servicio se añade la nueva, se escriben las dos durante un tiempo y se retira la vieja. Supón ahora que productos.nombre pasa a llamarse nombre_comercial. Las consultas de la aplicación que apuntaban a productos.nombre se rompen todas; las que apuntaban a la vista no, porque basta con redefinirla con SELECT p.nombre_comercial AS nombre, ... y el cambio queda absorbido ahí dentro.

La vista actúa como contrato estable: por fuera sigue habiendo una columna nombre, y por dentro el esquema puede evolucionar. Es la misma idea que una interfaz en programación, y es el motivo por el que muchos equipos exponen a los informes y a las herramientas de BI solo vistas, nunca tablas.

3.5. Exponer solo una parte

Una vista puede omitir columnas y filas. clientes tiene email, que es un dato personal, y referido_por_id, que es información comercial interna:

CREATE VIEW v_clientes_publico AS
SELECT c.id, c.nombre, c.ciudad, c.pais, c.fecha_registro
FROM   clientes AS c;

Quien la consulte no verá jamás un correo electrónico, porque no está en la vista. Lo mismo con filas: WHERE pais = 'España' en la definición crea una vista que solo muestra el mercado nacional.

Esto es una herramienta de seguridad, y aquí solo la mencionamos. Para que sirva de algo hay que quitar el permiso sobre la tabla y darlo sobre la vistaGRANT SELECT ON v_clientes_publico TO ...—, y eso, junto con los roles y el control de acceso a nivel de fila, es materia de 11-03.

  1. Vistas actualizables y WITH CHECK OPTION

Sorpresa razonable: sobre una vista se puede a veces escribir. PostgreSQL la considera automáticamente actualizableINSERT, UPDATE y DELETE funcionan sin más— cuando cumple todas estas condiciones:

Requisito v_productos_activos v_detalle_ventas v_ventas_por_categoria
Exactamente una tabla o vista en el FROM ❌ (cuatro)
Sin GROUP BY, HAVING, DISTINCT, LIMIT, OFFSET ❌ (GROUP BY)
Sin UNION, INTERSECT, EXCEPT
Sin funciones de ventana ni agregados en el SELECT
Las columnas escritas son referencias simples a columnas, no expresiones ❌ (importe es calculada)
¿Actualizable? No No

UPDATE v_productos_activos SET precio = 13.00 WHERE id = 1; responde UPDATE 1, y el cambio ha ido a la tabla productos. Y ahora el problema interesante. Añadamos activo a la vista —recuerda: añadir al final sí se puede— para poder escribirla:

CREATE OR REPLACE VIEW v_productos_activos AS
SELECT p.id, p.nombre, p.categoria_id, p.precio, p.stock, p.proveedor_id, p.activo
FROM   productos AS p
WHERE  p.activo = TRUE;

UPDATE v_productos_activos SET activo = FALSE WHERE id = 1;   -- UPDATE 1

Y el aceite de oliva acaba de desaparecer de la vista. Has escrito, a través de una ventana, una fila que la ventana ya no muestra: el UPDATE dice que ha tocado una fila, pero volver a consultarla es imposible desde aquí. A eso se le llama irse por la puerta de atrás, y se cierra así:

CREATE OR REPLACE VIEW v_productos_activos AS
SELECT p.id, p.nombre, p.categoria_id, p.precio, p.stock, p.proveedor_id, p.activo
FROM   productos AS p
WHERE  p.activo = TRUE
WITH CHECK OPTION;

UPDATE v_productos_activos SET activo = FALSE WHERE id = 1;

Con WITH CHECK OPTION, toda fila insertada o modificada debe seguir cumpliendo el WHERE de la vista:

ERROR:  new row violates check option for view "v_productos_activos"
DETAIL:  Failing row contains (1, Aceite de oliva virgen extra 500 ml, 1, 1, 12.50, 7.80, 120, f, 2025-01-15).

Tiene dos variantes: WITH LOCAL CHECK OPTION comprueba solo la condición de esta vista, y WITH CASCADED CHECK OPTION la de esta y la de todas las vistas sobre las que se apoya —es lo que se aplica si no dices nada—.

Y para las vistas que no son actualizables automáticamentev_detalle_ventas, por ejemplo— PostgreSQL ofrece dos salidas: un trigger INSTEAD OF, que intercepta la escritura y decide a mano qué tablas tocar, o el sistema de reglas (CREATE RULE), más antiguo y desaconsejado. La forma moderna es el trigger, y es exactamente lo que verás en 10-05.

  1. Rendimiento: la vista se expande, no se cachea

Este es el malentendido más caro de la lección: una vista no guarda nada y no ahorra ni un microsegundo de trabajo. El planificador la sustituye por su definición y optimiza el conjunto.

La buena noticia es que esa sustitución es inteligente: los filtros de fuera se empujan hacia dentro.

EXPLAIN (COSTS OFF)
SELECT cliente, importe FROM v_detalle_ventas WHERE pais = 'Francia';
 Hash Join
   Hash Cond: (lp.producto_id = p.id)
   ->  Hash Join
         Hash Cond: (pe.cliente_id = c.id)
         ->  Hash Join  (Hash Cond: lp.pedido_id = pe.id)
               ->  Seq Scan on lineas_pedido lp
               ->  Hash  ->  Seq Scan on pedidos pe
         ->  Hash
               ->  Seq Scan on clientes c
                     Filter: ((pais)::text = 'Francia'::text)
   ->  Hash  ->  Seq Scan on productos p

Fíjate en el Filter: pais = 'Francia': ha bajado hasta el escaneo de clientes. La vista no ha materializado 47 filas para filtrarlas después; el filtro forma parte del plan. Y en ese plan la palabra v_detalle_ventas no aparece por ninguna parte, que es justo lo que hay que entender.

El riesgo aparece con el anidamiento. v_ventas_por_categoria se apoya en v_detalle_ventas, que une cuatro tablas: dos niveles todavía se leen bien. Pero en bases de datos con años encima es habitual encontrar una vista sobre una vista sobre una vista, cada una con sus JOIN y sus LEFT JOIN "por si acaso"; al expandirlas todas, el planificador se encuentra con una consulta de veinte tablas que no sabe reordenar —a partir de join_collapse_limit, 8 por omisión, deja de probar combinaciones— y elige un plan mediocre. Los síntomas son inconfundibles: una consulta que pide tres columnas tarda cuatro segundos y su EXPLAIN ANALYZE está lleno de tablas que no habías pedido.

Tres reglas prácticas: dos niveles de anidamiento como máximo (si necesitas más, el nivel intermedio probablemente quiere ser una CTE dentro de la consulta final, 10-02, o una vista materializada); ante una vista lenta, EXPLAIN sobre la consulta que la usa, no sobre la vista sola (08-05); y nada de ORDER BY en la definición, que no se garantiza que sobreviva a la expansión.

  1. Vistas materializadas

Aquí se cierra la promesa de 08-04. Una vista materializada sí guarda las filas en disco: es el resultado de una consulta congelado en el tiempo.

CREATE MATERIALIZED VIEW mv_ventas_mensuales AS
SELECT to_char(pe.fecha_pedido, 'YYYY-MM')  AS mes,
       COUNT(DISTINCT pe.id)                AS pedidos,
       ROUND(SUM(lp.cantidad * lp.precio_unitario * (1 - lp.descuento)), 2) AS facturacion
FROM   pedidos       AS pe
JOIN   lineas_pedido AS lp ON lp.pedido_id = pe.id
GROUP  BY 1;

La respuesta es SELECT 12, no CREATE VIEW: ha ejecutado la consulta y ha escrito sus 12 filas.

SELECT * FROM mv_ventas_mensuales ORDER BY mes;
mes pedidos facturacion
2025-03 2 68.80
2025-04 2 61.28
2025-05 2 58.85
2025-06 2 95.48

(4 primeras de 12 filas; la serie completa, de 2025-03 a 2026-02, es la que usarás en 10-03.) Ahora se lee de disco sin tocar pedidos ni lineas_pedido. Y con eso llega el defecto: si mañana entra el pedido 21, esta tabla seguirá diciendo lo mismo. Los datos se quedan como estaban hasta que alguien refresque.

REFRESH MATERIALIZED VIEW mv_ventas_mensuales;               -- bloquea las lecturas
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_ventas_mensuales;  -- no las bloquea
Forma Bloqueo Requisito Coste
REFRESH ACCESS EXCLUSIVE: nadie puede ni leerla mientras se reconstruye Ninguno El más rápido
REFRESH ... CONCURRENTLY EXCLUSIVE: las lecturas siguen funcionando (09-05) Un índice UNIQUE sobre la vista materializada Más lento: calcula el resultado nuevo y aplica las diferencias

Ese requisito no es un capricho: para aplicar diferencias hay que poder identificar cada fila, y eso exige una clave — aquí, CREATE UNIQUE INDEX ux_mv_ventas_mensuales ON mv_ventas_mensuales (mes);. Otros detalles que se descubren tarde: se puede crear vacía con WITH NO DATA (y entonces consultarla da error hasta el primer REFRESH); admite sus propios índices, como una tabla; y no se refresca sola —hay que llamarla desde un cron, desde el planificador de la aplicación o desde pg_cron.

La tabla de decisión

Vista Vista materializada Tabla de resumen
Guarda datos No
Frescura Siempre al día Del último REFRESH La que tú mantengas
Coste de lectura El de la consulta original Muy bajo Muy bajo
Coste de escritura Ninguno El REFRESH completo Actualización incremental (trigger, proceso)
Se puede indexar No (indexa sus tablas)
Complejidad Mínima Baja: una sentencia programada Alta: hay que mantenerla y puede desincronizarse
Cuándo Casi siempre. Empieza aquí Informe caro que tolera datos de hace una hora Métrica que debe ser instantánea y exacta

Cuándo compensa una materializada, en concreto: el informe tarda segundos, se consulta muchas veces al día, y a nadie le importa que los datos sean de hace una hora. El cuadro de mando de dirección de TiendaVerde es el ejemplo perfecto; el stock disponible del carrito, exactamente el contrario.

  1. Metadatos: dónde viven las vistas

En psql, \dv lista las vistas, \dm las materializadas y \d+ v_detalle_ventas muestra las columnas y la definición. Desde SQL, SELECT viewname, viewowner FROM pg_views WHERE schemaname = 'public' y, sobre todo, SELECT pg_get_viewdef('v_productos_activos'::regclass, TRUE): ese segundo argumento a TRUE devuelve la definición formateada, y es la forma correcta de versionar vistas —se vuelca, se guarda en el repositorio junto al código y se revisa como cualquier otro fuente—. También existen las vistas del estándar (information_schema.views) y, para el grafo de dependencias —"¿qué se rompe si borro esta vista?"—, pg_depend.

Nota de dialecto:

Motor Vistas OR REPLACE Materializadas
PostgreSQL Sí, actualizables si son simples Sí, solo añadiendo columnas al final , con REFRESH manual
MySQL 8 Sí, actualizables, con WITH CHECK OPTION No existen: se emulan con una tabla y un evento programado
SQLite Sí, solo lectura No (DROP + CREATE) No
SQL Server CREATE OR ALTER VIEW Sí, se llaman indexed views y se mantienen solas (con muchas restricciones)
Oracle CREATE OR REPLACE VIEW, sin la restricción de columnas Sí, con refresco incremental (FAST REFRESH) y automático

Errores Comunes y Consejos

  • Creer que una vista acelera algo. Ejecuta exactamente el mismo trabajo. Lo que guarda datos es la vista materializada; una vista normal solo guarda texto.
  • Intentar renombrar o quitar una columna con CREATE OR REPLACE VIEW. cannot change name of view column. Solo se pueden añadir columnas al final; para lo demás, DROP + CREATE y recrear lo que dependiera.
  • Anidar vistas sobre vistas sobre vistas. Al expandirlas, el planificador ve una consulta de veinte tablas y deja de reordenar (join_collapse_limit). Máximo dos niveles.
  • Poner ORDER BY en la definición. No está garantizado que sobreviva y estorba: el orden lo pide quien consulta.
  • Escribir por una vista con WHERE sin WITH CHECK OPTION. Puedes insertar o actualizar filas que la vista ya no muestra, y desaparecen ante tus ojos. Y esperar que un INSERT funcione sobre una vista con JOIN o GROUP BY: no es automáticamente actualizable (cannot insert into view), hace falta un trigger INSTEAD OF (10-05).
  • Olvidar el REFRESH. Una vista materializada sin proceso de refresco es un informe congelado que la dirección lee como si fuera de hoy. Y sin índice UNIQUE no hay CONCURRENTLY: cada refresco bloqueará las lecturas.
  • Usar una vista como mecanismo de seguridad sin quitar el permiso sobre la tabla. No protege nada: quien pueda leer clientes seguirá leyendo los correos (11-03).
  • Consejo: nombra las vistas con un prefijo (v_, mv_), y versiona sus definiciones con pg_get_viewdef(..., TRUE). Una vista creada a mano en producción que nadie tiene en el repositorio es deuda técnica invisible.
  • Consejo: una vista por métrica de negocio. El objetivo real no es escribir menos, es que la facturación se calcule igual en todas partes.

Ejercicios

Ejercicio 1

Marketing quiere trabajar con una vista v_clientes_valor que dé, para cada uno de los 15 clientes: id, nombre completo, país, número de pedidos, unidades compradas y facturación (0 si no ha comprado nunca).

  1. Escríbela. Cuida el tipo de JOIN y el COALESCE.
  2. Consúltala ordenada por facturación descendente y comprueba que los clientes 13, 14 y 15 salen con ceros y que la suma de la columna da 727,95 €.
  3. ¿Es actualizable automáticamente? Justifica con la tabla del apartado 4.

Ejercicio 2

Sobre v_productos_activos definida con WITH CHECK OPTION, predice el resultado de cada sentencia y después compruébalo:

-- a)
UPDATE v_productos_activos SET stock = stock + 50 WHERE id = 5;
-- b)
UPDATE productos SET activo = FALSE WHERE id = 5;
SELECT COUNT(*) FROM v_productos_activos;
-- c)
INSERT INTO v_productos_activos (nombre, categoria_id, precio, stock) VALUES ('Té chai 100 g', 4, 6.90, 30);
-- d)
DROP VIEW v_detalle_ventas;

Ejercicio 3

El cuadro de mando de dirección ejecuta cada vez que se abre una consulta que tarda 4 segundos: facturación por mes y categoría desde el inicio de la actividad. Se abre unas 200 veces al día y se acepta un desfase de una hora.

  1. ¿Vista, vista materializada o tabla de resumen? Justifica con la tabla comparativa.
  2. Escribe el objeto elegido y lo necesario para poder refrescarlo sin bloquear a quien lo esté consultando.
  3. ¿Qué cambiaría si el requisito fuera "los datos deben ser del segundo actual"?

Soluciones

Solución 1

1. El LEFT JOIN no es negociable: con INNER desaparecerían los tres clientes sin pedidos.

CREATE VIEW v_clientes_valor AS
SELECT c.id, c.nombre || ' ' || c.apellidos AS cliente, c.pais,
       COUNT(DISTINCT pe.id)         AS pedidos,
       COALESCE(SUM(lp.cantidad), 0) AS unidades,
       COALESCE(ROUND(SUM(lp.cantidad * lp.precio_unitario * (1 - lp.descuento)), 2), 0) AS facturacion
FROM   clientes           AS c
LEFT JOIN pedidos         AS pe ON pe.cliente_id = c.id
LEFT JOIN lineas_pedido   AS lp ON lp.pedido_id  = pe.id
GROUP  BY c.id, c.nombre, c.apellidos, c.pais;

-- 2.
SELECT * FROM v_clientes_valor ORDER BY facturacion DESC, id LIMIT 4;
id cliente pais pedidos unidades facturacion
7 Sofia Moreira Costa Portugal 2 11 111.88
1 Lucía Martínez Soler España 3 15 107.60
9 Camille Dubois Francia 2 9 70.87
10 Julien Moreau Francia 1 8 66.90

(4 primeras de 15 filas.) Los clientes 13, 14 y 15 cierran con 0 y 0.00, y SELECT SUM(facturacion) FROM v_clientes_valor da 727.95. El COUNT(DISTINCT pe.id) es obligatorio: con COUNT(pe.id) Lucía tendría 9 "pedidos", uno por línea (04-04).

3. No es actualizable automáticamente, y falla en tres requisitos a la vez: tiene tres tablas en el FROM, tiene GROUP BY y tiene agregados en el SELECT. Un UPDATE sobre ella daría cannot update view "v_clientes_valor", con la pista de que hace falta un trigger INSTEAD OF. Y tiene sentido: ¿qué debería hacer la base de datos si le pides "pon la facturación de Sofia a 200 €"?

Solución 2

a) Funciona: UPDATE 1. La vista es actualizable —una sola tabla, sin agregados—, la fila 5 está activa antes y después, y el cambio va a productos.

b) UPDATE 1, y después COUNT devuelve 18. El UPDATE va contra la tabla, así que WITH CHECK OPTION no interviene: solo vigila las escrituras hechas a través de la vista. El tomate desaparece de v_productos_activos sin más aviso, que es precisamente el comportamiento deseado del borrado lógico.

c) Funciona: INSERT 0 1. El INSERT no menciona activo, así que la fila nueva toma el DEFAULT TRUE de la tabla, cumple el WHERE de la vista y WITH CHECK OPTION la deja pasar. Habría fallado escribiendo activo a FALSE explícitamente, o si el WHERE filtrara por una columna cuyo valor por omisión no lo satisficiera. Recarga el script después: acabas de añadir un producto 21 al catálogo.

d) Falla, porque v_ventas_por_categoria depende de ella:

ERROR:  cannot drop view v_detalle_ventas because other objects depend on it
DETAIL:  view v_ventas_por_categoria depends on view v_detalle_ventas
HINT:  Use DROP ... CASCADE to drop the dependent objects too.

DROP VIEW v_detalle_ventas CASCADE funcionaría, y borraría también v_ventas_por_categoria sin preguntar. Es el peligro del anidamiento del apartado 5, ahora en forma de dependencia.

Solución 3

1. Vista materializada. Los tres criterios coinciden con la columna del medio de la tabla: la lectura es cara (4 s), se repite mucho (200 veces al día = 800 segundos de CPU diarios) y se tolera desfase. Una vista normal no ahorraría nada; una tabla de resumen mantenida por triggers daría datos instantáneos, pero a cambio de complejidad y de un coste por cada INSERT en lineas_pedido que aquí nadie ha pedido.

2.

CREATE MATERIALIZED VIEW mv_ventas_mes_categoria AS
SELECT to_char(dv.fecha_pedido, 'YYYY-MM') AS mes,
       cat.id AS categoria_id, cat.nombre AS categoria,
       ROUND(SUM(dv.importe), 2) AS facturacion
FROM   v_detalle_ventas AS dv
JOIN   categorias       AS cat ON cat.id = dv.categoria_id
GROUP  BY 1, 2, 3;

-- Imprescindible para poder refrescar sin bloquear
CREATE UNIQUE INDEX ux_mv_vmc ON mv_ventas_mes_categoria (mes, categoria_id);

Y en el cron, cada hora: REFRESH MATERIALIZED VIEW CONCURRENTLY mv_ventas_mes_categoria;. Sin ese índice único, CONCURRENTLY falla y el refresco toma ACCESS EXCLUSIVE, dejando el cuadro de mando inaccesible durante los 4 segundos del recálculo.

3. Si los datos deben ser del segundo actual, la materializada queda descartada: una vista normal bien indexada (módulo 8) y, si aun así no baja de los 4 segundos, una tabla de resumen mantenida por triggers (10-05), con todo su coste de complejidad y su riesgo de desincronización. La pregunta honesta, antes de nada, es si "del segundo actual" es un requisito de verdad o una costumbre: en un informe mensual casi nunca lo es.

Conclusión

La primera herramienta del módulo 10 es también la más barata:

  • Una vista es una consulta con nombre. No almacena datos, se expande en la consulta que la usa —hasta el punto de que su nombre no aparece en el EXPLAIN— y por tanto no acelera nada.
  • CREATE OR REPLACE VIEW solo permite añadir columnas al final: ni renombrar, ni quitar, ni reordenar, ni cambiar tipos. Lo demás es DROP + CREATE, arrastrando a lo que dependiera.
  • Los cinco motivos: simplificar (los cuatro JOIN de v_detalle_ventas, 47 líneas y 727,95 €), estandarizar una métrica para que la facturación se calcule igual en todas partes, encapsular el borrado lógico (v_productos_activos, 19 de 20 productos — la promesa de 05-04), desacoplar la aplicación del esquema físico como contrato estable frente al expand/contract de 05-06, y exponer solo una parte de los datos (con los permisos en 11-03).
  • Una vista es actualizable automáticamente si viene de una sola tabla, sin agregados, sin DISTINCT, sin GROUP BY y con columnas simples; WITH CHECK OPTION impide escribir filas que la propia vista no mostraría. Para el resto, trigger INSTEAD OF (10-05).
  • Anidar vistas es cómodo y peligroso: pasados dos niveles, el planificador deja de reordenar los JOIN y el plan se degrada. Ante la duda, EXPLAIN de la consulta completa (08-05).
  • Las vistas materializadas sí guardan filas: 12 meses precalculados que se leen al instante y envejecen hasta el REFRESH. CONCURRENTLY evita bloquear las lecturas, pero exige un índice UNIQUE. Compensan en informes caros que toleran datos de hace una hora.

Una vista resuelve el problema de reutilizar una consulta entre sesiones y entre personas. Pero muchas veces lo que quieres no es reutilizarla, sino entenderla: descomponer una consulta de cuarenta líneas en pasos con nombre, aquí y ahora, sin crear ningún objeto permanente. En la lección siguiente, expresiones de tabla comunes (CTE), pondrás WITH delante del SELECT y verás cómo las tablas derivadas anidadas de tres niveles de 07-04 se convierten en una lista de pasos legibles; encadenarás varias CTE donde cada una se apoya en la anterior; entenderás por qué lo que dicen los tutoriales antiguos sobre materialización dejó de ser cierto en PostgreSQL 12; y, sobre todo, escribirás tu primera consulta recursiva para recorrer por fin la jerarquía completa de empleados y la cadena de referidos que 03-06 dejó a medias.

Curso de SQL

Módulo 1: Introducción a SQL

Módulo 2: Consultas básicas de SQL

Módulo 3: Trabajando con múltiples tablas

Módulo 4: Filtrado avanzado de datos

Módulo 5: Manipulación de datos

Módulo 6: Funciones avanzadas de SQL

Módulo 7: Subconsultas y consultas anidadas

Módulo 8: Índices y optimización de rendimiento

Módulo 9: Transacciones y concurrencia

Módulo 10: Temas avanzados

Módulo 11: SQL en la práctica

Módulo 12: Proyecto final

© Copyright 2026. Todos los derechos reservados