Help us learn about your current experience with the documentation. Take the survey.

添加数据库索引

索引可用于加速数据库查询,但何时应添加新索引?传统做法是为用于筛选或连接数据的每列添加索引。例如,考虑以下查询:

SELECT *
FROM projects
WHERE user_id = 2;

这里我们按 user_id 列筛选,因此开发者可能会决定为此列建立索引。

尽管在某些情况下,采用上述方法为列建立索引可能合理,但实际上可能产生负面影响。每次向表写入数据时,所有现有索引也必须更新。索引越多,更新速度可能越慢。此外,索引会占用大量磁盘空间,具体取决于索引的数据量和索引类型。例如,PostgreSQL 提供 GIN 索引,可用于索引常规 B 树索引无法处理的特定数据类型。但这类索引通常占用更多空间,且更新速度比 B 树索引慢。

因此,添加新索引时需重点考虑以下因素:

  1. 新查询是否尽可能复用现有索引?
  2. 表数据量是否足够大,以至于使用索引比遍历表行更快?
  3. 维护索引的开销是否值得查询时间的减少?

在某些情况下,可能不需要添加索引:

  • 表较小(记录少于 1,000 条)且预计不会指数级增长。
  • 现有索引已能筛选掉足够多的行。
  • 添加索引后查询时间减少不显著。

此外,索引无需匹配查询的所有筛选条件。我们只需覆盖足够的列,使索引查找的选择性足够小即可。

复用查询

首先确保查询尽可能复用现有索引。例如,考虑以下查询:

SELECT *
FROM todos
WHERE user_id = 123
AND state = 'open';

假设我们已有 user_id 列的索引,但 state 列没有。有人可能认为由于 state 未建立索引,此查询性能较差。但实际上,若 user_id 的索引能筛选掉足够多的行,查询性能可能完全正常。

判断索引是否被复用的最佳方式是使用 EXPLAIN ANALYZE 运行查询。根据关联表和筛选列的不同,您可能会发现额外索引几乎没有改善效果。

简而言之:

  1. 尽量编写查询以复用尽可能多的现有索引。
  2. 使用 EXPLAIN ANALYZE 运行查询并分析输出,找到最优查询方案。

部分索引

部分索引是带有 WHERE 子句的索引,仅匹配行的子集。相比完整索引,它们具有以下优势:

  • 减少索引大小和内存占用
  • 降低写入和清理开销
  • 提升特定条件下的查询性能

部分索引最适合始终基于已知条件筛选并针对特定数据子集的查询。常见用例包括:

  • 可空列:WHERE column IS NOT NULL
  • 布尔标志:WHERE feature_enabled = true
  • 软删除:WHERE deleted_at IS NULL
  • 状态筛选:WHERE status IN ('queued', 'running')

创建任何新部分索引前,请先检查现有索引是否可复用或修改。由于每个索引都会产生维护开销,应优先调整现有索引而非添加新索引。

示例

考虑以下引入新计数查询的应用代码:

def namespace_count
  NamespaceSetting.where(duo_features_enabled: duo_settings_value).count
end

def duo_settings_value
  params['duo_settings_value'] == 'default_on'
end

其中 namespace_settings 表有 100 万条记录,duo_features_enabled 是可空布尔列。

假设我们最近引入此列且未回填数据,这意味着 namespace_settings 表中大部分记录的 duo_features_enabled 值为 NULL。同时可知 duo_settings_value 只会返回 truefalse

为所有行建立索引效率低下,因为大部分值为 NULL。我们可以改为引入仅关注目标数据的部分索引:

CREATE INDEX index_namespace_settings_on_duo_features_enabled_not_null
ON namespace_settings (duo_features_enabled)
WHERE duo_features_enabled IS NOT NULL;

现在索引大小仅为完整索引的一小部分,查询规划器可有效跳过数十万条无关记录。

数据量

即使顺序扫描(遍历所有行)更快,数据库也可能不使用索引,尤其对于小表。

若表预计会增长且查询需筛选大量行,建议添加索引。若表较小(<1,000 条记录)或现有索引已能筛选足够多的行,则无需添加索引。

维护开销

每次表写入时索引都必须更新。在 PostgreSQL 中,向表写入数据时会更新所有现有索引。因此,同一表上的多个索引会减慢写入速度。需平衡查询性能与额外索引的维护开销。

