惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Microsoft Azure Blog
Microsoft Azure Blog
博客园_首页
Forbes - Security
Forbes - Security
WordPress大学
WordPress大学
P
Proofpoint News Feed
T
Threat Research - Cisco Blogs
L
LINUX DO - 热门话题
L
Lohrmann on Cybersecurity
Spread Privacy
Spread Privacy
D
Darknet – Hacking Tools, Hacker News & Cyber Security
大猫的无限游戏
大猫的无限游戏
博客园 - 三生石上(FineUI控件)
P
Privacy International News Feed
A
About on SuperTechFans
T
Tailwind CSS Blog
I
InfoQ
S
Securelist
云风的 BLOG
云风的 BLOG
罗磊的独立博客
Recent Announcements
Recent Announcements
T
The Exploit Database - CXSecurity.com
B
Blog RSS Feed
V
Visual Studio Blog
Know Your Adversary
Know Your Adversary
The GitHub Blog
The GitHub Blog
Jina AI
Jina AI
腾讯CDC
Cyberwarzone
Cyberwarzone
有赞技术团队
有赞技术团队
AWS News Blog
AWS News Blog
博客园 - 【当耐特】
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
F
Full Disclosure
S
Secure Thoughts
博客园 - 司徒正美
J
Java Code Geeks
Y
Y Combinator Blog
Google Online Security Blog
Google Online Security Blog
GbyAI
GbyAI
N
News and Events Feed by Topic
Help Net Security
Help Net Security
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Project Zero
Project Zero
T
Tenable Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
Tor Project blog
MyScale Blog
MyScale Blog
Scott Helme
Scott Helme
小众软件
小众软件
K
Kaspersky official blog

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
复制一个表结构和数据,我的索引和约束不见了?
ALGO阿狗 · 2026-04-20 · via 博客园_首页

最近测试环境要做一轮压测,测试同学提出一个需求:想复制一份真实数据出来,单独做回放测试,避免污染原始日志表。

这个需求听起来非常简单。组里新来的初级开发毫无悬念地接下了这个任务。

第二天,测试同学一大早就在群里发消息:

“这个日志查询接口怎么这么慢?昨天还好好的。”

过了一会儿,又来一条更离谱的消息:

“新增日志偶尔报主键冲突,能看下吗?”

紧接着又来:

“分页是不是有问题?第一页和第二页有重复数据,而且顺序老变。”

情景复现

业务团队有一张日志表:biz_operation_log。专门记录用户操作行为,比如:

  • 登录
  • 下单
  • 支付
  • 修改资料

想复制一份真实数据出来,单独做回放测试,避免污染原始日志表。做法就是创建一张备份表。

于是我新来的初级开发同学非常自信地敲下了这行 SQL:

CREATE TABLE biz_operation_log_bak SELECT * FROM biz_operation_log;

在开发环境执行成功!

他还特意查了一下数据量:

SELECT COUNT(1) FROM biz_operation_log_bak;

结果和原表一模一样,没问题,然后非常丝滑地提交到了测试环境。

那问题出在哪里?我们一步一步往下看。

首先,查看分页查询SQL的执行计划:

EXPLAIN SELECT *
FROM biz_operation_log_bak
WHERE biz_id = ?
ORDER BY create_time DESC
LIMIT 0,20;

结果:

type: ALL
rows: 12000000

全表扫描。1200 万日志记录,一页 20 条。

继续查原表结构:

SHOW INDEX FROM biz_operation_log;

原表有索引:

PRIMARY KEY(id)
INDEX idx_biz_id(biz_id)
INDEX idx_create_time(create_time)

再看备份表:

SHOW INDEX FROM biz_operation_log_bak;

返回结果为空。

于是问题原因找到了,备份表没有原始表的主键、索引信息!

  • 查询慢,是因为全表扫描了。
  • 主键冲突,是因为备份表根本没有主键这个字段,ID只是普通字段。程序继续按老逻辑插入,某些场景下业务层自己生成了重复 ID。直接报错:Duplicate entry for key PRIMARY
  • 分页混乱,是因为原表有主见ID,默认按主键物理顺序返回,相对稳定。但新表:没有主键、没有任何索引。MySQL返回顺序完全不保证。

