两列的唯一性

uniqueness in two columns

这是一个关于足球的简单数据集:一个 table team 和一个 table match

        Table « soccer.team »
┌─────────┬─────────┬───────────────┐
│ Column  │  Type   │ Modifiers     │
├─────────┼─────────┼───────────────┤
│ team_id │ integer │ not NULL      │
│ name    │ text    │ not NULL      │
│ code    │ text    │               │
└─────────┴─────────┴───────────────┘
Index:
    "team_pkey" PRIMARY KEY, btree (team_id)
Referenced by:
    TABLE "match" CONSTRAINT "match_foreign_team_id_fkey" FOREIGN KEY (foreign_team_id) REFERENCES team(team_id)
    TABLE "match" CONSTRAINT "match_home_team_id_fkey" FOREIGN KEY (home_team_id) REFERENCES team(team_id)

             Table « soccer.match »
┌────────────────────┬──────────┬───────────────┐
│      Column        │   Type   │ Modifiers     │
├────────────────────┼──────────┼───────────────┤
│ matchday           │ integer  │ not NULL      │
│ home_team_id       │ integer  │ not NULL      │
│ foreign_team_id    │ integer  │ not NULL      │
│ home_team_score    │ smallint │ not NULL      │
│ foreign_team_score │ smallint │ not NULL      │
└────────────────────┴──────────┴───────────────┘
Check constraints:
    "match_check" CHECK (home_team_id <> foreign_team_id)
    "match_foreign_team_score_check" CHECK (foreign_team_score >= 0)
    "match_home_team_score_check" CHECK (home_team_score >= 0)
Foreign keys:
    "match_foreign_team_id_fkey" FOREIGN KEY (foreign_team_id) REFERENCES team(team_id)
    "match_home_team_id_fkey" FOREIGN KEY (home_team_id) REFERENCES team(team_id)

每个比赛日每支球队最多只能打一场比赛,无论是接待外国球队还是外国球队。必须有 2 支球队,每场比赛只能有 2 支球队。 是否有一种设计可以确保一支球队每个比赛日最多比赛一次?

听起来像一个独特的约束符合要求:

ALTER TABLE match ADD UNIQUE (matchday, home_team_id);
ALTER TABLE match ADD UNIQUE (matchday, foreign_team_id);

如果一支球队可以是主队外国球队,事情就变得更复杂了,你需要这些扩展:

CREATE EXTENSION intarray SCHEMA public;    -- for the "gist__int_ops" opclass
CREATE EXTENSION btree_gist SCHEMA public;  -- for the "gist_int4_ops" opclass

然后你可以创建一个排除约束:

ALTER TABLE soccer.match ADD EXCLUDE USING gist (
   matchday gist_int4_ops WITH OPERATOR(pg_catalog.=),
   (ARRAY[home_team_id, foreign_team_id]) gist__int_ops WITH OPERATOR(public.&&)
);

基本上,如果两行的 matchday 相等并且 home_team_idforeign_team_id 组成的数组有共同的元素,则两行算作“相等”。

你可以在这里使用两位数比较技巧,例如:

create unique index ui on soccer.match (matchday, greatest(home_team_id,foreign_team_id), least(home_team_id,foreign_team_id))

如果您有如下所示的 table,您可以在 matchdayteam_id 上放置一个唯一约束或 multi-column 主键:

team_match table:

┌────────────────────┬──────────┬───────────────┐
│      Column        │   Type   │ Modifiers     │
├────────────────────┼──────────┼───────────────┤
│ matchday           │ integer  │ not NULL      │
│ team_id            │ integer  │ not NULL      │
│ is_home_team       │ integer  │ not NULL      │
│ match_id           │ integer  │ not NULL      │
│ score              │ integer  │ not NULL      │
└────────────────────┴──────────┴───────────────┘

match table:

┌────────────────────┬──────────┬───────────────┐
│      Column        │   Type   │ Modifiers     │
├────────────────────┼──────────┼───────────────┤
│ match_id           │ integer  │ not NULL      │
└────────────────────┴──────────┴───────────────┘

一支球队在每个比赛日不能比赛超过一次,因此 table 列出比赛日和球队。结合比赛号码和主场或客场指示器,您将获得所需的所有数据。例如:

             Table « soccer.match »
+-----------------------------------------------+
¦      Column        ¦   Type   ¦ Modifiers     ¦
+--------------------+----------+---------------¦
¦ matchday           ¦ integer  ¦ not NULL      ¦
¦ matchnum           ¦ integer  ¦ not NULL      ¦
¦ team_id            ¦ integer  ¦ not NULL      ¦
¦ teamtype           ¦ varchar  ¦ not NULL      ¦
¦ score              ¦ smallint ¦ not NULL      ¦
+-----------------------------------------------+
Check constraints:
    "match_teamtype_check" CHECK (teamtype IN ('HOME','AWAY'))
    "match_score_check" CHECK (score >= 0)
Indexes:
    "match_pkey" PRIMARY KEY, btree (matchday, match_no, team_id, teamtype)
    "match_team_once_per_day" UNIQUE KEY, btree (matchday, team_id)
    "match_two_teams_only" UNIQUE KEY, btree (matchday, matchnum, teamtype)

您的 table 设计看起来不错。但你是对的,它不能保证一个球队每个比赛日只打一次比赛。为此,您需要另一个 table。您可以做的是将另一个 table 添加到您现有的设计中并用触发器填充它。

             Table « soccer.match_team »
+-----------------------------------------------+
¦      Column        ¦   Type   ¦ Modifiers     ¦
+--------------------+----------+---------------¦
¦ matchday           ¦ integer  ¦ not NULL      ¦
¦ team_id            ¦ integer  ¦ not NULL      ¦
+-----------------------------------------------+
Index:
    "match_team_pkey" PRIMARY KEY, btree (matchday, team_id)

现在编写一个触发器,每当写入 match 条记录时,它就会写入两条记录。 (如果您允许更新和删除,您可能还想编写一些代码。)

CREATE EXTENSION IF NOT EXISTS intarray;
CREATE EXTENSION IF NOT EXISTS btree_gist;

alter table match add exclude using gist
  (
     matchday with =,
     (array[home_team_id,foreign_team_id]) with &&
  );

这将防止插入具有匹配比赛日和重叠 [home_team_id、foreign_team_id] 数组的行。