仪表盘拖慢了全库:视图、物化视图与权限

老板天天开的后台仪表盘每次都实时算全量报表,把库拖慢了。你用物化视图提速,顺手给运营开了只读账号。

学 40 min
练 65 min
盘 15 min
共 120 分钟

学 · 40 min

  1. 01视图不存数据、物化视图存数据

    场景老板的仪表盘每次打开都实时聚合百万行--先把「视图」和「物化视图」分清楚。

    视图 = 存起来的一条查询,每次 select 都重新执行(不省任何计算,价值在封装和安全);物化视图 = 查询结果落盘,查它就是查表,毫秒级。代价:数据是快照,必须定期 REFRESH 才更新。仪表盘这种「容忍分钟级延迟、查询巨重」的场景是物化视图的本命。

    create view v_monthly as
    select date_trunc('month', created_at) as 月份, count(*), sum(total_amount)
    from orders group by 1;                    -- 每次都实时算
    
    create materialized view mv_monthly as
    select date_trunc('month', created_at) as 月份, count(*), sum(total_amount)
    from orders group by 1;                    -- 算一次存下来
    
    refresh materialized view mv_monthly;      -- 手动更新快照

    易错视图改名/换定义不影响底表;但「以为视图能提速」是最常见误解--它是查询的别名,不是缓存。

  2. 02REFRESH MATERIALIZED VIEW CONCURRENTLY 需要唯一索引

    场景普通 REFRESH 期间仪表盘直接查询报错/排队--大促时这不能忍。

    普通 refresh 拿排他锁:刷新期间读也被挡。concurrently 版对比新旧结果、增量替换,刷新期间可读。代价和前提各一:刷新本身更慢;物化视图上必须有唯一索引

    create unique index on mv_monthly (月份);   -- 前提
    
    refresh materialized view concurrently mv_monthly;
    -- 另一个窗口此刻查询 mv_monthly 不受阻塞

    易错没有唯一索引时用 concurrently 直接报错--这是「为什么刷新锁表」排查时最先查的一项。

  3. 03物化视图的刷新代价与数据新鲜度取舍

    场景刷新多勤?这是个业务问题不是技术问题。

    刷新 = 全量(或增量)重算:表越大刷新越贵,还占双倍空间(新旧快照切换)。决策框架:业务能容忍多旧的数据?分钟级延迟换十倍查询提速,绝大多数报表都愿意。定时刷新(pg_cron / crontab)+ 查询时标注「数据截至 HH:MM」是标准交付形态。

    -- 看物化视图多大、上次刷新何时(自己维护一列刷新时间也行)
    select relname, pg_size_pretty(pg_relation_size(relname::regclass))
    from pg_class where relname = 'mv_monthly';

    易错对「必须实时」的数据上物化视图是方向错误--那该做的是优化查询本身或上缓存层。

  4. 04角色、GRANT 与最小权限原则

    场景运营要查数据,总不能把超级用户密码给他。

    create role readonly nologin 建角色,grant select on 表 to readonly 授权,再把登录账号(role login + 密码)加进角色。最小权限原则:每个账号的权限刚好够干自己的活、多一点都不给--运营只读,分析只读几张表,写入只走应用账号。

    create role readonly nologin;
    grant usage on schema public to readonly;
    grant select on all tables in schema public to readonly;
    
    create role ops_report login password '...';
    grant readonly to ops_report;    -- 运营账号继承只读权限
    
    -- 验证越权:用 ops_report 连上后
    -- update orders set status = 1;   -- ERROR: permission denied

    易错新表不会自动继承 grant(除非 alter default privileges)--「加了张表运营看不到」先查这个。

  5. 05行级安全(RLS)简介

    场景「每个用户只能看自己的订单」--不用改任何查询,数据库层强制。

    RLS 给表挂策略:alter table ... enable row level security 开关 + create policy ... using (...) 定义哪些行可见。开启后,普通角色查这张表自动被策略过滤(比如 user_id = current_setting(...))。多租户 SaaS 的标配防线。

    alter table orders enable row level security;
    
    create policy own_orders on orders
      using (user_id = current_setting('app.user_id')::uuid);
    -- 会话里 set app.user_id = '...' 后,select 自动只见自己的单

    易错表的 owner 默认绕过 RLS--要真拦住自己得 force row level security;应用连接千万别用 owner 账号。

练 · 65 min

  1. 把 D13 的月报查询建成普通视图
    参考答案

    视图只是把一条查询存了起来:对它 select 每次都重新聚合百万行,\timing 一开就能感受到。它的价值是封装和权限边界,不是提速。

    create view v_monthly as
    select date_trunc('month', created_at) as 月份,
           count(*) as 单数,
           sum(total_amount) as gmv
    from orders
    group by 1;
    
    select * from v_monthly order by 月份;
  2. 改成物化视图,对比两者查询耗时
    参考答案

    预期:物化视图快两三个量级。代价是数据停在建立那一刻--往 orders 插一单,mv_monthly 里看不到,必须 refresh 才更新。

    create materialized view mv_monthly as
    select date_trunc('month', created_at) as 月份,
           count(*) as 单数,
           sum(total_amount) as gmv
    from orders
    group by 1;
    
    -- 对比(\timing on)
    select * from v_monthly  order by 月份;   -- 实时聚合:秒级
    select * from mv_monthly order by 月份;   -- 查落盘快照:毫秒级
  3. 加唯一索引后用 CONCURRENTLY 刷新,验证刷新期间可读
    参考答案

    预期:窗口 B 的查询全部正常返回。对照组:去掉唯一索引改用普通 refresh,窗口 B 会被挡到刷新结束;而没有唯一索引时用 concurrently 会直接报错--这是「刷新为什么锁表」排查的第一项。

    -- 前提:物化视图上必须有唯一索引
    create unique index on mv_monthly (月份);
    
    -- 窗口 A:刷新(concurrently 版刷新期间可读)
    refresh materialized view concurrently mv_monthly;
    
    -- 窗口 B:刷新进行时反复查
    select * from mv_monthly order by 月份 desc limit 1;
  4. 建一个只读角色,授予部分表的 SELECT 权限并测试越权访问
    参考答案

    最小权限:ops_report 只能读授权过的三张表,写不进任何表。注意新表不会自动继承 grant,「加了张表运营看不到」先查 alter default privileges。

    create role readonly nologin;
    grant usage on schema public to readonly;
    grant select on orders, order_items, products to readonly;   -- 只给需要的表
    
    create role ops_report login password 'ops_dev_123';
    grant readonly to ops_report;
    
    -- 新开连接验证:psql "dbname=shop user=ops_report" 后
    -- select count(*) from orders;    -- OK
    -- select * from users;            -- ERROR: permission denied(没授权这张表)
    -- update orders set status = 1;   -- ERROR: permission denied
  5. 给 orders 加一条 RLS 策略,让「用户」只能看自己的订单
    参考答案

    current_setting 的第二个参数 true:参数没设置时返回 NULL 而不是报错,策略判假=一行都看不到(安全默认)。两个坑:超级用户和表 owner 天生绕过 RLS,必须换普通账号验证,或 alter table orders force row level security。

    alter table orders enable row level security;
    
    create policy own_orders on orders
      using (user_id = current_setting('app.user_id', true)::uuid);
    
    -- 用普通账号(如 ops_report,已授过 orders 的 select)连上后测试:
    -- set app.user_id = '00000000-0000-0000-0000-000000000042';
    -- select count(*) from orders;   -- 只剩该用户的单
    -- reset app.user_id;
过关标准 能说清物化视图的适用场景,以及它带来的数据延迟问题怎么权衡。