什么原因?

CREATE TABLE new_table AS SELECT * FROM old_table;
CREATE TABLE new_table SELECT * FROM old_table;

这种方式在 MySQL 中叫 CTAS(CREATE TABLE AS SELECT)。

它的特点是:

  • 复制字段
  • 复制数据
  • 不复制索引
  • 不复制主键
  • 不复制唯一约束
  • 不复制 AUTO_INCREMENT
  • 不复制外键
  • 不复制默认值(部分场景)

image

官方明确写了:CREATE TABLE ... SELECT does not automatically create any indexes

所以看到“索引不见了”,其实是这种语法的正常行为。

怎么解决?

正确的方式应该是先复制表结构,再把数据插进去。

第一步:复制表结构(包含索引)

CREATE TABLE new_table LIKE old_table;

这个语句会:

  • 复制字段
  • 复制主键
  • 复制普通索引
  • 复制唯一索引
  • 复制默认值
  • 复制自增属性
  • 不复制触发器

第二步:复制数据

INSERT INTO new_table SELECT * FROM old_table;

第三步:检查

#检查表的DDL
SHOW CREATE TABLE new_table;
SHOW CREATE TABLE old_table;
#检查数据量
SELECT COUNT(1) FROM  new_table;
SELECT COUNT(1) FROM  old_table;
#两张表的 checksum 相同,通常意味着数据完全一致:每一行数据都一样,每个字段值都一样
CHECKSUM TABLE old_table;
CHECKSUM TABLE new_table;

表复制的技术方案

在日常开发和数据库运维中,经常会遇到以下场景:

  • 创建测试表
  • 数据备份
  • 表结构迁移
  • 历史数据归档
  • 大表重构
  • 分库分表前的数据迁移
  • 灰度发布新表

这时候,“复制表结构和数据”就是一个高频操作。很多人只会一句:

CREATE TABLE new_table AS SELECT * FROM old_table;

但实际上,这只是最基础的一种方式。不同方式在 索引、主键、自增、约束、性能、锁表风险 上差异非常大。

接下来,我将详细讲述不同场景下的表复制与解决方案。

只复制表结构

只复制表结构,怎么做?

首先,回忆一下表结构(DDL)有哪些?

  • 字段
  • 字段类型
  • 默认值
  • NOT NULL
  • 主键
  • 唯一索引
  • 普通索引
  • 注释
  • 自增属性
  • 存储引擎
  • 字符集

强烈推荐的复制表结构方式:CREATE TABLE LIKE

CREATE TABLE new_table LIKE old_table;

相当于把原表的建表语句提取出来,然后再执行CREATE TABLE。最终得到的是一个和原始表一模一样的表。但是数据和配置是全新的。

谨慎使用"CREATE TABLE AS SELECT(CTAS)"的方式,除非你知道自己在做什么。

-- 其中where条件后面的“ 1=0 ”表示不复制数据’
CREATE TABLE new_table AS SELECT * FROM original_table WHERE 1=0;

复制表结构中的某些字段,不需要复制索引、约束等?

CREATE TABLE new_table
     AS SELECT 需要的字段... FROM original_table WHERE 1=0; #where条件恒不成立,不复制数据

注意这种方式:

  • 不复制索引
  • 不复制主键
  • 不复制唯一约束
  • 不复制 AUTO_INCREMENT
  • 不复制外键
  • 不复制默认值(部分场景)

复制表结构中的某些字段,如果这个字段有索引则也需要一并复制

不能直接实现

方案一:手动指定表结构。

方案二:用CREATE TABLE SELECT创建基础表,再Alter表的索引、约束等。

注意:复制时需确保新表与原表的字符集(如utf8、utf8mb4)、排序规则(如utf8_general_ci)一致,否则可能出现中文乱码、排序异常。若需修改,可在创建新表时指定(如`DEFAULT CHARSET=utf8mb4`)。