例如,若添加索引将 SELECT 时间减少 5 毫秒,但使 INSERT/UPDATE/DELETE 时间增加 10 毫秒,则新索引可能得不偿失。当 SELECT 时间减少且 INSERT/UPDATE/DELETE 时间不受影响时,新索引更有价值。

索引限制

GitLab 强制限制每表最多 15 个索引。此限制:

  • 有助于维持最佳数据库性能
  • 减少维护开销
  • 防止磁盘空间过度使用

若需为已有 15 个索引的表添加索引,请考虑:

  • 删除未使用的索引
  • 合并现有索引
  • 使用可服务多种查询模式的复合索引

部分表不应再添加索引

我们对频繁访问的选定表实施了 RuboCop 检查(PreventIndexCreation),禁止进一步添加新索引。这是由于 LockManager LWLock 争用

出于相同原因,这些表也实施了 RuboCop 检查(AddColumnsToWideTables),禁止添加新列。

尽可能同时添加索引和修改应用代码

为避免创建不必要的索引,请在同一合并请求中执行以下操作:

  • 修改应用代码。
  • 创建或删除索引。

创建索引的迁移通常执行时间短,不会显著增加合并请求的体积。这样后端和数据库审查人员无需在合并请求或提交间切换上下文,即可更高效地审查。

使用的迁移类型

权威指南是迁移风格指南。如有疑问,请查阅指南。

以下是常见场景及推荐选择,供快速参考。

为改善现有查询添加索引

使用部署后迁移。现有查询无需添加索引即可正常工作,且对应用运行非关键。

若索引创建耗时较长(部署后迁移应少于 10 分钟),请考虑异步创建索引

为支持新或更新的查询添加索引

始终检查新或更新查询的执行计划。首先确认它们在无专用索引时不会超时或显著超过推荐查询时间

若查询未超时或未超出查询时间限制:

  • 为提升新查询性能而添加的索引对应用运行非关键。
  • 使用部署后迁移创建索引。
  • 在同一合并请求中发布生成和使用新查询的应用代码更改。

超时或超出查询时间限制的查询需采取不同措施,具体取决于仅发生在 GitLab.com 还是所有 GitLab 实例。大多数功能仅需为 GitLab.com(最大的 GitLab 安装之一)提供专用索引。

新或更新查询在 GitLab.com 上运行缓慢

使用两个合并请求通过部署后迁移创建索引并修改应用代码:

  • 第一个合并请求使用部署后迁移创建索引。
  • 第二个合并请求修改应用代码。它应在第一个合并请求的部署后迁移在 GitLab.com 上执行后才能合并。

若能使用功能开关,可能通过单个合并请求在功能开关后修改代码。同时包含部署后迁移。部署后迁移执行后,即可启用功能开关。

对于 GitLab.com,我们通过持续集成在单个发布周期内执行部署后迁移:

  • 在时间 t,一组合并请求已合并并准备部署。
  • t+1,该组合并请求的常规迁移在 GitLab.com 的暂存和生产数据库上执行。
  • t+2,该组合并请求的应用代码变更以滚动方式开始部署

应用代码完全部署后,发布经理可在稍后时间自行决定执行部署后迁移。部署后迁移每天执行一次(取决于 GitLab.com 可用性)。因此,在合并第二个合并请求前,您需要确认第一个合并请求中包含的部署后迁移已在 GitLab.com 上执行。

新或更新查询可能在大型 GitLab 实例上运行缓慢

无法直接检查 GitLab 自托管实例的查询性能。PostgreSQL 基于数据分布生成执行计划,因此预测查询性能是困难的任务。

若您担心查询在 GitLab 自托管实例上的性能,并确定自托管实例必须拥有索引,请遵循以下建议:

  • 对于遵循零停机升级的 GitLab 自托管实例,部署后迁移在应用代码部署后执行升级时运行。
  • 对于不遵循零停机升级的 GitLab 自托管实例,管理员可能选择在稍后时间(其自行选择)在常规迁移执行后执行部署后迁移。应用代码在其升级时部署。

因此,应用不能假定同一发布中部署后迁移应用的数据库模式已生效。应用代码应继续在同一发布中无部署后迁移添加的索引的情况下正常工作。

根据创建索引所需时间,您有两种选择:

  1. 单发布:若常规迁移能快速创建所需索引(通常因为表是新表或极小表),可在常规迁移中创建索引,并在同一合并请求和里程碑中发布应用代码更改。
  2. 至少两个发布:若所需索引创建耗时较长,必须在一个发布中通过 PDM 创建索引,然后等待下一个发布再创建依赖该索引的应用代码更改。

