可以对两个可能的表之一执行MySQL外键吗?

Possible to do a MySQL foreign key to one of two possible tables?

提问人:Andrew G. Johnson 提问时间:1/14/2009 最后编辑:Bill KarwinAndrew G. Johnson 更新时间:10/20/2022 访问量:84000

问:

好吧,这是我的问题,我有三张表;地区、国家、州。国家可以在区域内,州可以在区域内。地区是食物链的顶端。

现在,我正在添加一个包含两列的popular_areas表;region_id和popular_place_id。是否有可能使popular_place_id成为国家州的外键。我可能必须添加一个popular_place_type列,以确定 id 是否以任何一种方式描述国家或州。

MySQL的

评论

1赞 PerformanceDBA 1/4/2022
@BillKarwin 你把Q is 和 Forced to Something it not (),这是一个非常狭隘和有限的 OO/ORM 思维方式(低于标准,相对于 & ),然后以这种狭隘的思维方式提供答案。寻求者的 Q 没有那种狭隘的思维方式,答案不需要局限于此。有关详细信息,请参阅我的。mysqlrelationalpolymorphic-associationsmysqlrelational

答:

13赞 Abie 1/14/2009 #1

这不是世界上最优雅的解决方案,但你可以使用具体的表继承来实现这一点。

从概念上讲,您提出了一类“可以成为热门区域的事物”的概念,您的三种类型的地方都继承了这些概念。您可以将其表示为一个表,例如,其中每一行都与 、 或 中的一行具有一对一的关系。(在地区、国家或州之间共享的属性(如果有)可以推送到此地点表中。然后,您将是对 places 表中某一行的外键引用,然后将您引导至地区、国家或州。placesregionscountriesstatespopular_place_id

你用第二列来描述关联类型的解决方案恰好是 Rails 如何处理多态关联,但我总体上不喜欢它。比尔非常详细地解释了为什么多态关联不是你的朋友。

评论

1赞 ErikE 7/25/2012
又名“超型-亚型模式”
1赞 Marco Staffoli 12/18/2018
此外,本文还阐述了概念 duhallowgreygeek.com/polymorphic-association-bad-sql-smell
339赞 Bill Karwin 1/14/2009 #2

你所描述的称为多态关联。也就是说,“外键”列包含一个 id 值,该值必须存在于一组目标表之一中。通常,目标表以某种方式相关,例如是某些常见数据超类的实例。您还需要在外键列旁边再增加一列,以便在每一行上指定引用哪个目标表。

CREATE TABLE popular_places (
  user_id INT NOT NULL,
  place_id INT NOT NULL,
  place_type VARCHAR(10) -- either 'states' or 'countries'
  -- foreign key is not possible
);

无法使用 SQL 约束对多态关联进行建模。外键约束始终引用一个目标表。

多态关联由 Rails 和 Hibernate 等框架支持。但是他们明确表示,您必须禁用 SQL 约束才能使用此功能。相反,应用程序或框架必须执行等效工作,以确保满足引用。也就是说,外键中的值存在于一个可能的目标表中。

多态关联在强制数据库一致性方面较弱。数据完整性取决于访问数据库的所有客户端都使用相同的引用完整性逻辑强制执行,并且强制执行必须没有错误。

下面是一些利用数据库强制引用完整性的替代解决方案:

为每个目标创建一个额外的表。例如,和 ,分别引用 和。这些“热门”表中的每一个也都引用了用户的配置文件。popular_statespopular_countriesstatescountries

CREATE TABLE popular_states (
  state_id INT NOT NULL,
  user_id  INT NOT NULL,
  PRIMARY KEY(state_id, user_id),
  FOREIGN KEY (state_id) REFERENCES states(state_id),
  FOREIGN KEY (user_id) REFERENCES users(user_id),
);

CREATE TABLE popular_countries (
  country_id INT NOT NULL,
  user_id    INT NOT NULL,
  PRIMARY KEY(country_id, user_id),
  FOREIGN KEY (country_id) REFERENCES countries(country_id),
  FOREIGN KEY (user_id) REFERENCES users(user_id),
);

这确实意味着要获取用户最喜欢的所有位置,您需要查询这两个表。但这意味着您可以依靠数据库来强制执行一致性。

地点表创建为超级表。正如 Abie 所提到的,第二种选择是你的热门位置引用一个表,如 ,它是 和 的父级。也就是说,state 和 countries 也有一个外键 to(你甚至可以让这个外键也是 和 的主键)。placesstatescountriesplacesstatescountries

CREATE TABLE popular_areas (
  user_id INT NOT NULL,
  place_id INT NOT NULL,
  PRIMARY KEY (user_id, place_id),
  FOREIGN KEY (place_id) REFERENCES places(place_id)
);

CREATE TABLE states (
  state_id INT NOT NULL PRIMARY KEY,
  FOREIGN KEY (state_id) REFERENCES places(place_id)
);

CREATE TABLE countries (
  country_id INT NOT NULL PRIMARY KEY,
  FOREIGN KEY (country_id) REFERENCES places(place_id)
);

使用两列。不要使用一个可以引用两个目标表之一的列,而应使用两列。这两列可能是 ;事实上,其中只有一个应该是非的。NULLNULL

CREATE TABLE popular_areas (
  place_id SERIAL PRIMARY KEY,
  user_id INT NOT NULL,
  state_id INT,
  country_id INT,
  CONSTRAINT UNIQUE (user_id, state_id, country_id), -- UNIQUE permits NULLs
  CONSTRAINT CHECK (state_id IS NOT NULL OR country_id IS NOT NULL),
  FOREIGN KEY (state_id) REFERENCES places(place_id),
  FOREIGN KEY (country_id) REFERENCES places(place_id)
);

在关系理论方面,多态关联违反了第一范式,因为实际上是一个具有两种含义的列:它要么是一个州,要么是一个国家。你不会将一个人和他们的存储在一个列中,出于同样的原因,你不应该同时存储在一个列中。这两个属性具有兼容的数据类型这一事实是巧合;它们仍然表示不同的逻辑实体。popular_place_idagephone_numberstate_idcountry_id

多态关联也违反了第三范式,因为列的含义取决于命名外键所引用的表的额外列。在第三范式中,表中的属性必须仅依赖于该表的主键。


来自@SavasVedova的回复评论:

我不确定我是否在没有看到表定义或示例查询的情况下遵循您的描述,但听起来您只是有多个表,每个表都包含一个引用中心表的外键。FiltersProducts

CREATE TABLE Products (
  product_id INT PRIMARY KEY
);

CREATE TABLE FiltersType1 (
  filter_id INT PRIMARY KEY,
  product_id INT NOT NULL,
  FOREIGN KEY (product_id) REFERENCES Products(product_id)
);

CREATE TABLE FiltersType2 (
  filter_id INT  PRIMARY KEY,
  product_id INT NOT NULL,
  FOREIGN KEY (product_id) REFERENCES Products(product_id)
);

...and other filter tables...

如果您知道要加入哪种类型的过滤器,则很容易将产品加入特定类型的过滤器:

SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)

