又是一个关于外键的问题

2022-01-23 03:55:18 +08:00
 lanlanye

我知道这个问题在站内已经讨论过无数次了,比如 关于外键,为什么国内基本都不推荐使用,国外基本都推荐使用?,但是直到现在也没有一个帖子能够达成共识,所以就一些不明确的地方提出些问题:

首先说前提:

  1. 首先关系库不可能放弃关联,所以这里讨论的不是"是否需要外键"而是"是否需要物理外键"。
  2. 既然用到外键,使用前提必然是对数据引用完整性(参照完整性)有一定要求的,我看到有人说业务对报错敏感的时候不使用,这点我不能理解,毕竟插入一条错误的数据和插入数据时失败同样是错误,当用户添加数据成功但找不到这条数据时,问题应该比添加失败更为严重才对。
  3. 外键对性能的影响在数据量不大时应该是不需要考虑的,这里讨论的共识应该是避开数据量极大的表(如日志表)。
  4. 针对分布式,我认为这个和上一条是一样的,当存储需要用到分布式时,说明数据量已经相当大,这种情况下自然不需要考虑外键。
  5. 由于软删除存在,级联删除意义不大,我们可以约定建立外键是不设置级联删除(或阻止删除被引用的数据以保证数据完整)。

接下来是疑问:

  1. 在避开单表大量数据和分布式存储的情况下,对于数据量通常不超过百万且经常需要插入和更新(软删除也算更新)的业务数据,物理外键是否优于逻辑外键?
  2. 对于开头链接中 15 楼的问题,一般默认的隔离级别( RR )并不能避免这个问题发生,业务约束同样需要对数据库加锁,且更依赖业务人员的水平,这是否可以说明逻辑外键对比物理外键并无优势?还是说有更好的方式能够解决这种问题?
  3. 有人提出导入表的顺序问题,我认为导入前整理数据之间的关系是很合理的要求,何况检查也可以被关闭,这一点并不能作为物理外键不好的理由,此外把外键设置成环同样是一个设计错误,并不是外键本身的问题。
  4. 即使考虑数据的积累,过早的禁止外键是否真的合理?当数据膨胀到使用外键会产生明显问题时再去除外键是否更合理?毕竟过早的设计会导致开发人员付出大量额外的工时来保证数据完整。

出于以上几点疑问,我感觉逻辑外键相比物理外键来说毫无优势(包括性能优势,因为需要加锁),还很可能因开发人员水平不足、考虑不周或在直接修改数据库时写错脚本从而损坏数据,那么为什么仍有相当多的开发者认为多数情况下应该避免使用物理外键呢?

以下是一些个人的想法:

大厂全面禁止外键,一方面可能是由于核心业务对性能敏感而不使用,最后为了管理方便干脆全部禁止,我至今没能找到一个合理的全面禁止使用外键的理由,如果有大厂高层,希望可以听到你们的看法。

在设计阶段加入外键,一定程度上可以降低开发人员的编码负担,减少系统错误,哪怕是不会考虑并发状况或对数据库不熟悉的开发者,外键也能阻止他将错误的数据写入库中,反过来产生的「接口总是报错」、「导入顺序不对报错」等问题,我认为是合理且必要的错误提示,一个接口要做的应该是在数据不合规范时阻止其写入,而不是强行写进去。

另外还有一个相关但关系不是很大的问题:

说到外键就一定会说到关联,我注意到也有部分人反对在业务查询中使用 JOIN ,主要理由是 JOIN 的效率低下,关于这个问题,希望有熟悉数据库的人能为我解惑:

如果表设计合理,关联查询是否都可以通过索引优化到比多次查询并在内存中拼接的方式更快?

以上。请注意我讨论这些问题的前提都是设计合理,对于数据库本身设计就无法很好的支撑业务,导致经常需要走弯路解决问题的情况,不属于本帖的讨论范围。

3252 次点击
所在节点    数据库
47 条回复
dcoder
2022-01-23 05:41:25 +08:00
本质原因是 SQL 就是个弱鸡抽象工具, 因为历史原因流行被滥用.
如果不使用 SQL 和关系函数那堆东西, 外键, join 都不需要用.
比如只使用"逻辑外键"的话, SQL 基本可以当成 KV store 使用.
由于 KV Store 做分布式 scale 容易, 大厂就干脆强制这样用了.
iseki
2022-01-23 05:42:46 +08:00
个人支持在大部分场景下使用数据库提供的“物理外键”,和 tl 争吵的结果是,他表示可以接受脏数据,但不能接受维护时(导入导出)的麻烦~~个人保留意见
iseki
2022-01-23 05:43:42 +08:00
@dcoder 私以为这种场合没有继续强行使用支持 SQL 的关系型数据库的必要
dcoder
2022-01-23 05:44:15 +08:00
👆上面的原因同时符合公司和程序员的利益
大厂: 禁止复杂且不 scale 的 SQL tricks
程序员: 终于不用折腾 SQL/关系函数 这弱鸡破玩意儿了
所以这个趋势, 未来还会继续.
Mac
2022-01-23 05:44:33 +08:00
@iseki #2 我也是因为数据导入的问题弃用的外键
dcoder
2022-01-23 05:46:27 +08:00
@iseki
即使是小场景应用, 程序员会想: 我少专研 SQL 这些技巧, 一样可以写好写清楚逻辑,为啥还要研究它们?? 剩下时间, 做做自己的项目, 或者刷题跳槽都行.
iseki
2022-01-23 05:46:59 +08:00
@dcoder 其实真不一定符合公司和程序员利益,大多时候一句 sql 出来的东西,写一坨代码,快慢不论,能不能写对都是值得考虑的~毕竟数据库积累了这么多年不是白积累的
iseki
2022-01-23 05:48:47 +08:00
@dcoder 哈哈这里确实见仁见智了,有人宁可自己写一大坨,跑的慢死还一堆问题,就不肯看看一句稍微“高级”那么一点的 SQL ~
dcoder
2022-01-23 05:49:38 +08:00
@iseki
你说的这个 SQL 高效论, 可以 apply 到任何一个小领域的 DSL.
但是大家就是不愿意专研各种 DSL, 因为性价比低下.
除非自己是个 data scientist
iseki
2022-01-23 05:52:17 +08:00
@dcoder 是啊,是这样啊,每个人选择不同,导致了不同的结果。