为现有表添加唯一索引作为约束

PostgreSQL 的唯一索引充当约束。向现有表添加唯一索引可能很棘手。

除非表在 GitLab.com 和 GitLab 自托管实例上绝对保证极小,否则必须使用多个发布中的多个部署后迁移来:

  • 删除和/或修复重复记录。
  • 引入约束现有列的唯一索引。

参考添加 NOT NULL 约束部分中概述的多发布方法。

PostgreSQL 的唯一索引与常规约束不同,无法以非验证状态引入。在删除和修复过程中,必须使用 PostgreSQL 的部分唯一索引和应用验证来强制新记录和更新记录的所需唯一性。

具体工作细节可能因情况而异,需采用不同方法。请咨询数据库团队、审查人员或维护者以规划工作。

删除未使用的索引

未使用的索引应被删除,因为它们会增加维护开销、占用磁盘空间,并在不提供任何性能益处的情况下降低查询规划效率。然而,删除仍在使用的索引可能导致查询性能下降或超时,甚至引发事故。删除前务必在 GitLab.com 和 GitLab 自托管实例上验证索引未被使用

  • 对于大表,考虑异步删除索引
  • 对于分区表,只能删除父索引。PostgreSQL 不允许独立删除子索引(即其分区上的对应索引)。

查找可能未使用的索引

要查看哪些索引有删除候选资格,可运行以下查询:

SELECT relname as table_name, indexrelname as index_name, idx_scan, idx_tup_read, idx_tup_fetch, pg_size_pretty(pg_relation_size(indexrelname::regclass))
FROM pg_stat_all_indexes
WHERE schemaname = 'public'
AND idx_scan = 0
AND idx_tup_read = 0
AND idx_tup_fetch = 0
ORDER BY pg_relation_size(indexrelname::regclass) desc;

此查询输出自上次统计重置以来未被使用的索引列表,并按索引大小降序排列。有关各列含义的更多信息,请参阅 https://www.postgresql.org/docs/16/monitoring-stats.html

对于 GitLab.com,您可以在 postgres.ai 上查看最新生成的生产报告,并检查 H002 Unused Indexes 文件。

这些报告仅显示自上次统计重置以来无记录使用的索引。它们不保证这些索引从未被使用。

验证索引未被使用

本节提供资源帮助您评估索引并确认其可安全删除。请注意这仅是建议指南,并非详尽无遗。最终目标是收集足够数据以证明删除索引的合理性。

请注意某些因素可能错误地显示索引未被使用,例如:

  • 可能有查询仅在 GitLab 自托管上运行,而非 GitLab.com。
  • 索引可能用于非常不频繁的进程,如定期 cron 作业。
  • 在数据量少的表中,PostgreSQL 可能最初优先选择顺序扫描而非索引扫描,直到表足够大。

调查索引使用情况

  1. 首先收集索引的所有元数据,验证其名称和定义。

    • 开发环境中的索引名称可能与生产环境不匹配。重要的是基于定义而非名称关联索引。要检查其定义,您可以:
    • 对于分区表,子索引的命名通常与父索引不同。要列出所有子索引,您可以:
      • Database Lab 中运行 \d+ <PARENT_INDEX_NAME>

      • 运行以下查询以更详细地查看完整的父子索引结构:

        SELECT
          parent_idx.relname AS parent_index,
          child_tbl.relname AS child_table,
          child_idx.relname AS child_index,
          dep.deptype,
          pg_get_indexdef(child_idx.oid) AS child_index_def
        FROM
          pg_class parent_idx
        JOIN pg_depend dep ON dep.refobjid = parent_idx.oid
        JOIN pg_class child_idx ON child_idx.oid = dep.objid
        JOIN pg_index i ON i.indexrelid = child_idx.oid
        JOIN pg_class child_tbl ON i.indrelid = child_tbl.oid
        WHERE
          parent_idx.relname = '<PARENT_INDEX_NAME>';
  2. 对于 GitLab.com,您可以在 Grafana 中查看索引使用数据。

    • 按相关索引筛选指标 pg_stat_user_indexes_idx_scan 至少最近 6 个月。以下查询显示所有数据库实例组合的索引使用率:

      sum by (indexrelname) (rate(pg_stat_user_indexes_idx_scan{env="gprd", relname=~"<TABLE_NAME_REGEX>", indexrelname=~"<INDEX_NAME_REGEX>"}[30d]))
    • 对于分区表,我们必须在删除父索引前确认所有子索引均未被使用

