数据库审查指南

此页面介绍数据库评审的相关内容。请参阅我们的代码审查指南,了解更广泛的代码审查建议和最佳实践。

一般流程

下列情况需要进行数据库审查:

  • 涉及数据库模式或执行数据迁移的更改,包括以下位置的文件:
    • db/
    • lib/gitlab/background_migration/
  • 对数据库工具的更改。例如:
    • 数据迁移或在 lib/gitlab/database/ 里面的 ActiveRecord helpers
    • 负载均衡
  • 产生超出显而易见的 SQL 查询的更改。通常由合并请求的作者决定是否引入复杂的查询以及是否需要数据库审查。
  • 使用 countdistinct_countestimate_batch_distinct_countsum 的服务数据指标的变化。这些指标可能对大表有复杂的查询。有关实施细节,请参阅产品智能指南

数据库审阅者应注意变更中过于复杂的查询,并仔细审阅这些查询。如果作者没有指出具体的查询,并且也没有过于复杂的查询,那么专注于审查数据迁移就足够了。

### 必需产物

当您请求一次数据库审查时,您必须提供以下内容。如果您的合并请求描述不包括以下内容,审查工作将重新分配给作者。

数据迁移

如果引入了新的数据迁移,数据库审查者必须审查所有数据迁移的执行 (db:migrate) 和回滚 (db:rollback) 的输出。

我们有极狐GitLab 的自动化工具(由流水线作业 db:check-migrations 提供),可在 CI 作业日志中提供此输出。作者不需要在合并请求描述中提供此输出,但这样做可能对审阅者有所帮助。该机器人还会检查迁移是否正确可逆。

查询

如果引入了新 SQL 查询或更新了现有 SQL 查询,您需要提供:

  • 合并请求中包含的每个原始 SQL 查询的查询计划以及每个原始 SQL 片段后面的查询计划链接。
  • 所有更改或添加的查询的原始 SQL(从 ActiveRecord 查询中翻译而来)。
    • 在更新现有查询的情况下,应提供查询的旧版本和新版本的原始 SQL 及其查询计划。

有关如何提供此信息的信息,请参阅添加或修改查询时的准备工作

角色和流程

合并请求作者的角色要:

数据库审查员的角色要:

  • 确保以正确的格式提供必需产物,如果没有,请将合并请求重新分配给作者。
  • 对 MR 进行一次审核,并向作者提出改进建议。
  • 一旦满意,用 ~”database::reviewed” 重新标记 MR,批准它,并请求评审者轮盘建议的数据库维护者进行审查。完成此操作后,将自己移除审阅者。

数据库维护者的角色要:

  • 对 MR 执行最终数据库审查。
  • 与数据库审阅者和 MR 作者讨论进一步改进或其他相关更改。
  • 最后批准 MR 并用 ~”database::approved” 重新标记 MR。
  • 如果没有其他待批准,则合并 MR,或根据需要将其传递给其他维护者(前端、后端、文档)。
    • 如果不合并,请将自己移除审阅者。

分配审核工作量

使用评审者轮盘示例)分配审阅工作量。MR 作者应请求建议的数据库审阅者进行审阅。当他们审阅完毕时,就会将 MR 移交给建议的数据库维护者

如果评审者轮盘没有建议的数据库审阅者和维护者,请确保您已应用标签 ~database 并重新运行 danger-review 流水线作业,或者从 @gl-database团队中挑选某人。

### 如何为数据库审查准备合并请求

为了使审阅更容易并因此更快,请考虑以下准备工作。

添加迁移时的准备

  • 确保按照文档更新 db/structure.sql,并另外确保在 db/schema_migrations 下添加或删除了相关版本文件。
  • 确保数据库字典已按文档更新。
  • 通过使用 change 方法或者使用 up 方法时包含 down 方法,确保数据迁移是可逆的。
    • 包括回滚过程或描述如何回滚更改。
  • 检查 db:check-migrations流水线作业是否已成功运行以及迁移回滚是否按预期运行。
    • 确保 db:check-schema 作业已成功运行,并且回滚中没有引入意外的模式更改。如果数据库模式已更改,此作业可能只会触发警告。
    • 每当您在审查过程中修改数据迁移时,验证前面提到的作业是否继续成功。
  • 如有必要,在 spec/migrations 为迁移添加测试。有关详细信息,请参阅在极狐GitLab 测试 Rails 迁移
  • 当数据迁移涉及高流量的表时,使用方法 enable_lock_retries开启锁重试。查看用例和解决方案中相关的例子
  • 除非有正当理由,否则确保 RuboCop 检查不会被禁用。
  • 当需要为大表添加索引时,在 Database Lab 中使用 CREATE INDEX CONCURRENTLY 测试其执行情况,并将执行时间添加到 MR 描述中:
    • Database Lab 和 JihuLab.com 之间的执行时间差异很大,但是 Database Lab 的执行时间的增加能表明在 JihuLab.com 上的执行时间也相当长。
    • 如果 Database Lab 的执行时间长于 一小时,则应将索引移至部署后迁移。请记住,在这种情况下,您可能需要把数据迁移和应用程序更改拆分到不同的发布中,以确保在部署需要它的代码时索引就位。
  • 在流水线 测试 阶段手动触发数据库测试作业(db:gitlabcom-database-testing)。
    • 该作业在 Database Lab 的克隆中运行数据迁移并将其发现(查询、运行时、大小更改)发布到 MR。
    • 查看迁移运行时和任何警告。

