上周一个做后台系统的朋友甩给我一个 40MB 的.db文件说要把里面的订单表和订单明细表搬到另一个已经跑了两年的库里索引、触发器、自增序列一个都不能少。这种需求听起来像是sqlite3里一行ATTACH就能搞定的事但真动手你会发现坑全在细节里结构没带过去、主键撞了、sqlite_sequence忘了同步、大表一次性插入把内存撑爆、目标库被锁死十分钟。我这些年搬过的 SQLite 表少说也有几十次从几百行的配置表到几千万行的日志表都碰过所以这篇就把将数据库的表导入到另一个数据库这件事拆开揉碎讲清楚。这篇内容适合三类人手上只有 SQLite、不想引入额外中间件的后端开发者做客户端或桌面应用、要在升级时合并本地库的工程师以及做数据整理、需要把多个.db文件攒成一个的分析人员。哪怕你只是刚学会sqlite3命令行的新手照着文里的命令序列走一遍也能把一张表连同它的索引和约束完整搬过去并且知道为什么每一步要这么写。1. 先想清楚一次搬表到底要搬哪几样东西很多人对导表的直觉是复制数据行这是最容易出问题的地方。SQLite 里一张表在物理层面分布在sqlite_master的多条记录里typetable的那条是建表语句typeindex的若干条是索引typetrigger的若干条是触发器再加上一张特殊的sqlite_sequence表保存着AUTOINCREMENT的当前值。你只搬数据搬过去的就是一个没有约束、没有索引、自增从头开始的裸表后续所有依赖这些约束的业务逻辑都会静默失效。1.1 表结构、数据、附属对象三件套我把一次完整的迁移拆成三件套按顺序处理几乎不会出错。第一件是表结构也就是CREATE TABLE语句本身。它定义了列名、类型亲和性、NOT NULL、DEFAULT、CHECK、PRIMARY KEY、FOREIGN KEY。注意 SQLite 的类型是亲和性而不是强类型约束INTEGER列里塞123是允许的这意味着一路搬过去可能悄悄把文本存进了整型列后面查出来排序不对你在业务层排查半天。第二件是数据行。这里有三个高频陷阱主键冲突目标库已有相同 id、NULL与空字符串的区分和NULL在业务上完全是两回事、BLOB列的字节完整性用文本方式中转会被破坏。第三件是附属对象包括索引、触发器、视图以及AUTOINCREMENT对应的序列值。索引忘了建搬完之后查询从 3ms 变成 800ms触发器忘了建后续写入不再产生审计日志序列值不同步下一条插入直接撞主键。注意目标库如果已经有同名表先确认你是要追加数据还是替换整表。这两种语义对应完全不同的命令混用会丢数据。1.2 三条可行路径的取舍实际可操作的路线有三条我按适用场景排一下。第一条是ATTACH DATABASE直连。把目标库挂到当前连接上当作第二个 schema然后用INSERT INTO dst.tbl SELECT * FROM main.tbl一条 SQL 完成搬运。优点是快、走的是同一套存储引擎、不需要落中间文件缺点是必须在同一次连接里操作且结构部分要另外处理。第二条是sqlite3命令行的.dump与重定向回放。适合整个库搬走或者离线交接给同事的场景产物是可读的 SQL 文本你可以用编辑器改表名、改字段。缺点是文本体积通常是原库的两三倍超大表回放会很慢。第三条是脚本搬运用 Python 的sqlite3模块或者任何支持 SQLite 驱动的语言逐行或分批读写。适合需要边搬边转换改字段、脱敏、补默认值的场景也最适合做分批控制。我的默认选择是结构用sqlite_master里取 DDL 手工建数据用ATTACH直连插入附属对象最后单独补。这条路线既没有中间文件的磁盘开销又能精确控制每一步。下面几节就按这条主线展开另外两条作为对照方案给出。2. 动手之前的环境准备与前置检查我在任何一次搬表前都会花三分钟做检查这三分钟能省掉后面两小时的排查。检查项不多但每一条都对应过实际事故。2.1 版本、编译选项与必查 PRAGMA先看版本sqlite3 --version或者连接里执行SELECT sqlite_version();。版本差异会直接影响可用语法VACUUM INTO需要 3.27.0 以上.import --csv --skip 1需要 3.32 以上RETURNING需要 3.35 以上STRICT表需要 3.37 以上。老版本上跑新语法报的是语法错误容易误判成拼写问题。再看几个 PRAGMA它们决定了行为边界PRAGMA foreign_keys; -- 外键约束开没开默认是 0 PRAGMA journal_mode; -- 常见是 delete 或 wal PRAGMA page_size; -- 默认 4096 PRAGMA encoding; -- UTF-8 / UTF-16 PRAGMA auto_vacuum;foreign_keys默认是关闭的这一点很多人不知道。也就是说你插入一条父表里不存在的子记录SQLite 根本不拦你。迁移时我的做法是搬运阶段显式关闭外键全部搬完并校验通过后再打开跑一次PRAGMA foreign_key_check;。这样既不会被逐行外键检查拖慢速度又能在最后统一兜底。encoding这一项要特别留意。如果源库是 UTF-16 而目标库是 UTF-8文本内容本身不会有问题SQLite 会自动转换但如果你中间用文本文件中转编码搞错就会变成一堆问号。2.2 备份先复制再动手这条听起来像废话但确实是唯一能兜住所有错误的操作。SQLite 是单文件数据库备份简单到不可思议cp /path/to/target.db /path/to/target.db.bak.$(date %Y%m%d%H%M)有个细节如果目标库正开着 WAL 模式且有未 checkpoint 的内容直接cp单个文件会丢数据。必须先执行PRAGMA wal_checkpoint(TRUNCATE);再复制或者用 SQLite 自带的备份能力sqlite3 /path/to/target.db .backup /path/to/target.db.bak.backup命令走的是在线备份接口即使有别的连接在写也能拿到一致快照。3.27 以上还可以用VACUUM INTO /path/to/copy.db产物是紧凑的、没有空闲页的干净副本缺点是需要额外的磁盘空间且耗时与库大小成正比。提示备份文件不要放在同一个目录下尤其是做容器化部署的时候一个卷挂掉两个文件一起没。3. 方案一ATTACH INSERT INTO SELECT 完整实操这条路线是我最常用的核心就两步先建结构再灌数据。下面给出可直接抄的命令序列。3.1 从 sqlite_master 取结构并落到目标库先用命令行连上源库把目标表的 DDL 打出来sqlite3 source.db SELECT sql || ; FROM sqlite_master WHERE typetable AND nameorders;输出大致是这样一行CREATE TABLE orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, amount INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT pending, created_at TEXT NOT NULL, payload BLOB );拿到这行之后不要直接往目标库塞先确认目标库里没有同名表sqlite3 target.db SELECT name FROM sqlite_master WHERE typetable AND nameorders;有输出就说明冲突了需要决定是重命名源表比如orders_import_2024还是先清空目标表。我倾向于重命名后导入再在业务层做合并因为直接在目标表上做 upsert一旦中途失败很难回滚到干净状态。确认没冲突后把 DDL 在目标库执行。有一个小技巧可以避免手工复制粘贴出错——用管道把 DDL 直接喂给目标库sqlite3 source.db SELECT sql || ; FROM sqlite_master WHERE typetable AND nameorders; \ | sqlite3 target.db如果目标表名要和源表不同把输出里的表名改掉再执行比如用sed做一次替换sqlite3 source.db SELECT sql || ; FROM sqlite_master WHERE typetable AND nameorders; \ | sed s/CREATE TABLE orders/CREATE TABLE orders_import/ \ | sqlite3 target.db注意sed这种文本替换是有风险的如果 DDL 里恰好有别的orders字样比如某个CHECK约束里写了字符串会被误伤。稳妥的做法是先输出到文件人工核对一遍再执行。表结构这一步值得多花两分钟因为它决定了后面所有数据的落点。3.2 用 ATTACH 做数据搬运的标准写法结构建好之后开一个连接把目标库挂上来sqlite3 source.db进入交互式 shell 后依次执行ATTACH DATABASE /path/to/target.db AS dst; BEGIN IMMEDIATE; INSERT INTO dst.orders (id, user_id, amount, status, created_at, payload) SELECT id, user_id, amount, status, created_at, payload FROM main.orders; COMMIT; DETACH DATABASE dst;几个地方值得展开说。显式列名而不是SELECT *。INSERT INTO ... SELECT *依赖两边的列顺序完全一致而列顺序在CREATE TABLE里是可以随便排的。你今天从源库复制的 DDL 顺序一致明天别人改了一下列顺序这条语句就会在毫无提示的情况下把status灌进amount里。显式写列名是多打二十个字但能救你一次数据事故。BEGIN IMMEDIATE而不是裸BEGIN。SQLite 的BEGIN是延迟加锁的先读后写如果两个连接同时这么干很容易在升级写锁的时候撞上SQLITE_BUSY。IMMEDIATE一开始就拿写锁冲突会立刻暴露而不是等到中途。事务必须包住整批插入。这是性能差异最大的一步。不开事务时每条INSERT都是一次独立事务意味着一次 journal 写入加一次 fsync。实测在普通 SSD 上10 万行数据裸插大概要 90 秒包在一个事务里只要 1.5 秒左右差了两个数量级。原因很直白fsync 是物理磁盘操作一次几十毫秒十万次就是几十分钟。跨库操作是原子的吗在同一个连接里ATTACH后的多个库共享同一个事务上下文ROLLBACK会一起回滚。但要注意 journal 模式如果两个库的journal_mode不同一个 WAL 一个 DELETE事务的原子性行为会有细微差别。我一般会先统一成同一模式再操作。注意ATTACH有数量上限默认编译值是 10 个SQLite 3.32 之后放宽到 125 个。日常搬表一个dst足够不用纠结。3.3 大表分批与内存边界一张表几百万行的时候单条INSERT INTO ... SELECT会把整个结果集在内存里物化吗实测不会一次性把全部数据读进内存SQLite 是流式处理的但它会在一个事务里累积大量脏页如果cache_size不够脏页会频繁溢出到磁盘速度骤降同时 WAL 文件会膨胀。我的经验阈值是单表超过 200MB 或 300 万行就改成分批。分批的写法是靠主键切片BEGIN IMMEDIATE; INSERT INTO dst.orders (id, user_id, amount, status, created_at, payload) SELECT id, user_id, amount, status, created_at, payload FROM main.orders WHERE id 0 AND id 500000; COMMIT;然后循环推进下界。每批 20 万到 50 万行比较合适具体看单行宽度。单行 200 字节的话50 万行约 100MB脏页压力可控。每批之间提交一次让 WAL 有机会被 checkpoint。如果表没有整型主键比如用 UUID 文本做主键切片就麻烦一些可以退化成用LIMIT/OFFSET分页INSERT INTO dst.orders (id, user_id, amount, status, created_at, payload) SELECT id, user_id, amount, status, created_at, payload FROM main.orders ORDER BY id LIMIT 500000 OFFSET 0;但OFFSET是线性扫描越往后越慢第 20 页的时候性能会明显退化。所以我更推荐给源表临时加一个自增辅助列来做切片或者在脚本侧用游标推进。分批期间有几个 PRAGMA 可以临时调整PRAGMA cache_size -65536; -- 64MB 页缓存负数单位是 KB PRAGMA temp_store MEMORY; -- 临时表放内存 PRAGMA synchronous NORMAL; -- WAL 模式下足够安全比 FULL 快很多synchronous OFF我不建议在正式迁移里用它确实能再快一点但一旦迁移过程中断电或者进程被杀目标库可能直接损坏。为了省那 20% 的时间赌一把数据完整性不划算。4. 方案二dump 与 .import 的取舍命令行工具自带的能力经常被低估。如果你的场景是把一个表交出去或者从别人那里收一个表文本格式反而是最省心的中介。4.1 只导出指定表.dump支持指定表名sqlite3 source.db .dump orders orders.sql产物大致长这样PRAGMA foreign_keysOFF; BEGIN TRANSACTION; CREATE TABLE orders (...); INSERT INTO orders VALUES(1,1001,29900,paid,2024-03-01T10:22:31,NULL); INSERT INTO orders VALUES(2,1002,15800,pending,2024-03-01T10:23:04,X89504E47...); COMMIT;注意BLOB会被导出成X...的十六进制字面量这是好事二进制内容不会在文本中转里损坏。而NULL保持NULL空字符串导出成两者的区别保住了。回放到目标库sqlite3 target.db orders.sql这条命令会新建表并插入全部数据。如果目标库里已经有同名表回放会报table orders already exists并中止。想跳过建表只插数据就得手工编辑这个 SQL 文件把CREATE TABLE那行删掉。有一个更精细的导出方式只导数据不导 DDLsqlite3 source.db .mode insert orders SELECT * FROM orders; orders_data.sql.mode insert 表名让每条输出都是INSERT INTO orders VALUES(...)的形式。这样你就把结构和数据彻底分开了结构走第 3 节的 DDL 路线数据走这里控制粒度更细。4.2 .import 的三个必踩坑.import是导入 CSV 或分隔文本的工具用起来像这样sqlite3 target.db.mode csv .import --skip 1 /path/to/orders.csv orders这个命令的坑比ATTACH多得多我列三个最常见的。第一目标表必须事先存在。.import不会自动按 CSV 建表如果表不存在它会把所有列当成 TEXT 建一张表你后面查数值排序就会得到1, 10, 100, 2这种结果。所以永远先建表再.import。第二分隔符和引号处理。.mode csv用的是标准 CSV 规则逗号分隔、双引号包裹、双写引号转义。如果你的源文件是制表符分隔要改成.mode tabs如果是自定义分隔符用.separator |。一旦分隔符搞错所有列会挤成一列或者错位而 SQLite 不会报错它只会忠实地把错位的数据写进去。第三--skip 1只跳过一行。如果 CSV 头上面还有两三行说明文字--skip 3才行。而且.import对空行的处理是当成一行全 NULL 插入如果有尾部空行你会多出几条垃圾记录。如果数据源本身就在另一个 SQLite 库里我强烈建议用ATTACH而不是绕一圈 CSV。CSV 中转会丢失类型信息所有值都变成文本靠类型亲和性再转回来浮点数会有精度损失0.1 0.2的经典问题在文本往返后可能变成0.30000000000000004BLOB更是直接没法走 CSV。5. 方案三脚本搬运与批量优化需要边搬边转换的时候脚本是唯一的选择。比如目标是只搬statuspaid的订单、把金额从分转成元、给敏感字段做哈希、顺便把created_at从本地时间转成 UTC 时间戳。这种需求ATTACH一条 SQL 也能写但可读性和可调试性远不如脚本。5.1 Python 脚本骨架下面这个骨架我用了很多次直接改改就能用import sqlite3 import time SRC /path/to/source.db DST /path/to/target.db TABLE orders COLS [id, user_id, amount, status, created_at, payload] BATCH 50000 src sqlite3.connect(SRC) src.row_factory sqlite3.Row dst sqlite3.connect(DST) dst.execute(PRAGMA journal_modeWAL) dst.execute(PRAGMA synchronousNORMAL) dst.execute(PRAGMA foreign_keysOFF) placeholders ,.join(? * len(COLS)) sql fINSERT INTO {TABLE} ({,.join(COLS)}) VALUES ({placeholders}) t0 time.time() total 0 cur src.execute(fSELECT {,.join(COLS)} FROM {TABLE} ORDER BY id) while True: rows cur.fetchmany(BATCH) if not rows: break with dst: # 自动 begin / commit dst.executemany(sql, [tuple(r) for r in rows]) total len(rows) print(f{total} rows, {total / (time.time() - t0):.0f} rows/s) print(done, total, f{time.time() - t0:.1f}s)with dst:这个写法值得说一句sqlite3 模块的连接对象作为上下文管理器时只负责事务提交或回滚不关闭连接。所以你能在一个循环里反复进入退出每批一个事务既控制了事务大小又保证了原子性。如果没有with就得手动dst.commit()忘记提交的话程序正常退出数据一行都不落还不会报错这是新手最容易踩的坑。5.2 参数与实测数据对照我把这套骨架在不同批量下的实测结果整理了一下环境是普通 SSD、单核、库在本机磁盘上批量大小10 万行耗时100 万行耗时内存峰值备注1逐行约 62 秒约 640 秒很低不可接受1000约 1.9 秒约 21 秒约 5MB稳妥50000约 1.4 秒约 15 秒约 60MB日常首选500000约 1.3 秒约 14 秒约 400MB需留意内存一次性 fetchall约 1.3 秒直接 OOM数 GB不要这么干从这张表能看出两件事批量的收益在 1 万到 5 万之间就基本吃满了再往上加只是增加内存压力而逐行插入和批量插入之间差了两个数量级这个差距在百万行级别就是十分钟和十五秒的区别。内存估算方法很简单批量 × 单行字节数 × 2。乘以 2 是因为 Python 对象本身的开销远大于原始字节一行六个字段大概占 300 到 500 字节的 Python 对象内存。50 万行的话就是 200MB 左右接近很多小内存机器的警戒线。如果目标表有AUTOINCREMENT而你显式插入了 id记得搬完之后同步序列值这个下一节细说。6. 附属对象的补齐与序列同步数据搬完只是完成了一半剩下的附属对象如果不处理问题会在几天之后才暴露出来而且很难定位。6.1 sqlite_sequence 与自增序列用AUTOINCREMENT的表SQLite 会维护一张叫sqlite_sequence的内部表记录每张表用过的最大 rowid。你用INSERT显式带 id 插入了 100 万行sqlite_sequence在目标库里是会自动更新的——只要 id 是显式写入的SQLite 会把序列值推到插入的最大值。但有两种情况会出问题。一是你用ALTER TABLE ... RENAME或者CREATE TABLE AS SELECT建的目标表序列记录可能不存在或不对应。二是你搬的是不带AUTOINCREMENT只有INTEGER PRIMARY KEY的表那走的是 rowid 复用逻辑删掉最大 id 行之后新插入会复用这个 id跟源库的行为不一样源库如果也是纯INTEGER PRIMARY KEY行为一致但如果源库是AUTOINCREMENT行为就不一致了。检查方法SELECT * FROM dst.sqlite_sequence WHERE nameorders;如果为空或者值偏小手动修正UPDATE dst.sqlite_sequence SET seq (SELECT MAX(id) FROM dst.orders) WHERE name orders; INSERT INTO dst.sqlite_sequence (name, seq) SELECT orders, MAX(id) FROM dst.orders WHERE NOT EXISTS (SELECT 1 FROM dst.sqlite_sequence WHERE nameorders);注意sqlite_sequence表在库里还没有任何AUTOINCREMENT表时是不存在的直接UPDATE会报no such table。别慌这种情况说明本来就没有序列需要维护。6.2 索引、触发器的迁移顺序索引必须在数据搬完之后建而不是之前。原因很实际边插数据边维护 B 树索引每次插入都要调整索引结构100 万行的插入时间会从 15 秒涨到 60 秒以上。而且大批量插入时索引页的填充因子会很差建出来的索引体积臃肿。正确的顺序是建表 → 关外键 → 灌数据 → 建索引 → 建触发器 → 建视图 → 开外键 → 校验。索引的 DDL 同样从sqlite_master取sqlite3 source.db SELECT sql || ; FROM sqlite_master WHERE typeindex AND tbl_nameorders AND sql IS NOT NULL;注意sql IS NOT NULL这个条件很重要。SQLite 会为UNIQUE和PRIMARY KEY约束自动创建索引这些索引在sqlite_master里的sql字段是NULL因为它们是约束的一部分不是独立定义的。取出来之后如果发现sql是NULL的行说明那是自动索引不用管——你建表时的约束已经包含了。触发器更要注意顺序。如果触发器里引用了别的表那张表必须已经存在。另外触发器在你灌数据的时候如果是存在的会被逐行触发可能产生大量审计日志或者触发级联更新。所以一定要在灌数据之后再建触发器这一点在跨库迁移里尤其容易被忽略。视图则比较宽松CREATE VIEW只做语法解析不要求依赖的表有数据但要求表存在。放在最后建最安全。7. 常见问题与排查技巧实录这一节是我这些年踩坑攒下来的速查表报错信息按原文列出方便你直接搜索匹配。7.1 报错速查表报错信息典型原因处理办法no such table: main.orders表名拼错或连错了库文件SELECT name FROM sqlite_master WHERE typetable;确认no such table: dst.ordersATTACH后用了错误的 schema 前缀确认ATTACH ... AS dst的别名拼写一致database is locked有别的连接持有写锁或自己忘了COMMIT关掉其他连接检查是否遗留未提交事务UNIQUE constraint failed: orders.id目标表已有相同主键改用INSERT OR IGNORE或先清空目标表NOT NULL constraint failed: orders.status源数据有NULL目标列不允许加DEFAULT或在 SELECT 里用COALESCE兜底FOREIGN KEY constraint failed父表记录还没搬过去调整搬运顺序或临时关外键最后统一校验datatype mismatch往INTEGER PRIMARY KEY列插了文本检查源数据里是否有非数字主键disk I/O error磁盘满、权限不对、文件被占用看df -h看目录权限database disk image is malformed源库本身损坏或复制时没 checkpoint先对源库跑PRAGMA integrity_check;database is locked这个我想单独说两句。SQLite 的锁是文件级的一个写事务会锁住整个库。如果你在 GUI 工具里开着目标库很多客户端会持有一个读事务不放脚本这边就会一直等默认busy_timeout是 0直接报错。解决办法是给连接设一个超时conn sqlite3.connect(DST, timeout30) # 等 30 秒或者在 SQL 层设置PRAGMA busy_timeout 30000;这个参数单位是毫秒设 30000 就是 30 秒。我一般所有脚本都带上这个设置能省掉一大半莫名其妙的失败。7.2 数据校验的三层检查搬完之后怎么确认真的搬对了我按三层来做从粗到细。第一层行数对账。SELECT (SELECT COUNT(*) FROM main.orders) AS src_cnt, (SELECT COUNT(*) FROM dst.orders) AS dst_cnt;数量不等就直接停别往下走。不等的原因通常是INSERT OR IGNORE吞掉了主键冲突的行或者WHERE条件写窄了。第二层关键字段聚合对账。SELECT (SELECT SUM(amount) FROM main.orders) AS src_sum, (SELECT SUM(amount) FROM dst.orders) AS dst_sum, (SELECT MAX(id) FROM main.orders) AS src_max, (SELECT MAX(id) FROM dst.orders) AS dst_max;金额求和对比能发现大部分部分行丢失和数值被截断的问题。MAX(id)对比能发现尾部数据没搬完。如果表里有时间字段再比一下MIN和MAX时间戳。第三层抽样逐字段比对。随机抽一百行把整行内容拼成字符串做比较SELECT COUNT(*) FROM main.orders a JOIN dst.orders b ON a.id b.id WHERE a.user_id IS NOT b.user_id OR a.amount IS NOT b.amount OR a.status IS NOT b.status OR a.created_at IS NOT b.created_at;结果应该是 0。这里用IS NOT而不是因为遇到NULL会返回NULL即假NULL与NULL的差异就查不出来。这个细节能抓到空字符串变成 NULL这类隐蔽问题。最后再跑一次完整性检查PRAGMA dst.integrity_check; PRAGMA dst.foreign_key_check;两条都返回空或ok才算收工。foreign_key_check特别值得跑因为它能找出你在关外键期间插入的孤儿记录。8. 几个容易被忽略的实操细节写得差不多了最后补几个平时不太有人提、但确实会咬人的点。页大小的差异。源库page_size是 1024目标库是 4096插入速度会受到影响但数据本身没问题。新的空库如果想调页大小必须在写入任何数据之前执行PRAGMA page_size 8192;然后VACUUM;一旦有数据了再改就无效。实践上 4096 默认值对绝大多数场景够用不用折腾。日期和时间的存储。SQLite 没有原生日期类型全靠约定。常见的三种存法ISO8601 文本2024-03-01T10:22:31Z、Unix 时间戳整数、Julian Day 浮点。迁移时如果源库用文本、目标表用整数直接插过去会得到一堆字符串存进整数列查询时WHERE created_at 1700000000会匹配不上。这类转换必须在 SELECT 里显式做用strftime(%s, created_at)之类的函数转换别指望类型亲和性帮你。WAL 文件的遗留。迁移完成后如果目标库启用了 WAL.db-wal和.db-shm两个文件会一直存在。做归档或者拷贝的时候必须把三个文件一起处理只拷.db会丢最近的事务。要生成一个单文件干净库用VACUUM INTOVACUUM INTO /path/to/clean_copy.db;这个命令会把当前库完整写到一个新文件里自动合并 WAL、回收空闲页产物可以直接分发。迁移后的统计信息。SQLite 的查询优化器依赖sqlite_stat1表里的统计信息。如果你把表搬到新库ANALYZE生成的统计不会自动跟过来。搬家之后在目标库跑一次ANALYZE dst.orders;没有统计信息的时候优化器会按默认的启发式规则选索引遇到多表 JOIN 可能选错执行计划查询慢十倍都有可能。大表迁移别在业务高峰期做。SQLite 写事务会锁整个库哪怕只搬一张表目标库在迁移期间对其他连接来说是不可写的。分批提交能把锁窗口切小但仍然建议放在低峰期或者干脆对目标库做一份副本、在副本上搬完、校验无误后再切过去。我自己的习惯是任何超过 10 万行的迁移都先在副本上完整跑一遍并记录耗时和校验结果再用同样的脚本对正式库执行。这个演练步骤多花十分钟但能提前暴露掉 90% 的问题包括磁盘空间不够、权限不对、字段类型不匹配这类只要跑一次就会现原形的东西。备份永远在演练永远先做分批永远别省这三条是我搬了这么多次表之后觉得最值钱的经验。
企业数字化 ERP 产品动态
相关推荐
Univer 深度解析:Canvas 渲染与插件架构如何构建在线表格底座 1. 从“univer”这个名字说起:它到底想解决什么问题第一次听到“univer”这个词,很多人会以为是某个大学(university)的缩写,或者某个开源社区的新玩具。但如果你最近在关注前端表格、在线文档、协同编辑这一块&#x… · 2026/11/3 4:36:50
基于MCP与Docker的LLM Agent长期记忆系统:hindsight架构设计与实现 1. 从“hindsight”说起:为什么我们需要给Agent装上记忆“hindsight”这个词本身很有意思,字面意思是“事后的洞察力”,也就是我们常说的“后见之明”。放在LLM Agent的语境里,它指向一个非常具体且棘手的问题:Agent如… · 2026/11/3 4:36:50
九江公积金系统升级:金仓数据库高并发实践 1. 项目背景与核心价值九江公积金系统升级项目是一个典型的民生领域数字化改造案例。这个覆盖33万在职职工的公积金管理系统,其稳定性和响应速度直接关系到职工住房贷款、提取等核心业务的办理效率。在系统日均交易量突破5万笔的业务压力下,原有系统已出… · 2026/11/3 4:36:50
Fast-LIO实时SLAM原理与GPS融合坐标系对齐避坑指南 简介:本资源是一份面向自动驾驶与机器人领域研究人员及工程师的Fast-LIO算法深度解析与工程改进文档,聚焦多传感器融合建图与定位中的关键瓶颈——点云特征缺失导致的建图失败问题。作者在原始Fast-LIO(融合IMU与激光雷达)基础上&… · 2026/11/3 5:06:54
Fast-LIO+GPS紧耦合融合:状态扩展与鲁棒观测建模实战 简介:本资源是一份面向机器人与自动驾驶领域研究人员及工程师的Fast-LIO算法深度解析与工程改进文档,聚焦多传感器融合建图与定位中的关键瓶颈——点云特征缺失导致的建图失败问题。作者在原始Fast-LIO(IMU激光雷达紧耦合)基础上&… · 2026/11/3 5:06:54
深入理解switch-case:语法、优化与多语言实践 1. 理解switch-case的基本概念switch-case是一种多分支选择结构,它允许程序根据表达式的值从多个代码块中选择一个执行。这种结构在几乎所有主流编程语言中都存在,包括C、C、Java、JavaScript等。switch语句的基本工作原理是:首先计算一个表达… · 2026/11/3 5:06:54
SE注意力机制原理与PyTorch实现详解 1. 通道注意力机制概述通道注意力(Channel Attention)是计算机视觉领域中一种重要的注意力机制,它通过学习不同特征通道的重要性权重,来增强有用特征并抑制无用特征。SE(Squeeze-and-Excitation)注意力是最… · 2026/11/3 5:06:54
2025届AI科研平台实测:五大工具深度对比与选型指南 1. 2025届AI科研平台实测背景与选型标准在2023-2024年全球AI技术爆发式增长的背景下,科研领域的工具生态发生了革命性变化。根据斯坦福大学《AI Index 2024》报告显示,学术工作者使用AI工具的比例从2021年的17%激增至2024年的89%,其中专门针对… · 2026/11/3 5:06:54
基于STM32与AD5542的微伏级脑电信号模拟发生系统设计 简介:面向脑机接口测试场景的高精度脑电信号模拟发生系统设计资料,适合医疗设备制造商、科研工作者及脑机接口开发团队参考。针对传统信号发生器系统复杂、体积庞大、精度不足且难以生成真实脑电模型的问题,资料围绕数模转换、同相比例放大与… · 2026/11/3 5:05:54
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18