先看规划,再谈感觉。EXPLAIN (ANALYZE, BUFFERS) 是唯一诚实的读者。

假设博客要按栏目列出已发布文章:

SQLmigrations/0006_posts_topic.sql
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) 能服务「已发布 + 某栏目 + 按时间倒序」。把最常等于的列放前面,把排序列放最后。

SQL
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,查询就直接等值。

SQL
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 时盯三件事:节点类型、实际行数、是否从缓存读。行数估计差一个数量级,规划器就会选错路。那才是该建统计信息、该改写法的信号。