复制表结构+数据

那我既要复制表结构,又要复制表数据,应该怎么做呢?正确的做法是什么?

先复制结构,再复制数据

第一步:复制结构
CREATE TABLE new_table LIKE old_table;

第二步:插入数据
INSERT INTO new_table SELECT * FROM old_table;

谨慎使用"CREATE TABLE AS SELECT(CTAS)"的方式,除非你知道自己在做什么。

数据量太大时,INSERT INTO ... SELECT ... 在迁移表数据时,可能会产生什么问题?

  • 执行很慢:因为有索引,每插入一行都要维护 B+Tree。
  • 锁表问题
  • 主从延迟:大事务会让 binlog 一次性写入大量数据,从库可能延迟几十分钟。
  • 磁盘 IO 打满:连续全表扫描 + 连续写入(大量顺序读+大量索引写),可能直接把数据库拖慢。

解决方案1:分批次(在Java层面管理id的范围)

第一批:INSERT INTO new_table SELECT * FROM old_table WHERE id > 0 AND id < 100000;
第二批:INSERT INTO new_table SELECT * FROM old_table WHERE id > 100000 AND id < 200000;
......
//Java实现伪代码:
long lastId = 0;
int batchSize = 100000;

while (true) {
    int rows = jdbc.execute("""
        INSERT INTO new_table
        SELECT *
        FROM old_table
        WHERE id > ?
        ORDER BY id
        LIMIT ?
    """, lastId, batchSize);

    if (rows == 0) break;

    lastId += batchSize;
}

解决方案2:先建无索引表,导完数据后再单独建索引

先建无索引表:CREATE TABLE new_table AS SELECT * FROM old_table;

导完数据后再单独建索引:ALTER TABLE ADD INDEX ...

其他注意点:

  • 原表中的NULL值、默认值会被正常复制,但需注意`CREATE TABLE ... AS SELECT`创建的表,默认值不会保留,需手动添加(如`DEFAULT CURRENT_TIMESTAMP`)。
  • 若新表已存在数据,插入数据时需避免主键重复,可通过`INSERT IGNORE`(忽略重复数据)、`REPLACE INTO`(替换重复数据)或`ON DUPLICATE KEY UPDATE`(更新重复数据)处理
  • 示例:
-- 忽略重复主键的数据
INSERT IGNORE INTO 新表名 SELECT * FROM 原表名;

-- 替换重复主键的数据(删除原重复数据,插入新数据)
REPLACE INTO 新表名 SELECT * FROM 原表名;

-- 重复主键时,更新指定字段
INSERT INTO 新表名 SELECT * FROM 原表名 ON DUPLICATE KEY UPDATE order_no = VALUES(order_no);

增量同步

前面我们已经创建了用于回放测试的备份表,但是现在测试给我们说,要把原始日志表的变动实时同步到备份表,更方便他做测试。

所以,我们现在的目标就是:把原始表的变动(DML操作),全部同步到备份表上。

此外,为了通用性的表达,有如下定义:原始表、旧表(old_table),新表(new_table)

Trigger触发器

主要思路:在某一时刻起,先为原始表创建DML操作的触发器,再把原始表的数据批量插入到新表。等待回归测试结束,再删除原表上的触发器。

主要步骤

  1. 创建新表
  2. 为old_table创建触发器(包括:INSERT、UPDATE、DELETE)
  3. 数据迁移(分批次)
  4. 校验数据一致性
  5. 删除触发器、新表

执行步骤:

1) 创建新表

CREATE TABLE new_table LIKE old_table;

2) 为old_table创建触发器(包括:INSERT、UPDATE、DELETE)

DELIMITER $$
CREATE TRIGGER trg_old_table_insert
AFTER INSERT ON old_table
FOR EACH ROW
BEGIN
    INSERT INTO new_table
    VALUES (
        NEW.id,
        NEW.col1,
        NEW.col2,
        NEW.col3
        -- 所有字段
    );
