如何判断什么时候可以给大表加索引?
随着业务的快速增长,用户中心的用户表user单表数据量越来越大,此时,如果我们业务调整,想给test_user表添加索引,便于提升性能。
或者,通过慢查询日志发现了一条慢SQL,相关业务表随着数据增加已达千万级,需要加索引进行优化查询,想给test_user表添加索引,便于提升性能。
实际上,直接给大表加索引、加字段属于DDL(数据定义语言)操作,很可能会引起锁表,报错Waiting for meta data lock,造成业务崩溃。
任何对MySQL大表的DDL操作都值得警惕,那么如何对大表进行加索引操作?
两种索引构建的方式
例如,MySQL 在构建索引时,可以使用在线(Online)模式或者离线(Offline)模式。
在线模式(Online DDL):
这种模式允许在构建索引的同时,数据库可以继续进行读写操作,对业务的影响较小。
但不是所有的存储引擎和数据库版本都支持这种方式。
如果支持,例如在较新的 InnoDB 存储引擎版本中,可以通过设置参数来使用在线模式构建索引。
不过,在线模式可能会消耗更多的系统资源。
离线模式(Offline):
在构建索引时,会对表进行锁定,禁止其他读写操作,直到索引构建完成。
这种方式比较简单直接,但会对业务产生较大的影响。
如果数据库可以承受一段时间的停机或者业务低峰期足够长,离线模式也是一种选择。
关于DDL 和DML , 给大家 说明一下
DDL(Data Definition Language)即数据定义语言,是用于定义、修改和删除数据库对象(如数据库、表、索引、视图、存储过程等)的 SQL 语句集合。它的主要作用是构建和管理数据库的结构,为存储和操作数据提供框架。
DML(Data Manipulation Language)即数据操作语言,用于对数据库中的数据进行操作。它主要包括 INSERT(插入)、UPDATE(更新)、DELETE(删除)操作,这些操作可以让用户在数据库的表中添加新的数据、修改现有数据的值或者删除不需要的数据。
早期DDL操作,属于 离线模式(Offline) 类型
如果一张表数据量级是千万级别以上的,那么,给这张表添加索引,你需要怎么做呢?
这个和数据库的版本,有关系。
先看看 MySQL5.6.7之前的早期DDL原理。
早期DDL操作分为copy table和inplace两种方式,属于 离线模式(Offline), 会对表进行锁定,禁止其他读写操作(DML),直到索引构建完成。
早期DDL操作分为copy table和inplace两种方式,具体如下:
方式1:copy table 方式
1.
创建与原表相同的临时表,并在临时表上执行DDL语句
2.
锁原表,不允许DML(数据操作语言),允许查询
3.
将原表中数据逐行拷贝至临时表(过程没有排序)
4.
原表升级锁,禁止读写,即原表暂停服务
5.
rename操作,将临时表重命名原表
假设我们有一个名为orders的表,想要添加一个名为idx_order_date的索引到order_date列。
代码如下:
java
ALTER TABLE orders ADD INDEX idx_order_date (order_date), ALGORITHM=COPY;
在这个例子中,ALGORITHM=COPY指定了使用copy table方式来执行DDL操作。
数据库系统会创建一个新的临时表 ,添加索引,然后将旧表的数据复制到新表中。
完成后,旧表会被重命名为临时名称,新表会重命名为旧表的名称,完成DDL操作。
方式2:inplace 方式(fast index creation,仅支持索引的创建跟删除)
1.
创建frm(表结构定义文件)临时文件
2.
锁原表,不允许DML(数据操作语言),允许查询
3.
根据聚集索引顺序构建新的索引项,按照顺序插入新的索引页
4.
原表升级锁,禁止读写,即原表暂停服务
5.
rename操作,替换原表的frm文件
继续使用上面的orders表,如果我们想要以最小的业务影响添加索引,我们可以使用inplace方式:
java
ALTER TABLE orders ADD INDEX idx_order_date (order_date), ALGORITHM=INPLACE;
在这个例子中,ALGORITHM=INPLACE指定了使用inplace方式来执行DDL操作。
数据库系统会在原表上直接添加索引,这种方式通常更快,因为它避免了数据的复制过程。
早期copy VS inplace 两种方式的对比
inplace 方式相对于 copy 方式来说,inplace 不会生成临时表,不会发生数据拷贝,所以减少了I/O资源占用。
inplace 只适用于索引的创建与删除,不适用于其他类的DDL语句。
但是,不论是早期copy 模式还是早期inplace 模式的DDL,都会进行锁表操作,不允许DML操作,仅允许查询。
所以,在数据库的早期版本中,DDL(Data Definition Language)可以理解离线(Offline)操作,因为这些操作往往会锁定表,阻止其他用户进行数据的插入、更新或删除操作,直到DDL操作完成。
"离线模式" 下,对数据的DML操作(如新增数据/删除数据)很可能会引起锁表,报错Waiting for meta data lock,造成业务崩溃。
总之,无论 copy方式还是 inplace 方式,数据库表在DDL操作期间不可用,因此被称为"离线模式"。
MySQL5.6.7 之前, 如何在线模式 为大表添加索引?
MySQL5.6.7 之前由于DDL实现机制的局限性,有两种 在线模式 为大表添加索引的方式:
"影子策略"
pt-online-schema-change 方案
这两种模式,都是 从 mysql 外部进行。
方式一:"影子策略"
在MySQL 5.6.7及之前的版本中,由于DDL(Data Definition Language)操作的实现机制存在局限性,常常需要使用"影子策略"来执行DDL操作,以保证DML(Data Manipulation Language)操作的在线进行。
影子策略的核心思想是在不影响原始数据库性能的情况下,创建一个或多个与原始表结构和数据完全一致的数据表副本,这些副本被称为影子表。影子表可以用于备份、测试、分析或灾难恢复。
"影子策略"具体实践案例,大致如下:
1.
创建一张与原表结构相同的新表(例如tb_new)。
2.
在新表上创建索引。
3.
重命名原表为其他表名(例如tb重命名为tb_tmp),新表重命名为原表名(tb_new重命名为tb)。
4.
为原表(tb_tmp)新增索引。
5.
交换表,新表改回最初的名称(tb),原表改回最初的名称(tb_tmp)。
6.
把新表数据导入原表(即把新表承担业务期间产生的数据导入到原表中)。
"影子策略" 的优点
这种方法可以减少DDL操作对业务的影响,新增索引期间, 原表可以正常的 DML 数据 增删改的操作, 不影响 业务处理。
属于在线 模式。
"影子策略" 的缺点
在新表新增索引期间,旧表业务增删改操作,期间可能产生的数据(更新和删除)丢失问题,也就是数据一致性的问题。
方案二:pt-online-schema-change 工具
PERCONA提供若干维护MySQL的小工具,其中 pt-online-schema-change(简称pt-osc)便可用来相对安全地对大表进行DDL操作。
pt-online-schema-change 方案利用三个触发器(DELETE\UPDATE\INSERT触发器)解决了"影子策略"存在的问题,让新老表数据同步时发生的数据变动也能得到同步。
pt-online-schema-change 工作原理
1.
创建一张与原表结构相同的新表
2.
对新表进行DDL操作(如加索引)
3.
然后在原表上加三个触发器,DELETE/UPDATE/INSERT,将原表中要执行的语句也在新表中执行
4.
将原表数据以数据块(chunk)的形式复制到新表
5.
表交换,原表重命名为old表,新表重命名原表名
6.
删除旧表,删除触发器
pt-online-schema-change 的优点
这种方法可以减少DDL操作对业务的影响,新增索引期间, 旧表可以正常的 DML 数据 增删改的操作, 不影响 业务处理。属于在线 模式。
同时,在新表新增索引期间,旧表业务增删改操作,通过触发器 同步到了 新表,不产生的数据(更新和删除)丢失问题,实现了新表老表的数据一致性。
pt-online-schema-change 的问题:
表要有主键,否则会报错
表不能有trigger
尽管它是尽量减少对业务的影响,但在数据复制和同步阶段仍然会消耗一定的系统资源,包括 CPU、磁盘 I/O 和内存。对于大型表,这个过程可能会比较耗时,并且可能会对数据库的性能产生一定的影响。因此,最好在数据库负载较低的时候使用这个工具。
pt-online-schema-change 的方案,其实也是属于 "影子策略" 的一个方案变种, 是一个保证了 原表和 影子表 之间的 数据一致性的 "影子策略" 方案。
方案三:MySQL5.6.7 之后的内部 ONLINE DDL
MySQL5.6.7 之前由于DDL实现机制的局限性,常用"影子策略"和 pt-online-schema-change 方案进行DDL操作,保证相对安全性。
MySQL5.6.7 之前的"影子策略" 包括 (pt-online-schema-change 方案),属于 外部干预的 ONLINE DDL 方案。
在 MySQL5.6.7 版本中新推出了内部的 Online DDL 特性,支持"无锁"DDL。
5.7版本已趋于成熟,所以在5.7之后可以直接利用 ONLINE DDL特性。
MySQL5.6.7 Online DDL 的三个阶段
大致可分为三个阶段:
-
Prepare 阶段
-
执行
-
提交
MySQL 5.6.7 版本中 Online DDL 的执行主要分为以下三个阶段:
1.
Prepare 阶段:
2.
在这个阶段,MySQL 会创建新的临时 frm 文件(与 InnoDB 无关)。
持有 MDL(metadata lock)写锁,禁止读写操作(禁止 DML 和 DDL)。
根据 ALTER TABLE 类型,确定执行方式(copy, online-rebuild, online-no-rebuild)。对于 InnoDB 存储引擎,如果增加的是辅助索引(非主键索引),并且表没有外键约束,MySQL 可以使用 Online-Rebuild 算法。这种方式不需要复制整个表,而是在原表上重建索引,同时允许 DML 操作继续进行
更新数据字典的内存对象。
分配 row_log 对象记录增量DML log(仅 rebuild 类型需要)。
生成新的临时 ibd 文件(仅 rebuild 类型需要)。
3.
DDL 执行阶段:
4.
降级MDL(metadata lock)写锁 成为 MDL读锁,允许读写操作(允许 DML,禁止 DDL)。
为了保证数据一致性,记录 DDL 执行过程中产生的增量DML log 到 row_log。在这个阶段,与此同时,原表表的所有DML操作日志写入row_log。
扫描原表的聚集索引每一条记录。
遍历新表的聚集索引和二级索引,逐一处理。
根据记录构造对应的索引项。
将构造索引项插入 sort_buffer 块排序。
将 sort_buffer 块更新到新索引树上。
重放 row_log 中的操作到新索引上, 重放该阶段产生的 Row Log日志到新索引树。
5.
Commit 阶段:
6.
当前 Block 为 row_log 最后一个时,禁止读写,升级到MDL(metadata lock)写锁。
重做 row_log 中最后一部分增量。
更新 InnoDB 的数据字典表。
提交事务(刷事务的 redo 日志)。
修改统计信息。
rename 临时 ibd 文件,frm 文件。
变更完成,释放MDL(metadata lock)写锁 。
这三个阶段共同确保了 Online DDL 操作能够在不影响现有 DML 操作的情况下执行,从而提高了大型数据库操作的可用性和并发性。
MySQL5.6.7 Online DDL 如何保证数据 一致性
Online DDL 操作过程中, 从扫描原表的聚集索引每一条记录, 构建 索引项 刷入到 新的 索引树上。
这个扫描过程, 原表的数据可能会 发生 DML 变更, 从而导致 新的索引树 的数据 和聚集索引 上的数据不一致性。
MySQL5.6.7 Online DDL 如何保证数据 一致性?
使用的 是 row log 结构。
在数据库执行 DDL(Data Definition Language)操作期间,记录 DML(Data Manipulation Language)操作的 Row Log(行日志)非常重要。
这种日志主要用于在 DDL 操作过程中保证数据的一致性和完整性。
例如,当对一个表进行结构修改(如添加列、修改列的数据类型等)时,同时可能有其他事务在对该表进行 DML 操作(插入、更新或删除数据行)。
记录这些 DML 操作的 Row Log 可以在 DDL 操作完成后,根据日志中的信息来更新受影响的数据行,确保数据的准确性。
DDL 的row log 核心结构row_log_t
在线online处理DDL 的row log 的核心代码在文件row0log.cc中,有兴趣的可以进行详细解读。
online ddl的细节逻辑是:通过一个日志缓存,保留在ddl期间的 dml操作,然后进行缓存日志回复。
online ddl 的原理 类似于gh-ost工具,只不过后者采用binlog进行dml操作回放,而mysql内部是单独维护一个核心缓存结构------row_log_t
java
/** @brief Buffer for logging modifications during online index creation
All modifications to an index that is being created will be logged by
row_log_online_op() to this buffer.
All modifications to a table that is being rebuilt will be logged by
row_log_table_delete(), row_log_table_update(), row_log_table_insert()
to this buffer.
When head.blocks == tail.blocks, the reader will access tail.block
directly. When also head.bytes == tail.bytes, both counts will be
reset to 0 and the file will be truncated. */
struct row_log_t {
int fd; /*!< file descriptor */
ib_mutex_t mutex; /*!< mutex protecting error,
max_trx and tail */
page_no_map *blobs; /*!< map of page numbers of off-page columns
that have been freed during table-rebuilding
ALTER TABLE (row_log_table_*); protected by
index->lock X-latch only */
dict_table_t *table; /*!< table that is being rebuilt,
or NULL when this is a secondary
index that is being created online */
bool same_pk; /*!< whether the definition of the PRIMARY KEY
has remained the same */
const dtuple_t *add_cols;
/*!< default values of added columns, or NULL */
const ulint *col_map; /*!< mapping of old column numbers to
new ones, or NULL if !table */
dberr_t error; /*!< error that occurred during online
table rebuild */
trx_id_t max_trx; /*!< biggest observed trx_id in
row_log_online_op();
protected by mutex and index->lock S-latch,
or by index->lock X-latch only */
row_log_buf_t tail; /*!< writer context;
protected by mutex and index->lock S-latch,
or by index->lock X-latch only */
row_log_buf_t head; /*!< reader context; protected by MDL only;
modifiable by row_log_apply_ops() */
ulint n_old_col;
/*!< number of non-virtual column in
old table */
ulint n_old_vcol;
/*!< number of virtual column in old table */
const char *path; /*!< where to create temporary file during
log operation */
};
从说明可以看出,mysql内部将online ddl分为两类:
一类是增加索引类,即调用row_log_online_op函数来进行dml操作缓存填写;
一类是其他ddl。则调用row_log_table_delete, row_log_table_update, row_log_table_insert进行缓存区填充。
下面说要一下,核心结构体中row_log_t各字段含义:
fd,path :分别是在ddl操作期间,用于保存dml操作记录的临时文件的句柄和文件名;从源码可以看到该目录为innodb_tmpdir指定,若该值为空,则设置为tmpdir对应目录。其获取临时目录的函数为innobase_mysql_tmpdir()
blobs:记录的写入是按照记录块的方式,该字段表示记录块的数量;
table:不为null表示重建表,为null表示online 添加索引
tail,head:该成员就是记录块,分别用于写入和回放。具体结构 row_log_buf_t 下面会详细说明
DDL 的 Row Log 记录内容
操作类型记录
对于 DML 操作,日志文件会详细记录操作的类型,即插入、更新或删除。例如,使用特定的代码或标识符来表示这三种操作类型。如果是更新操作,还会记录更新的是哪个或哪些列。
比如,记录 "UPDATE 操作,更新了 customer 表中的 customer_name 列和 customer_email 列"。
数据行标识记录
为了能够准确地定位需要处理的数据行,日志文件会记录数据行的关键标识信息。
在有主键的表中,通常会记录主键值。例如,对于 customer 表,若 customer_id 是主键,在记录更新操作的 Row Log 中会包含 customer_id 的值,这样在后续处理时可以快速找到对应的行。
对于没有主键的表,可能会记录其他唯一标识或者数据行在表中的物理位置(如数据页编号和页内偏移量等信息,在 InnoDB 存储引擎中)。
数据值记录
对于插入操作,会记录插入的完整数据行内容。对于更新操作,会记录更新前和更新后的列值。
例如,记录 "更新前 customer_name 的值为 'John',更新后的值为 'Johnny'"。
对于删除操作,会记录被删除行的数据内容或者关键标识,以便在需要时可以恢复或审计这些数据。
在 DDL 操作中的处理流程
DDL 操作开始前的日志记录开启
当检测到即将进行 DDL 操作时,数据库会开启专门的日志记录机制来捕获 DML 操作的 Row Log。这个机制可能会涉及分配内存缓冲区来暂存日志记录,或者直接将日志记录指向现有的重做日志(redo log)和撤销日志(undo log)系统(如在 InnoDB 存储引擎中)。
DDL 操作期间的日志记录
在 DDL 操作过程中,如修改表结构,所有对该表的 DML 操作的 Row Log 都会被持续记录。例如,在将一个表的列数据类型从整数改为字符串的过程中,如果有新的数据行插入,日志会记录插入操作的详细信息;如果有数据行更新,会记录更新前后的数据值和列信息。这些日志记录是按照时间顺序或者操作顺序进行存储的,以保证可以准确地还原操作过程。
DDL 操作完成后的日志处理
当 DDL 操作完成后,数据库会根据记录的 Row Log 来处理受影响的数据行。如果是添加列的 DDL 操作,会根据日志中的插入和更新操作记录,为新列填充合适的值或者更新数据行的结构。例如,通过日志中记录的插入操作,为新添加列填充默认值;通过更新操作记录,按照新的列结构调整数据行。如果是修改列的数据类型的 DDL 操作,会根据日志中的更新操作记录,对数据行中的列值进行转换或者重新组织,确保数据行与新的表结构相匹配。
与数据恢复和一致性维护的关系
数据恢复场景
在 DDL 操作过程中,如果系统出现故障(如崩溃、断电等),记录 DML 操作的 Row Log 可以用于数据恢复。数据库可以在重启后,根据日志中的信息来恢复在 DDL 操作期间未完成的 DML 操作,确保数据的完整性。例如,在故障发生前,有部分数据行已经完成插入操作但未在 DDL 操作后进行相应的处理,通过日志可以重新执行这些处理步骤。
数据一致性维护
通过记录和处理这些 Row Log,可以避免在 DDL 操作过程中由于数据行的变化而导致的数据不一致问题。例如,在修改表结构的同时,确保新插入的数据行能够符合新的结构要求,更新的数据行能够正确地转换列值。这样,无论是在正常的操作过程中还是在出现异常情况后,都能够维护数据库数据的一致性。
DDL 的 Row Log (DML增量日志)
我们先来看一个结构,它叫DDL 的DML增量 Row Log ,用于在DDL过程中, 记录DML操作的日志 。
一条 DML增量操作日志, 结构为
操作flag + 事务id + 操作记录