先看规划,再谈感觉。EXPLAIN (ANALYZE, BUFFERS) 是唯一诚实的读者。
假设博客要按栏目列出已发布文章:
1create table posts (2 id bigserial primary key,3 slug text not null unique,4 topic text not null,5 status text not null,6 published_at timestamptz7);8 9explain (analyze, buffers)10select id, slug, title11from posts12where topic = 'TypeScript'13 and status = 'published'14order by published_at desc15limit 20;如果输出是 Seq Scan on posts,意思是:每一行都看一遍。一千行你感觉不到,一百万行时它会变成一次全表散步。
让索引覆盖过滤条件
BTree 索引的左前缀规则很实际:(status, topic, published_at) 能服务「已发布 + 某栏目 + 按时间倒序」。把最常等于的列放前面,把排序列放最后。
1create index posts_feed_idx2 on posts (status, topic, published_at desc)3 where deleted_at is null;部分索引把回收站排除在外。规划器一旦选中它,你应看到 Index Scan using posts_feed_idx。
函数会毁掉索引
where lower(slug) = lower($1) 让索引变成摆设,除非你建的是表达式索引。能把数据规范化就规范化:存小写 slug,查询就直接等值。
1-- 坏:函数包在列上2select * from posts where lower(slug) = 'ts-discriminated-unions';3 4-- 好:列保持可索引的形状5select * from posts where slug = 'ts-discriminated-unions';索引是给已经想清楚的访问路径准备的,不是给所有 WHERE 的护身符。
栏目过滤能走索引之后,列表页才撑得住。应用层怎么把查询收成类型安全的路由,见 hono-typed-routes。
看 EXPLAIN 时盯三件事:节点类型、实际行数、是否从缓存读。行数估计差一个数量级,规划器就会选错路。那才是该建统计信息、该改写法的信号。