END$$
DELIMITER ;

DELIMITER $$
CREATE TRIGGER trg_old_table_update
AFTER UPDATE ON old_table
FOR EACH ROW
BEGIN
    UPDATE new_table
    SET
        col1 = NEW.col1,
        col2 = NEW.col2,
        col3 = NEW.col3
    WHERE id = NEW.id;
END$$
DELIMITER ;

DELIMITER $$
CREATE TRIGGER trg_old_table_delete
AFTER DELETE ON old_table
FOR EACH ROW
BEGIN
    DELETE FROM new_table
    WHERE id = OLD.id;
END$$
DELIMITER ;

3) 数据迁移(分批次)

一次性迁移:INSERT INTO new_table SELECT * FROM old_table;
全量分批迁移:
INSERT INTO new_table SELECT * FROM old_table
    WHERE id > lastId AND id < maxId;

4) 校验数据一致性

#校验数量是否一致
SELECT COUNT(1) FROM old_table;
SELECT COUNT(1) FROM new_table;

#两张表的 checksum 相同,通常意味着数据完全一致:每一行数据都一样,每个字段值都一样
CHECKSUM TABLE old_table;
CHECKSUM TABLE new_table;

5) 删除触发器、新表

DROP TRIGGER IF EXISTS trg_old_table_insert;
DROP TRIGGER IF EXISTS trg_old_table_update;
DROP TRIGGER IF EXISTS trg_old_table_delete;

增量的DML操作数据如何同步到新表的?全量数据在分批复制,触发器在同步增量,两条线并行运行。触发器保证增量数据同步到新表

在“触发器刚创建”和“复制开始”之间,会不会有空档?

根本不存在这个窗口期,也就是说不会存在有数据丢失的情况。

虽然不会有数据丢失,但可能有“重复同步”。

重复同步会不会有问题?不会

情况1:复制时读到的是新值。这种情况是没问题的

情况2:复制时插入旧数据,触发器再 update 一次,最终结果还是正确的(幂等覆盖)

为什么用 AFTER 触发器?如果使用BEFORE,可能会由于事务回滚导致了脏数据移动到了new_table。AFTER表示old_table数据已经执行插入,则可以进行同步到new_table

技术方案评价

  • 优势
    • 简单易理解
    • 利用数据库原生能力就可以实现,无需引入额外的组件
  • 劣势
    • 影响old_table的DML操作,例如影响写性能
    • 触发器逻辑复杂难维护,如果是单表还好,如果涉及到多表同步更新
    • 失败难排查,如果触发器内执行报错了,数据不可恢复的丢失
  • 总结:适合中小规模在线迁移

Binlog解析

这个方案的主要思路是:伪装成MySQL的一个从库,解析binlog,回放主库上目标表的DML操作,从而实现表数据的增量同步。

开源的解决方案有:Canal、Debezium。这里不再展开。

总结

在复制表结构和数据时,旧表的DML,也需要增量同步到新表上,有两类解决方案:

  • 基于触发器实现
    • 优势:简单易理解,不需要引入额外组件
    • 劣势:侵入数据库,操作复杂,容易出错且不方便debug
    • 适用场景:少量数据迁移,单表数据同步,对性能要求不高
  • 基于binlog实现
    • 优势:无侵入,性能好,可以拓展性(可以整合ES、Kafka等组件)
    • 劣势:引入新组件,实现比较复杂
    • 适用场景:性能要求高,适合数据同步、迁移和数据平台等场景。

总结

本文主要介绍了再复制表结构时的三个重要问题:

1) 复制表结构时尽量首选这种方案:CREATE TABLE new_table LIKE old_table;

2) 复制数据时有2种核心的操作方式:

方式一:INSERT INTO new_table SELECT * FROM old_table;
方式二:INSERT INTO new_table SELECT * FROM old_table WHERE id > lastId AND id < maxId;

3) 增量同步的解决方案有Trigger和Binlog解析

本文结束。