如果希望筛选器类型是动态的,则必须编写应用程序代码来构造 SQL 查询。SQL 要求在编写查询时指定并固定表。不能使联接表根据 的 的各个行中的值动态选择。Products

唯一的其他选项是使用外部联接联接到所有筛选器表。那些没有匹配product_id将仅作为单行 null 返回。但是,您仍然必须对所有联接表进行硬编码,如果添加新的筛选器表,则必须更新代码。

SELECT * FROM Products
LEFT OUTER JOIN FiltersType1 USING (product_id)
LEFT OUTER JOIN FiltersType2 USING (product_id)
LEFT OUTER JOIN FiltersType3 USING (product_id)
...

联接到所有筛选器表的另一种方法是按顺序执行:

SELECT * FROM Product
INNER JOIN FiltersType1 USING (product_id)
UNION ALL
SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)
UNION ALL
SELECT * FROM Products
INNER JOIN FiltersType3 USING (product_id)
...

但这种格式仍然要求你写入对所有表的引用。这是没有办法的。

评论

0赞 Savas Vedova 11/27/2013
你会推荐哪一个比尔?我正在设计一个数据库,但我迷路了。我基本上需要将过滤器与产品相关联,过滤器的值将填充到不同的表中。但问题是过滤器将由管理员生成,因此根据过滤器类型,数据可能会有所不同,因此目标也会发生变化......我是不是太复杂了还是什么?帮助!join
0赞 Rob 10/21/2014
+1 谢谢你提供一个很棒的解决方案。我对第一个/第二个解决方案的一个问题是:多个表可以引用该元表中的同一主键这一事实是否存在任何规范化违规?我知道你可以用逻辑解决这个问题,但我看不出数据库有什么办法来强制执行它,除非我遗漏了什么。
5赞 alex_b 8/19/2015
我真的很喜欢“CONSTRAINT CHECK”的方法。但是如果我们将“OR”更改为“XOR”,则可以改进它。这样,我们就可以确保集合中只有一列不是 NULL
1赞 Bill Karwin 8/19/2015
@alex_b,是的,这很好,但逻辑 XOR 不是标准 SQL,并非所有 SQL 品牌都支持。MySQL有,但PostgreSQL没有。甲骨文拥有它,但Microsoft直到 2016 年才拥有它。等等。
1赞 onedaywhen 10/21/2016
“这两列可能是 NULL;事实上,它们中只有一个应该是非 NULL“——这将违反 1NF!
7赞 onedaywhen 10/21/2016 #3