若数据显示索引使用量为零或可忽略不计,则它是删除的有力候选。但请注意,这仅限于 GitLab.com 上的使用。我们仍应调查所有相关查询,以确保其在 GitLab 自托管实例上可安全删除。

若索引显示使用率较低,但我们可以确认其他现有索引能充分支持使用它的查询,则仍可删除该索引。PostgreSQL 基于数据分布统计决定使用哪个索引,因此在某些情况下,即使两个索引都能充分支持查询,它也可能略微偏好其中一个索引,这可能解释了偶尔的使用。

调查相关查询

以下是查找可能使用该索引的所有查询的方法。理解查询执行或可能执行的上下文很重要,以确定该索引:

  • 在 GitLab.com 和 GitLab 自托管上均无依赖它的查询。
  • 可被其他现有索引充分支持。
  1. 调查索引的来源。

    • 挖掘提交历史、相关合并请求和引入索引的问题。
    • 尝试回答以下问题:
      • 索引最初为何添加?旨在支持哪个查询?
      • 该查询是否仍存在并执行?
      • 是否仅适用于 GitLab 自托管实例?
  2. 检查运行 rspec:merge-auto-explain-logs CI 作业输出的查询。

    • 该作业收集并分析通过测试执行的查询。输出保存为工件:auto_explain/auto_explain.ndjson.gz
    • 由于我们并非总是有 100% 的测试覆盖率,该作业可能无法捕获所有可能的查询和变体。
  3. 检查 postgres 日志 中记录的查询。

    • 通常,您可以筛选包含表名和索引定义中关键列的 json.sql 值。示例 KQL:

      json.sql: <TABLE_NAME> AND json.sql: *<COLUMN_NAME>*
    • 虽然影响索引使用的因素很多,但查询的筛选和排序子句通常影响最大。一般指导原则是找到条件与索引结构对齐的查询。例如,PostgreSQL 更可能利用 B 树索引来筛选索引的前导列并满足其部分谓词(如果有)。

    • 注意:我们仅保留最近 7 天的日志,此数据不适用于 GitLab 自托管使用。

  4. 手动搜索 GitLab 代码库。

    • 此过程可能繁琐,但这是确保未遗漏先前操作中任何查询的最可靠方法,特别是那些不频繁或仅适用于 GitLab 自托管实例的查询。
    • 可能在索引最初添加后引入了某些查询,因此我们不能总是依赖索引来源;还必须检查代码库的当前状态。
    • 为帮助定向搜索,尝试收集表的使用方式和访问它的功能的上下文。查找涉及索引定义中关键列的查询,特别是那些作为筛选或排序子一部分的列。
    • 另一种方法是按模型/表名和任何相关列进行关键字搜索。但这可能是一个棘手且漫长的过程,因为某些查询可能由跨多个文件的代码动态编译。

收集相关查询后,您可以获取 EXPLAIN 计划 来帮助评估查询是否依赖该索引。此过程需要充分理解索引如何支持查询及其使用如何受数据分布变化的影响。我们建议寻求数据库领域专家的指导以协助评估。

索引命名要求

具有复杂定义的索引必须显式命名,而非依赖迁移方法的隐式命名行为。简而言之,这意味着您必须为使用以下任一选项创建的索引提供显式名称参数:

  • where
  • using
  • order
  • length
  • type
  • opclass

索引命名注意事项

请参阅我们的约束命名约定页面。

为何需要显式名称

由于 Rails 与数据库无关,它仅从所有索引的必需选项(表名和列名)生成索引名称。例如,假设在迁移中创建以下两个索引:

def up
  add_index :my_table, :my_column

  add_index :my_table, :my_column, where: 'my_column IS NOT NULL'
end

第二个索引的创建会失败,因为 Rails 会为两个索引生成相同的名称。

index_exists? 方法的行为进一步加剧了此命名问题。它进行比较时仅考虑表名、列名和索引的唯一性规范。考虑:

def up
  unless index_exists?(:my_table, :my_column, where: 'my_column IS NOT NULL')
    add_index :my_table, :my_column, where: 'my_column IS NOT NULL'
  end
end

:my_table:my_column 上存在任何索引,index_exists? 调用将返回 true,并跳过索引创建。

