SQLite 大概是世界上部署量最大的数据库——你的手机、浏览器、车机里都跑着它。但它不是"小号的 MySQL",而是另一类东西:一个链接进你程序里的库。想清楚这一点,后面很多取舍就顺了。本文所有 SQL 均在 SQLite 3.46.1 实测通过。

一、先决定:该不该用 SQLite
选型选错了,后面全是坑。这一节比任何语法都重要。
适合的场景:
- 移动 App、桌面客户端的本地存储(数据量几百 MB 以内)
- 嵌入式设备、IoT 网关
- 单机分析脚本、日志归档
- 测试环境替身——比启动一个 MySQL 容器快得多
- 中小型网站、日活几千以内、读写比高
别用的场景:
- 多台服务器要共享同一份数据。SQLite 是单文件库,走不了网络。硬要用就得挂 NFS,然后你就等着数据库损坏吧。
- 高并发写入。SQLite 是库级写锁,同一时刻只能有一个写事务。写入 QPS 上不去。
- 需要严格的用户权限体系。SQLite 没有用户概念,文件权限就是它全部的安全模型。
- 数据量上了几十 GB 且要复杂查询。它能撑,但会难受。
一句话判断:如果你的程序和数据在同一台机器的同一个进程里,SQLite 大概率是对的。
二、安装与版本确认
2.1 安装
Linux(Debian/Ubuntu):
sudo apt install sqlite3macOS 自带 sqlite3,想用新版走 Homebrew:
brew install sqliteWindows 到官网下载页取 sqlite-tools-win-x64-*.zip,解压后把目录加进 PATH。
2.2 版本很重要
SQLite 的功能是按版本递增的,UPSERT(3.24+)、RETURNING(3.35+)、unixepoch()(3.38+)都不是老版本有的。动手前先看一眼:
sqlite3 --version3.46.1 2024-08-13 09:16:08 c9c2ab54ba1f5f46360f1b4f35d849cd3f080e6fc2b6c60e91b16c63f69aalt1 (64-bit)本文以下内容都以 3.46 为准。如果你在写要分发给别人的脚本,先确认目标的 SQLite 版本,否则会遇到莫名其妙的"语法错误"。
三、命令行工具:先分清"点命令"和 SQL
这是新手最容易栽的地方,也是很多网上教程自己都没搞清的地方。
sqlite3 命令行里有两类输入:
- SQL 语句——以分号
;结尾,是数据库能听懂的 - 点命令(dot commands)——以
.开头,是 CLI 工具自己的指令(.tables、.mode、.import……),数据库根本不知道它们存在
点命令不能写进 SQL 文件,也不能通过程序里的 execute() 调用。它只在交互式 CLI 或 .read 进来的时候才有意义。
# 正确:点命令直接敲进 CLI
sqlite3 mydatabase.db进入后提示符变成 sqlite>,此时可以混着敲:
sqlite> CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT);
sqlite> .tables
t
sqlite> .quit上面这一整段(建表 + .tables + .quit)不能复制进一个 xxx.sql 文件然后 sqlite3 db < xxx.sql——.tables 和 .quit 会报错。
3.1 常用点命令
| 命令 | 作用 |
|---|---|
.open file.db | 打开(不存在则创建)数据库 |
.databases | 列出当前连接的所有数据库(含 ATTACH 的) |
.tables | 列出所有表 |
.schema users | 显示某张表的建表语句 |
.indexes users | 列出某张表的索引 |
.mode column / .mode csv / .mode box | 切换输出格式 |
.headers on | 输出带列名 |
.width 10 20 | 设置列宽(配合 column 模式) |
.output file.txt / .output stdout | 输出重定向到文件 / 回到屏幕 |
.import file.csv table | 从 CSV 导入 |
.dump | 导出整个库为 SQL |
.read file.sql | 执行一个 SQL 文件 |
.backup file.db | 在线备份 |
.show | 显示当前所有设置 |
.quit | 退出 |
四、数据库连接
4.1 打开或创建
# 文件不存在会自动创建,所以"连接"和"建库"是同一个动作
sqlite3 mydatabase.db
# 内存库:进程退出数据即消失,适合测试
sqlite3 :memory:有两点容易踩:
- 文件名别写错。写错了不会报错,它会默默给你建个新库,然后你对着空表发呆。
sqlite3后面不加参数会打开一个临时内存库,退出就没了。
4.2 从程序连接
CLI 只用于调试。真正用的是语言绑定,各语言的连接方式略有差异(见第十一节 Python 示例),但底层的打开语义一致。
五、类型系统:SQLite 的类型是"建议"不是"强制"
如果只记一件事,记这个:SQLite 是动态类型。你声明 INTEGER 的列,照样能塞进去一个字符串。
它用的是类型亲和性(type affinity):建表时声明的类型名决定列的"偏好",但引擎在存储时会尽量转换,转不了就原样存。
| 声明的类型关键字 | 亲和性 | 实际行为 |
|---|---|---|
INT、INTEGER、BIGINT、BOOLEAN | INTEGER | 能转整数就转 |
TEXT、VARCHAR(n)、CLOB | TEXT | 存为文本 |
REAL、FLOAT、DOUBLE | REAL | 存为浮点 |
NUMERIC、DECIMAL、DATE、DATETIME | NUMERIC | 能转数字/整数就转,否则存原文 |
BLOB、未指定类型 | BLOB | 原样存 |
注意 VARCHAR(50) 的 50 是无效的——SQLite 不会截断也不会报错,长度约束得自己用 CHECK(length(x) <= 50) 加。
CREATE TABLE affinity_demo (
a INTEGER,
b TEXT,
c REAL,
d NUMERIC,
e BLOB
);5.1 AUTOINCREMENT 的真相
很多人以为 INTEGER PRIMARY KEY 就是自增,但又看到别人写 AUTOINCREMENT,搞不清区别。
真相是:INTEGER PRIMARY KEY 本身就已经是 rowid 的别名,天然自增,不需要任何额外关键字。而 AUTOINCREMENT 反而带来额外开销——它要求 SQLite 维护一个 sqlite_sequence 表,并且保证 ID 永不重用(删除后也不会回收)。
-- 推荐:普通自增,性能好,ID 可能被重用
CREATE TABLE t_plain (id INTEGER PRIMARY KEY, name TEXT);
-- 只在"主键绝不能重用"时用(比如 ID 会暴露给外部系统)
CREATE TABLE t_autoinc (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);绝大多数情况用前者。后者是给有特殊需求的场景准备的。
六、建表与约束
CREATE TABLE users (
id INTEGER PRIMARY KEY, -- rowid 别名,自增
name TEXT NOT NULL,
email TEXT UNIQUE, -- 唯一约束
age INTEGER CHECK (age >= 0),
created_at TEXT DEFAULT (datetime('now')) -- 默认值
);约束类型:
| 约束 | 说明 |
|---|---|
PRIMARY KEY | 主键,INTEGER PRIMARY KEY 即 rowid |
NOT NULL | 非空 |
UNIQUE | 唯一(可含多列组成联合唯一) |
CHECK(表达式) | 自定义校验 |
DEFAULT 值 | 默认值,函数要加括号 DEFAULT (datetime('now')) |
REFERENCES | 外键(见下一节,默认不生效) |
修改表结构:
ALTER TABLE users ADD COLUMN phone TEXT; -- 加列(最常用)
ALTER TABLE users RENAME TO app_users; -- 改名
ALTER TABLE users RENAME COLUMN phone TO mobile; -- 改列名(3.25+)
ALTER TABLE users DROP COLUMN mobile; -- 删列(3.35+)SQLite 的 ALTER TABLE 能力有限(改不了列类型、改不了约束)。老版本要改这些得走"建新表 → 拷数据 → 删旧表 → 改名"四步。
七、CRUD
7.1 插入
INSERT INTO users (name, email) VALUES
('张三', 'z@example.com'),
('李四', 'l@example.com');7.2 查询
SELECT * FROM users; -- 全部列
SELECT id, name FROM users WHERE age > 18; -- 指定列 + 条件
SELECT DISTINCT name FROM users; -- 去重
SELECT * FROM users ORDER BY name ASC LIMIT 10 OFFSET 20; -- 排序分页7.3 更新与删除
UPDATE users SET name = '张三丰' WHERE id = 1;
DELETE FROM users WHERE id = 3;UPDATE / DELETE 忘了写 WHERE 会更新/删除全表,没有确认提示。在 CLI 里可以先 SELECT 一遍同样的 WHERE 确认范围。
7.4 UPSERT(插入或更新)
从 3.24 起支持 ON CONFLICT,这是做"存在即更新"的标准写法:
-- 主键冲突时更新
INSERT INTO users (id, name, email) VALUES (1, '张三', 'z@example.com')
ON CONFLICT(id) DO UPDATE SET email = excluded.email;
-- 邮箱冲突时不动作
INSERT INTO users (id, name, email) VALUES (1, '张三', 'z@example.com')
ON CONFLICT(email) DO NOTHING;excluded 是关键字,代表"这次本来想插入的那行",用它就能取到新值。
7.5 INSERT 的几种"加固"形式
INSERT OR IGNORE INTO users (id, name) VALUES (1, '张三'); -- 冲突则跳过
INSERT OR REPLACE INTO users (id, name) VALUES (1, '张三'); -- 冲突则删除旧行再插入INSERT OR REPLACE 要小心:它是先删后插,会触发 ON DELETE 级联和触发器。
7.6 RETURNING(3.35+)
不想插入后再查一遍?直接让它返回:
INSERT INTO users (name, email) VALUES ('王五', 'w@example.com')
RETURNING id, name;3|王五八、外键:默认是关闭的(重要)
这是 SQLite 最容易被忽视的坑之一:即使你在建表时写了 REFERENCES,外键约束默认也不生效。
sqlite3 demo.db "PRAGMA foreign_keys;"00 就是关闭。每个连接都要手动打开:
PRAGMA foreign_keys = ON;而且——
- 它是连接级设置,断开重连又变回关闭
- 程序里每次建立连接都要执行一次
- 老版本 SQLite 编译时还可能没开外键支持
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
product TEXT,
amount REAL
);打开外键后,ON DELETE CASCADE 才真的会级联删除。没打开的话,你删了用户,订单还在,而且引擎一声不吭——这种静默数据损坏最难查。
九、日期与时间
SQLite 没有专门的日期类型,日期时间统一按文本或整数存。核心就一组函数:
SELECT date('now'); -- 2026-09-22
SELECT datetime('now'); -- 2026-09-22 10:45:15(UTC)
SELECT strftime('%Y-%m-%d %H:%M:%S', 'now'); -- 同上,格式自定
SELECT unixepoch('now'); -- 1790073915(秒级时间戳,3.38+)
SELECT date('now', '+1 month', 'start of month'); -- 2026-10-01(可串联修饰符)修饰符(modifier)可以叠加,很灵活:
| 修饰符 | 作用 |
|---|---|
'+N day' / '-N day' | 加减天数 |
'+N month' / '+N year' | 加减月/年 |
'start of month' / 'start of day' | 取月初 / 当天零点 |
'weekday N' | 下一个星期 N |
'localtime' | 转本地时区 |
注意所有 now 默认是 UTC。要本地时间加 'localtime':
SELECT datetime('now', 'localtime');十、事务与并发
10.1 事务
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- 出错时用 ROLLBACK;SQLite 默认是自动提交——每条 SQL 单独一个事务。要原子性就显式 BEGIN。
10.2 WAL 模式
默认日志模式是 delete(回滚日志)。改成 WAL(Write-Ahead Logging)能显著提升并发读能力:
sqlite3 demo.db "PRAGMA journal_mode=WAL;"
sqlite3 demo.db "PRAGMA journal_mode;"walWAL 的好处是读写不互相阻塞(写还是单线程)。代价是会多出 -wal 和 -shm 两个伴随文件,并且这两个文件必须和主库一起备份,否则可能拿到不一致的快照。
10.3 并发边界
无论哪种模式,同一时刻只有一个写事务。多进程并发写会拿到 SQLITE_BUSY。标准处理:
PRAGMA busy_timeout = 5000; -- 忙时等待 5 秒再放弃,而不是立刻报错十一、索引与查询优化
CREATE INDEX idx_users_email ON users(email); -- 单列
CREATE INDEX idx_orders_user_amount ON orders(user_id, amount); -- 复合
CREATE UNIQUE INDEX idx_users_name ON users(name); -- 唯一索引
DROP INDEX idx_users_name; -- 删(注意:没有 ON 子句)11.1 用执行计划看有没有走索引
EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = 'z@example.com';QUERY PLAN
`--SEARCH users USING INDEX sqlite_autoindex_users_1 (email=?)看到 SEARCH ... USING INDEX 说明走索引了。如果显示 SCAN users,就是全表扫描,该考虑加索引了。
11.2 几个 PRAGMA 调优
PRAGMA cache_size = -8000; -- 8000 KiB ≈ 8 MB 页缓存注意这个单位:cache_size 为负数时单位是 KiB(不是页数)。默认是 -2000,也就是 2 MB。网上很多教程把它当成"2000 页"来算,是错的。
PRAGMA synchronous = NORMAL; -- WAL 模式下的推荐值,兼顾安全与速度
PRAGMA mmap_size = 268435456; -- 256 MB 内存映射,可加速读十二、备份与恢复
12.1 在线备份(推荐)
sqlite3 demo.db ".backup '/path/to/backup.db'".backup 是在线热备份,不需要停服务,也不会因为并发写入而拿到损坏的副本。这是唯一推荐的生产备份方式。
不要用 cp 直接复制正在写入的数据库文件——除非确定没有写入,或者用了 WAL 并且连 -wal、-shm 一起复制。
12.2 导出为 SQL 文本
sqlite3 demo.db .dump > dump.sql.dump 输出的是 SQL 语句,可以跨版本、跨平台重建:
/* WARNING: Script requires that SQLITE_DBCONFIG_DEFENSIVE be disabled */
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
...恢复:
sqlite3 new.db < dump.sql12.3 数据库修复
库文件损坏(比如断电、磁盘错误)时,能读多少救多少:
sqlite3 corrupt.db .dump > recovery.sql # 尽量导出
sqlite3 new.db < recovery.sql # 导入新库.dump 对损坏页通常会跳过并继续,所以哪怕报错也值得试一试。
另外有个专门做底层恢复的工具叫 sqlite3_analyzer,以及官方推荐的 .recover 命令(3.29+):
sqlite3 corrupt.db .recover > recovery.sql.recover 比 .dump 更强壮,专为损坏库设计。
十三、CSV 导入导出
再次强调:这一节全是点命令,必须在 CLI 里执行,不能写进 .sql 文件。
13.1 导出
交互式:
sqlite3 demo.dbsqlite> .headers on
sqlite> .mode csv
sqlite> .output users.csv
sqlite> SELECT * FROM users;
sqlite> .output stdout导出结果:
id,name,email
1,"张三",z@example.com
2,"李四",l@example.com更省事的非交互写法(推荐):
sqlite3 -header -csv demo.db "SELECT id, name, email FROM users;" > users.csv13.2 导入
CSV 导入分两步:先建目标表,再 .import。
sqlite3 demo.dbsqlite> CREATE TABLE temp_import(name TEXT, email TEXT);
sqlite> .mode csv
sqlite> .import /path/to/new_users.csv temp_import然后再用 SQL 把数据并进正式表:
INSERT INTO users (name, email) SELECT name, email FROM temp_import;
DROP TABLE temp_import;.import 有个坑:它不认表头,会把 CSV 第一行也当数据插进去。如果 CSV 带表头,要么先删掉表头行,要么用一个"多一列"的临时表把表头行吃掉。
十四、全文搜索(FTS5)
SQLite 内置了 FTS5 全文检索引擎,做站内搜索、笔记搜索非常够用。
CREATE VIRTUAL TABLE docs USING fts5(title, content);
INSERT INTO docs VALUES
('SQLite Guide', 'Comprehensive guide'),
('Python Tutorial', 'Learn Python');
SELECT title FROM docs WHERE docs MATCH 'guide OR python';SQLite Guide
Python TutorialMATCH 支持布尔运算符(AND/OR/NOT)、短语("exact phrase")、前缀(sql*)。中文分词需要额外配置——FTS5 默认按 Unicode 分词,中文效果一般,可以考虑 trigram tokenizer。
十五、Python 集成
Python 标准库自带 sqlite3,不用装任何东西。
import sqlite3
from datetime import datetime
conn = sqlite3.connect('mydatabase.db')
conn.execute('PRAGMA foreign_keys = ON') # 别忘了这句
cur = conn.cursor()
cur.execute('''
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
product TEXT,
amount REAL,
order_date TEXT
)
''')
rows = [
(1, 'Laptop', 1200.50, datetime.now().isoformat()),
(1, 'Mouse', 25.99, datetime.now().isoformat()),
]
cur.executemany(
"INSERT INTO orders (user_id, product, amount, order_date) VALUES (?, ?, ?, ?)",
rows,
)
# 查询:注意按列下标取值
cur.execute('''
SELECT users.name, orders.product, orders.amount
FROM users JOIN orders ON users.id = orders.user_id
''')
for name, product, amount in cur.fetchall():
print(f"{name} 买了 {product},花了 ${amount:.2f}")
conn.commit()
conn.close()这段代码可以直接跑。几个要点:
- 参数化查询用
?占位,不要把值拼进 SQL 字符串——这是防 SQL 注入的基本功。 fetchall()返回的是元组,for name, product, amount in ...是解包,比row[0]干净。- 记得
PRAGMA foreign_keys = ON,Python 的连接同样是默认关闭。 with conn:上下文管理器会自动提交或回滚,比手动commit()更稳:
with sqlite3.connect('mydatabase.db') as conn:
conn.execute("INSERT INTO users (name) VALUES (?)", ('赵六',))
# 出异常自动回滚,正常结束自动提交十六、常见问题排查
16.1 数据库被锁(database is locked)
# 看看是谁占着文件
lsof mydatabase.db处理办法:
- 应用层设
PRAGMA busy_timeout - 把长事务拆短,别在一个事务里跑几分钟
- 开 WAL 模式,让读不被写阻塞
- 检查是不是有进程崩溃后残留了
-wal/-shm(正常退出会清理)
注意:PRAGMA wal_checkpoint 不是"解锁"命令——它的作用是把 WAL 里的数据合并回主库文件。网上把它当解锁手段的说法是错的。
16.2 数据看似"没写进去"
多半是忘了 commit(),或者进程异常退出。用 with 语句块能避免。
16.3 跨库查询
ATTACH DATABASE 'another.db' AS other;
SELECT * FROM main.users UNION ALL SELECT * FROM other.users;
DETACH DATABASE other;ATTACH 常用于把多个分库合起来查,或者做数据迁移。
十七、速查表
# —— 连接 ——
sqlite3 db.db # 打开/创建
sqlite3 :memory: # 内存库
sqlite3 -header -csv db.db "SQL" # 一行出 CSV
# —— CLI 点命令 ——
.tables .schema t .indexes t .databases
.mode column .headers on .width 10 20
.output f .import f.csv t .dump .read f.sql
.backup f .recover .quit
# —— 必开的 PRAGMA ——
PRAGMA foreign_keys = ON; # 外键(默认关!)
PRAGMA journal_mode = WAL; # 提升并发读
PRAGMA busy_timeout = 5000; # 忙等待
PRAGMA cache_size = -8000; # 8MB 缓存(负值单位为 KiB)-- —— 常用语句 ——(每行为独立示意,勿当作连续脚本执行)
INSERT INTO t(a,b) VALUES (1,2);
INSERT INTO t(a,b) VALUES (1,2) ON CONFLICT(a) DO UPDATE SET b=excluded.b;
INSERT INTO t(a,b) VALUES (1,2) RETURNING a;
SELECT * FROM t WHERE a > 10 ORDER BY b LIMIT 5;
EXPLAIN QUERY PLAN SELECT * FROM t WHERE a = 1;
CREATE INDEX idx ON t(a);
VACUUM; -- 回收空间、整理碎片十八、实际应用场景
18.1 移动 App 本地存储(Android 片段)
// 片段示意,需在 Activity/Context 中调用
SQLiteDatabase db = openOrCreateDatabase("app_data.db", MODE_PRIVATE, null);
db.execSQL("CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)");
ContentValues values = new ContentValues();
values.put("key", "theme");
values.put("value", "dark");
db.insert("settings", null, values);生产项目更推荐 Room 或 SQLDelight,它们在上层包了迁移和类型安全。
18.2 单机日志分析
import sqlite3
conn = sqlite3.connect('weblog.db')
conn.execute('''CREATE TABLE IF NOT EXISTS visits (
id INTEGER PRIMARY KEY,
ip TEXT,
url TEXT,
visited_at TEXT DEFAULT (datetime('now'))
)''')
conn.execute("INSERT INTO visits (ip, url) VALUES (?, ?)", ('192.168.1.1', '/homepage'))
conn.commit()
# 统计每个 URL 的访问量
for url, cnt in conn.execute(
"SELECT url, COUNT(*) FROM visits GROUP BY url ORDER BY COUNT(*) DESC"
):
print(f"{cnt:>6} {url}")
conn.close()几十 GB 的日志用 SQLite 做聚合,配合索引,速度常常比你想的快。真到瓶颈了再上 DuckDB 或 ClickHouse。
学习资源
- SQLite 官方文档 —— 最权威,尤其是SQL 语法和PRAGMA 列表
- SQLite 官方教程 —— CLI 点命令的权威说明(网上教程大半错在这)
- SQLite Fiddle —— 在线实验
- DB Browser for SQLite / SQLiteStudio —— 图形化管理,调试数据方便
graph TD
A[启动 SQLite] --> B{数据库文件存在?}
B -->|是| C[打开数据库]
B -->|否| D[创建新数据库]
C --> E[PRAGMA foreign_keys = ON]
D --> E
E --> F[执行 SQL 操作]
F --> G[COMMIT]
G --> H[退出 / 关闭连接]到这里,SQLite 的主要面基本都覆盖了。真正要带走的其实就两条:想清楚它是不是那个对的工具,以及记住那些默认值陷阱(外键关着、cache_size 单位是 KiB、点命令不是 SQL)。其余的,用时查文档就行。
参考&致谢
系列教程
数据库系列
- SQL 命令使用教程:从入门到精通 —— SQL 标准命令,含 MySQL/PostgreSQL/SQLite 方言对照
- SQLite 使用全面教程:轻量级数据库的终极指南 —— 嵌入式数据库,类型系统、外键陷阱、WAL、FTS5
- MySQL 命令行使用全面教程:从入门到精通 ——
mysql客户端工具,批处理、备份、性能诊断 - MySQL 使用全面指南:从入门到高级实践 —— MySQL 安装配置、字符集、索引、事务锁、复制
- PostgreSQL 使用全面指南:从入门到企业级应用 —— PG 认证配置、JSONB、分区、备份与高可用
- PostgreSQL 命令行使用教程:掌握 psql 工具 ——
psql元命令、脚本化、\copy与pg_dump - PostgreSQL 实现原理深度剖析:高性能数据库引擎的核心机制 —— MVCC、WAL、B-tree 内核机制与可验证观察方法

