问题现象
线上巡检时发现某个分区表索引状态异常:
\d+ parent_table
发现父表上的一个分区索引显示为 INVALID:
parent_idx_xxx
btree (col_a, col_b)
WHERE status = 1
AND flag = 0
INVALID
但是查看各个分区表时,却发现对应索引都是正常的。
最开始怀疑:
-
索引损坏
-
REINDEX失败
-
分区索引元数据异常
于是开始排查。
一、确认父索引状态
首先查看父索引在系统表中的状态:
SELECT
c.oid::regclass AS index_name,
i.indisvalid,
i.indisready,
i.indislive
FROM pg_class c
JOIN pg_index i
ON i.indexrelid = c.oid
WHERE c.oid =
'parent_idx_xxx'::regclass;
结果:
index_name | parent_idx_xxx
indisvalid | f
indisready | t
indislive | t
说明:
索引已存在
索引可见
索引已准备完成
但不被认为是有效索引
也就是说 PostgreSQL 认为这个父索引尚未完成构建。
二、检查子分区索引状态
查看所有分区索引:
WITH parent_idx AS (
SELECT 'parent_idx_xxx'::regclass AS idx_oid
),
parts AS (
SELECT inhrelid AS part_table
FROM pg_inherits
WHERE inhparent = 'parent_table'::regclass
),
attached AS (
SELECT i.inhrelid AS child_idx
FROM pg_inherits i
JOIN parent_idx p
ON i.inhparent = p.idx_oid
)
SELECT
p.part_table::regclass,
ci.indexrelid::regclass,
ix.indisvalid,
ix.indisready,
CASE
WHEN a.child_idx IS NULL
THEN 'NOT ATTACHED'
ELSE 'ATTACHED'
END AS attach_status
FROM parts p
LEFT JOIN pg_index ci
ON ci.indrelid = p.part_table
LEFT JOIN pg_index ix
ON ix.indexrelid = ci.indexrelid
LEFT JOIN attached a
ON a.child_idx = ci.indexrelid
WHERE pg_get_indexdef(ci.indexrelid) LIKE '%col_a%'
AND pg_get_indexdef(ci.indexrelid) LIKE '%col_b%';
结果显示:
所有已存在的子索引:
indisvalid=t
indisready=t
ATTACHED
说明现有的子索引全部正常。
三、发现关键线索
继续检查:
SELECT
(
SELECT count(*)
FROM pg_inherits
WHERE inhparent =
'parent_table'::regclass
) AS partition_count,
(
SELECT count(*)
FROM pg_inherits
WHERE inhparent =
'parent_idx_xxx'::regclass
) AS attached_index_count;
结果:
partition_count = 27
attached_index_count = 26
这里发现异常:
分区数 ≠ 已挂载索引数
也就是说:
至少有一个分区缺少对应索引
这也是导致父索引始终 INVALID 的真正原因。
四、定位缺失分区
执行:
WITH parts AS (
SELECT inhrelid AS part_table
FROM pg_inherits
WHERE inhparent =
'parent_table'::regclass
),
attached_idx AS (
SELECT ci.indrelid AS part_table
FROM pg_inherits pi
JOIN pg_index ci
ON ci.indexrelid = pi.inhrelid
WHERE pi.inhparent =
'parent_idx_xxx'::regclass
)
SELECT
p.part_table::regclass AS missing_partition
FROM parts p
LEFT JOIN attached_idx a
ON a.part_table = p.part_table
WHERE a.part_table IS NULL;
结果:
parent_table_default
找到问题分区:
Default分区缺少对应索引
五、补建索引
查看发现 Default 分区确实没有该索引。
创建索引:
CREATE INDEX CONCURRENTLY idx_default_xxx
ON parent_table_default (col_a, col_b)
WHERE status = 1
AND flag = 0;
由于线上环境,建议使用:
CREATE INDEX CONCURRENTLY
避免长时间锁表。
六、Attach到父索引
创建完成后:
SET lock_timeout = '2s';
ALTER INDEX parent_idx_xxx
ATTACH PARTITION idx_default_xxx;
再次检查:
SELECT
indisvalid,
indisready,
indislive
FROM pg_index
WHERE indexrelid =
'parent_idx_xxx'::regclass;
结果:
indisvalid = t
indisready = t
indislive = t
恢复正常。
原因分析
PostgreSQL 分区索引本质上由两部分组成:
-
父索引(元数据定义)
-
各个分区上的实际索引
只有当:
所有分区
都有对应索引
并且全部ATTACH
父索引才会被标记为:
indisvalid = true
如果哪怕缺少一个分区索引:
父索引就会显示 INVALID
即使其它所有分区索引全部正常。
快速排查模板
以后遇到:
分区表父索引 INVALID
优先执行:
SELECT
(
SELECT count(*)
FROM pg_inherits
WHERE inhparent='父表'::regclass
) AS partition_count,
(
SELECT count(*)
FROM pg_inherits
WHERE inhparent='父索引'::regclass
) AS attached_index_count;
如果:
partition_count > attached_index_count
基本可以判断:
有分区缺失对应索引
然后进一步定位:
WITH parts AS (
SELECT inhrelid AS part_table
FROM pg_inherits
WHERE inhparent = '父表'::regclass
),
attached_idx AS (
SELECT ci.indrelid AS part_table
FROM pg_inherits pi
JOIN pg_index ci
ON ci.indexrelid = pi.inhrelid
WHERE pi.inhparent = '父索引'::regclass
)
SELECT p.part_table::regclass
FROM parts p
LEFT JOIN attached_idx a
ON a.part_table = p.part_table
WHERE a.part_table IS NULL;
总结
本次问题最终并不是索引损坏,也不是需要 REINDEX,而是:
Default分区缺少对应索引
↓
父索引少了一个已Attach的子索引
↓
父索引状态变为INVALID
↓
补建索引并ATTACH后恢复正常
对于 PostgreSQL 分区索引来说,看到父索引 INVALID 时,不要第一时间执行 REINDEX。
建议优先检查:
-
子索引是否全部存在
-
子索引是否全部 valid
-
分区数量是否等于 attached 子索引数量
-
是否遗漏了 Default 分区
很多情况下,问题就出在最后一条。