add_concurrent_index 辅助方法是为已填充表创建索引的要求。由于它不能在事务性迁移中使用,它内置了检测索引是否已存在的检查。若发现匹配,则跳过索引创建。若无显式名称参数,Rails 可能对 index_exists? 返回误报,导致必需索引未正确创建。通过始终要求某些类型的索引具有名称,可大大减少错误发生的几率。

测试索引是否存在

按名称测试索引是否存在最简单的方法是使用 index_name_exists? 方法,但 index_exists? 方法也可与名称选项一起使用。例如:

class MyMigration < Gitlab::Database::Migration[2.1]
  INDEX_NAME = 'index_name'

  def up
    # 由于架构不一致,必须条件性创建索引
    unless index_exists?(:table_name, :column_name, name: INDEX_NAME)
      add_index :table_name, :column_name, name: INDEX_NAME
    end
  end

  def down
    # 无操作
  end
end

请注意,并发索引辅助方法(如 add_concurrent_indexremove_concurrent_indexremove_concurrent_index_by_name)已内置存在性检查。

临时索引

有时索引可能仅临时需要。

例如,在迁移中,表的列可能被条件性更新。为查询查询性能指南中哪些列需要更新,需要一个否则不会使用的索引。

在这些情况下,请考虑临时索引。要指定临时索引:

  1. 在索引名称前缀添加 tmp_ 并遵循命名约定
  2. 创建后续问题以在下一个(或未来)里程碑中删除索引。
  3. 在迁移中添加注释提及删除问题。

临时迁移示例如下:

INDEX_NAME = 'tmp_index_projects_on_owner_where_emails_disabled'

def up
  # 临时索引,将在 13.9 中删除 https://gitlab.com/gitlab-org/gitlab/-/issues/1234
  add_concurrent_index :projects, :creator_id, where: 'emails_disabled = false', name: INDEX_NAME
end

def down
  remove_concurrent_index_by_name :projects, INDEX_NAME
end

在批量后台迁移前分析新索引

有时需要添加索引以支持批量后台迁移。通常通过创建两个部署后迁移完成:

  1. 添加新索引,通常是临时索引
  2. 排队批量后台迁移

大多数情况下无需额外工作。新索引创建后,在排队和执行批量后台迁移时会按预期使用。

然而,表达式索引在创建时不会为新索引生成统计信息。自动清理最终会运行 ANALYZE 并更新统计信息,从而使新索引被使用。仅在索引创建后立即需要时(如上述后台迁移场景)才显式运行 ANALYZE

要在索引创建后触发 ANALYZE,请更新索引创建迁移以分析表:

# 在 db/post_migrate/ 中

INDEX_NAME = 'tmp_index_projects_on_owner_and_lower_name_where_emails_disabled'
TABLE = :projects

disable_ddl_transaction!

def up
  add_concurrent_index TABLE, '(creator_id, lower(name))', where: 'emails_disabled = false', name: INDEX_NAME

  connection.execute("ANALYZE #{TABLE}")
end

ANALYZE 应仅在部署后迁移中运行,且不应针对大表。若需要在更大的表上执行此行为,请在 #database Slack 频道寻求帮助。

分区表的索引

无法并发创建索引于分区表。然而,非并发创建索引会对正在索引的表持有写锁。因此,在热系统中创建索引时必须使用 CONCURRENTLY 以避免服务中断。

作为替代方案,数据库团队提供了 add_concurrent_partitioned_index。此辅助方法在不持有写锁的情况下为分区表创建索引。

在底层,add_concurrent_partitioned_index

  1. 使用 CONCURRENTLY 在每个分区上创建索引。
  2. 在父表上创建索引。

Rails 迁移示例:

# 在 db/post_migrate/ 中

class AddIndexToPartitionedTable < Gitlab::Database::Migration[2.1]
  include Gitlab::Database::PartitioningMigrationHelpers

  disable_ddl_transaction!

  TABLE_NAME = :table_name
  COLUMN_NAMES = [:partition_id, :id]
  INDEX_NAME = :index_name

  def up
    add_concurrent_partitioned_index(TABLE_NAME, COLUMN_NAMES, name: INDEX_NAME)
  end

  def down
    remove_concurrent_partitioned_index_by_name(TABLE_NAME, INDEX_NAME)
  end
end

异步创建索引

