如果我的外键已经是唯一约束的一部分,我应该索引它们吗?

Posted

技术标签:

【中文标题】如果我的外键已经是唯一约束的一部分,我应该索引它们吗?【英文标题】:Should I index my foreign keys if they are already part of a unique constraint? 【发布时间】:2020-02-16 04:04:24 【问题描述】:

在 Postgresql 中,我有一个对两个外键有唯一约束的表,如下所示:

CREATE TABLE project_ownerships(
  id BIGSERIAL  PRIMARY KEY,
  project_id BIGINT REFERENCES projects ON DELETE CASCADE,
  user_id BIGINT REFERENCES users ON DELETE CASCADE,
  role SMALLINT,
  CONSTRAINT project_user_unique UNIQUE (project_id, user_id)
);

project_iduser_id 这两个外键上设置了唯一约束后,psql 是否也会自动为它们中的每一个创建一个索引?还是我应该仍然为他们手动创建索引?如下:

CREATE TABLE project_ownerships(
  id BIGSERIAL  PRIMARY KEY,
  project_id BIGINT REFERENCES projects ON DELETE CASCADE,
  user_id BIGINT REFERENCES users ON DELETE CASCADE,
  role SMALLINT,
  CONSTRAINT project_user_unique UNIQUE (project_id, user_id)
);

CREATE INDEX po_project_id_idx ON project_ownerships (project_id);
CREATE INDEX po_user_id_idx ON project_ownerships (user_id);

我已阅读 the text here,只是想确保我在实际实现细节方面正确理解它。具体来说,当我在project_iduser_id 或两者上执行join 时,如果创建了复合索引-CONSTRAINT project_user_unique UNIQUE (project_id, user_id)-,Postgresql 是否能够执行索引扫描(如果认为有必要)?我还需要为每个外键分别创建一个索引吗?

【问题讨论】:

如果你“只是想确保我理解正确”,你的理解是什么?你在哪里卡住了?为什么? @philipxy 我的理解是创建多列的唯一约束将创建它们的“复合”索引。但我不确定复合索引是否真的会为每一列创建一个索引,以便在它们中的任何一个上执行join 会在认为合适时利用它们的索引,因此这个问题.. 请通过编辑澄清,而不是评论。如果您的“问题”没有给出您的期望、决定和理由,那么它们就无法解决,这只是要求文档的另一个通用演示和定制教程,这太宽泛了,但可能仍然是重复的.此外,如果您不知道如何检查您拥有的约束和索引,那也在手册和重复问题中。请参阅 How to Ask、其他 help center 链接和投票箭头鼠标悬停文本。 【参考方案1】:

(project_id, user_id) 上的 unique 约束创建一个索引,相当于:

create unique index unq_project_ownerships_project_user
    on project_ownerships(project_id, user_id);

使用此索引,project_ownerships(project_id) 上的索引将是多余的。只要可以使用此索引,就可以使用唯一索引。

不过,project_ownerships(user_id) 上的索引可能仍然有用,具体取决于您的查询。它可以用于不使用唯一索引的情况。

【讨论】:

但是,请记住,每个索引都会增加插入、更新和删除的成本。仅在必要时添加它们。 @TheImpaler 我将在这两个 fk 上使用joins,所以我想它会超过成本..? Gordon,所以我只需要为唯一约束中的第二列(以及后续列,如果有的话)手动创建一个索引? There's no need to manually create indexes on unique columns; doing so would just duplicate the automatically-created index.link 如果user_id 是多列唯一约束中的列之一,听起来也会自动创建索引? @kyw 。 . .我认为这是指整个复合索引,而不是单个列。【参考方案2】:

这是一个口味问题,但我个人认为您不需要这里的代理键 (id)。 (它曾经使用过吗?)另外:role 是一个(非保留)关键字。避免将其用作标识符。

对于外键,索引是绝对必要的,否则 CASCADEing 删除或更新将(在内部)导致对每个已删除/更新的用户或项目元组进行顺序扫描。

对于像这样的junction(桥)表,创建一个索引(或UNIQUE 约束)以相反顺序的关键元素就足够了。这也可以作为 FK 的支撑指数。 [可以使用复合索引的第一个元素,就好像只有这些字段的索引存在一样]

索引中额外的关键字段可以启用仅索引扫描(例如:当不需要the_role字段时)


CREATE TABLE project_ownerships
  ( project_id BIGINT REFERENCES projects (id) ON DELETE CASCADE
  , user_id BIGINT REFERENCES users(id) ON DELETE CASCADE
  , the_role INTEGER
  , PRIMARY KEY  (project_id, user_id)
  , CONSTRAINT reversed_pk UNIQUE (user_id, project_id)
  );

一个小的测试设置 (我需要禁用排序和哈希连接,因为对于像这样的小表,这些实际上会导致更便宜的计划;-)


SET search_path=tmp;
SELECT version();

CREATE TABLE projects
        ( id bigserial not NULL PRIMARY KEY
        , the_name text UNIQUE
        );

CREATE TABLE users
        ( id bigserial not NULL PRIMARY KEY
        , the_name text UNIQUE
        );