@Mac 是不是有点太过于因噎废食了…
dcoder
2022-01-23 05:52:37 +08:00
@iseki
数据库查询要高效,主要靠认真设计好 schema 和 index
本质上是面向 schema+index 编程, 效率靠底层的 BTree 这些.
并不是搞折腾好 SQL 这种 DSL 的各种无聊 tricks.
iseki
2022-01-23 05:54:25 +08:00
@iseki #2 最神奇的是,他直接要求删掉外键的理由是,外键耽误他无视逻辑关系的 delete 删数据了……
iseki
2022-01-23 05:59:14 +08:00
@dcoder 认真设计 schema 和 index 是最基本的要求,这和“正确地”使用工具并不冲突。当然这里我对“正确”的定义可能有些狭隘。
iseki
2022-01-23 06:07:51 +08:00
@dcoder 解释一下我的上条发言,就是说有人可能不愿意学习某个特定的 DSL ,导致本来一行代码完成的内容需要他额外写一百行来实现。脱离具体的上下文很难说谁更“高效”,只不过是个人的选择而已,我所谓的“正确”也是在其中夹杂了我的个人偏见。
dcoder
2022-01-23 06:09:06 +08:00
@iseki 也是最重要的要求,schema+index 搞好了其余都是次要问题.
你对"正确地"使用(SQL)工具, 确实有点执着, 哈哈
但是从程序员自身收益来看, 不爱研究 DSL 是可以理解的
dcoder
2022-01-23 06:20:12 +08:00
@iseki 不爱学 DSL. 更精确说, 不爱学应用面狭窄技巧, 甚至思维方式.
比如外键, join 相关的很多知识就是. 折腾它们性价比低.
你说 SQL tricks xxx 可以抵得上 100 行自己实现, 极端情况有可能. 但是何必呢? 我还可以说 database stored procedures 能抵得上 1000 行用通用代码写的逻辑, 但是大家爱用 stored procedures 么? 反而是精通它的 DBA 都失业了...

而且一旦按照 SQL 全家桶思路设计了, 各种 migrate 都不方便. 比如我就用 uuid 作为"逻辑外键了", 回头我把这部分数据 migrate 到另外的 service B with database Y, 一点问题没有. 但是我把 SQL 外键, join 这些都一股脑全用上了, 那 migrate 就麻烦了! SQL 这套东西, 自成一派, 不灵活, 不通用, 全家桶都用上的话, 容易作茧自缚. 你可以说, 我家就这么用, 以后永远不改设计了, 但是谁知道呢...
iseki
2022-01-23 06:25:54 +08:00
@dcoder 是啊,这一切都需要在特定的上下文中进行权衡,也就是我上文说的脱离具体上下文很难讲该如何做,性价比到底是高还是低。
xuanbg
2022-01-23 08:49:38 +08:00
@dcoder 滥用关系型数据库确实存在,但不能因此否认关系型数据库的特定用途。你说的当成 KV store 使用的这种现象,就是典型的滥用关系型数据库。

另外,关系型数据库可不是什么弱鸡抽象工具。
cwek
2022-01-23 10:26:19 +08:00
物理外键没必要,但业务外键由应用处理,对应列用索引解决。
xhcarlin
2022-01-23 10:28:17 +08:00
很多人都反对使用物理外键,一般都是说外键维护麻烦、影响性能,表示用逻辑外键会更好。

我对此感到很疑惑,因为我感觉逻辑外键的开销也不小。比如说有一条更新操作的 SQL ,要自己实现逻辑外键,一条 SQL 就变成了两条,先查询后更新,然后我就感觉复杂度一下子就不一样了。

首先,我要引入事务,保证这两条 SQL 一次完成;
接着,受限于隔离级别,要避免脏读之类的问题,需要对应的行进行加锁;
再接着,锁用多了又影响性能(毕竟去除物理外键的原因之一就是为了性能),还得自己实现锁的机制。

这一连串下来,我感觉在维护这个数据完整性约束上的操作比直接用物理外键要麻烦很多,而且还不一定能设计的很完美。很多公司的项目连单元测试都不写,就算写了有很多也不过是 “Happy-path testing”,没尽量考虑各种异常问题。

还是说我把问题想得复杂呢?应该不引入事务、锁机制?

还有一些观点是对于数据库完整性要求低的项目可以不用物理外键,我比较好奇什么样的业务才对数据库完整性要求低?而且这样日积月累脏数据不会越来越多吗?我还是比较认同楼主所提到的,插入错误的数据比插入数据失败的危害要大得多。

最后提一句,我只是刚入行 1 年多的新人,懂得不多,还希望有经验的大佬能多提点一下。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/830011

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX