在 PostgreSQL 11 引入 INCLUDE 列之后,max_index_keys 这个名字变得有些误导:它统计的已经不只是参与索引排序和查找的 key,还包括覆盖索引中的非 key 列。但这个上限仍然固定为 32,而且并不是简单改一个配置值就能突破的限制。
理解这一点很重要。设计覆盖索引时,如果把大量返回列都塞进 INCLUDE,很容易在索引还没有明显变宽之前,先撞上列数上限。
max_index_keys 实际限制了什么
传统索引可以简单理解为:
CREATE INDEX orders_customer_status_idx
ON orders (customer_id, status);
这里有两个索引 key,它们参与 B-tree 的排序、比较和查找。
加入 INCLUDE 后,索引可以携带额外列:
CREATE INDEX orders_covering_idx
ON orders (customer_id, status)
INCLUDE (created_at, total_amount, shipping_address_id);
created_at、total_amount 和 shipping_address_id 不参与索引排序,但它们仍然是索引元组的一部分。PostgreSQL 计算索引列数量时,会把 key 列和 INCLUDE 列放在同一个总数里计算:
索引列总数 = key 列数量 + INCLUDE 列数量
因此,在默认上限下,下面这类设计最多只能包含 32 列,而不是 32 个 key 再加任意数量的覆盖列。
为什么上限仍然是 32
max_index_keys 看起来像一个普通的 GUC,但 32 并不只是为了防止索引定义过于复杂。索引元组的磁盘表示需要记录索引列的数量,而现有的 on-disk tuple format 为这个信息预留了固定空间。
这意味着 PostgreSQL 不能只把 GUC 的最大值从 32 改成 64,然后期待一切自动工作。要提高上限,还需要考虑索引元组格式、读取和写入逻辑,以及不同版本或不同构建之间的存储兼容性。换句话说,这个限制更接近存储格式边界,而不是普通的运行时策略开关。
实际运维中,可以这样查看当前设置:
SHOW max_index_keys;
SELECT name, setting, unit, context, short_desc
FROM pg_settings
WHERE name = 'max_index_keys';
即使某个环境显示了可调整的配置入口,也不应该把“修改参数”当成常规的扩容方案。对于生产系统,更现实的做法是重新评估索引列设计。
用一个可运行的例子验证列数边界
下面的示例会创建一张带有 33 个普通列的临时表,然后创建一个包含 31 个 key 和 1 个 INCLUDE 列的索引。整个索引正好有 32 列,通常可以通过;接着再尝试创建 32 个 key 加 1 个 INCLUDE 列的索引,总数为 33,应该触发上限错误。
请在测试数据库或事务中运行,不要直接对生产表执行:
DROP TABLE IF EXISTS index_key_limit_demo;
CREATE TABLE index_key_limit_demo (
id bigint,
payload text
);
DO $$
BEGIN
FOR i IN 1..33 LOOP
EXECUTE format(
'ALTER TABLE index_key_limit_demo ADD COLUMN c%s integer',
i
);
END LOOP;
END
$$;
-- 31 个 key + 1 个 INCLUDE 列 = 32 列
DO $$
DECLARE
key_columns text;
BEGIN
SELECT string_agg(format('c%s', i), ', ' ORDER BY i)
INTO key_columns
FROM generate_series(1, 31) AS s(i);
EXECUTE format(
'CREATE INDEX index_key_limit_demo_32_idx
ON index_key_limit_demo (%s) INCLUDE (c32)',
key_columns
);
END
$$;
-- 32 个 key + 1 个 INCLUDE 列 = 33 列,预期失败
DO $$
DECLARE
key_columns text;
BEGIN
SELECT string_agg(format('c%s', i), ', ' ORDER BY i)
INTO key_columns
FROM generate_series(1, 32) AS s(i);
EXECUTE format(
'CREATE INDEX index_key_limit_demo_33_idx
ON index_key_limit_demo (%s) INCLUDE (c33)',
key_columns
);
END
$$;
测试时,第二个 CREATE INDEX 通常会报告索引列数超过 32 的错误。具体错误文本可能随 PostgreSQL 版本略有不同,但关键点是:INCLUDE 列同样消耗索引列名额。
如果只想检查已有索引的定义,可以使用:
SELECT schemaname,
tablename,
indexname,
indexdef
FROM pg_indexes
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
ORDER BY schemaname, tablename, indexname;
indexdef 会展示 INCLUDE (...) 部分,适合用来快速筛查覆盖列很多的索引。需要注意的是,目录查询不会直接给出“key 列数量加 INCLUDE 列数量”的汇总值;若要自动审计,可以基于 pg_index.indnkeyatts 和 pg_index.indnatts 做进一步分析。前者表示 key 列数量,后者表示索引中的总列数量:
SELECT n.nspname AS schema_name,
c.relname AS table_name,
irel.relname AS index_name,
x.indnkeyatts AS key_columns,
x.indnatts AS total_index_columns,
x.indnatts - x.indnkeyatts AS include_columns
FROM pg_index AS x
JOIN pg_class AS c
ON c.oid = x.indrelid
JOIN pg_class AS irel
ON irel.oid = x.indexrelid
JOIN pg_namespace AS n
ON n.oid = c.relnamespace
WHERE x.indnatts >= 28
ORDER BY x.indnatts DESC, n.nspname, c.relname, irel.relname;
这个查询会优先列出接近 32 列上限的索引,便于在上线前发现风险。
设计覆盖索引时应该怎么取舍
INCLUDE 很适合支持 index-only scan,但它不是免费的“把查询需要的所有字段都复制进索引”的开关。每增加一列,都会带来索引膨胀、写放大、VACUUM 和维护成本;列数还会受到 max_index_keys 的硬限制。
可以按下面的顺序评估:
- 先确认查询模式。 只有稳定、频繁且有明确性能收益的查询,才值得专门设计覆盖索引。
- 把过滤和排序列放在 key 部分。 这些列决定索引的组织方式,顺序需要结合谓词和排序需求设计。
- 只把真正用于返回的窄列放入
INCLUDE。 大型text、jsonb或高频变化的列可能让索引迅速膨胀。 - 检查索引总列数。 不能只数括号中参与排序的列,还要把
INCLUDE部分一起计算。 - 用
EXPLAIN (ANALYZE, BUFFERS)验证收益。 覆盖索引存在,并不意味着查询一定会选择 index-only scan;可见性映射、选择性和成本估算都会影响计划。
一个实用的上线前检查可以直接写成阈值告警:
SELECT n.nspname AS schema_name,
c.relname AS table_name,
irel.relname AS index_name,
x.indnatts AS total_columns,
x.indnkeyatts AS key_columns,
x.indnatts - x.indnkeyatts AS include_columns
FROM pg_index AS x
JOIN pg_class AS c ON c.oid = x.indrelid
JOIN pg_class AS irel ON irel.oid = x.indexrelid
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE x.indnatts >= 30;
把 30 换成团队希望的预警阈值即可。接近 32 时,不要等到下一次需求再增加覆盖列;应先合并重复索引、拆分查询,或重新判断哪些列确实值得放进索引。
结论:把它当成存储格式边界,而不是普通调优参数
max_index_keys 的名字没有完整反映 PostgreSQL 当前的行为。自 PostgreSQL 11 起,INCLUDE 列也会计入索引列总数,而 32 的上限仍受索引 tuple 的磁盘格式约束。
对应用和数据库团队来说,最稳妥的策略是:
- 将 key 列和
INCLUDE列一起计算; - 对接近 32 列的索引建立审计和预警;
- 优先减少不必要的覆盖列,而不是寻找绕过上限的方法;
- 在测试环境验证索引大小、写入开销和实际执行计划;
- 不把修改 GUC 当作无风险的生产配置变更。
覆盖索引的目标是减少查询成本,而不是把整行数据复制一份。理解这个边界,能让索引设计在性能收益和长期维护成本之间保持平衡。