添加数据迁移时的准备

数据迁移本身就存在风险。需要采取其他措施来减少可能导致生产数据损坏或丢失的错误的可能性。

在 MR 描述中包含:

  • 如果迁移本身不可逆,则详细说明在发生事件时如何恢复数据更改。例如,对于删除记录的数据迁移(大多数情况下不会自动恢复的操作),如何恢复被删除的记录。
  • 如果迁移删除数据,请应用标签 ~data-deletion
  • 简明描述可能对用户体验造成的影响的错误;例如,”Issue 会意外地从 Epics 中丢失”。
  • 来自查询计划的能够表明查询按预期工作的相关数据;例如被修改或删除记录的大致数量。

#### 添加或修改查询时的准备工作

##### 原始 SQL

  • 在 MR 描述中写入原始 SQL。最好使用 pgFormatter 或者 https://paste.depesz.com 整理格式,使用常规引号 (比如,"projects"."id") 并且避免使用智能引号 (比如, “projects”.“id”)。
  • 对于使用参数动态生成的查询,每种变体都应该有一个原始 SQL 查询。

例如,议题的查找器可能将项目作为可选过滤器的参数,应该包括只使用议题查询的版本,以及使用关联议题和项目应用过滤器的版本。

当 finder 或其他方法生成非常大量的排列时,无需详尽地添加所有可能生成的查询语句,只需添加包含所有参数的查询语句,以及为每一种类型生成的查询语句。

例如,如果 join 或 group by 子句是可选的,则应包括没有 group by 子句的查询,和较少 join 的查询,同时为其余表保留适当的过滤器。

  • 如果查询始终与限制和偏移量一起使用,则查询语句应始终包含在使用的最大允许限制和非 0 偏移量。

##### 查询计划

  • 合并请求中包含的每个原始 SQL 查询的查询计划以及每个原始 SQL 片段后面的查询计划链接。
  • 提供聊天机器人提供的来自 postgres.ai 的查询计划链接。
    • 如果无法在 Database Lab 中获得准确的情况,您可能需要为开发环境准备数据,并提供来自 explain.depesz.comexplain.dalibo.com 的链接。请务必粘贴表单中使用的查询计划和查询。
  • 提供查询计划时,确保它命中足够的数据:
    • 要生成具有足够数据的查询计划,您可以使用以下 ID:
      • 命名空间 gitlab-org (namespace_id = 9970),用于涉及组的查询。
      • 项目 gitlab-org/gitlab-foss (project_id = 13083) 或者 gitlab-org/gitlab (project_id = 278964),用于涉及项目的查询。
      • 用户 gitlab-qa (user_id = 1614863),用于涉及用户的查询。
        • 或者,您也可以使用您自己的 user_id 或在项目或组中具有长期历史的用户的 user_id 用于生成查询计划。
    • 这意味着任何查询计划都不应返回 0 条记录或少于提供的限制(如果包含限制)的记录。如果在批处理中使用查询,则应确定并提供包含足够结果的适当示例。
    • 如果您的查询属于 JihuLab.com 中的一项新功能,因此它们不会返回生产中的数据:
      • 您可以分析查询并从本地环境提供查询计划。
      • postgres.ai 允许更新数据 (exec UPDATE issues SET ...) 和创建新表和列 (exec ALTER TABLE issues ADD COLUMN ...)。
    • 有关如何查找实际返回记录数的更多信息可以在了解 EXPLAIN 查询计划中找到。
  • 对于查询更改,最好同时提供 SQL 查询以及更改前后的计划。这有助于快速发现差异。
  • 包括显示性能改进的数据,最好以基准的形式。

