加载中...

加载中...

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

SQLite 定位示意

一、先决定:该不该用 SQLite

选型选错了,后面全是坑。这一节比任何语法都重要。

适合的场景

  • 移动 App、桌面客户端的本地存储(数据量几百 MB 以内)
  • 嵌入式设备、IoT 网关
  • 单机分析脚本、日志归档
  • 测试环境替身——比启动一个 MySQL 容器快得多
  • 中小型网站、日活几千以内、读写比高

别用的场景

  • 多台服务器要共享同一份数据。SQLite 是单文件库,走不了网络。硬要用就得挂 NFS,然后你就等着数据库损坏吧。
  • 高并发写入。SQLite 是库级写锁,同一时刻只能有一个写事务。写入 QPS 上不去。
  • 需要严格的用户权限体系。SQLite 没有用户概念,文件权限就是它全部的安全模型。
  • 数据量上了几十 GB 且要复杂查询。它能撑,但会难受。

一句话判断:如果你的程序和数据在同一台机器的同一个进程里,SQLite 大概率是对的

二、安装与版本确认

2.1 安装

Linux(Debian/Ubuntu):

sudo apt install sqlite3

macOS 自带 sqlite3,想用新版走 Homebrew:

brew install sqlite

Windows 到官网下载页sqlite-tools-win-x64-*.zip,解压后把目录加进 PATH

2.2 版本很重要

SQLite 的功能是按版本递增的,UPSERT(3.24+)、RETURNING(3.35+)、unixepoch()(3.38+)都不是老版本有的。动手前先看一眼:

sqlite3 --version
3.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:

有两点容易踩:

  1. 文件名别写错。写错了不会报错,它会默默给你建个新库,然后你对着空表发呆。
  2. sqlite3 后面不加参数会打开一个临时内存库,退出就没了。

4.2 从程序连接

CLI 只用于调试。真正用的是语言绑定,各语言的连接方式略有差异(见第十一节 Python 示例),但底层的打开语义一致。

五、类型系统:SQLite 的类型是"建议"不是"强制"

如果只记一件事,记这个:SQLite 是动态类型。你声明 INTEGER 的列,照样能塞进去一个字符串。

它用的是类型亲和性(type affinity):建表时声明的类型名决定列的"偏好",但引擎在存储时会尽量转换,转不了就原样存。

声明的类型关键字亲和性实际行为
INTINTEGERBIGINTBOOLEANINTEGER能转整数就转
TEXTVARCHAR(n)CLOBTEXT存为文本
REALFLOATDOUBLEREAL存为浮点
NUMERICDECIMALDATEDATETIMENUMERIC能转数字/整数就转,否则存原文
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;"
0

0 就是关闭。每个连接都要手动打开

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;"
wal

WAL 的好处是读写不互相阻塞(写还是单线程)。代价是会多出 -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.sql

12.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.db
sqlite> .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.csv

13.2 导入

CSV 导入分两步:先建目标表,再 .import

sqlite3 demo.db
sqlite> 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 Tutorial

MATCH 支持布尔运算符(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()

这段代码可以直接跑。几个要点:

  1. 参数化查询用 ? 占位,不要把值拼进 SQL 字符串——这是防 SQL 注入的基本功。
  2. fetchall() 返回的是元组for name, product, amount in ... 是解包,比 row[0] 干净。
  3. 记得 PRAGMA foreign_keys = ON,Python 的连接同样是默认关闭。
  4. 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。

学习资源

  1. SQLite 官方文档 —— 最权威,尤其是SQL 语法PRAGMA 列表
  2. SQLite 官方教程 —— CLI 点命令的权威说明(网上教程大半错在这)
  3. SQLite Fiddle —— 在线实验
  4. 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)。其余的,用时查文档就行。

参考&致谢

系列教程

全部文章RSS订阅

数据库系列


SQLite使用全面教程:轻量级数据库的终极指南
发布于
2025年11月22日
许可协议。转载请注明来源
评论
数据加载中 ...
 上一篇

阅读全文

MySQL 使用全面指南:从入门到高级实践
MySQL 使用全面指南:从入门到高级实践 MySQL 使用全面指南:从入门到高级实践
MySQL 是 Web 应用里最常打交道的数据库。它的坑大多不在语法,而在配置和设计——字符集选错存不了 emoji、金额用了 FLOAT、GROUP BY 报错、锁等待查不出原因。本文从安装配置讲到索引优化、事务锁、慢查询和主从复制,尽量
2025-11-22
下一篇 

阅读全文

MySQL命令行使用全面教程:从入门到精通
MySQL命令行使用全面教程:从入门到精通 MySQL命令行使用全面教程:从入门到精通
mysql 命令行客户端是每个后端和运维都要打交道的工具。它有不少自己的选项和技巧——垂直输出、批处理模式、tee、备份参数——用好了能省很多事。本文从连接讲起,覆盖批处理、导入导出、性能诊断和常见故障处理。
2025-11-22