如何在零停机时间内实现Django索引定义的现代化

如果您最近阅读过Model.Meta.index_together的Django文档,则可能已经注意到以下说明:

请改用索引选项。 较新的索引选项比index_together提供更多功能。 index_together将来可能会被弃用。

Django历史上为Field(db_index = True)的单个字段以及Meta.index_together中的多个字段提供了索引控制。 这些选项非常适合为一个或多个字段指定索引,但是它们不能让您访问数据库索引的全部功能。

Django 1.11(2017)中添加了Meta.indexes选项,以允许通过Index()类使用更多索引功能。 最初,Index()添加了对降序索引的支持。 现在,它支持db_tablespace来控制存储,支持opclass以将PostgreSQL的各种运算符类用于索引,并支持创建不包含每一行的部分索引的条件。

“升级”
那么,如何将Field(db_index = True)或Meta.index_together一起“升级”到Meta.indexes?

首先,这不是必需的。 这两个功能实际上均已弃用,也不太可能。 如果您有一个使用Field(db_index = True)或Meta.index_together的旧项目,则最好将其保留在原处,并使用索引作为新索引。

但是,此更改是如何以低风险进行“零停机时间”迁移的一个很好的例子。 学习更多有关Django迁移的信息可能是一个很好的练习。

让我们采用以下模型:

(注:Status类正在使用Django 3.0的新枚举类型。)

我们的模型使用index_together,我们将其更改为使用索引。 该过程应类似于将Field(db_index = True)更改为使用索引。

我们将研究两种方法。 第一个使用索引的重建,这可能需要一些时间才能在大型表上运行。 第二个保留了“零停机时间”的现有索引。

请注意,我们根本不会更改索引的定义。 如果要升级索引以使用Index()的任何其他功能(例如条件),则数据库通常无法就地更改索引。 您需要在一次迁移中添加新索引,然后在第二次迁移中删除原始索引。

重建方法
要重建,我们只需要删除index_together并添加定义了等效Index()的索引:

当我们运行makemigrations时,最终将得到一个这样的迁移文件:

这是功能。 但是,如果运行sqlmigrate,我们将看到它执行DROP INDEX,然后执行CREATE INDEX:

这不适用于大型表,在大型表中创建索引可能需要几个小时。另外,PostgreSQL和SQLite在创建新索引时将锁定该表以进行写入。在PostgreSQL上,我们可以将AddIndex替换为AddIndexConcurrently(Django 3.0+)以防止锁定。

让我们看一下避免重新创建索引的工作的第二种方法。

零停机时间方法
为了实现零下降,我们需要使用现有的索引名称添加新的Index()定义,然后编写一个迁移,告诉Django数据库中无需进行任何更改。

我们需要的第一件事是Django自动为索引生成的名称。这将合并表名称,包含的字段名称和哈希。在Django的历史记录版本中,哈希算法已更改了两次,因此,为了安全起见,我们将从数据库中检索索引名称。我们可以使用dbshel​​l中的一些SQL来做到这一点。

例如,在SQLite上,我们可以运行.indexes命令在模型表上列出索引,然后从列表中选择索引:

在MariaDB / MySQL上,要运行的查询为:

On PostgreSQL, the query to run is:

值得检查的索引名称在您的所有环境(开发,登台,生产)中是否相同。 如果最初使用不同的Django版本以及不同的哈希算法创建数据库,则环境之间的名称可能会有所不同。 如果名称确实不同,我们可能要在所有环境中重命名索引以匹配生产。

其次,我们想使用找到的名称将其移动到Meta.indexes内的Index()定义中:

如果此时运行check命令,则会看到错误消息:

新的Index()将其名称限制为30个字符,以便与Oracle兼容。 这足够公平,尤其适用于应该与所有数据库后端兼容的Django核心和第三方软件包。 如果您使用的是Oracle,则旧的index_together名称应小于30个字符。

对于其他后端,我们可以使用更多字符:

SQLite-1,000,000
PostgreSQL-63
MariaDB / MySQL-64
在这种情况下,我们可以安全地禁用该检查。 通过将检查ID添加到SILENCED_SYSTEM_CHECKS设置中来执行此操作:

这有点危险,因为它删除了每个索引的检查。 但是,测试应该发现将来的索引是否具有过长的索引名称,因为数据库在迁移期间会引发错误。

(N.B.有一张开放的票证,可以使系统检查更安静些。)

正在运行的检查将显示它现在已被静音:

然后,我们应该运行带有标志的makemigrations进行新的迁移:

(我们传递了–name以避免自动迁移名称。)

我们的新迁移与先前导致停机的迁移相同。我们需要对其进行修改,以允许Django的迁移在不运行任何SQL的情况下考虑所应用的这些更改。

输入SeparateDatabaseAndState。

该操作类采用两个迁移操作列表。 database_operations编译为SQL并在数据库上运行。 state_operations适用于模型的内存版本。这种分离允许我们在数据库中执行一些操作,并告诉Django实际模型类发生了另一件事。这对于无法正确自动检测到的更改很有用,例如,将ManyToManyField更改为使用直通模型。

在我们的例子中,我们不希望对数据库做任何事情,因此我们提供了database_operations = []。在状态层中,我们想告诉Django我们已经“删除”了index_together并“添加”了数据库中的新索引。为此,我们可以将自动生成的AlterIndexTogether和AddIndex操作移到我们的state_operations列表中。

我们的迁移最终像这样:

我们可以在运行迁移之前验证两组操作。

首先,我们可以检查sqlmigrate确实对database_operations无效:

很好-除了正常的BEGIN和COMMIT之外,没有SQL语句。

其次,我们可以检查state_operations是否确实告诉Django我们的迁移符合我们模型的最新定义。 我们通过运行makemigrations –dry-run来做到这一点,以确保自动检测器没有发现任何要更改的内容:

在我们正常的测试套件通过后,现在应该可以部署了:)

原文:https://adamj.eu/tech/2020/07/27/how-to-modernize-your-django-index-definitions/