对于非常大的表,索引管理可能具有挑战性。虽然 add_concurrent_index 以不阻塞普通流量的方式创建索引,但当索引创建运行数小时时仍可能存在问题。必要的数据库操作(如 autovacuum)无法运行,且在 GitLab.com 上,部署过程被阻塞等待索引创建完成。

为限制对 GitLab.com 的影响,存在一个在周末时段异步创建索引的流程。由于流量通常较低且部署较少,索引创建可在较低风险级别下进行。

在低影响时段安排索引创建

  1. 安排索引创建
  2. 验证合并请求已部署且索引存在于生产环境
  3. 添加迁移以同步创建索引

安排索引创建

  1. 创建包含部署后迁移的合并请求,该迁移为异步创建准备索引。
  2. 创建后续问题以添加同步创建索引的迁移。
  3. 在准备异步索引的合并请求中,添加注释提及后续问题。

使用异步索引辅助方法创建索引的示例如下。此迁移将索引名称和定义输入 postgres_async_indexes 表。周末运行的过程会从此表拉取索引并尝试创建它们。

# 在 db/post_migrate/ 中

INDEX_NAME = 'index_ci_builds_on_some_column'

# TODO: 索引将在 https://gitlab.com/gitlab-org/gitlab/-/issues/XXXXX 中同步创建
def up
  prepare_async_index :ci_builds, :some_column, name: INDEX_NAME
end

def down
  unprepare_async_index :ci_builds, :some_column, name: INDEX_NAME
end

对于分区表,使用:

# 在 db/post_migrate/ 中

include Gitlab::Database::PartitioningMigrationHelpers

PARTITIONED_INDEX_NAME = 'index_p_ci_builds_on_some_column'

# TODO: 分区索引将在 https://gitlab.com/gitlab-org/gitlab/-/issues/XXXXX 中同步创建
def up
  prepare_partitioned_async_index :p_ci_builds, :some_column, name: PARTITIONED_INDEX_NAME
end

def down
  unprepare_partitioned_async_index :p_ci_builds, :some_column, name: PARTITIONED_INDEX_NAME
end

异步索引仅支持 GitLab.com 环境,因此 prepare_async_indexprepare_partitioned_async_index 在其他环境中为空操作。

prepare_partitioned_async_index 仅异步创建分区的索引。它不会将分区索引附加到分区表。 在分区表的下一步中,add_concurrent_partitioned_index 不仅会同步添加索引,还会将分区索引附加到分区表。

验证合并请求已部署且索引存在于生产环境

  1. 使用 ChatOps 通过 /chatops run auto_deploy status <merge_sha> 验证部署后迁移已在 GitLab.com 上执行。若输出返回 db/gprd,则部署后迁移已在生产数据库中执行。更多信息请参阅如何确定部署后迁移已在 GitLab.com 上执行
  2. 对于异步创建的索引,等待下一周以便索引可在周末创建。
  3. 使用 Database Lab 检查创建是否成功。确保输出未指示索引为 invalid

添加迁移以同步创建索引

验证索引存在于生产数据库后,创建第二个合并请求以同步添加索引。架构更改必须在此第二个合并请求中更新并提交到 structure.sql。同步迁移在 GitLab.com 上为空操作,但您仍应像预期一样为其他安装添加迁移。以下块演示了为上述异步示例创建第二个迁移。

在合并带有 add_concurrent_index 的第二个迁移前,验证索引存在于生产环境。若第二个迁移在索引创建前部署,则索引将在第二个迁移执行时同步创建。

# 在 db/post_migrate/ 中

INDEX_NAME = 'index_ci_builds_on_some_column'

disable_ddl_transaction!

def up
  add_concurrent_index :ci_builds, :some_column, name: INDEX_NAME
end

def down
  remove_concurrent_index_by_name :ci_builds, INDEX_NAME
end

为分区表同步创建索引

# 在 db/post_migrate/ 中

include Gitlab::Database::PartitioningMigrationHelpers

PARTITIONED_INDEX_NAME = 'index_p_ci_builds_on_some_column'

disable_ddl_transaction!

def up
  add_concurrent_partitioned_index :p_ci_builds, :some_column, name: PARTITIONED_INDEX_NAME
end

def down
  remove_concurrent_partitioned_index_by_name :p_ci_builds, PARTITIONED_INDEX_NAME
end

本地测试数据库索引更改

在创建合并请求前,必须本地测试数据库索引更改。

验证异步创建的索引

