用 sqlite3 把散落的记账 CSV 并成一张表:五个坑我都踩了
三份 CSV,一份是记账 App 导的,一份是自己手抄的,还有一份从旧手机里抠出来的。想查「去年 12 月在吃饭上花了多少」,得挨个打开、筛选、加总。Excel 打开这三份要等十几秒,慢到让人不想查。
后来把它们并进一个 SQLite 库,一条命令出结果。整个过程没装任何服务,就是一个命令行工具。
先确认要做的三件事:把 CSV 变成表、把列类型调对(金额得是数字不是文本)、加索引让查询别全表扫。三步都在命令行里,不用写代码。
第一步:导入
sqlite3 expenses.db .mode csv .import 2023.csv expenses .import 2024.csv expenses .import 2025.csv expenses .tables
.import 的第二个参数是表名,表不存在会按 CSV 首行自动建列。
坑一:路径写错,它不报错,直接给你建个空库
我第一次跑的是 sqlite3 expense.db "select * from expenses;",文件名少了个 s。它没抱怨,只回了:
Error: in prepare, no such table: expenses
看着像个正常的「表不存在」,我去查表名,查了十分钟。真相是:SQLite 打开不存在的文件时不报错,直接创建一个 0 字节的新库。后来一看目录,多出来一个 expense.db,0 字节。
判据很简单:报 no such table 的时候,先确认你连的是哪个文件。
sqlite3 expense.db ".databases"
它会打印主库的真实路径。路径不对,一眼就看出来。
坑二:中文列名在 -column 模式下被吞掉一半
想看得整齐点,加了 -header -column:
sqlite3 -header -column expenses.db "select * from expenses limit 2;"
输出是这样:
月 条 金额 -- -- ---- 1月 午 23.5 1月 打 18
「月份」变成「月」,「午餐」变成「午」。数据没丢,是显示被截断了。sqlite3 算列宽按字节数,一个中文字 3 字节,它以为宽度够 2 个字符位。
.mode box 也一样:表头两个字符,下面一行只剩一个。要正常看中文,用默认的 list 模式,或者直接导成 CSV:
sqlite3 -csv expenses.db "select * from expenses limit 5;"
坑三:表头跑进了列名里
第一版导入后 count(*) 是 4,CSV 明明有 5 行(1 行表头 + 4 行数据)。查 .schema 才明白:表头被当成列名了,这是对的行为。但如果 CSV 没表头、或者表头行里有脏字符,列名会变成 column1、column2,之后想按列名查就得记这些名字。
保险做法:导入前自己建表、明确写类型,再跳过首行。
CREATE TABLE expenses(month TEXT, item TEXT, amount REAL); .import --skip 1 expenses.csv expenses
--skip 1 别省,否则表头会作为一行数据混进去,后面 sum 的时候莫名多一条。
坑四:金额全是文本,sum 出来是 0
.import 自动建表时所有列都是 TEXT。看着没问题(值是 23.5),但 select sum(amount) 返回 0。
SQLite 会把 TEXT 里的「23.5」按数字规则转换,但列声明成 TEXT、里面又混了非数字内容时,聚合会退化成 0。我当时那列里混了一行表头(「金额」两个字),整列就都不认了。
已经导完也能救:
ALTER TABLE expenses ADD COLUMN amount_num REAL; UPDATE expenses SET amount_num = CAST(amount AS REAL);
坑五:database is locked (5)
一边跑查询、一边另一个窗口在写,偶尔报:
Error: stepping, database is locked (5)
SQLite 是单写多读,写的时候拿排他锁。我那次是两个进程同时写(一个在导数据,一个在补一行)。(5) 就是忙,不是数据坏了。三种处理:等几秒重试;把所有写操作收在一个连接里;打开时设 busy timeout。
sqlite3 -cmd ".timeout 5000" expenses.db
最后:加索引
三份 CSV 合起来几万行,按月份查要扫全表。加个索引:
CREATE INDEX idx_month ON expenses(month);
然后用查询计划确认真的走了索引:
EXPLAIN QUERY PLAN SELECT sum(amount_num) FROM expenses WHERE month='12月';
看到 SEARCH expenses USING INDEX idx_month 就对了。看到 SCAN expenses 说明没走,常见原因是类型不匹配——两边都得是文本,或者两边都是数字。
收尾
这套东西我用了几个月,库里二十多万行,查询没超过 0.1 秒。没有服务、没有端口、没有账号,就是一个文件。备份就是复制那个 .db。
要注意的只有一条:它太安静了。路径写错不报错、表头混进数据不报错、类型不对只在你 sum 的时候给个 0。想少踩坑,每步之后用 select count(*) 和 .schema 各看一眼。
- 点赞
- 收藏
- 关注作者
评论(0)