仪表盘拖慢了全库:视图、物化视图与权限
老板天天开的后台仪表盘每次都实时算全量报表,把库拖慢了。你用物化视图提速,顺手给运营开了只读账号。
学 · 40 min
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; -- 手动更新快照易错视图改名/换定义不影响底表;但「以为视图能提速」是最常见误解--它是查询的别名,不是缓存。
02REFRESH MATERIALIZED VIEW CONCURRENTLY 需要唯一索引
场景普通 REFRESH 期间仪表盘直接查询报错/排队--大促时这不能忍。
普通 refresh 拿排他锁:刷新期间读也被挡。
concurrently版对比新旧结果、增量替换,刷新期间可读。代价和前提各一:刷新本身更慢;物化视图上必须有唯一索引。create unique index on mv_monthly (月份); -- 前提 refresh materialized view concurrently mv_monthly; -- 另一个窗口此刻查询 mv_monthly 不受阻塞易错没有唯一索引时用 concurrently 直接报错--这是「为什么刷新锁表」排查时最先查的一项。
03物化视图的刷新代价与数据新鲜度取舍
场景刷新多勤?这是个业务问题不是技术问题。
刷新 = 全量(或增量)重算:表越大刷新越贵,还占双倍空间(新旧快照切换)。决策框架:业务能容忍多旧的数据?分钟级延迟换十倍查询提速,绝大多数报表都愿意。定时刷新(pg_cron / crontab)+ 查询时标注「数据截至 HH:MM」是标准交付形态。
-- 看物化视图多大、上次刷新何时(自己维护一列刷新时间也行) select relname, pg_size_pretty(pg_relation_size(relname::regclass)) from pg_class where relname = 'mv_monthly';易错对「必须实时」的数据上物化视图是方向错误--那该做的是优化查询本身或上缓存层。
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)--「加了张表运营看不到」先查这个。
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
- 把 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 月份; - 改成物化视图,对比两者查询耗时
参考答案
预期:物化视图快两三个量级。代价是数据停在建立那一刻--往 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 月份; -- 查落盘快照:毫秒级 - 加唯一索引后用 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; - 建一个只读角色,授予部分表的 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 - 给 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;