这是对 Bill Karwin 的“超级表”方法的更正,它使用复合键来解决感知到的正常形式冲突:( place_type, place_id )

CREATE TABLE places (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) NOT NULL
     CHECK ( place_type = 'state', 'country' ),
  UNIQUE ( place_type, place_id )
);

CREATE TABLE states (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) DEFAULT 'state' NOT NULL
     CHECK ( place_type = 'state' ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
  -- attributes specific to states go here
);

CREATE TABLE countries (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) DEFAULT 'country' NOT NULL
     CHECK ( place_type = 'country' ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
  -- attributes specific to country go here
);

CREATE TABLE popular_areas (
  user_id INT NOT NULL,
  place_id INT NOT NULL,
  UNIQUE ( user_id, place_id ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
);

这种设计不能确保其中的每一行都存在一行(但不是两者)。这是 SQL 中外键的限制。在完全符合 SQL-92 标准的 DBMS 中,您可以定义可延迟的表间约束,这些约束将允许您实现相同的目标,但它很笨拙,涉及事务,并且这样的 DBMS 尚未进入市场。placesstatescountries

0赞 Toolsmythe 1/23/2019 #4

我意识到这个线程很旧,但我看到了这个,我想到了一个解决方案,我想我会把它扔在那里。

地区、国家和州是位于层次结构中的地理位置。

您可以通过创建一个名为 geographical_location_type 的域表来完全避免您的问题,您将用三行(区域、国家、州)填充该表。

接下来,创建一个外键为 geographical_location_type_id 的 geographical_location 表,而不是三个位置表(以便您知道实例是“区域”、“国家/地区”还是“省/市/自治区/直辖市/自治区”)。

通过使此表自引用来对层次结构进行建模,以便 State 实例将 fKey 保存到其父 Country 实例,而父 Country 实例又将 fKey 保存到其父 Region 实例。区域实例将在该 fKey 中保存 NULL。这与你对这三个表所做的没有什么不同(你会有 1 - 地区和国家之间以及国家和州之间的许多关系),只是现在它们都在一个表中。

popular_user_location表将是用户和georgraphical_location之间的范围解析表(因此许多用户可能喜欢很多地方)。

所以。。。

enter image description here

CREATE TABLE [geographical_location_type] (
    [geographical_location_type_id] INTEGER NOT NULL,
    [name] VARCHAR(25) NOT NULL,
    CONSTRAINT [PK_geographical_location_type] PRIMARY KEY ([geographical_location_type_id])
)

-- Add 'Region', 'Country' and 'State' instances to the above table


CREATE TABLE [geographical_location] (
   [geographical_location_id] BIGINT IDENTITY(0,1) NOT NULL,
    [name] VARCHAR(1024) NOT NULL,
    [geographical_location_type_id] INTEGER NOT NULL,
    [geographical_location_parent] BIGINT,  -- self referencing; can be null for top-level instances
    CONSTRAINT [PK_geographical_location] PRIMARY KEY ([geographical_location_id])
)

CREATE TABLE [user] (
    [user_id] BIGINT NOT NULL,
    [login_id] VARCHAR(30) NOT NULL,
    [password] VARCHAR(512) NOT NULL,
    CONSTRAINT [PK_user] PRIMARY KEY ([user_id])
)


CREATE TABLE [popular_user_location] (
    [popular_user_location_id] BIGINT NOT NULL,
    [user_id] BIGINT NOT NULL,
    [geographical_location_id] BIGINT NOT NULL,
    CONSTRAINT [PK_popular_user_location] PRIMARY KEY ([popular_user_location_id])
)

ALTER TABLE [geographical_location] ADD CONSTRAINT [geographical_location_type_geographical_location] 
    FOREIGN KEY ([geographical_location_type_id]) REFERENCES [geographical_location_type] ([geographical_location_type_id])



ALTER TABLE [geographical_location] ADD CONSTRAINT [geographical_location_geographical_location] 
    FOREIGN KEY ([geographical_location_parent]) REFERENCES [geographical_location] ([geographical_location_id])



ALTER TABLE [popular_user_location] ADD CONSTRAINT [user_popular_user_location] 
    FOREIGN KEY ([user_id]) REFERENCES [user] ([user_id])



ALTER TABLE [popular_user_location] ADD CONSTRAINT [geographical_location_popular_user_location] 
    FOREIGN KEY ([geographical_location_id]) REFERENCES [geographical_location] ([geographical_location_id])

不确定目标数据库是什么;以上是MS SQL Server。

0赞 Chris P 5/6/2020 #5

好吧,我有两张表:

  1. 歌曲

a) 歌曲编号 b) 歌曲名称 ....

  1. 播放列表 a) 播放列表编号 b) 播放列表标题 ...

