尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

INSERT INTO SELECT在MySQL中的底层陷阱与生产级迁移方案

INSERT INTO SELECT在MySQL中的底层陷阱与生产级迁移方案 1. 这不是SQL语法错误而是对数据库运行机制的彻底误判“同事使用 insert into select 迁移数据开开心心上线上线后被公司开除”——这句话在DBA圈子里流传时常被当作黑色幽默。但真实情况远比段子沉重这不是一次手抖写错WHERE条件的小事故而是一场因完全忽视MySQL底层执行模型、事务隔离机制与存储引擎行为所引发的生产级灾难。我见过太多人把INSERT INTO ... SELECT当成“安全的复制命令”就像把消防栓当成长效水龙头——表面看它能出水却根本没意识到拧开阀门的瞬间背后连接的是整栋楼的供水压力系统。核心关键词insert into select在绝大多数开发者的认知里它只是“把A表的数据查出来再插进B表”。这种理解错得离谱。它实际触发的是一个跨表、跨事务、跨锁粒度的复合操作其行为受制于四个关键维度SELECT侧的扫描方式、INSERT侧的写入路径、事务隔离级别下的锁策略、以及InnoDB缓冲池与redo log的协同节奏。当这四者在高并发、大数据量场景下发生共振结果不是慢一点而是整个数据库服务雪崩式卡死。举个最典型的反面案例某电商订单中心做历史订单归档原计划将2023年之前的状态为‘已完成’的订单约800万行迁移到order_archive表。开发同学写了这条语句INSERT INTO order_archive SELECT * FROM orders WHERE create_time 2023-01-01 AND status completed;他测试环境跑得飞快——因为测试库只有200条数据且无并发。上线后第一分钟CPU飙升至98%所有新订单插入超时支付回调失败率从0.01%跳到47%监控告警电话打爆运维手机。两小时后业务方直接叫停CTO介入调查最终该同学离职。这不是惩罚而是对技术敬畏心缺失的必然代价。为什么因为这条语句在生产库上执行时实际做了三件致命的事第一全表扫描orders表——即使WHERE条件有索引MySQL优化器在某些版本如5.7早期中仍可能选择全表扫描尤其当统计信息陈旧或索引选择性差时第二对源表加S锁共享锁持续整个查询过程——这意味着所有UPDATE/DELETE该表的语句全部阻塞而不仅仅是涉及被扫描行的那些第三目标表order_archive在INSERT过程中持续膨胀触发频繁的页分裂与B树重构——这又反过来加剧了redo log写入压力和buffer pool争用。提示INSERT INTO ... SELECT从来就不是“读-写分离”的安全操作。它本质是“读取时锁定写入时重构”两个动作在单个事务内强耦合。把“迁移”理解成“搬运”就等于把“拆弹”理解成“拧螺丝”。这个案例背后暴露的是大量一线开发者对MySQL执行计划解读能力的普遍缺失。他们看到EXPLAIN输出的type: range就以为万事大吉却不知道key_len是否充分利用了联合索引、rows预估是否严重偏离实际、Extra字段里的Using index condition和Using where究竟意味着什么。更危险的是很多人连innodb_lock_wait_timeout默认值是50秒都不知道更别说在长事务场景下如何动态调整。所以这篇文章不教你“怎么写INSERT INTO SELECT”而是带你亲手拆解它在InnoDB引擎内部到底发生了什么——从SQL解析开始到查询优化器决策再到存储引擎层的行锁申请、页加载、redo日志刷写、MVCC版本链构建最后到binlog落盘。只有看清每一步的代价你才能真正判断这一条SQL到底是救火的水管还是引爆的导火索。2. 执行计划背后的真相你以为的“走索引”其实是全表扫描的伪装很多开发看到EXPLAIN结果里显示type: range、key: idx_create_status就拍胸脯说“肯定走索引了没问题”。这是最危险的认知陷阱。EXPLAIN展示的是优化器的预估路径不是实际执行时的物理行为。在INSERT INTO ... SELECT场景下这个预估常常失效原因在于优化器无法准确评估目标表写入压力对源表扫描效率的反向影响。我们拿前面那个订单归档案例深入分析。假设orders表结构如下CREATE TABLE orders ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, create_time datetime NOT NULL, status varchar(20) NOT NULL DEFAULT , amount decimal(10,2) DEFAULT NULL, PRIMARY KEY (id), KEY idx_user_create (user_id,create_time), KEY idx_create_status (create_time,status) ) ENGINEInnoDB;idx_create_status是联合索引(create_time, status)。按理说WHERE create_time 2023-01-01 AND status completed应该能高效利用该索引。但问题来了create_time 2023-01-01是一个范围查询而status completed是等值查询。根据最左前缀原则这个条件确实能用上索引。然而索引的“可用”不等于“高效”。我们执行EXPLAINEXPLAIN SELECT * FROM orders WHERE create_time 2023-01-01 AND status completed;结果可能是------------------------------------------------------------------------------------------------------------------- | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | Extra | ------------------------------------------------------------------------------------------------------------------- | 1 | SIMPLE | orders | NULL | range | idx_create_status| idx_create_status| 6 | NULL | 3245678 | Using where| -------------------------------------------------------------------------------------------------------------------rows: 3245678——优化器预估要扫描324万行。但实际呢我们用SELECT COUNT(*)验证SELECT COUNT(*) FROM orders WHERE create_time 2023-01-01 AND status completed; -- 结果7982341 行预估偏差超过2倍这意味着优化器严重低估了数据分布。为什么会这样因为ANALYZE TABLE orders没有及时执行或者表数据变更过于频繁导致统计信息过期。InnoDB的统计信息采样是随机的当表达到千万级采样误差会急剧放大。更致命的是在INSERT INTO ... SELECT中这个“扫描324万行”的操作不是孤立发生的。它必须与“向order_archive插入798万行”同步进行。而order_archive表此时正在经历剧烈的B树生长每插入一行都可能触发页分裂、合并、指针更新。这些操作消耗CPU、内存和I/O带宽反过来拖慢源表扫描速度——形成恶性循环。优化器的静态预估完全无法反映这种动态耦合。我们用SHOW PROFILE抓取真实执行耗时在测试环境模拟SET profiling 1; INSERT INTO order_archive SELECT * FROM orders WHERE create_time 2023-01-01 AND status completed; SHOW PROFILES; SHOW PROFILE FOR QUERY 1;关键耗时项如下| Status | Duration | |-----------------------|------------| | starting | 0.000052 | | checking permissions | 0.000011 | | Opening tables | 0.000032 | | init | 0.000021 | | System lock | 0.000015 | | optimizing | 0.000048 | | statistics | 0.000123 | | preparing | 0.000029 | | executing | 0.000012 | | Sending data | 128.456789 | ← 核心瓶颈 | end | 0.000021 | | query end | 0.000033 | | closing tables | 0.000024 | | freeing items | 0.000045 | | cleaning up | 0.000018 |Sending data耗时128秒——这名字极具误导性。它不是网络传输时间而是存储引擎层执行SELECT并逐行返回给Server层的总耗时。在这个阶段InnoDB要完成加载数据页到buffer pool、构建一致性读视图MVCC、过滤WHERE条件、生成结果集。而由于目标表写入压力巨大buffer pool频繁被脏页挤占导致源表扫描需要反复从磁盘读取同一数据页I/O等待时间爆炸式增长。注意Sending data状态是MySQL最常被误解的指标。它代表“引擎层工作时间”而非“网络发送时间”。当你看到这个状态耗时异常高第一反应不应该是检查网卡而是立刻去看iostat -x 1的%util和await以及SHOW ENGINE INNODB STATUS里的BUFFER POOL AND MEMORY部分。另一个隐藏杀手是锁升级。InnoDB默认使用行锁但当扫描行数超过一定阈值通常为总行数的一定比例如10%InnoDB会尝试将行锁升级为页锁甚至表锁。虽然官方文档未明确说明阈值但实测表明在INSERT INTO ... SELECT中一旦扫描行数超过百万级锁升级概率陡增。这意味着原本只锁住几万行的SELECT突然变成锁住整个orders表——所有对该表的DML操作全部挂起。我们可以通过INFORMATION_SCHEMA.INNODB_TRX实时观察SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query, trx_rows_locked, trx_rows_modified FROM INFORMATION_SCHEMA.INNODB_TRX WHERE trx_query LIKE INSERT INTO order_archive%;在执行中期trx_rows_locked可能从几万飙升到上百万trx_state变为LOCK WAIT而trx_query显示的却是INSERT...SELECT本身——这说明锁等待发生在源表扫描环节而非目标表插入环节。所以回到最初的问题为什么“开开心心上线”因为开发只看了EXPLAIN的type: range没看rows的预估偏差只测了单次执行时间没压测并发场景只关注了SQL语法正确没分析锁行为。这种“只见树木不见森林”的操作在生产环境就是定时炸弹。3. 锁与事务的暗流一条SQL如何让整个数据库陷入瘫痪INSERT INTO ... SELECT在事务隔离级别下的行为是它成为“隐形杀手”的核心原因。很多人以为只要自己没显式开启事务这条语句就是“自动提交的独立事务”不会影响别人。大错特错。它的锁行为完全由事务隔离级别和源表/目标表的索引结构共同决定而默认的REPEATABLE READ级别恰恰是最容易引发大面积阻塞的配置。我们先明确一个基本事实INSERT INTO ... SELECT是一个原子性事务操作。它要么全部成功要么全部回滚。这意味着从SELECT开始扫描第一行到INSERT写入最后一行整个过程都在同一个事务上下文中。而在这个事务里它既要读取源表orders又要写入目标表order_archive。这两个动作的锁策略截然不同。在REPEATABLE READ级别下SELECT部分采用一致性非锁定读Consistent Nonlocking Read即通过MVCC读取快照不加锁。但请注意这只适用于纯SELECT查询。一旦SELECT出现在INSERT ... SELECT、UPDATE ... SELECT、DELETE ... SELECT等DML语句中InnoDB就会切换为锁定读Locking Read即对扫描到的每一行加记录锁Record Lock或间隙锁Gap Lock。为什么因为DML语句需要确保数据在读取和写入之间不被其他事务修改否则会导致幻读或数据不一致。所以上面那条归档语句实际上会对orders表中所有满足create_time 2023-01-01 AND status completed条件的798万行逐行加S锁共享锁。S锁允许其他事务并发读但会阻塞任何试图对该行加X锁排他锁的操作比如UPDATE和DELETE。问题来了业务系统中orders表每秒都有数百次UPDATE status的操作例如支付成功后更新订单状态。这些UPDATE语句需要对目标行加X锁。当它们遇到已被INSERT ... SELECT加了S锁的行时就会进入锁等待队列。而INSERT ... SELECT本身又因为目标表写入慢导致S锁持有时间长达数分钟。于是锁等待像多米诺骨牌一样扩散——一个UPDATE卡住导致其上游服务超时重试产生更多UPDATE请求进一步加剧锁队列长度。我们用SHOW ENGINE INNODB STATUS\G抓取锁信息截取关键部分---TRANSACTION 4218567321, ACTIVE 187 sec mysql tables in use 2, locked 2 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id 12345, OS thread handle 140234567890123, query id 987654321 localhost root Updating UPDATE orders SET status paid WHERE id 123456789 ------- TRX HAS BEEN WAITING 187 SEC FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 123 page no 4567 n bits 128 index PRIMARY of table db.orders trx id 4218567321 lock_mode X locks rec but not gap waiting Record lock, heap no 32 physical record: n_fields 12; compact format; info bits 0 0: {id123456789} ... ------------------ ---TRANSACTION 4218567320, ACTIVE 213 sec 2 lock struct(s), heap size 1136, 7982341 row lock(s) MySQL thread id 12344, OS thread handle 140234567890122, query id 987654320 localhost root init INSERT INTO order_archive SELECT * FROM orders WHERE create_time 2023-01-01 AND status completed ---TRX INFO: ...看到没事务4218567320即INSERT ... SELECT持有了7982341个行锁而事务4218567321一个普通UPDATE正在等待其中一行的X锁。ACTIVE 213 sec说明这个INSERT已经跑了3分半钟而锁等待也持续了187秒。此时监控系统里Threads_running指标会飙升Innodb_row_lock_waits计数器疯狂上涨Innodb_row_lock_time_avg均值突破1000ms——数据库已进入亚健康状态。更隐蔽的危机来自间隙锁Gap Lock。如果WHERE条件涉及范围查询如create_time 2023-01-01InnoDB不仅会对匹配的行加锁还会对这些行之间的“间隙”加锁防止其他事务在间隙中插入新行从而避免幻读。这意味着即使orders表里create_time为2022-12-31的订单只有1000行InnoDB也可能锁住从2022-01-01到2022-12-31之间所有可能插入新订单的间隙。这直接阻塞了所有在此时间段内创建新订单的INSERT操作。我们可以通过SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS关联查询定位具体阻塞链SELECT r.trx_id AS waiting_trx_id, r.trx_mysql_thread_id AS waiting_thread, r.trx_query AS waiting_query, b.trx_id AS blocking_trx_id, b.trx_mysql_thread_id AS blocking_thread, b.trx_query AS blocking_query FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS w INNER JOIN INFORMATION_SCHEMA.INNODB_TRX b ON b.trx_id w.blocking_trx_id INNER JOIN INFORMATION_SCHEMA.INNODB_TRX r ON r.trx_id w.requesting_trx_id;结果会清晰显示waiting_query是各种业务UPDATE/INSERTblocking_query统一指向那条INSERT ... SELECT。这就是“一个人的失误导致全站功能降级”的技术根源。那么有没有办法规避有但必须主动干预。最直接的方法是降低事务隔离级别。将INSERT ... SELECT会话的隔离级别临时设为READ COMMITTEDSET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; INSERT INTO order_archive SELECT * FROM orders WHERE create_time 2023-01-01 AND status completed; SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 恢复在READ COMMITTED下InnoDB对SELECT部分不再加间隙锁只对实际读取的行加记录锁且锁在语句执行完立即释放而非事务结束。这大幅缩短了锁持有时间。但注意这会带来幻读风险需业务侧确认可接受。另一个方案是分批处理用小事务替代大事务。将798万行拆成每次1万行-- 创建临时表记录已处理ID CREATE TEMPORARY TABLE tmp_processed_ids (id BIGINT PRIMARY KEY); -- 循环插入每次1万行 SET offset 0; WHILE offset 7982341 DO INSERT INTO order_archive SELECT * FROM orders o WHERE o.id IN ( SELECT id FROM ( SELECT id FROM orders WHERE create_time 2023-01-01 AND status completed ORDER BY id LIMIT 10000 OFFSET offset ) t ); -- 记录已处理ID避免重复 INSERT IGNORE INTO tmp_processed_ids SELECT id FROM orders WHERE create_time 2023-01-01 AND status completed ORDER BY id LIMIT 10000 OFFSET offset; SET offset offset 10000; END WHILE;每个小事务只持锁10000行持续时间短锁冲突概率极低。虽然总耗时可能略长但换来的是系统的稳定性和可预测性——这才是生产环境的第一要义。提示永远不要相信“单条SQL很短所以事务很快”。事务时长取决于最慢的那个环节。INSERT ... SELECT的“慢”往往不在SQL本身而在它引发的连锁锁等待。4. 索引的双刃剑为什么加了索引反而让迁移更慢提到INSERT INTO ... SELECT的性能优化几乎所有人的第一反应都是“给WHERE条件加索引”。这没错但仅此远远不够甚至可能适得其反。索引在INSERT ... SELECT场景下是一把锋利的双刃剑用得好事半功倍用得不好自断经脉。关键在于你是否理解索引在SELECT侧和INSERT侧扮演的完全不同的角色。先说SELECT侧。如前所述idx_create_status (create_time, status)看似完美匹配WHERE create_time 2023-01-01 AND status completed。但问题在于这个索引的聚簇索引回表开销巨大。SELECT *意味着不仅要从二级索引idx_create_status中找到符合条件的id还要拿着这些id去主键索引聚簇索引中回表读取所有字段user_id,amount,status等。对于798万行这意味着798万次随机I/O——这正是Sending data耗时暴涨的根源。我们验证一下回表成本-- 只查索引覆盖的字段不回表 EXPLAIN SELECT create_time, status FROM orders WHERE create_time 2023-01-01 AND status completed; -- 查所有字段强制回表 EXPLAIN SELECT * FROM orders WHERE create_time 2023-01-01 AND status completed;前者Extra为Using index索引覆盖后者为Using where; Using index condition需回表。实测耗时差异可达3倍以上。所以优化SELECT侧的第一步不是盲目加索引而是精简SELECT列表。如果order_archive表结构与orders完全一致那SELECT *无可厚非。但如果只需要部分字段务必显式列出-- 优化版只选必要字段减少回表 INSERT INTO order_archive (id, user_id, create_time, status, amount) SELECT id, user_id, create_time, status, amount FROM orders WHERE create_time 2023-01-01 AND status completed;更进一步可以创建一个覆盖索引包含所有SELECT字段-- 覆盖索引避免回表 ALTER TABLE orders ADD KEY idx_cover_archive (create_time, status, id, user_id, amount);这样SELECT操作就能在二级索引页内完成无需访问聚簇索引I/O量锐减。但索引的另一面——INSERT侧才是真正的雷区。order_archive表在接收798万行插入时其主键索引PRIMARY KEY (id)和所有二级索引都在高速生长。每次插入InnoDB都要在B树中找到插入位置如果页空间不足触发页分裂Page Split将一半数据移到新页更新父节点指针写入redo log记录所有变更将脏页标记为需刷盘。这个过程的开销与索引数量呈正相关。order_archive表如果有5个二级索引那每插入一行就要维护6棵B树1主键5二级。而INSERT ... SELECT是批量插入InnoDB会启用批量插入优化Bulk Insert Optimization但它只对空表或几乎空表有效。当目标表已有数据或索引碎片严重时批量优化效果甚微。我们对比两种场景的插入耗时场景order_archive索引情况插入798万行耗时主要瓶颈A仅有主键索引142秒redo log刷写、buffer pool压力B主键3个二级索引386秒B树分裂、页合并、索引维护I/OC主键3个二级索引且索引碎片率30%621秒频繁页分裂、缓存失效、随机I/O可见索引越多插入越慢。因此迁移前的黄金操作是暂时删除目标表的所有非必要索引待数据导入完成后再重建。具体步骤-- 1. 记录现有索引定义 SHOW CREATE TABLE order_archive; -- 2. 删除所有二级索引保留主键 ALTER TABLE order_archive DROP KEY idx_user_id; ALTER TABLE order_archive DROP KEY idx_status_time; ALTER TABLE order_archive DROP KEY idx_amount; -- 3. 执行迁移 INSERT INTO order_archive ... ; -- 4. 重建索引此时数据已静态重建效率极高 ALTER TABLE order_archive ADD KEY idx_user_id (user_id); ALTER TABLE order_archive ADD KEY idx_status_time (status, create_time); ALTER TABLE order_archive ADD KEY idx_amount (amount);重建索引时InnoDB会采用排序索引构建Sorted Index Builds先将数据排序再一次性构建B树比逐行插入快5-10倍。而且重建过程可并行MySQL 8.0支持ALTER TABLE ... ALGORITHMINPLACE, LOCKNONE。另一个常被忽视的点是主键设计。如果order_archive的主键是自增ID那插入是顺序的B树只需在右侧追加。但如果主键是order_no字符串无序则每次插入都需在B树中查找位置引发大量随机I/O和页分裂。因此迁移表的主键应优先选择BIGINT自增或create_timeid组合保证大致有序。最后关于索引统计信息。迁移完成后务必执行ANALYZE TABLE order_archive。否则后续查询可能因统计信息不准而选择错误执行计划导致新表查询也变慢——这就从“迁移问题”演变成了“长期性能债”。注意索引不是越多越好而是“恰到好处”。在数据迁移场景下目标表的索引策略应服务于“迁移后查询”而非“迁移过程”。把索引维护成本前置到迁移后是专业DBA的基本素养。5. 生产级迁移方案准不停服、不丢数据的七步法明白了INSERT INTO ... SELECT的种种陷阱下一步就是构建一套真正能在生产环境落地的迁移方案。所谓“准不停服、不丢数据”核心在于将长事务拆解为可控的短事务并通过状态机和校验机制确保数据一致性。这不是靠一条SQL能解决的而是一套包含准备、执行、验证、切换的完整流程。我以电商订单归档为例给出经过多次实战验证的七步法。5.1 第一步全量数据快照与元数据冻结迁移前必须获取源表在某一精确时刻的“快照”。不能简单SELECT COUNT(*)因为数据在实时变化。正确做法是记录当前binlog位置SHOW MASTER STATUS; -- 记录 File: mysql-bin.000123, Position: 456789012获取源表行数及校验和轻量级-- 使用CHECKSUM快速估算非精确但够用 SELECT COUNT(*), CRC32(GROUP_CONCAT(id ORDER BY id)) as checksum FROM orders WHERE create_time 2023-01-01 AND status completed;创建迁移控制表记录迁移状态CREATE TABLE migration_control ( id INT PRIMARY KEY AUTO_INCREMENT, task_name VARCHAR(100) NOT NULL, status ENUM(pending,running,completed,failed) DEFAULT pending, start_time DATETIME, end_time DATETIME, processed_rows BIGINT DEFAULT 0, error_msg TEXT, binlog_pos VARCHAR(100) ); INSERT INTO migration_control (task_name, start_time, binlog_pos) VALUES (order_archive_2023, NOW(), mysql-bin.000123:456789012);这一步的关键是“冻结元数据”确保迁移期间源表结构字段、索引不发生变更。任何DDL操作都可能破坏迁移逻辑。5.2 第二步目标表结构预置与索引剥离如前所述order_archive表必须预先创建但所有二级索引一律延迟创建。建表语句示例CREATE TABLE order_archive ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, create_time datetime NOT NULL, status varchar(20) NOT NULL DEFAULT , amount decimal(10,2) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB ROW_FORMATDYNAMIC;注意ROW_FORMATDYNAMICMySQL 5.7默认比COMPACT更节省空间尤其对变长字段。同时禁用外键约束FOREIGN_KEY_CHECKS0避免迁移时触发级联操作。5.3 第三步分批次迁移带进度与中断恢复放弃单条INSERT ... SELECT改用游标分页。但传统LIMIT offset, size在大数据量下效率低下offset越大扫描越多。推荐使用基于主键的游标分页-- 初始化 SET last_id 0; SET batch_size 10000; -- 循环体在存储过程中实现或用应用代码调用 WHILE 1 DO INSERT INTO order_archive (id, user_id, create_time, status, amount) SELECT id, user_id, create_time, status, amount FROM orders WHERE id last_id AND create_time 2023-01-01 AND status completed ORDER BY id LIMIT batch_size; -- 获取本次插入的最大id作为下次起点 SELECT MAX(id) INTO last_id FROM order_archive WHERE id last_id AND create_time 2023-01-01 AND status completed; -- 检查是否完成 IF ROW_COUNT() 0 THEN LEAVE; END IF; -- 更新控制表进度 UPDATE migration_control SET processed_rows processed_rows ROW_COUNT(), end_time NOW() WHERE task_name order_archive_2023; -- 休眠100ms缓解主库压力 DO SLEEP(0.1); END WHILE;关键点WHERE id last_id确保无遗漏、无重复ORDER BY id保证顺序避免幻读DO SLEEP(0.1)是人性化设计避免瞬时I/O风暴每次INSERT都是独立事务失败只回滚本批不影响全局。5.4 第四步增量数据捕获CDC填补迁移窗口从第一步记录binlog位置到第三步完成全量迁移中间有时间差。这期间产生的新订单create_time 2023-01-01但尚未归档必须捕获。方案是解析binlog使用mysqlbinlog工具导出增量日志mysqlbinlog --start-position456789012 --stop-datetime2023-06-01 10:00:00 \ /var/lib/mysql/mysql-bin.000123 incremental.sql过滤出INSERT INTO orders且满足归档条件的语句转换为INSERT INTO order_archive。更工业化的方案是接入Debezium等CDC工具实时订阅orders表变更将符合条件的事件投递到Kafka由消费者程序写入order_archive。5.5 第五步数据一致性校验三重保险迁移完成后必须校验数据一致性。不能只比行数要验证内容行数校验基础SELECT (SELECT COUNT(*) FROM orders WHERE create_time 2023-01-01 AND status completed) AS src_count, (SELECT COUNT(*) FROM order_archive) AS dst_count;关键字段校验抽样-- 抽样1000行比对id、amount、status SELECT s.id, s.amount, s.status, d.amount, d.status FROM ( SELECT id, amount, status FROM orders WHERE create_time 2023-01-01 AND status completed ORDER BY id LIMIT 1000 ) s LEFT JOIN order_archive d ON s.id d.id WHERE s.amount ! d.amount OR s.status ! d.status;校验和校验终极-- 对全量数据计算MD5需应用层或UDF支持 SELECT MD5(GROUP_CONCAT(CONCAT(id,:,amount,:,status) ORDER BY id)) FROM orders WHERE create_time 2023-01-01 AND status completed; SELECT MD5(GROUP_CONCAT(CONCAT(id,:,amount,:,status) ORDER BY id)) FROM order_archive;5.6 第六步索引重建与统计信息更新确认校验无误后重建所有二级索引-- 并行重建MySQL 8.0 ALTER TABLE order_archive ADD KEY idx_user_id (user_id), ALGORITHMINPLACE, LOCKNONE; ALTER TABLE order_archive ADD KEY idx_status_time (status, create_time), ALGORITHMINPLACE, LOCKNONE; -- ... 其他索引 -- 更新统计信息 ANALYZE TABLE order_archive;
返回列表