CREATE TABLE project_ownerships
  ( project_id BIGINT REFERENCES projects (id) ON DELETE CASCADE
  , user_id BIGINT REFERENCES users(id) ON DELETE CASCADE
  , the_role INTEGER
  , PRIMARY KEY  (project_id, user_id)
  , CONSTRAINT reversed_pk UNIQUE (user_id, project_id)
  );

INSERT INTO projects( the_name)
SELECT 'project-' || gs::text
FROM generate_series(1,1000) gs
        ;

INSERT INTO users( the_name)
SELECT 'name_' || gs::text
FROM generate_series(1,1000) gs
        ;

INSERT INTO project_ownerships (project_id,user_id,the_role)
SELECT p.id, u.id , (random()* 100)::integer
FROM projects p
JOIN users u ON random() < .10
        ;

VACUUM ANALYZE projects,users,project_ownerships;


SET enable_hashjoin = 0;
SET enable_sort = 0;
-- SET enable_seqscan = 0;

EXPLAIN ANALYZE
SELECT p.the_name AS project_name
        , po.the_role AS the_role
FROM projects p
JOIN project_ownerships po ON po.project_id = p.id
        AND EXISTS (
        SELECT *
        FROM users u
        WHERE u.id = po.user_id
        AND u.the_name >= 'name_10'
        AND u.the_name < 'name_20'
        );



EXPLAIN ANALYZE
SELECT u.the_name AS user_name
        , po.the_role AS the_role
FROM users u
JOIN project_ownerships po ON po.user_id = u.id
        AND EXISTS (
        SELECT *
        FROM projects p
        WHERE p.id = po.project_id
        AND p.the_name >= 'project-10'
        AND p.the_name < 'project-20'
        );

产生的查询计划:


SET
                                                 version                                                  
----------------------------------------------------------------------------------------------------------
 PostgreSQL 11.6 on armv7l-unknown-linux-gnueabihf, compiled by gcc (Raspbian 8.3.0-6+rpi1) 8.3.0, 32-bit
(1 row)
SET
SET
                                                                      QUERY PLAN                                                                      
------------------------------------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=0.97..4693.68 rows=11924 width=15) (actual time=0.333..153.660 rows=11157 loops=1)
   ->  Nested Loop  (cost=0.69..1204.55 rows=11924 width=12) (actual time=0.268..53.192 rows=11157 loops=1)
         ->  Index Scan using users_the_name_key on users u  (cost=0.28..7.02 rows=119 width=8) (actual time=0.126..0.317 rows=112 loops=1)
               Index Cond: ((the_name >= 'name_10'::text) AND (the_name < 'name_20'::text))
         ->  Index Scan using reversed_pk on project_ownerships po  (cost=0.42..9.06 rows=100 width=20) (actual time=0.015..0.308 rows=100 loops=112)
               Index Cond: (user_id = u.id)
   ->  Index Scan using projects_pkey on projects p  (cost=0.28..0.29 rows=1 width=19) (actual time=0.005..0.005 rows=1 loops=11157)
         Index Cond: (id = po.project_id)
 Planning Time: 6.218 ms
 Execution Time: 162.319 ms
(10 rows)

                                                                            QUERY PLAN                                                                            
------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=0.97..4057.79 rows=11022 width=12) (actual time=0.084..93.584 rows=11236 loops=1)
   ->  Nested Loop  (cost=0.69..832.59 rows=11022 width=12) (actual time=0.063..25.260 rows=11236 loops=1)
         ->  Index Scan using projects_the_name_key on projects p  (cost=0.28..6.84 rows=110 width=8) (actual time=0.037..0.163 rows=112 loops=1)
               Index Cond: ((the_name >= 'project-10'::text) AND (the_name < 'project-20'::text))
         ->  Index Scan using project_ownerships_pkey on project_ownerships po  (cost=0.42..6.51 rows=100 width=20) (actual time=0.010..0.111 rows=100 loops=112)
               Index Cond: (project_id = p.id)
   ->  Index Scan using users_pkey on users u  (cost=0.28..0.29 rows=1 width=16) (actual time=0.004..0.004 rows=1 loops=11236)
         Index Cond: (id = po.user_id)
 Planning Time: 0.971 ms
 Execution Time: 99.671 ms
(10 rows)

【讨论】:

感谢idrole 的留言!我想知道如果我同时执行PRIMARY KEYUNIQUE constraint,它会创建一些重复的索引吗? 是的,重复索引。那不会有害的。相反,它将支持来自两个单键方向的仅索引扫描。它将比代理及其索引更有价值。 感谢您扩展您的答案。如果我可以跟进另一个问题:为什么需要颠倒顺序?我不明白这部分the first element(s) of a composite index can be used as if an index with only these fields existed..

以上是关于如果我的外键已经是唯一约束的一部分,我应该索引它们吗?的主要内容,如果未能解决你的问题,请参考以下文章

Oracle外键需要建索引吗?

MYSQL中唯一约束和唯一索引的区别

1Z0-051-DDL-2简单索引的创建和删除

mySQL:是啥阻止了我的外键约束?

在星型模式中,事实和维度之间的外键约束是不是必要?

Postgresql多个表具有相同的外键唯一约束