PostgreSQL 的 max_index_keys:为什么 INCLUDE 列也会占用 32 个名额

2026-09-15 28 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

在 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_attotal_amountshipping_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.indnkeyattspg_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 的硬限制。

可以按下面的顺序评估:

  1. 先确认查询模式。 只有稳定、频繁且有明确性能收益的查询,才值得专门设计覆盖索引。
  2. 把过滤和排序列放在 key 部分。 这些列决定索引的组织方式,顺序需要结合谓词和排序需求设计。
  3. 只把真正用于返回的窄列放入 INCLUDE 大型 textjsonb 或高频变化的列可能让索引迅速膨胀。
  4. 检查索引总列数。 不能只数括号中参与排序的列,还要把 INCLUDE 部分一起计算。
  5. 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 当作无风险的生产配置变更。

覆盖索引的目标是减少查询成本,而不是把整行数据复制一份。理解这个边界,能让索引设计在性能收益和长期维护成本之间保持平衡。


相关推荐