我知道这个问题在站内已经讨论过无数次了,比如 关于外键,为什么国内基本都不推荐使用,国外基本都推荐使用?,但是直到现在也没有一个帖子能够达成共识,所以就一些不明确的地方提出些问题:
首先说前提:
接下来是疑问:
出于以上几点疑问,我感觉逻辑外键相比物理外键来说毫无优势(包括性能优势,因为需要加锁),还很可能因开发人员水平不足、考虑不周或在直接修改数据库时写错脚本从而损坏数据,那么为什么仍有相当多的开发者认为多数情况下应该避免使用物理外键呢?
以下是一些个人的想法:
大厂全面禁止外键,一方面可能是由于核心业务对性能敏感而不使用,最后为了管理方便干脆全部禁止,我至今没能找到一个合理的全面禁止使用外键的理由,如果有大厂高层,希望可以听到你们的看法。
在设计阶段加入外键,一定程度上可以降低开发人员的编码负担,减少系统错误,哪怕是不会考虑并发状况或对数据库不熟悉的开发者,外键也能阻止他将错误的数据写入库中,反过来产生的「接口总是报错」、「导入顺序不对报错」等问题,我认为是合理且必要的错误提示,一个接口要做的应该是在数据不合规范时阻止其写入,而不是强行写进去。
另外还有一个相关但关系不是很大的问题:
说到外键就一定会说到关联,我注意到也有部分人反对在业务查询中使用 JOIN
,主要理由是 JOIN
的效率低下,关于这个问题,希望有熟悉数据库的人能为我解惑:
如果表设计合理,关联查询是否都可以通过索引优化到比多次查询并在内存中拼接的方式更快?
以上。请注意我讨论这些问题的前提都是设计合理,对于数据库本身设计就无法很好的支撑业务,导致经常需要走弯路解决问题的情况,不属于本帖的讨论范围。
1
dcoder 2022-01-23 05:41:25 +08:00
本质原因是 SQL 就是个弱鸡抽象工具, 因为历史原因流行被滥用.
如果不使用 SQL 和关系函数那堆东西, 外键, join 都不需要用. 比如只使用"逻辑外键"的话, SQL 基本可以当成 KV store 使用. 由于 KV Store 做分布式 scale 容易, 大厂就干脆强制这样用了. |
2
iseki 2022-01-23 05:42:46 +08:00 via Android
个人支持在大部分场景下使用数据库提供的“物理外键”,和 tl 争吵的结果是,他表示可以接受脏数据,但不能接受维护时(导入导出)的麻烦~~个人保留意见
|
4
dcoder 2022-01-23 05:44:15 +08:00
👆上面的原因同时符合公司和程序员的利益
大厂: 禁止复杂且不 scale 的 SQL tricks 程序员: 终于不用折腾 SQL/关系函数 这弱鸡破玩意儿了 所以这个趋势, 未来还会继续. |
6
dcoder 2022-01-23 05:46:27 +08:00
@iseki
即使是小场景应用, 程序员会想: 我少专研 SQL 这些技巧, 一样可以写好写清楚逻辑,为啥还要研究它们?? 剩下时间, 做做自己的项目, 或者刷题跳槽都行. |
7
iseki 2022-01-23 05:46:59 +08:00 via Android
@dcoder 其实真不一定符合公司和程序员利益,大多时候一句 sql 出来的东西,写一坨代码,快慢不论,能不能写对都是值得考虑的~毕竟数据库积累了这么多年不是白积累的
|
8
iseki 2022-01-23 05:48:47 +08:00 via Android
@dcoder 哈哈这里确实见仁见智了,有人宁可自己写一大坨,跑的慢死还一堆问题,就不肯看看一句稍微“高级”那么一点的 SQL ~
|
9
dcoder 2022-01-23 05:49:38 +08:00 1
|
10
iseki 2022-01-23 05:52:17 +08:00 via Android
|
11
dcoder 2022-01-23 05:52:37 +08:00
@iseki
数据库查询要高效,主要靠认真设计好 schema 和 index 本质上是面向 schema+index 编程, 效率靠底层的 BTree 这些. 并不是搞折腾好 SQL 这种 DSL 的各种无聊 tricks. |
12
iseki 2022-01-23 05:54:25 +08:00 via Android
@iseki #2 最神奇的是,他直接要求删掉外键的理由是,外键耽误他无视逻辑关系的 delete 删数据了……
|
13
iseki 2022-01-23 05:59:14 +08:00 via Android
@dcoder 认真设计 schema 和 index 是最基本的要求,这和“正确地”使用工具并不冲突。当然这里我对“正确”的定义可能有些狭隘。
|
14
iseki 2022-01-23 06:07:51 +08:00
@dcoder 解释一下我的上条发言,就是说有人可能不愿意学习某个特定的 DSL ,导致本来一行代码完成的内容需要他额外写一百行来实现。脱离具体的上下文很难说谁更“高效”,只不过是个人的选择而已,我所谓的“正确”也是在其中夹杂了我的个人偏见。
|
15
dcoder 2022-01-23 06:09:06 +08:00
|
16
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 这套东西, 自成一派, 不灵活, 不通用, 全家桶都用上的话, 容易作茧自缚. 你可以说, 我家就这么用, 以后永远不改设计了, 但是谁知道呢... |
17
iseki 2022-01-23 06:25:54 +08:00
@dcoder 是啊,这一切都需要在特定的上下文中进行权衡,也就是我上文说的脱离具体上下文很难讲该如何做,性价比到底是高还是低。
|
18
xuanbg 2022-01-23 08:49:38 +08:00
|
19
cwek 2022-01-23 10:26:19 +08:00
物理外键没必要,但业务外键由应用处理,对应列用索引解决。
|
20
xhcarlin 2022-01-23 10:28:17 +08:00 1
很多人都反对使用物理外键,一般都是说外键维护麻烦、影响性能,表示用逻辑外键会更好。
我对此感到很疑惑,因为我感觉逻辑外键的开销也不小。比如说有一条更新操作的 SQL ,要自己实现逻辑外键,一条 SQL 就变成了两条,先查询后更新,然后我就感觉复杂度一下子就不一样了。 首先,我要引入事务,保证这两条 SQL 一次完成; 接着,受限于隔离级别,要避免脏读之类的问题,需要对应的行进行加锁; 再接着,锁用多了又影响性能(毕竟去除物理外键的原因之一就是为了性能),还得自己实现锁的机制。 这一连串下来,我感觉在维护这个数据完整性约束上的操作比直接用物理外键要麻烦很多,而且还不一定能设计的很完美。很多公司的项目连单元测试都不写,就算写了有很多也不过是 “Happy-path testing”,没尽量考虑各种异常问题。 还是说我把问题想得复杂呢?应该不引入事务、锁机制? 还有一些观点是对于数据库完整性要求低的项目可以不用物理外键,我比较好奇什么样的业务才对数据库完整性要求低?而且这样日积月累脏数据不会越来越多吗?我还是比较认同楼主所提到的,插入错误的数据比插入数据失败的危害要大得多。 最后提一句,我只是刚入行 1 年多的新人,懂得不多,还希望有经验的大佬能多提点一下。 |
21
IvanLi127 2022-01-23 10:37:13 +08:00 via Android
如果不用物理外键,那为啥用关系型数据库嘞?感觉这限制就是吃里扒外。
|
22
liprais 2022-01-23 11:05:39 +08:00 via iPhone
人均比优化器聪明系列
还有你们谈论数据库的时候是不是只是在谈论 mysql |
23
Gota 2022-01-23 11:13:45 +08:00 1
@xhcarlin 你说得没错, 对于数据本身的各类约束, 在越靠近数据的地方做, 实现成本就越低, 可靠性也越高.
看之前帖子里还有几个人说软删除的问题. 这类问题明明有更好的解决方式. 软删除会导致大量不活跃的数据堆积在数据库里, 然后导致数据持续膨胀, 接着就得分库分表, 这实在是没事找事. 其实只要能老老实实把各种事件日志, 审计日志打到 logstore 里面, 等到需要恢复的时候重播下特定的事件就可以了. 只要做到关系型数据库里只存热数据, 不用的数据该删就删, 冷数据该归档就归档. 80% 场景是用不着分库分表的. |
24
lanlanye OP 醒来看了一下各位的讨论,有以下几种情况:
@iseki 领导坚持不用这种问题确实存在,这里只考虑技术上使用物理外键是否比逻辑外键更好。我自认为已经把上下文定义得相对清晰了,即:非单表大量数据,非分布式情况,通常不超过百万但含有较复杂引用关系的业务数据。 @dcoder 确实存在认为数据库应该只做存储这一件事的看法,但这其实是放弃了 RDBMS 的部分功能,关于程序员希望少关心 SQL 这件事我也感到疑惑,因为 ORM 固然强大,但很难只靠它就写好业务逻辑,对一个后端开发人员来说了解数据库相关知识应该是必须的,而外键并不算什么特别生僻的知识吧? 典型场景下:如需要确保引用的数据存在,开发上需要先查询并加锁,再执行插入,而目前我接触过的 ORM 都不会在查询时加锁,这说明 ORM 也默认了开发者理解数据库行为并会自己去处理这件事,这显然不符合你说的 “少研究 SQL 也能写清楚逻辑”,而我疑惑的原因是手动加锁和使用外键约束相比并不高效,还需要投入额外的开发精力。 @cwek 如果 “没必要” 是说无论有和没有都不会影响功能的实现,那我也同意,但本帖想要讨论的问题是 “在(如正文描述的)一般情况下使用物理外键是否更合理”,因为我找不到使用逻辑外键可以比物理外键更高效、可靠的理由,能否从这个角度讨论问题呢? @Gota 受教了,不过归档数据这项工作是在删除时完成更好还是定期由独立的程序完成更好呢?后者的话其实并不能避免开发时编写软删除逻辑 |
25
Gota 2022-01-23 12:57:29 +08:00
@lanlanye 归档这项工作可以由两个子项构成. 一项是将数据写入冷存储, 另一项是从热存储中将数据删除.
这两个工作其实没有必要同时进行. 因为有事件日志或者事件流的存在, 所以写冷存储这一步是可以实时进行的, 启动一个消费者就行了. 从热存储删数据倒可以做成定时任务, 挑一个负载较低的时段分批删就不错. 我的核心论点是: 关系型数据库里面最好只存热数据, 这样能将不少问题在发生前就消灭掉. |
26
Gota 2022-01-23 13:13:58 +08:00
@lanlanye 我上面有个地方写得不对, 如果冷存储是实时写的话, 删数据直接删就可以了. 有一种例外情况: 如果一次性删除的数据量很大, 那确实得做个临时的软删除, 然后在后台执行实际的删除操作, 不然对数据库性能会有冲击.
|
27
adoal 2022-01-23 13:18:11 +08:00 via iPhone 2
不少 24K 氪金纯程认为程序员在技术层面的必备技能只有某门工业级编程语言的语法入门,其他技术知识(比如 DB 、特定 OS 的 API ,运维、安全)都不是程序员的事。除了会一点语言语法之外,最重要的是往业务方向上贴,以及吹牛当 PPT 架构师。技术只是修齐治平的手段,反正世界是草台班子驱动的,能吹着牛升职加薪的才是好技术。
阿里等少数几个互联网大厂在特定业务场景和历史技术背景下总结了一些 best practice ,他们一看是阿里的,照搬呗。至于自己的业务是不是典型互联网特征,规模是不是大厂,who tmd cares ,阿里都这么用,难道不好吗。 |
28
lanlanye OP @Gota 涉及到引用关系的数据在写入冷存储时似乎也只能靠开发人员约束,这就和使用逻辑外键一样了,同理还有需要在开始写入冷存储到实际删除数据前的这段时间里避免产生新的引用,也得依靠主动加锁。
要避免加锁,可以先对要删除的数据做软删除来避免后续业务引用它,再逐步删除已经存在的引用,但这依然要求开发人员在写入数据时做检查,同时因为目标数据和引用数据分别删除,需要考虑后续删除失败时手动回滚,感觉问题会变得更复杂…… |
29
iseki 2022-01-23 15:40:11 +08:00
总有人会选择无视自己的现实状况,盲目跟从某些大厂根据自己的实际状况制定的最佳实践,却忽视了所谓最佳实践总是有其特定上下文的
|
30
Gota 2022-01-23 17:00:39 +08:00 2
@lanlanye 我之前回复有一搭没一搭的没能把这事讲清楚, 不好意思啊. 这次完整描述下, 看能不能把逻辑理顺.
先假设有一个简单的论坛系统, 基本操作只有发帖, 回复, 删帖. 设计表结构的时候也就三张表 users, posts, replys. posts 里的 user_id 是指向 users 的外键约束. reply 里的 post_id 是指向 posts 的外键约束. 刚开始因为数据规模不大, 级联删除可以打开. 实现业务逻辑的时候, 以发帖为例: 1. 首先开一个数据库事务. 2. 往数据库里插一条数据, 拿到自动生成的主键 ID. 3. 往 logstore 里写一条事件 {"action": "post:create", "user_id": "xxx", "post_id": "<ID>", "content": ""}. 4. 提交事务. (任意一步失败, 回滚事务) 删帖和发回复也是类似的流程. 此时如果要撤销删帖, 只要把之前与该贴相关的, 直到删帖前的事件重放一遍就可以. 后来论坛流量大了, 需要把长时间无回复, 无更新的贴子及里面的回复视为冷数据 (不是用户删掉的帖子, 用户删掉的帖子直接从 RDS 删除就好), 从 RDS 里去掉, 放在冷存储中. 这时候可以跑一个定时任务, 在每天低峰期的时候, 分批将 RDS 中不活跃的数据找到, encode 成 JSON 存入 MongoDB 或者 OSS 这类冷存储中, 并从数据库中删除. JSON 格式大概这样: {"id": "123", "user_id": "xxx", "create_time": "2021-02-02T00:00:00Z", "content": "xxx", "replys": [...]} 之后如果有用户要查看冷数据, 可以从冷存储直接查, 必要的时候也可以写回 RDS. 这样设计的好处是, logstore 里的 events 作为整个系统唯一真实数据来源, 自带了审计日志的功能, 并且如果之后要切新数据库, 也只要写个新的消费者把 events 重放一遍就可以了. 当然限于篇幅有很多东西还是没法展开, 比如如何提高事件重放的效率, 如何保证消费时的幂等性, 如何在 RDS 中保留归档数据的元信息等. 这些如果遇到具体场景的话可以再设计对应的方案. 希望这次能讲的比较清楚, 若发现疏漏也欢迎指出. |
31
Gota 2022-01-23 17:14:45 +08:00
@lanlanye 写半天忘记回答 28 楼的提问了, 惭愧 XD. 因为冷数据的更新频率不是很高, 所以可以在 RDS 删除前确认下最后一次的事件, 如果开始写冷存储后有新的事件就这条数据就不冷了, 也就不用从 RDS 里删除, 这时是不需要加锁的.
|
32
lanlanye OP @Gota 非常感谢您的回复!解答了我的很多疑惑。
根据上述方案,可以将业务库的数据控制在一个较小的级别,让物理外键的存在不至于导致性能问题,同时可以利用好其完整性约束和级联操作等特性。一切的前提是需要引入一套高效可靠的日志服务,对吗? 我 28 楼的提问主要是考虑 **软删除** 这个情况下需要将数据存档。如果存在可以回放的日志,那就不需要软删除了,对数据的存档也早在记录日志时就已经完成了,这个问题也得到了解决。 |
33
Gota 2022-01-23 18:02:20 +08:00 2
@lanlanye 很高兴帮到你, 我们现在是用的阿里云 SLS 做 logstore 可靠性和性能都很不错.
至于"事件源架构"这套架构模式是几年前听一个国外大牛在上海做演讲时了解的. 如果想深入研究这套架构的话, 可以从这些文章入手: https://www.infoq.cn/article/zC4O4tA8QYOHIMJXddSs https://docs.microsoft.com/zh-cn/azure/architecture/patterns/event-sourcing |
34
adoal 2022-01-23 19:06:11 +08:00 via iPhone
@liprais 恐怕他们连 mysql 都觉得有太多没用的 rich features 了…在他们眼里 db 层只要有 kv 就行,应用层架构师人均吊打 Jim Gray 、Michael Stonebraker…
|
35
sagaxu 2022-01-23 19:56:19 +08:00
三个原因,
1. 用外键麻烦,数据清理和迁移更烦 2. 用了外键拆表拆库困难,伸缩性不好 3. 一般业务不需要很强的数据完整性 国内互联网行业普遍跟时间赛跑,下午 6 点提需求当天上线也不是新鲜事,最重要的是尽快上线,有问题随时再调整。 |
36
dcoder 2022-01-24 05:23:47 +08:00
@xhcarlin
@lanlanye 你们说加外键的操作, 有时可以避免 transaction, 甚至更低级的锁, 我同意. 但是外键远远不能完全避免它们, 所以直接把 transaction 和锁用熟悉性价比更高. 其实从本质上讲, 底层都是锁. 可以直接用 transaction + 逻辑外键 来处理跨多个表的修改. 甚至有时上 serializable transaction + conflict 处理. 另外跑个题, 我开发的很多 features 是跨 services 和不同数据库的. 最后我的经验是, 分布式锁用熟悉了, 其实性价比最高... 真正值得研究的是 分布式锁 + 分布式 transaction, 当然你们会喷我跑题... |
37
lanlanye OP @dcoder 你说的这个我也不反对,跨库场景下外键当然起不到作用,但非跨库的情况下你做的无非是自己实现了一遍数据库做的事。
对于一个团队来说,这种方式想要达到和数据库一样的效果,需要每个成员均熟悉相关知识,以及依靠大量的测试和代码 review ,即使这样也很难说完全不会出错,所以代价比直接设置外键高多了。 |
38
dcoder 2022-01-24 12:23:18 +08:00
@lanlanye
"非跨库的情况下你做的无非是自己实现了一遍数据库做的事。" 是的, 以一种更加通用的方式. 只要理解更通用更强大的概念: transaction, lock 就行. 喜不喜欢这种思维方式, 其实是个设计思路的差异. 场景-1 如果全体同意: 我家数据库就是, 静态, 单体, 永远不变, 并且大家爱专研 SQL tricks, 完全可以. 场景-2 现实世界是, 这种纯 SQL 情况越来越少见了. 现在不是 20 年前, 只有 Oracle / mySQL 流行的时代了. 现在设计和研究纯 SQL 的 tricks, 对于公司和程序员来说,性价比都很低. 我重复下这句话, 研究这个*性价比都很低*, 通用方法能搞定的话, 不用深究, 除非公司要求. 这里要说个历史遗留问题, 为啥纯 SQL 这些知识就该是程序员要掌握的基础? 为啥不是 MongoDB, DynamoDB, ElasticSearch, Redis 的细节知识更基础? 其实 SQL 更早流行, 但是不等于是说, SQL 的知识就能基础, 更基石. 相反, 我觉得 SQL 的流行, 其实算是点错了科技树... 我知道这样说很招喷, 但是这个问题太大了, 一两句说不清楚... 大家现在有意无意选择少专研 SQL tricks 是很有道理的. |
39
fengjianxinghun 2022-01-24 14:42:53 +08:00
大家只想用图灵完备的通用语言,而不想碰 DSL 。
SQL 作为一个 DSL ,从性价比来说碰的越少越好,利益触发就要想办法干掉一切 SQL 和数据库存在的土壤。 所以这无关那种办法更好,除非你是 Oracle 公司的员工。 |
40
yangyaofei 2022-01-24 15:52:38 +08:00
作为弱鸡菜鸟且看不懂昨天自己写的代码的人来说, 外键什么的肯定是能加就加.
就比如用 final, const, private, 尽量全参构造器不用无参构造器 等的原因是一样的. 毕竟不是那些可以背诵 mysql 源码和默写 jvm 优化的那些大神. 但是有一点不明白了,该用外键的时候都不用了, 都不在乎数据对不对了,直接上 NoSQL 不是更爽? 直接 ES 一波梭不爽? SQL 都是上世纪无用技能了, 还用啥 SQL 啊. 就像那些 post 接万物, http status code 不用,非要写一个 msg 里面是状态码的大神一样, 我们这些菜鸡还真是理解不了呢 |
41
xsm1890 2022-01-24 16:34:14 +08:00
国内基本都不推荐,国外基本都推荐。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
|
42
dcoder 2022-01-24 17:28:07 +08:00
辟谣针对外键的谣言: "国外基本都推荐外键"
我就在硅谷, 一般都是在写 micro/distributed services, 基本不会写外键 从我认识的人里聊天, 基本没见着用外键的, 除非是针对 legacy services, 单体 DB 那种才会用... |
43
cwek 2022-01-24 21:16:58 +08:00
@lanlanye 物理外键还要考虑修改到时需要校验主键。多一层 IO 读。物理外键等应该是数据库主管业务时产生的强逻辑验证,既然业务迁移到应用上,物理外键就可以被“业务”的概念外键取代。
|
44
liub34177 2022-01-25 11:39:01 +08:00
|
45
cus 2022-01-25 13:08:12 +08:00 via iPhone
如果外键已经导致了性能问题
再去考虑性能优化? |
46
aguesuka 2022-01-25 16:57:36 +08:00
外键只是一个运行时保险丝, 编码时还是要捕捉外键抛出的异常. 无论是不是使用外键, 问题应该要在测试时暴露出来. 也就是说外键不会减少工作量, 也不会减少 bug, 只是能降低 bug 的影响.
另外, 本来想打一堆字的, 不过 tricks 太恰当了. |
47
liprais 2022-03-01 11:18:47 +08:00 via iPhone
@fengjianxinghun sql 就是图灵完备的通用语言
你们不喜欢 sql 就说不喜欢呗,如果真的要喷高低也 fact check 一下... |