我还有第三个

  1. songs_to_playlist_relation

问题是某些类型的播放列表链接到其他播放列表。但是在mysql中,我们没有与两个表关联的外键。

我的解决方案:我将songs_to_playlist_relation中放入第三列。该列将为布尔值。如果为 1,则为 song,否则将链接到播放列表表。

所以:

  1. songs_to_playlist_relation

a) Playlist_number (int) b) 是歌曲(布尔值) c) 相对编号(歌曲编号或播放列表编号)(int)(任何表的外键)

 #create table songs
    queries.append("SET SQL_MODE = NO_AUTO_VALUE_ON_ZERO;")
    queries.append("CREATE TABLE songs (NUMBER int(11) NOT NULL,SONG POSITION int(11) NOT NULL,PLAY SONG tinyint(1) NOT NULL DEFAULT '1',SONG TITLE varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,DESCRIPTION varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,ARTIST varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστος καλλιτέχνης',AUTHOR varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστος στιχουργός',COMPOSER varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστος συνθέτης',ALBUM varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστο άλμπουμ',YEAR int(11) NOT NULL DEFAULT '33',RATING int(11) NOT NULL DEFAULT '5',IMAGE varchar(600) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,SONG PATH varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,SONG REPEAT int(11) NOT NULL DEFAULT '0',VOLUME float NOT NULL DEFAULT '1',SPEED float NOT NULL DEFAULT '1') ENGINE=InnoDB DEFAULT CHARSET=utf8;")
    queries.append("ALTER TABLE songs ADD PRIMARY KEY (NUMBER), ADD UNIQUE KEY POSITION (SONG POSITION), ADD UNIQUE KEY TITLE (SONG TITLE), ADD UNIQUE KEY PATH (SONG PATH);")
    queries.append("ALTER TABLE songs MODIFY NUMBER int(11) NOT NULL AUTO_INCREMENT;")

#create table playlists
queries.append("CREATE TABLE `playlists` (`NUMBER` int(11) NOT NULL,`PLAYLIST POSITION` int(11) NOT NULL,`PLAYLIST TITLE` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`PLAYLIST PATH` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;")
queries.append("ALTER TABLE `playlists` ADD PRIMARY KEY (`NUMBER`),ADD UNIQUE KEY `POSITION` (`PLAYLIST POSITION`),ADD UNIQUE KEY `TITLE` (`PLAYLIST TITLE`),ADD UNIQUE KEY `PATH` (`PLAYLIST PATH`);")
queries.append("ALTER TABLE `playlists` MODIFY `NUMBER` int(11) NOT NULL AUTO_INCREMENT;")

#create table for songs to playlist relation
queries.append("CREATE TABLE `songs of playlist` (`PLAYLIST NUMBER` int(11) NOT NULL,`SONG OR PLAYLIST` tinyint(1) NOT NULL DEFAULT '1',`RELATIVE NUMBER` int(11) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;")
queries.append("ALTER TABLE `songs of playlist` ADD KEY `PLAYLIST NUMBER` (`PLAYLIST NUMBER`) USING BTREE;")
queries.append("ALTER TABLE `songs of playlist` ADD CONSTRAINT `playlist of playlist_ibfk_1` FOREIGN KEY (`PLAYLIST NUMBER`) REFERENCES `playlists` (`NUMBER`) ON DELETE RESTRICT ON UPDATE RESTRICT")

就这样!

playlists_query = "SELECT s1.*, s3.*, s4.* FROM songs as s1 INNER JOIN `songs of playlist` as s2 ON s1.`NUMBER` = s2.`RELATIVE NUMBER` INNER JOIN `playlists` as s3 ON s3.`NUMBER` = s2.`PLAYLIST NUMBER` INNER JOIN `playlists` as s4 ON s4.`NUMBER` = s2.`RELATIVE NUMBER` ORDER BY s3.`PLAYLIST POSITION`,`s1`.`SONG POSITION`"
6赞 PerformanceDBA 1/4/2022 #6

