别拿 SQL 不当代码,那些年我们一起埋过的坑
知其所以然
「知其所以然」是数据库 DevOps 流程中审核规范落地的关键。
第一个原因是数据库变更对于很多开发团队来说属于「发射后不管」,我提交后哪管他洪水滔天,反正甩给 DBA 解决就行。
第二个原因是对于开发者而言完全不理解各项 SQL 审核规范有何意义,既然数据库提供了这个功能,凭什么不能用?
常见 SQL 规范背后的意义
对象命名规范
表名、字段名应富有含义:这两类对象经常需要被外部引用,因此名称应该有含义,这类规范对于开发者并不陌生,毕竟应用代码中的变量名等也有类似要求。
表名、字段名中禁止使用特殊字符、大写字母、保留字:很多数据库引擎默认是对大小写不敏感或者是限制使用某些保留字的,但却留了一道口子:通过引号将字符串括起来则不受此约束,但引用时也需要加上引号。坑了无数开发者的「对象不存在」、「语法不正确」问题就此产生。如果某开发者创建表时使用名为 'ABC',另一名开发访问时使用名 ABC(未加引号),无论你检查多少遍连接、拼写、权限,数据库引擎也只会默默告诉你「对象不存在」或「语法不正确」,DBA 如果没有接到开发者的此类求助那生涯就是不完整的。此外,使用特殊字符还可能导致与许多生态工具产生兼容性问题。所以为了不坑人坑己,无特殊情况请默默的遵循「小写字母加下划线」的规范吧。
约束、索引等内部对象按照特定命名模板:这些对象虽然不被外部应用使用,但日常管理维护经常需要打交道。以索引为例,索引名应该遵循 idx_表名_字段名,这样的命名风格一眼就能了解索引的影响范围,免去了查询复杂数据库内部视图的不便。
待删除表的命名约束:删除是一个高危动作,因此应该更为谨慎,一个可行的方法是强制要求对待删除表增加以「_delete」为结尾的后缀才允许删除,额外的操作可以有效减少失误发生概率。请不要觉得这样麻烦,当你体验过误删除那刻大脑一片空白的感觉,你就会知道小小的不便是值得的。
表创建规范
建表必须有主键:强制主键的意义有很多。以 MySQL 最常使用的 InnoDB 存储引擎为例,如果该表没有主键或唯一非空索引,系统也会利用一个全局共享的序列来生成一个内置 ID 索引,一旦发生高并发的插入,这个全局共享序列很可能由于争用而成为性能瓶颈。除此之外,大量的数据库生态工具例如数据同步工具、大表变更工具等都要求目标表存在主键。因此为了不给应用运行与数据库管理埋下隐患,请尽可能给每张表创建主键。
主键必须是 INT 或 BIGINT:主键无法轻易改变,而业务的发展常常超出预期。为了避免业务变化带来的主键冲突(例如业务合并带来的数据合并),主键应该使用一个与业务无关的字段,一般推荐的方式是使用一个整形类型字段。更进一步的要求是使用自增整型,这可以确保数据的物理存储结构更优化。
字段必须有注释:即便你有能力 1 分钟内看明白自己一年前写的不带注释的应用代码,也请老老实实给字段加上注释,因为哪怕是个明确的单词也可能有多重含义。来看一个真实的反面例子,sys 是什么意思?你以为是系统保留字?其实他是「输液室」的缩写。
限制使用分区表:这是一条 MySQL 特有的规范。在一些数据库中分区表是一个非常常用的功能,但是 MySQL 更多用于 OLTP 场景,倾向于单个物理表不要过大,加之其分区表技术相对不够成熟,因此业内更倾向于使用 ShardingSphere 这样的基于数据库中间件的分库分表方案,而弃用 MySQL 引擎的分区表。
限制使用外键:外键到底是好是坏,在 Hacker News 上引起了一场百人参与的大讨论 Do you use foreign keys in relational databases? (https://news.ycombinator.com/item?id=32731916) 支持者认为使用引擎层提供的外键约束可以降低开发成本减少应用数据错误的可能性,而反对者认为使用外键导致数据库变更、扩展等工作的难度明显增加,甚至无法使用一些生态工具。没人能够否认外键的好处,但也确实带来了管理上的弊端,从实践中看,确实越来越多研发组织倾向于让数据库结构尽可能简单,因此禁用了外键。
索引创建规范
限制索引数量:索引能够加速查询性能,但在执行 DML 操作(增删改)时需要额外的更新索引成本进而导致语句性能降低,滥用索引甚至可能使得索引存储空间与更新成本比表本身还大的多。因此对于 OLTP 系统来说一般不建议单表超过 5 个索引。
限制索引字段数量:索引的基本原理是将索引字段进行一次额外的排序存储方便检索。以最常用的 B+ 树索引为例,如果将表想象成一本书,索引就是目录,且是按一页一页的方式存储。由于每一页的空间是有限的,如果索引字段过多,每一页都存不下几条索引目录,每次检索都要翻多页目录,导致整体性能降低。一般建议单个索引的字段不要超过 5 个。
表字段规范
列类型限制:这是一个 DBA 与开发团队经常爆发矛盾的场景,最常见的就是限制大字段(如 BLOB 或 TEXT)的使用。大字段特指一些超大的数据类型,例如图片、视频、超长文本等。将所有应用需要的数据存入一个数据库似乎是方便了,但这将导致整个表的存储结构变得非常低效,进而影响查询性能,影响日常的备份、恢复、变更等操作,甚至带来大量不可预期的运行错误。此外多种数据库生态工具都对此类字段类型有特殊限制,也给高效使用数据库带来了不便。开发阶段偷的懒运行阶段全部要还。因此在关系型数据库中,并不建议存储此类数据,可以考虑使用对象存储或其他特种数据库。
字段必须有 NOT NULL 约束:NULL 在不同的数据库引擎中含义不完全一致,但一致的是都会带来各类负面影响。以 MySQL 为例,最典型的是 count() 函数将不统计含有 NULL 值的行导致查询与预期结果不符,另一个问题是 NULL 值可能让对应字段的索引失效。即便不从维护管理的角度出发,业务上允许大量的 NULL 值也给未来进行统计分析、AI 模型训练等需要高质量数据的应用场景带来负面影响。添加 NOT NULL 约束也许让你的应用开发更麻烦一些,毕竟每一行的每个字段都需要有一个确定的值,但他绝对是值得的。
一般语法规范
禁止使用左模糊作为查询条件:业务上经常需要使用 like 关键字实现模糊的查询过滤条件,但是数据库引擎在进行索引过滤时是按从左到右的顺序匹配字段,如果一个查询条件是like '%ABC',意味着首字母开始就包含所有可能,数据库将无法进行字段匹配过滤,只能对全表或全索引进行扫描,其性能可想而知有多糟糕。如果业务上有此类模糊的全文检索需求,请使用特种数据库,而不要在关系型数据库中实现。 查询必须带有过滤条件:不带查询条件意味着获取数据需要对全表进行扫描,哪怕是一张较小的表,单次执行资源开销并不明显,但实际业务中可能这一语句会被非常高频的调用成千上万次,量变引起质变,带来的资源开销极为恐怖,因此对于较为大型的业务系统应该严格限制此类操作。 查询不允许 SELECT *:这种写法带来的弊端同样是在性能层面,它会强制要求读取一行的所有字段从而产生了不必要的额外资源开销,严重时还会大大超出内存缓冲空间导致性能的急剧降低。这种性能损失少则数倍,多则上千倍,因此也应该严格限制。 INSERT 必须写出目标列:我们知道 INSERT 语句可以只写需要插入的值而不显式写明对应字段,对于一张含有大量字段的表,这种方式极易出错且难以排查,还是那句话,开发阶段偷的懒运行阶段全部要还。 对 DELETE/UPDATE 语句禁止使用 ORDER BY 子句:天知道为什么会有开发同学对这种非查询场景有排序的需求,无端消耗系统资源,可现实中就是存在。 在变更语句中禁止使用 Commit:对于一般的小型变更操作,我们期望保持原子事务,要么全部成功要么全部失败,这样一旦发生异常可以简单的直接重跑。如果是一个超大事务,则建议单独作为一个事务执行。
与 VCS 集成带来良好的审核体验
理解了规范背后的含义,后续就是设计良好的审核流程体验。Bytebase 提供了多种 SQL 审核模式以应对不同开发团队的工作习惯。下图是与 GitHub/GitLab 的无缝审核能力集成,更多的审核模式请期待我们后续的介绍。