在 Postgresql 中索引外键

Posted

技术标签:

【中文标题】在 Postgresql 中索引外键【英文标题】:Indexing Foreign Keys in Postgresql 【发布时间】:2020-03-22 19:51:48 【问题描述】:

像许多 Postgres n00bs 一样,我们有很多带有未索引的外键约束的表。在某些情况下,这不应该对性能造成很大影响 - 但这需要进一步分析。

我已阅读以下文章:https://www.cybertec-postgresql.com/en/index-your-foreign-key/

并使用以下查询查找所有没有索引的外键:

SELECT c.conrelid::regclass AS "table",
       /* list of key column names in order */
       string_agg(a.attname, ',' ORDER BY x.n) AS columns,
       pg_catalog.pg_size_pretty(
          pg_catalog.pg_relation_size(c.conrelid)
       ) AS size,
       c.conname AS constraint,
       c.confrelid::regclass AS referenced_table
FROM pg_catalog.pg_constraint c
   /* enumerated key column numbers per foreign key */
   CROSS JOIN LATERAL
      unnest(c.conkey) WITH ORDINALITY AS x(attnum, n)
   /* name for each key column */
   JOIN pg_catalog.pg_attribute a
      ON a.attnum = x.attnum
         AND a.attrelid = c.conrelid
WHERE NOT EXISTS
        /* is there a matching index for the constraint? */
        (SELECT 1 FROM pg_catalog.pg_index i
         WHERE i.indrelid = c.conrelid
           /* the first index columns must be the same as the
              key columns, but order doesn't matter */
           AND (i.indkey::smallint[])[0:cardinality(c.conkey)-1]
               @> c.conkey::int[])
  AND c.contype = 'f'
GROUP BY c.conrelid, c.conname, c.confrelid
ORDER BY pg_catalog.pg_relation_size(c.conrelid) DESC;

这向我展示了具有复合唯一约束的表,只有唯一索引中的“一”列:

\d topics_items;
-----------------+---------+--------------+---------------+------------------------------
 topics_items_id | integer |              | not null      | generated always as identity
 topic_id        | integer |              | not null      |
 item_id         | integer |              | not null      |
Index:
    "topics_items_pkey" PRIMARY KEY, btree (topics_items_id)
    "topic_id_item_id_unique" UNIQUE CONSTRAINT, btree (topic_id, item_id)
Foreign Keys:
    "topics_items_item_id_fkey" FOREIGN KEY (item_id) REFERENCES items(item_id) ON DELETE CASCADE
    "topics_items_topic_id_fkey" FOREIGN KEY (topic_id) REFERENCES topics(topic_id) ON DELETE CASCADE

在这种情况下,检查查询仅找到 item_id 而不是 topic_id 作为未索引字段。

公平地说,这只是所使用查询的问题,我必须分别索引两个字段(topic_id 和 item_id) - 或者是否涉及一些黑色巫术,只有 item_id 需要索引?

【问题讨论】:

【参考方案1】:

tl;dr您需要在item_id 上添加索引。 Postgres 索引的“黑魔法”在11. Indexes 中有介绍。

您在(topic_id, item_id) 上有一个复合索引,列顺序很重要。 Postgres 可以使用它来索引topic_id 上的查询、topic_iditem_id 上的查询,但不能(或效率较低)单独item_id

来自11.3. Multicolumn Indexes...

多列 B 树索引可用于涉及索引列的任何子集的查询条件,但当前导(最左侧)列存在约束时,索引效率最高。

-- indexed
select *
from topics_items
where topic_id = ?

-- also indexed
select *
from topics_items
where topic_id = ?
  and item_id = ?

-- probably not indexed
select *
from topics_items
where item_id = ?

这是因为像(topic_id, item_id) 这样的复合索引首先存储主题 ID,然后是也具有该主题 ID 的项目 ID。为了在该索引中有效地查找项目 ID,Postgres 必须首先使用主题 ID 缩小搜索范围。


Postgres 可以在认为值得努力的情况下反转索引。如果可能的主题 ID 数量较少,而可能的索引 ID 数量较多,则会在每个主题 ID 中搜索索引 ID。

例如,假设您有 10 个可能的主题 ID 和 1000 个可能的项目 ID,并且您的索引为 (topic_id, index_id)。这就像有 10 个清晰标记的主题 ID 存储桶,每个存储桶内部都有 1000 个清晰标记的项目 ID 存储桶。要访问项目 ID 存储桶,它必须查看每个主题 ID 存储桶的内部。要在 where item_id = 23 上使用此索引,Postgres 必须在 10 个主题 ID 桶中的每一个桶中搜索项目 ID 为 23 的所有桶。

但如果您有 1000 个可能的主题 ID 和 10 个可能的项目 ID,Postgres 将不得不搜索 1000 个主题 ID 存储桶。它很可能会改为进行全表扫描。在这种情况下,您需要反转索引并将其设为 (item_id, topic_id)

这在很大程度上取决于拥有良好的表统计信息,这意味着确保 autovacuum 正常工作。

因此,如果一列的可变性远小于另一列,则可以为两列使用单个索引。


Postgres can also use mulitple indexes if it thinks it will make the query run faster。例如,如果您在topic_id 上有一个索引,在item_id 上有一个索引,它可以使用这两个索引并组合结果。例如where topic_id = 23 or item_id = 42 可以使用topic_id 索引搜索主题ID 23,使用item_id 索引搜索项目ID 42,然后合并结果。

这通常比使用复合 (topic_id, item_id) 索引要慢。它也可能比使用单个索引慢,所以如果 Postgres 决定不使用多个索引,请不要感到惊讶。


通常,对于 b-tree 索引,当您有两列时,您有三种可能的组合。

a + b 一个 b

而且你需要两个索引。

(a, b) -- a 和 a + b (b) -- b

(a, b) 涵盖对 a 和 a + b 的搜索。 (b) 涵盖搜索 b

当你有三列时,你有七种可能的组合。

a + b + c a + b a + c 一个 b + c b c

但你只需要三个索引。

(a, b, c) -- a, a + b, a + b + c (b, c) -- b, b + c (c, a) -- c, c + a

但是,您实际上可能希望避免在三列上创建索引。它通常较慢。你真正想要的是这个。

(a, b) (b, c) (c, a)

应谨慎使用多列索引。在大多数情况下,单个列上的索引就足够了,并且可以节省空间和时间。除非表格的使用非常风格化,否则超过三列的索引不太可能有帮助。

从索引读取比从表读取慢。您希望您的索引减少必须读取的行数,但您不希望 Postgres 必须进行任何不必要的索引扫描。

右侧列的约束...在索引中检查,因此它们可以正确保存对表的访问,但不会减少必须扫描的索引部分。例如,给定 (a, b, c) 上的索引和查询条件 WHERE a = 5 AND b >= 42 AND c = 77 的索引条目将被跳过,但仍必须扫描它们。

【讨论】:

【参考方案2】:

使用(topic_id, item_id) 上的索引可以有效地找到具有特定topic_id 的行,这就是我的查询认为外键被覆盖的原因。

索引按topic_id排序,在所有具有相同topic_id的条目中,它按item_id排序。这使得它可以单独用于搜索 topic_id

【讨论】:

以上是关于在 Postgresql 中索引外键的主要内容,如果未能解决你的问题,请参考以下文章

postgres 外键是不是意味着索引?

PostgreSQL入门,PostgreSQL和mysql

postgresql:具有外键的多个多列索引?

postgresql 不使用索引作为主键 = 外键

B-Tree 和 GiST 索引方法(在 PostgreSQL 中)有啥区别?

何为PostgreSQL?