派生表不能建索引,因其是动态生成的中间结果集,无物理存储和元数据;性能优化关键在于原始表的复合索引、字段精简及合理使用CTE替代嵌套派生表。

派生表本身不能建索引——它不是物理表,而是查询执行时动态生成的中间结果集。想靠“给派生表加索引”提速,这条路走不通。
为什么你不能对(SELECT ...)执行CREATE INDEX
数据库在解析FROM (SELECT ...)时,根本不会把它当作一个可持久化对象:没有存储、没有元数据、不进pg_class(PostgreSQL)或information_schema.tables(MySQL)。所谓“派生表索引”,实际是优化器能否把子查询“合并”进外层、从而复用原表索引的问题。
- MySQL 5.7 及更早版本默认强制物化,子查询先执行完生成无索引临时表,再参与JOIN → 必然全表扫描
- PostgreSQL 16 默认尝试合并,但一旦含
GROUP BY、LIMIT、ORDER BY或聚合函数,就触发物化 → 临时结果无索引 - 哪怕你写
SELECT * FROM (SELECT id, name FROM users WHERE status = 1) AS u WHERE u.id > 1000,真正起作用的是users(status, id)这个复合索引,不是“u表”的索引
EXPLAIN里看不到Using index?先检查子查询内部是否走索引
物化前的性能瓶颈,往往卡在子查询自己就很慢。别急着优化外层,先确认子查询单跑是否高效:
- 单独执行子查询:
EXPLAIN SELECT user_id, MAX(created_at) FROM orders GROUP BY user_id→ 如果显示type: ALL或rows极大,说明orders表缺(user_id, created_at)复合索引 - MySQL 8.0.22+ 可验证条件是否下推:
EXPLAIN FORMAT=TREE中找-> Filter on dt.user_id字样,有则说明外层WHERE被塞进子查询执行了 - PostgreSQL 中用
EXPLAIN (ANALYZE, BUFFERS)看Subquery Scan节点的Actual Rows和Buffers,若远大于预期,大概率已物化且未走索引
真正能“等效索引”的三个实操动作
与其纠结“怎么给派生表建索引”,不如做这三件事:
- 在子查询涉及的**原始表**上建好复合索引:比如
FROM (SELECT user_id, COUNT(*) FROM logs WHERE log_time >= '2024-01-01' GROUP BY user_id) AS stats,必须确保logs(log_time, user_id)有索引 - 删掉子查询里的
SELECT *,只选外层真正需要的字段:减少物化时的内存占用和传输开销,尤其影响MySQL临时表大小上限(tmp_table_size) - 当必须物化时,用
WITH替代嵌套派生表:CTE在PostgreSQL 12+支持MATERIALIZED提示(WITH stats AS MATERIALIZED (SELECT ...)),MySQL 8.0+ CTE虽不可显式物化,但比多层FROM (SELECT (SELECT ))更易被优化器识别和复用
最容易被忽略的一点:物化与否不是由“写法多优雅”决定的,而是由GROUP BY、LIMIT、聚合函数这些硬性条件触发的。哪怕你加了/*+ DERIVED_CONDITION_PUSHDOWN */提示,只要子查询里有ORDER BY created_at DESC LIMIT 1,MySQL 就不会下推,PostgreSQL 就会物化——这时候再想“索引”,就得回到原始表本身去补。

