在本地环境中使用异步索引辅助方法测试创建索引的更改:

  1. 在 Rails 控制台中运行 Feature.enable(:database_async_index_creation)Feature.enable(:database_reindexing) 启用功能开关。
  2. 运行 bundle exec rails db:migrate 以在 postgres_async_indexes 表中创建条目。
  1. 运行 bundle exec rails gitlab:db:execute_async_index_operations:all 以在所有数据库上异步创建索引。
  1. 要验证索引,使用 GDK 命令 gdk psql 打开 PostgreSQL 控制台并运行命令 \d <index_name> 检查您的新创建索引是否存在。
    • 对于在分区上创建的索引,检查是否为该表自动生成了唯一名称 \d gitlab_partitions_dynamic.<table_name>

异步删除索引

对于非常大的表,索引删除可能具有挑战性。虽然 remove_concurrent_index 以不阻塞普通流量的方式删除索引,但当索引删除运行数小时时仍可能存在问题。必要的数据库操作(如 autovacuum)无法运行,且 GitLab.com 上的部署过程在等待索引删除完成时被阻塞。

为限制对 GitLab.com 的影响,请使用以下流程在周末时段异步删除索引。由于流量通常较低且部署较少,索引删除可在较低风险级别下进行。

  1. 安排索引删除
  2. 验证合并请求已部署且索引不再存在于生产环境
  3. 添加迁移以同步销毁索引

安排索引删除

  1. 创建包含部署后迁移的合并请求,该迁移为异步删除准备索引。
  2. 创建后续问题以添加同步销毁索引的迁移。
  3. 在准备异步索引删除的合并请求中,添加注释提及后续问题。

例如,使用异步索引辅助方法销毁索引:

# 在 db/post_migrate/ 中

INDEX_NAME = 'index_ci_builds_on_some_column'

# TODO: 索引将在 https://gitlab.com/gitlab-org/gitlab/-/issues/XXXXX 中同步销毁
def up
  prepare_async_index_removal :ci_builds, :some_column, name: INDEX_NAME
end

def down
  unprepare_async_index :ci_builds, :some_column, name: INDEX_NAME
end

此迁移将索引名称和定义输入 postgres_async_indexes 表。周末运行的过程会从此表拉取索引并尝试删除它们。

在创建合并请求前,您必须本地测试数据库索引更改。将测试输出包含在合并请求描述中。

验证合并请求已部署且索引不再存在于生产环境

  1. 使用 ChatOps 通过 /chatops run auto_deploy status <merge_sha> 验证部署后迁移已在 GitLab.com 上执行。若输出返回 db/gprd,则部署后迁移已在生产数据库中执行。更多信息请参阅如何确定部署后迁移已在 GitLab.com 上执行
  2. 对于异步删除的索引,等待下一周以便索引可在周末删除。
  3. 使用 Database Lab 检查删除是否成功Database Lab 在尝试查找已删除索引时应报告错误。若未报告,则索引可能仍存在。

添加迁移以同步销毁索引

验证索引不再存在于生产数据库后,创建第二个合并请求以同步删除索引。架构更改必须在此第二个合并请求中更新并提交到 structure.sql。同步迁移在 GitLab.com 上为空操作,但您仍应像预期一样为其他安装添加迁移。例如,为上述异步示例创建第二个迁移:

在合并带有 remove_concurrent_index_by_name 的第二个迁移前,验证索引不再存在于生产环境。若第二个迁移在索引删除前部署,则索引将在第二个迁移执行时同步销毁。

# 在 db/post_migrate/ 中

INDEX_NAME = 'index_ci_builds_on_some_column'

disable_ddl_transaction!

def up
  remove_concurrent_index_by_name :ci_builds, name: INDEX_NAME
end

def down
  add_concurrent_index :ci_builds, :some_column, name: INDEX_NAME
end

验证异步删除的索引

要测试删除索引的更改,请在本地环境中使用异步索引辅助方法:

  1. 在 Rails 控制台中运行 Feature.enable(:database_reindexing) 启用功能开关。
  2. 运行 bundle exec rails db:migrate 应在 postgres_async_indexes 表中创建条目。
  3. 运行 bundle exec rails gitlab:db:reindex 异步销毁索引。
  4. 要验证索引,使用 GDK 命令 gdk psql 打开 PostgreSQL 控制台并运行 \d <index_name> 检查被销毁的索引是否不再存在。