CSV 导入在命令行工具里,不在 SQL 里
SQLite 的 SQL 方言中没有 IMPORT 语句。加载 CSV 是 sqlite3 命令行 shell 的功能:一个名为 .import 的点命令。如果你用过 MySQL 的 LOAD DATA INFILE 或 Postgres 的 COPY,这里需要转变一下思路:那些命令在服务器上运行,而 .import 是客户端工具替你完成的,它读取文件,并在底层执行 INSERT 语句。
所以本页的所有内容都假设你在 sqlite3 shell 中:
sqlite3 mydata.db
如果你需要在应用代码中导入(Python、Node、Go),就用你的语言读取 CSV,再使用参数化的 INSERT 语句。我们会在应用集成一章介绍这种方法。这里我们专注于命令行工具。
基本的 .import
最短的路径:告诉 SQLite 文件是 CSV 格式,然后让 .import 指向文件和表名。
.mode csv
.import people.csv people
根据 people 是否已经存在,会发生两种情况:
- 表不存在:SQLite 会创建它,并把 CSV 的第一行用作列名。每一列都是
TEXT亲和类型。 - 表已存在:SQLite 把文件的每一行都作为数据插入。如果有表头,表头也会变成一行。
第二种情况是大多数人第一次尝试时踩的坑。如果你的 CSV 有表头,而表已经存在,就必须明确地跳过表头。
导入已有的表时跳过表头
用 --skip 1 告诉 .import 忽略开头的 N 行:
CREATE TABLE people (
name TEXT,
age INTEGER,
city TEXT
);
.import --csv --skip 1 people.csv people
--csv 是 .mode csv 的简写,只对这一条命令生效,所以你不必单独设置模式。--skip 1 会去掉表头。剩下的行按列的顺序插入到 people 中。
导入后快速检查一下:
SELECT count(*) FROM people;
SELECT * FROM people LIMIT 5;
文件中的列顺序必须和表中的列顺序一致。它不会根据表头来对应列:.import 只是把第 N 个字段对应到第 N 列。
让 SQLite 替你建表
做探索性工作时,最简单的方法是完全跳过 CREATE TABLE,让 .import 根据表头建表:
.mode csv
.import sales.csv sales
.schema sales
.schema sales 会显示类似这样的内容:
CREATE TABLE sales(
"order_id" TEXT,
"amount" TEXT,
"ordered_at" TEXT
);
注意每一列都是 TEXT。这是有意为之:.import 不会尝试推断类型。如果你希望 amount 是实数、ordered_at 是真正的时间戳,就先自己用正确的类型建表,再用 --skip 1 导入。SQLite 的类型亲和性会在插入时把数字字符串转换成整数和实数。
自定义分隔符:TSV、竖线、分号
.mode csv 使用逗号。对于制表符分隔的文件,切换模式即可:
.mode tabs
.import data.tsv events
对于其他分隔符,先选好模式,再用 .separator:
.mode csv
.separator "|"
.import pipe_data.txt events
有一点需要知道:.mode csv 遵循 RFC 4180 的引号规则,只要字段用 " 正确地括起来,其中包含逗号或换行也没问题。.mode tabs 是更简单的按字符拆分模式,不处理引号。如果你的文件中有带引号且包含分隔符的字段,就保持 .mode csv,只修改分隔符。
一个真实的完整流程
假设 orders.csv 是这样的:
order_id,customer,amount,ordered_at
1001,Ada,49.99,2026-01-12
1002,Boris,12.50,2026-01-13
1003,"Chen, Wei",199.00,2026-01-14
注意第 3 行的一个带引号字段里有逗号。下面是完整的会话:
在真实的 shell 中,这段 INSERT 会换成一条 .import --csv --skip 1 orders.csv orders。"Chen, Wei" 这个字段保持完整,因为 CSV 模式会识别引号。得益于列类型,amount 最终是实数,order_id 是整数。
把导入包在事务中
.import 每一行执行一条 INSERT。几千行没问题。如果有一百万行,它会慢得让人痛苦,除非你把整个导入包在一个事务中,这样 SQLite 就不会每插入一行就提交一次:
BEGIN;
.import --csv --skip 1 big_file.csv events
COMMIT;
只改这一处,就能把几分钟的导入缩短到几秒钟。如果导入中途出了错,ROLLBACK 会撤销已加载的部分,重试时也很方便。
你还可以在导入前删除索引、导入后重建索引,进一步加快速度:每一行都要维护索引,累积起来开销不小。
常见错误及解决方法
Error: expected N columns but found M:某一行的字段数与表不一致。通常是因为:
- 没有引号的字段中混进了逗号。用正确的 CSV 引号规则重新导出,或者改用
.mode csv(RFC 4180)而不是.mode tabs。 - 文件末尾有一个空行。编辑文件,或者灵活地使用
--skip。 - 表的列比 CSV 多。要么在文件中补上缺少的列,要么先插入到结构合适的临时表中,再复制到真正的表里。
表头被当成了数据:你在已有的表上忘了加 --skip 1。删除那一行(DELETE FROM t WHERE rowid = 1),然后加上参数重新运行。
数字被存成了字符串:你让 .import 自动建表,所以每一列都是 TEXT。删除这张表,用 INTEGER/REAL 列明确定义它,再重新导入。
Error: no such file:路径是相对于你启动 sqlite3 时所在的目录,而不是相对于数据库文件。使用绝对路径,或者在打开 shell 之前 cd 到正确的目录。
命令行工具会在报错时打印行号,这是在大文件中找到问题行最快的方法。
快速回顾
.import是命令行工具的点命令,不是 SQL。在sqlite3shell 中运行它。- 用
--csv正确处理引号,用--skip 1忽略表头。 - 如果表不存在,
.import会根据表头创建它,但每一列都会是TEXT。想要正确的类型,就自己建表。 - 大批量导入时用
BEGIN/COMMIT包起来,避免每一行都是一个事务。 - 文件中的列顺序必须和表中的列顺序一致。
下一篇:导出数据
导入只是故事的一半。同一个 shell 也能把查询结果或整张表导出为 CSV、JSON 或 SQL,适用于备份、数据管道,以及把数据交给其他工具。这就是接下来的导出数据。
常见问题
如何把 CSV 文件导入 SQLite?
用 sqlite3 命令行工具打开数据库,用 .mode csv 切换到 CSV 模式,然后运行 .import data.csv table_name。如果表还不存在,SQLite 会创建它,并把文件的第一行用作列名。如果表已经存在,文件的每一行都会作为数据插入,所以通常要用 .import --skip 1 跳过表头。
如何把带表头的 CSV 导入 SQLite 中已有的表?
使用 .import --csv --skip 1 data.csv table_name。--skip 1 参数告诉 SQLite 忽略第一行,这样表头就不会变成一行数据。不加它的话,你会得到一行内容正是列名本身的数据。
为什么 SQLite 导入 CSV 时报错“expected N columns but found M”?
文件中有些行的列数与表不一致,通常是因为字段里有逗号、引号没有转义,或者末尾有空行。使用 .mode csv(或 --csv)而不是 .mode tabs,让 SQLite 按 RFC 4180 处理引号,再用文本编辑器检查文件中多余的分隔符。命令行工具会打印出错的行号,这是找到问题行最快的方法。