关系答案

注意标签,这意味着 ,因为 SQL 是 Codd 关系模型中定义的数据子语言。mysqlrelational

  • 解决方案简单明了,我们在 RM 之前就有了它,自 1981 年以来,我们一直有一个关系解决方案。
  • 关系解决方案提供引用完整性(物理,在 SQL 级别)和关系完整性(逻辑)。
  • 为了遵守开放架构标准(健全性),所有约束;业务规则;管理数据以及所有事务的等应该部署在数据库中,而不是框架中,而不是应用程序 GUI,而不是应用程序中间层。请注意,它是一个恢复单元。

这个标签是错误的,它不是 OP 请求的。 强迫它进入 OO/ORM 思维模式,然后以该思维模式证明解决方案,这超出了问题的范围。polymorphic-associations

  • 此外,它需要一个框架和代码来强制执行约束;等等,在数据库之外,这是不合格的。
  • 此外,它不具有关系解决方案的基本完整性,更不用说关系完整性了。
  • 此外,它违反了 and(详见 Karvan 答复)。1NF3NF
  • Null 是规范化错误,不应存储它们。
  • Nullable 是严重的规范化错误。FOREIGN KEY

溶液

好吧,这是我的问题,我有三张表;地区、国家、州。国家可以在区域内,州可以在区域内。地区是食物链的顶端。

让它成为关系型的

让我们了解一下在关系上下文中是什么。它是表的典型层次结构。

  • 不要使用字段。不要将它们声明为 ,这只会让您感到困惑,因为它不是 Key,它不能提供关系模型中要求的行唯一性IDPRIMARY KEY
  • 密钥必须由数据组成
  • 字段不是数据。它始终是一个附加字段和一个附加索引ID
  • 使用字段,您也许能够实现引用完整性(物理、SQL),但您没有机会实现关系完整性(逻辑)ID
  • 有关完整讨论,包括 SQL 代码,请参阅:
    创建具有 2 个不同auto_increment的关系表,仅 §1 和 2。

基表

Foo

表示法

  • 我所有的数据模型都是用 IDEF1X 呈现的,IDEF1X 是关系数据建模的符号,我们自 1980 年代初以来一直拥有它,在 1993 年成为关系数据建模标准,最后更新于 2016 年。

  • IDEF1X 简介对于那些不熟悉关系模型或其建模方法的人来说是必不可少的读物。请注意,IDEF1X 模型具有丰富的细节和精度,可以显示所有必需的细节,而自主开发的模型由于不了解标准的要求,因此定义要少得多。这意味着,需要完全理解符号。

  • ERD不是一个标准,它不支持关系模型,它完全不适合建模。

  • 学术界和“教科书”将反关系作为“关系”来教导和推销是犯罪行为。

现在,我正在添加一个包含两列的popular_areas表;region_id和popular_place_id。是否有可能使popular_place_id成为任何一个国家或州的外键。

完全没问题。关系模型建立在数学中;逻辑,它完全合乎逻辑。ORXOR门是逻辑的基础。在关系或 SQL 范式中,它被称为子类型集群。

  • 即使在不符合 SQL 的免费软件“SQL”中,它也是以完全引用完整性完成的

    • 认为它无法做到,或者它需要学术界推销的可怕的额外领域和指数的想法是错误的。
  • 有关完整的实现详细信息,包括指向 SQL 代码的链接,请参阅子类型文档。

  • 有关示例和讨论,请参阅:
    如何在子类型中实现参照完整性

  • 为了澄清混淆这个问题的问题,从而混淆其他答案:
    书籍图的关系架构

我可能必须添加一个popular_place_type列,以确定 id 是否以任何一种方式描述国家或州。

没错,你是在逻辑上思考。这里我们需要一个异或门,它需要一个鉴别器

添加放置表

Foo

关系完整性

引用完整性是 SQL 中提供的物理特性,而逻辑关系完整性则在此之上(当建模正确时,逻辑先于物理)。

这是关系完整性的一个很好的简单示例。请注意子类型中的第二个。FOREIGN KEY

  • PlaceCountry被限制为 与CountryRegionPlace.Region

  • PlaceState被限制为 与StateRegionPlace.Region

  • 请注意,这仅适用于关系键(复合)

    • 在原始的记录归档系统中,关系完整性是不可能的,这些系统以字段为“密钥”,并被学者和作者大量宣传为“关系”ID
    • 在这样的原始文件(它们不是表)中,将允许任何 ,它不能被限制为与 相同的 。PlaceCountryCountryCountryRegionPlace.Region