向现有表添加外键时的准备工作

  • 在添加外键之前,包含一个数据迁移以删除源表中的孤立行。
  • 删除可能不再需要的 dependent: ... 语句。

添加表时的准备

  • 根据排序表列指南对列进行排序。
  • 将外键添加到任何指向其他表中数据的列,包括索引
  • WHEREORDER BYGROUP BYJOIN 等语句中使用的字段添加索引。
  • 新的表和列不一定有风险,但随着时间的推移,一些访问模式本来就难以扩展。为了提前识别这些风险模式,我们必须记录访问权限和大小的期望。在 MR 描述中包含对这些问题的回答:
    • 在接下来的 3 个月、6 个月、1 年内,新表的预期增长是多少?这些假设是基于什么?
    • 您预计该表在 3 个月、6 个月或 1 年内每小时有多少读写操作?在什么情况下更新行?这些假设是基于什么?
    • 根据预期的数据量和访问模式,新表是否会对 JihuLab.com 或私有化部署的实例构成可用性风险?提议的设计规模是否可以支持 JihuLab.com 和私有化部署客户的需求?

删除列、表、索引或其他结构时的准备工作

  • 遵循删除列的指南
  • 通常,最佳做法(但不是硬性规定)是在部署后迁移中删除索引和外键。
    • 例外情况包括删除小表的索引和外键。
  • 如果您要添加复合索引,另一个索引可能会变得多余,因此请在同一次迁移中将其删除。例如,添加 index(column_A, column_B, column_C) 会使 index(column_A, column_B)index(column_A) 变得冗余。

如何审查数据库

  • 检查数据迁移
    • 审查关系建模和设计选择
    • 查看迁移遵循数据库迁移风格指南,例如
    • 确保迁移在事务中执行或仅包含并发索引/外键助手(禁用事务)
    • 如果在 Database Lab 中添加了一个大表的索引并且其执行时间增加(超过 1 小时):
      • 确保它在添加在部署后迁移。
      • 在合并请求合并后,维护者在公司沟通工具中通知发布经理。
    • 检查 db/structure.sql 的一致性,并且迁移是可逆的。
    • 检查 db/schema_migrations 下面的相关版本文件是否被添加或删除。
    • 检查查询时间(如果有的话):在单个事务中,迁移中执行的累积查询时间需要在 JihuLab.com 上小于 15 秒 —— 最好远小于这个时间。
    • 对于列删除,请确保该列在以前的版本中已被忽略
  • 检查批量后台迁移:
    • 预估它在 JihuLab.com 上的执行时间。出于历史目的,强烈建议将此预估包含在合并请求描述中。这可以是预期批次的数量乘以延迟间隔。
    • 测试 阶段手动触发数据库测试作业(db:gitlabcom-database-testing)。
    • 如果单个 更新 低于 1 秒,查询可以直接放置在常规迁移中(在 db/migrate 中)。
    • 通常使用后台迁移,但不限于:
      • 在更大的表中迁移数据。
      • 对数据集中的每条记录进行大量 SQL 查询。
    • 审查查询(例如,确保批量大小合适)。
    • 由于执行时间可能比常规迁移长,因此建议将后台迁移视为部署后迁移:将它们放在 db/post_migrate 而不是 db/migrate 中。
  • 检查数据迁移时间指南
  • 检查迁移是否可逆并实现 #down 方法。
  • 检查新表迁移:
    • 规定的访问模式和数量是否合理?他们所基于的假设是否合理?这些模式是否对稳定性构成风险?
    • 列的顺序是是否节省空间
    • 是否有引用其他表的外键?
  • 检查数据迁移:
    • 预估它在 JihuLab.com 上的执行时间。
    • 根据时间的不同,数据迁移可以放在常规迁移、部署后迁移或后台迁移中。
    • 数据迁移也应该是可逆的,或者在可能的情况下附带如何逆转的描述。这适用于所有类型的迁移(常规、部署后、后台)。
  • 查询性能
    • 检查是否有任何过于复杂的查询和作者特别指出以供审核的查询(如果有)。
    • 如果不存在,请作者提供在 Database Lab 中的 SQL 查询和查询计划。
    • 对于给定的查询,查看有关数据分布的参数。
    • 检查查询计划并提出改进查询的建议(更改查询、模式或添加索引等)。
    • 一般准则是查询的执行时间低于 100 毫秒
    • 避免 N+1 问题并最小化查询次数