1. SQLite 简介
SQLite 是世界上最广泛部署的数据库引擎,据估计全球有超过一万亿(1 万亿)个 SQLite 数据库在活跃使用。它不是一个传统的数据库管理系统,而是一个嵌入式关系型数据库引擎,以 C 语言库的形式存在,可以直接链接到应用程序中。它的设计哲学可以概括为三个词:自包含、无服务器、零配置。与传统的客户端-服务器数据库架构(如 MySQL、PostgreSQL)不同,SQLite 不需要独立的服务器进程,也不需要配置文件或管理员。它直接读写普通的磁盘文件,一个完整的数据库就是一个单一的文件,这种简洁的设计使它成为了嵌入式领域的首选数据库方案。
SQLite 的创造者是 D. Richard Hipp,最初于 2000 年发布,至今已有超过 20 年的发展历史。在这 20 多年间,SQLite 的代码经过了极其严格的测试——其测试覆盖率超过 100%(分支覆盖率),拥有独立的测试套件,并在各种硬件故障、内存不足、电源中断等极端条件下进行过验证。正是这种对可靠性的极致追求,使得 SQLite 被广泛应用于航天器、汽车电子、智能手机等对稳定性要求极高的领域。
1.1 核心特性
核心特性详解:
零配置:安装 SQLite 不需要配置数据库服务器,不需要启动守护进程,不需要管理用户权限,不需要设置端口号,也不需要创建用户账户。下载一个可执行文件或链接一个库文件即可开始使用。这种零配置的特性使得 SQLite 非常适合快速原型开发、教学演示以及嵌入式环境。在开发和测试阶段,使用 SQLite 可以大幅减少环境搭建的时间成本。
无服务器:SQLite 没有独立的服务器进程。它直接读写磁盘文件,应用程序进程就是数据库引擎。这意味着:
- 没有网络延迟
- 没有配置端口和连接池的烦恼
- 没有进程间通信的开销
嵌入式:SQLite 的库文件只有约 600KB(功能完整版本),可以轻松嵌入到任何应用程序中。C 语言 API 简单直观,支持多种编程语言绑定。与 Oracle 或 SQL Server 需要几百兆甚至上 GB 的安装空间不同,SQLite 可以在资源极其受限的环境中运行——从智能手表的微控制器到路由器的嵌入式 Linux 系统,SQLite 都能完美胜任。它的体积虽小,但功能却非常强大,支持复杂的 SQL 查询、事务、触发器、视图、窗口函数等高级特性。
事务性:SQLite 支持 ACID(原子性、一致性、隔离性、持久性)事务。即使在系统崩溃或电源故障的情况下,事务也是安全的。这意味着当你在写入数据库的过程中遇到突然断电,SQLite 的恢复机制能够自动将数据库回滚到最近一次提交的状态,不会出现数据损坏或不一致的情况。SQLite 通过两种机制实现这一点:回滚日志模式(默认)和预写日志模式(WAL)。事务的隔离级别是串行化(SERIALIZABLE),这是最高的隔离级别,确保了并发操作下的数据一致性。
跨平台:SQLite 的数据库文件格式在所有支持的平台上完全一致。你可以将一个在 Windows 上创建的数据库文件复制到 Linux 服务器或 macOS 系统上直接使用,完全不需要任何转换。这种跨平台的兼容性得益于 SQLite 在文件格式上采用了大端字节序(big-endian)作为统一标准,并精确控制了浮点数表示、页面大小等底层细节。
1.2 与 MySQL / PostgreSQL 对比
| 特性 | SQLite | MySQL | PostgreSQL |
|---|---|---|---|
| 架构 | 嵌入式库 | 客户端-服务器 | 客户端-服务器 |
| 配置 | 零配置 | 需配置服务器 | 需配置服务器 |
| 文件 | 单文件数据库 | 多个系统文件 | 多个系统文件 |
| 并发写入 | 单写入者(WAL 支持并发读) | 多写入者 | 多写入者 |
| 用户管理 | 无 | 完善的用户权限 | 完善的用户权限 |
| 存储过程 | 不支持 | 支持 | 支持 |
| 扩展性 | 单机 | 集群/主从 | 集群/流复制 |
| 部署大小 | ~600KB | ~200MB+ | ~50MB+ |
| 适用场景 | 嵌入式/移动/桌面/小型网站 | Web 应用/大型系统 | 复杂查询/企业应用 |
适用 SQLite 的场景:
- 移动应用(iOS/Android):每个智能手机中几乎都有数十个应用在使用 SQLite。微信、支付宝等国民级应用的核心数据存储都依赖 SQLite。本地笔记、通讯录、消息记录等场景完美契合 SQLite 的特性。
- 桌面应用程序:本地数据库存储用户配置、缓存数据和离线数据。Electron 应用(如 Slack、VS Code)、Qt 应用等内置 SQLite 支持。
- 嵌入式设备:物联网传感器、路由器固件、智能家居设备等资源受限的环境。
- 中小型网站:日均 PV 小于 10 万的网站完全可以使用 SQLite 作为主数据库,尤其适合个人博客、公司官网、内部管理系统等低并发场景。
- 测试环境:在 CI/CD 流水线中使用内存数据库进行单元测试,零配置、速度快、每次测试都是独立环境。
- 开发和原型验证:快速搭建原型时使用 SQLite 取代大型数据库,可以大幅缩短开发周期。
- 数据分析与数据转换:作为中间格式处理 CSV、JSON 等数据文件,利用 SQL 强大的查询能力进行数据清洗和转换。
不适合 SQLite 的场景:
- 高并发写入:如果多个进程需要同时写入同一个数据库文件,SQLite 的单写入者限制会成为瓶颈。这种情况下 MySQL 或 PostgreSQL 是更好的选择。
- 大规模分布式系统:SQLite 不支持网络访问,不能作为共享数据库被多台服务器访问。需要搭建数据库集群的场景应选用分布式数据库。
- 需要细粒度用户权限管理的应用:SQLite 没有用户概念,不支持 GRANT/REVOKE 等权限控制语句。多用户应用需要在上层应用层实现访问控制。
- 需要存储过程或 PL/SQL 的应用:SQLite 不支持存储过程和用户自定义函数(虽然可以通过 C 扩展实现自定义函数,但无法在 SQL 层面编写复杂的业务逻辑)。
1.3 文件格式与限制
SQLite 数据库是一个普通的磁盘文件,具有以下特性:
- 跨平台:数据库文件可以在 32 位和 64 位系统之间自由复制,大小端字节序自动处理。这意味着你在树莓派(ARM 架构)上创建的数据库可以直接复制到 x86 服务器上使用,完全不需要任何额外操作。
- 版本兼容:SQLite 承诺数据库文件格式向前兼容。从 SQLite 3 发布至今,所有版本都可以读写其他版本创建的文件,不需要数据迁移或导出导入。
- WAL 模式:使用 WAL 模式时,会额外生成一个
.wal(预写日志文件)和一个.shm(共享内存文件),但它们不是必需的备份文件。 - 单一文件:完整的数据库包含表结构、索引、视图、触发器和数据,全部存储在一个文件中。这使得备份、传输和部署变得极其简单——复制一个文件就等同于完整备份了整个数据库。
主要限制:
| 限制项 | 默认上限 |
|---|---|
| 最大数据库大小 | 约 281 TB |
| 最大页面大小 | 65536 字节 |
| 最大行大小 | 约 1 GB(默认页面大小下) |
| 最大列数 | 32767(实际受限于行大小,一般为 2000 左右) |
| 最大表大小 | 约 140 TB(默认页面大小 4KB) |
| 最大连接数(VFS 级别) | 不受限 |
| 最大同时写入者 | 1 |
| 字符串最大长度 | 约 10 亿字节 |
| BLOB 最大长度 | 约 10 亿字节 |
| SQL 语句最大长度 | 约 10 亿字节 |
| 表/索引/视图最大数量 | 约 2^64 |
| 复合 SELECT 最大分支数 | 500 |
1.4 安装方式
SQLite 的安装非常简单,几乎在所有主流操作系统和编程语言中都有现成的支持。
命令行工具
Linux(Ubuntu/Debian):
sudo apt-get install sqlite3
# 同时安装开发库(编译 C 扩展时需要)
sudo apt-get install libsqlite3-devLinux(CentOS/RHEL/Fedora):
sudo yum install sqlite
# 或 dnf
sudo dnf install sqlitemacOS:
brew install sqlite3
# macOS 自带 sqlite3,但版本可能较旧,Homebrew 可以获取最新版本Windows:
从 SQLite 官网 下载 sqlite-tools-win32-*.zip(32 位)或 sqlite-tools-win-x64-*.zip(64 位),解压后将 sqlite3.exe 所在目录加入系统 PATH 环境变量。
验证安装:
sqlite3 --version
# 输出类似:3.46.0 2024-05-23 13:25:27Python 中使用
Python 标准库内置了 sqlite3 模块,无需额外安装:
import sqlite3
print(sqlite3.sqlite_version)
# 输出:3.46.0Node.js 中使用
npm install better-sqlite3
# 或者
npm install sql.js # 浏览器端可用Go 中使用
go mod init myapp
go get github.com/mattn/go-sqlite3Rust 中使用
# Cargo.toml
[dependencies]
rusqlite = "0.31"Java / Android 中使用
Android 内置 SQLite,直接使用 android.database.sqlite.SQLiteDatabase API。纯 Java 版本可使用 sqlite-jdbc:
<!-- Maven -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.46.0.0</version>
</dependency>2. sqlite3 CLI 使用
SQLite 的命令行工具 sqlite3 是日常操作数据库最直接的方式。以下详述其核心命令。
2.1 打开与创建数据库
# 打开(或创建)一个数据库
sqlite3 mydb.db
# 使用内存数据库
sqlite3 :memory:
# 指定初始 SQL
sqlite3 mydb.db "SELECT 1;"进入 CLI 后,提示符变为 sqlite>。
2.2 基本元命令(以 . 开头的命令)
sqlite3 CLI 中的元命令都以英文句点(.)开头,它们不是 SQL 语句,而是由 CLI 工具直接解释的指令。这些命令能帮助你快速浏览数据库结构、调整输出格式和管理数据。
-- 列出所有表
.tables
-- 查看表结构
.schema users
-- 等价于:
SELECT sql FROM sqlite_master WHERE type='table' AND name='users';
-- 查看所有对象的创建语句(表、索引、视图、触发器)
.schema
-- 查看索引
.indices users
-- 等价于:
SELECT sql FROM sqlite_master WHERE type='index' AND name LIKE '%users%';
-- 查看所有索引
.indices
-- 查看数据库信息
.database
-- 输出数据库文件路径
-- 查看当前配置
.show2.3 数据导入与导出
导出为 SQL 文本:
# 完整导出
sqlite3 mydb.db .dump > backup.sql
# 导出特定表
sqlite3 mydb.db ".dump users" > users.sql
# 恢复数据
sqlite3 newdb.db < backup.sql
# 或在 CLI 中
.read backup.sql导入 CSV 文件:
# 先创建表结构
sqlite3 mydb.db "CREATE TABLE users(id INTEGER, name TEXT, email TEXT);"
# 导入 CSV(默认逗号分隔,第一行不视为表头)
sqlite3 mydb.db ".import data.csv users"
# 导入 CSV(第一行为表头)
sqlite3 mydb.db ".mode csv"
sqlite3 mydb.db ".import --csv data.csv users"
# 或合成为一行
sqlite3 mydb.db ".mode csv" ".import --csv data.csv users"导出为 CSV:
sqlite3 mydb.db ".headers on" ".mode csv" ".output out.csv" "SELECT * FROM users;" ".output stdout"2.4 输出格式控制
-- 默认模式
.mode list
-- CSV 模式
.mode csv
.headers on
SELECT * FROM users;
-- 列对齐模式(人类可读)
.mode column
.headers on
.width 10 20 30 -- 设置各列宽度
SELECT * FROM users;
-- Markdown 表格模式(SQLite 3.46+)
.mode markdown
SELECT * FROM users;
-- JSON 模式
.mode json
SELECT * FROM users;
-- INSERT 语句模式(用于生成备份)
.mode insert users
SELECT * FROM users WHERE id < 10;
-- 引用值模式
.mode quote
SELECT * FROM users;完整示例演示:
# 建表插入数据
sqlite3 test.db <<EOF
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL,
stock INTEGER DEFAULT 0
);
INSERT INTO products VALUES (1, '键盘', 99.9, 50);
INSERT INTO products VALUES (2, '鼠标', 49.9, 120);
INSERT INTO products VALUES (3, '显示器', 1999.0, 30);
EOF
# 不同格式对比
sqlite3 test.db ".headers on" ".mode column" "SELECT * FROM products;"
echo "---"
sqlite3 test.db ".headers on" ".mode json" "SELECT * FROM products;"
echo "---"
sqlite3 test.db ".headers on" ".mode markdown" "SELECT * FROM products;"输出示例:
id name price stock
-- ------- ------- -----
1 键盘 99.9 50
2 鼠标 49.9 120
3 显示器 1999.0 30
---
[{"id":1,"name":"键盘","price":99.9,"stock":50},
{"id":2,"name":"鼠标","price":49.9,"stock":120},
{"id":3,"name":"显示器","price":1999.0,"stock":30}]
---
| id | name | price | stock |
|----|--------|--------|-------|
| 1 | 键盘 | 99.9 | 50 |
| 2 | 鼠标 | 49.9 | 120 |
| 3 | 显示器 | 1999.0 | 30 |2.5 其他实用元命令
-- 显示执行时间
.timer on
-- 查看数据库文件信息
.filectrl --info
-- 退出
.quit
-- 或按 Ctrl+D
-- 加载扩展
.load /path/to/ext.so
-- 统计命令执行时间并输出
.timer on
SELECT COUNT(*) FROM large_table;2.6 索引管理
-- 创建索引
CREATE INDEX idx_users_email ON users(email);
-- 创建唯一索引
CREATE UNIQUE INDEX idx_users_username ON users(username);
-- 创建复合索引
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at DESC);
-- 删除索引
DROP INDEX IF EXISTS idx_users_email;
-- 查看查询是否使用了索引(需要先开启)
EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = 'test@example.com';EXPLAIN QUERY PLAN 输出示例:
QUERY PLAN
|--SEARCH users USING INDEX idx_users_email (email=?)3. SQL 兼容性
SQLite 实现了大部分 SQL-92 标准,但在一些高级特性上有所取舍。熟悉 MySQL 或 PostgreSQL 的开发者在使用 SQLite 时,需要注意一些关键的差异点。
3.1 数据类型
SQLite 采用动态类型系统(Dynamic Typing),而非传统 SQL 数据库的静态类型(Static Typing)。这是 SQLite 与其他主流数据库最根本的区别之一。在 MySQL 中,如果你将某列定义为 INT,那么插入一个字符串会导致错误或者隐式转换。但在 SQLite 中,列类型更多的是一个"提示"而非强制约束——你可以在同一列中存储不同类型的数据,数据库会根据插入的值自动确定其存储类型。
这种设计的理念来源于 SQLite 的定位——它是一个嵌入式数据库,通常与动态类型语言(如 Python、JavaScript、Ruby)配合使用。动态类型语言本身就不强调编译时的类型检查,因此 SQLite 的动态类型系统与这些语言配合得更加自然。
五大原生类型:
| 类型 | 说明 | 示例 |
|---|---|---|
NULL |
空值 | NULL |
INTEGER |
有符号整数,1-8 字节变长 | 42, -999, 0 |
REAL |
8 字节 IEEE 浮点数 | 3.14, -0.001, 1e10 |
TEXT |
UTF-8/UTF-16 字符串 | 'hello', "你好" |
BLOB |
二进制数据 | X'F00D', ?(参数化) |
类型亲缘性(Type Affinity):虽然 SQLite 允许任意列存储任意类型的数据,但它会根据声明的列类型设置"亲缘性",影响存储优化:
CREATE TABLE affinity_demo (
a TEXT, -- TEXT 亲缘性
b INTEGER, -- INTEGER 亲缘性
c REAL, -- REAL 亲缘性
d NUMERIC, -- NUMERIC 亲缘性
e BLOB, -- BLOB 亲缘性(无转换)
f -- BLOB 亲缘性(未指定类型)
);
-- 插入各种值,查看实际存储类型
INSERT INTO affinity_demo VALUES (42, 42, 42, 42, 42, 42);
SELECT typeof(a), typeof(b), typeof(c), typeof(d), typeof(e), typeof(f)
FROM affinity_demo;
-- 输出:text|integer|real|integer|blob|blob常见类型映射:
-- 以下类型全部映射为 INTEGER 亲缘性
INT, INTEGER, TINYINT, SMALLINT, MEDIUMINT, BIGINT, INT2, INT8
-- 以下类型映射为 TEXT 亲缘性
CHARACTER, VARCHAR, VARYING CHARACTER, NCHAR, NATIVE CHARACTER, TEXT, CLOB
-- 以下类型映射为 BLOB 亲缘性
BLOB, 无数据类型声明3.2 不支持的 SQL 特性
SQLite 在 SQL 兼容性上最重要的缺失是连接类型和嵌套事务。如果你以前主要使用 MySQL 或 PostgreSQL,在迁移到 SQLite 时最容易遇到的就是 JOIN 语法的问题。了解这些差异并掌握替代写法,可以帮助你更顺畅地使用 SQLite。
不支持 FULL OUTER JOIN:
-- ❌ SQLite 不支持
SELECT * FROM a FULL OUTER JOIN b ON a.id = b.id;
-- ✅ 替代方案:LEFT JOIN + RIGHT JOIN 组合
SELECT * FROM a LEFT JOIN b ON a.id = b.id
UNION ALL
SELECT * FROM a RIGHT JOIN b ON a.id = b.id WHERE a.id IS NULL;
-- 但 RIGHT JOIN 也不支持,所以用 LEFT JOIN 的对称操作不支持 RIGHT JOIN:
-- ❌ SQLite 不支持
SELECT * FROM a RIGHT JOIN b ON a.id = b.id;
-- ✅ 等效写法,交换表顺序
SELECT * FROM b LEFT JOIN a ON a.id = b.id;不支持嵌套事务:
-- SQLite 不支持真正的嵌套事务
BEGIN;
BEGIN; -- ❌ 错误
COMMIT;
-- ✅ 使用 SAVEPOINT 实现类似效果
BEGIN;
SAVEPOINT sp1;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
ROLLBACK TO sp1; -- 回滚到保存点
-- 或 RELEASE sp1; -- 提交保存点
COMMIT;3.3 日期与时间函数
SQLite 没有原生的日期或时间数据类型,这与 MySQL 的 DATETIME、TIMESTAMP 类型不同。在 SQLite 中,日期和时间以以下三种形式存储:
- TEXT:ISO-8601 格式的字符串,例如
"2024-09-14 14:30:00" - INTEGER:Unix 时间戳,例如
1726300800 - REAL:儒略日(Julian Day Numbers),例如
2460568.5
推荐使用 TEXT 格式(ISO-8601),因为它具有可读性、可排序性,并且与其他系统的交互最方便。如果你需要做时间计算,则 INTEGER 格式(Unix 时间戳)更为方便。
SQLite 提供了 8 个内置的日期时间函数:date()、time()、datetime()、julianday()、strftime()、unixepoch()、timediff() 和 dateadd()。
-- 获取当前日期时间
SELECT datetime('now'); -- 2024-09-14 14:30:00
SELECT date('now'); -- 2024-09-14
SELECT time('now'); -- 14:30:00
SELECT strftime('%Y-%m-%d %H:%M:%S', 'now'); -- 2024-09-14 14:30:00
-- 时间计算
SELECT datetime('now', '+7 days'); -- 一周后
SELECT datetime('now', '-1 month'); -- 一个月前
SELECT datetime('now', 'start of year'); -- 今年第一天
SELECT datetime('now', 'start of month', '+1 month', '-1 day'); -- 本月最后一天
-- Unix 时间戳转换
SELECT datetime(1726300800, 'unixepoch'); -- 时间戳 → 日期时间
SELECT strftime('%s', 'now'); -- 日期时间 → 时间戳
-- 儒略日
SELECT julianday('now');
SELECT date(julianday('now'));
-- 日期差值
SELECT julianday('2024-12-31') - julianday('2024-09-14'); -- 108 天
SELECT strftime('%j', '2024-12-31') - strftime('%j', '2024-09-14'); -- 108
-- 周几
SELECT strftime('%w', '2024-09-14'); -- 6(周六,0=周日)
-- 格式化字符串
SELECT strftime('%Y年%m月%d日 %H时%M分%S秒', 'now');
-- 2024年09月14日 14时30分00秒
-- 实际建表示例
CREATE TABLE events (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TRIGGER update_timestamp AFTER UPDATE ON events
BEGIN
UPDATE events SET updated_at = datetime('now', 'localtime') WHERE id = NEW.id;
END;3.4 窗口函数(3.25+)
自 SQLite 3.25.0 起支持窗口函数,3.28.0 起支持 EXCLUDE 子句。
-- 准备示例数据
CREATE TABLE sales (
year INTEGER,
quarter INTEGER,
amount REAL
);
INSERT INTO sales VALUES
(2023, 1, 1000), (2023, 2, 1500), (2023, 3, 1200), (2023, 4, 1800),
(2024, 1, 1100), (2024, 2, 1600), (2024, 3, 1300), (2024, 4, 1900);
-- ROW_NUMBER:行号
SELECT year, quarter, amount,
ROW_NUMBER() OVER (ORDER BY amount DESC) AS rank
FROM sales;
-- RANK / DENSE_RANK
SELECT year, quarter, amount,
RANK() OVER (ORDER BY amount DESC) AS r,
DENSE_RANK() OVER (ORDER BY amount DESC) AS dr
FROM sales;
-- LAG / LEAD:前后行访问
SELECT year, quarter, amount,
LAG(amount) OVER (ORDER BY year, quarter) AS prev_quarter,
LEAD(amount) OVER (ORDER BY year, quarter) AS next_quarter,
amount - LAG(amount) OVER (ORDER BY year, quarter) AS growth
FROM sales;
-- 分区统计
SELECT year, quarter, amount,
SUM(amount) OVER (PARTITION BY year) AS year_total,
ROUND(amount * 100.0 / SUM(amount) OVER (PARTITION BY year), 2) AS pct
FROM sales;
-- 移动平均(过去 2 个季度 + 当前季度 = 3 期移动平均)
SELECT year, quarter, amount,
ROUND(AVG(amount) OVER (
ORDER BY year, quarter
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
), 2) AS moving_avg_3q
FROM sales;
-- FIRST_VALUE / LAST_VALUE
SELECT year, quarter, amount,
FIRST_VALUE(amount) OVER (PARTITION BY year ORDER BY quarter) AS year_start,
LAST_VALUE(amount) OVER (PARTITION BY year ORDER BY quarter
RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS year_end
FROM sales;
-- NTILE:分桶(四分位)
SELECT year, quarter, amount,
NTILE(4) OVER (ORDER BY amount DESC) AS quartile
FROM sales;3.5 UPSERT 操作
SQLite 支持 INSERT ... ON CONFLICT(3.24.0+)和 INSERT OR REPLACE 两种 UPSERT 语法。
-- 准备表
CREATE TABLE users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
login_count INTEGER DEFAULT 0,
updated_at TEXT DEFAULT (datetime('now'))
);
-- 方式一:INSERT OR REPLACE
-- 如果发生冲突,先 DELETE 旧行再 INSERT 新行
INSERT OR REPLACE INTO users (id, username, email, login_count)
VALUES (1, 'alice', 'alice@example.com', 5);
-- 注意:如果冲突由 UNIQUE 约束触发,旧行被删除,新行插入。
-- 这意味着未指定的列会恢复为默认值!
-- 方式二:INSERT OR IGNORE
-- 冲突时直接忽略,不插入也不报错
INSERT OR IGNORE INTO users (id, username, email)
VALUES (2, 'bob', 'bob@example.com');
-- 方式三:ON CONFLICT ... DO NOTHING
INSERT INTO users (id, username, email)
VALUES (3, 'charlie', 'charlie@example.com')
ON CONFLICT(username) DO NOTHING;
-- 方式四(推荐):ON CONFLICT ... DO UPDATE
-- 冲突时更新特定字段,不删除旧行
INSERT INTO users (id, username, email, login_count)
VALUES (1, 'alice', 'alice@new.com', 1)
ON CONFLICT(id) DO UPDATE SET
email = excluded.email,
login_count = excluded.login_count,
updated_at = datetime('now');
-- 更复杂的场景:累加登录次数
INSERT INTO users (id, username, email, login_count)
VALUES (4, 'dave', 'dave@example.com', 1)
ON CONFLICT(username) DO UPDATE SET
login_count = users.login_count + 1,
updated_at = datetime('now');INSERT OR REPLACE 与 ON CONFLICT DO UPDATE 的区别:
| 特性 | INSERT OR REPLACE | ON CONFLICT DO UPDATE |
|---|---|---|
| 行为 | 删除旧行,插入新行 | 原地更新指定列 |
| 未指定列 | 重置为默认值 | 保持原值 |
| 自增 ID | 可能变化 | 不变 |
| 外键引用 | 级联删除可能触发 | 安全 |
| 性能 | 较低(DELETE+INSERT) | 较高(直接 UPDATE) |
4. SQLite 特色
SQLite 有许多独特的功能,使其在嵌入式数据库领域独树一帜。这些特性包括:WAL 模式下的高并发读写能力、零开销的内存数据库、自动清理的临时表、强大的虚拟表机制,以及可以在同一连接中操作多个数据库的附加数据库功能。理解这些特色功能,能够帮助你充分发挥 SQLite 的潜力。
4.1 WAL 模式(预写日志)
SQLite 默认使用回滚日志(rollback journal)模式,在这种模式下,当一个连接正在执行写操作时,所有其他连接(无论读还是写)都会被阻塞,直到写操作完成。这在桌面应用或单用户场景下不是问题,但在需要并发读写的 Web 应用或服务端场景中就会成为严重的瓶颈。
为了解决这个问题,SQLite 在 3.7.0 版本中引入了 WAL(Write-Ahead Logging)模式。在 WAL 模式下,写入操作不会直接修改数据库文件,而是将修改追加到 .wal 文件中。这使得读操作永远不会被写操作阻塞,反之亦然——一个写入者不会阻塞任何读取者。这是 SQLite 用于服务端场景时最重要的优化手段。
-- 开启 WAL 模式
PRAGMA journal_mode = WAL;
-- 验证当前模式
PRAGMA journal_mode;
-- 输出:walWAL 模式的优势:
- 并发读写:一个写入者不会阻塞读取者。读取者和写入者可以同时工作。
- 写入性能更好:批量写入时速度更快。
- 读性能略微提升:读操作不阻塞,总体吞吐量更高。
WAL 文件说明:
# 开启 WAL 后会产生两个额外文件
mydb.db # 数据库主文件
mydb.db-wal # 预写日志文件
mydb.db-shm # 共享内存文件(用于同步)WAL 的三种模式:
-- 默认 WAL:自动检查点,正常效率
PRAGMA journal_mode = WAL;
-- WAL 且不自动截断 WAL 文件(NORMAL 模式下的默认行为)
-- 适用于长连接场景
-- TRUNCATE 模式:提交时自动截断 WAL
PRAGMA journal_mode = TRUNCATE;手动管理 WAL:
-- 手动执行检查点,将 WAL 内容写回主文件
PRAGMA wal_checkpoint;
-- 返回:busy 或 ok
-- 全量检查点(阻塞读写)
PRAGMA wal_checkpoint(FULL);
-- 截断检查点(写回并截断 WAL 文件)
PRAGMA wal_checkpoint(TRUNCATE);4.2 内存数据库
SQLite 可以完全在内存中运行,不写入任何磁盘文件。这种模式下的数据库读写速度极快,因为所有的 I/O 操作都发生在 RAM 中,消除了磁盘寻道和传输的延迟。内存数据库适合以下场景:缓存临时数据、单元测试环境、需要极快速度的数据处理管道。
创建内存数据库非常简单,只需在连接时将文件路径指定为 :memory: 即可。特别需要注意的是,每次使用 :memory: 都会创建一个独立的、全新的数据库实例——不同连接之间不共享内存数据库。
-- 创建内存数据库
sqlite3 :memory:
-- 在 Python 中使用内存数据库import sqlite3
# 创建内存数据库
conn = sqlite3.connect(':memory:')
cursor = conn.cursor()
# 建表并操作
cursor.execute('''CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)''')
cursor.execute('''INSERT INTO test VALUES (1, 'hello')''')
cursor.execute('''SELECT * FROM test''')
print(cursor.fetchall()) # [(1, 'hello')]
# 内存数据库回滚到起始状态
conn.rollback()
cursor.execute('''SELECT * FROM test''')
print(cursor.fetchall()) # [] —— 回滚了整个会话!使用文件数据库的内存缓存:
-- 在 WAL 模式下,内存中的页面缓存对性能至关重要
PRAGMA cache_size = -64000; -- 设置缓存为 64MB混合模式:文件 + 内存:
import sqlite3
# 使用内存数据库作为临时存储
conn = sqlite3.connect(':memory:')
# 同时附加一个磁盘数据库
conn.execute("ATTACH DATABASE 'data.db' AS disk")
conn.execute("CREATE TABLE disk.users AS SELECT * FROM temp.users")4.3 临时表
临时表是 SQLite 中一个非常实用的功能,它们只在当前数据库连接中可见,当连接关闭时自动删除。这意味着不同连接之间的临时表互不干扰,即使在同一个数据库文件上操作,每个连接的临时表也是完全隔离的。这个特性在多租户应用或并行处理的场景中非常有用。
临时表默认创建在 temp 数据库中,你可以通过 CREATE TEMP TABLE 或 CREATE TEMPORARY TABLE 语法来创建。临时表的名字可以与永久表相同——在这种情况下,临时表会"遮蔽"永久表,直到临时表被删除或连接关闭。
-- 创建临时表(只对当前连接可见)
CREATE TEMP TABLE temp_data (
id INTEGER PRIMARY KEY,
value TEXT
);
-- 或使用 TEMPORARY 关键字
CREATE TEMPORARY TABLE session_cache AS
SELECT * FROM main.users WHERE active = 1;
-- 临时表与普通表重名时,临时表优先
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
CREATE TEMP TABLE users (id INTEGER PRIMARY KEY, name TEXT, session_id TEXT);
INSERT INTO users VALUES (1, 'temp_user', 'abc123');
SELECT * FROM users; -- 访问的是临时表
-- 显式指定数据库名可访问主表
INSERT INTO main.users SELECT id, name FROM temp.users;
-- 查看临时表
.tables
-- 临时表的索引
CREATE TEMP INDEX idx_temp_value ON temp_data(value);临时表的典型用途:
-- 1. 复杂查询的中间结果
CREATE TEMP TABLE top_customers AS
SELECT customer_id, SUM(amount) AS total
FROM orders
WHERE order_date >= date('now', '-30 days')
GROUP BY customer_id
ORDER BY total DESC
LIMIT 100;
-- 2. 批量数据清洗
CREATE TEMP TABLE staging AS SELECT * FROM imported_data;
UPDATE staging SET email = lower(trim(email));
DELETE FROM staging WHERE email IS NULL OR email = '';
-- 3. ETL 过程中的转换表
CREATE TEMP TABLE mapping (
old_id INTEGER,
new_id INTEGER
);4.4 虚拟表
虚拟表(Virtual Table)是 SQLite 最强大和最具扩展性的机制之一。虚拟表看起来和操作起来与普通表完全一样(支持 SELECT、INSERT、UPDATE、DELETE 等操作),但它不存储数据在数据库文件中——数据可以由外部代码(C 扩展或内置模块)动态生成和处理。这意味着你可以用 SQL 语法来查询 CSV 文件、JSON 数据、甚至是远程 API 的返回结果。
SQLite 内置了多种虚拟表模块,其中最常用的是:FTS5(全文搜索)和 JSON 查询支持。
FTS5 全文搜索(3.9.0+)
FTS5(Full-Text Search 5)是 SQLite 内置的全文搜索引擎,是 FTS3/FTS4 的继承者。FTS5 提供了高效的全文索引和搜索能力,支持多种分词器、排名函数、前缀搜索、短语搜索等高级功能。它的性能非常出色——对于百万级别的文档,搜索响应时间通常在毫秒级别。
要使用 FTS5,你需要编译时启用了该扩展(大多数发行版默认启用)。FTS5 通过创建虚拟表的方式工作:
-- 启用 FTS5 扩展(需要编译时支持)
-- 创建全文搜索虚拟表
CREATE VIRTUAL TABLE articles_fts USING fts5(
title,
body,
content='articles', -- 内容表名称(可选)
content_rowid='id', -- 内容表行 ID 列名
tokenize='porter unicode61' -- 分词器设置
);
-- 插入数据到原始表
CREATE TABLE articles (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
body TEXT NOT NULL,
published_at TEXT
);
INSERT INTO articles VALUES
(1, 'SQLite 入门', 'SQLite 是一个轻量级的嵌入式数据库引擎...', '2024-09-01'),
(2, '全文搜索实战', '使用 FTS5 实现高效全文检索...', '2024-09-10'),
(3, '数据库性能优化', '合理使用索引可以大幅提升查询性能...', '2024-09-14');
-- 填充 FTS 索引
INSERT INTO articles_fts(rowid, title, body)
SELECT id, title, body FROM articles;
-- 全文搜索
SELECT rowid, rank, title, snippet(articles_fts, 0, '<b>', '</b>', '...', 20)
FROM articles_fts
WHERE articles_fts MATCH 'SQLite OR 全文搜索'
ORDER BY rank;
-- 使用 bm25 排名函数
SELECT rowid, bm25(articles_fts) AS score, title
FROM articles_fts
WHERE articles_fts MATCH '数据库'
ORDER BY score;
-- 前缀搜索
SELECT * FROM articles_fts WHERE articles_fts MATCH '数据*';
-- 短语搜索
SELECT * FROM articles_fts WHERE articles_fts MATCH '"嵌入式数据库"';
-- 就近搜索(NEAR)
SELECT * FROM articles_fts WHERE articles_fts MATCH 'SQLite NEAR 嵌入式';
-- 删除 FTC 内容关联(当原始表更新时,需要同步更新 FTS 表)
-- 使用触发器保持同步
CREATE TRIGGER articles_ai AFTER INSERT ON articles BEGIN
INSERT INTO articles_fts(rowid, title, body) VALUES (new.id, new.title, new.body);
END;
CREATE TRIGGER articles_ad AFTER DELETE ON articles BEGIN
INSERT INTO articles_fts(articles_fts, rowid, title, body)
VALUES ('delete', old.id, old.title, old.body);
END;
CREATE TRIGGER articles_au AFTER UPDATE ON articles BEGIN
INSERT INTO articles_fts(articles_fts, rowid, title, body)
VALUES ('delete', old.id, old.title, old.body);
INSERT INTO articles_fts(rowid, title, body) VALUES (new.id, new.title, new.body);
END;
-- 外部内容表(content= 语法):自动同步,无需触发器
-- 创建时指定 content=,FTS 表会自动从原始表读取内容
-- 但需要定期重建索引
INSERT INTO articles_fts(articles_fts) VALUES('rebuild');JSON 查询(3.9.0+)
SQLite 内置了强大的 JSON 函数,无需额外扩展。
-- 创建含 JSON 字段的表
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
attributes TEXT -- JSON 字符串
);
INSERT INTO products VALUES
(1, '笔记本', '{"brand":"联想","specs":{"cpu":"i7","ram":"16GB","storage":"512GB"},"price":5999,"colors":["银","灰"],"in_stock":true}'),
(2, '手机', '{"brand":"华为","specs":{"cpu":"麒麟","ram":"8GB","storage":"256GB"},"price":3999,"colors":["黑","白","蓝"],"in_stock":true}'),
(3, '平板', '{"brand":"苹果","specs":{"cpu":"M2","ram":"8GB","storage":"128GB"},"price":3499,"colors":["灰","金"],"in_stock":false}');
-- 提取 JSON 值
SELECT name,
json_extract(attributes, '$.brand') AS brand,
json_extract(attributes, '$.price') AS price
FROM products
WHERE json_extract(attributes, '$.in_stock') = true;
-- 使用 -> 和 ->> 运算符(3.38.0+)
SELECT name,
attributes -> '$.brand' AS brand, -- 返回 JSON 值(带引号的字符串)
attributes ->> '$.specs.cpu' AS cpu -- 返回 SQL 值(不带引号)
FROM products;
-- 嵌套 JSON 数组操作
SELECT name,
json_extract(attributes, '$.colors[0]') AS first_color
FROM products;
-- JSON 数组包含判断
SELECT name
FROM products
WHERE json_extract(attributes, '$.colors') LIKE '%"银"%';
-- 或使用 json_each 展开数组
SELECT p.name, c.value AS color
FROM products p, json_each(p.attributes -> '$.colors') AS c
WHERE c.value = '银';
-- 更新 JSON 字段
UPDATE products
SET attributes = json_set(attributes, '$.price', 5499)
WHERE id = 1;
-- 合并 JSON
UPDATE products
SET attributes = json_patch(attributes, '{"warranty": "2年"}')
WHERE id = 1;
-- 删除 JSON 键
UPDATE products
SET attributes = json_remove(attributes, '$.colors')
WHERE id = 3;
-- 创建 JSON 对象和数组
SELECT json_object(
'id', id,
'name', name,
'price', json_extract(attributes, '$.price')
) AS product_json
FROM products;
-- 聚合为 JSON 数组
SELECT json_group_array(
json_object('id', id, 'name', name)
) AS all_products
FROM products;
-- JSON 验证
SELECT json_valid('[1,2,3]'); -- 1
SELECT json_valid('{bad json'); -- 04.5 附加数据库
ATTACH DATABASE 是 SQLite 中一个非常实用的功能,它允许你在同一个数据库连接中同时打开和操作多个数据库文件。这意味着你可以跨库进行 JOIN 查询、跨库复制数据、甚至将内存数据库与磁盘数据库结合使用。这个功能在数据迁移、分库管理、以及多数据源整合的场景中极为有用。
当你打开一个 SQLite 数据库时,该数据库自动被命名为 main。通过 ATTACH DATABASE 附加的数据库会被赋予你指定的别名。
-- 附加一个数据库
ATTACH DATABASE 'archive.db' AS archive;
-- 使用附加数据库中的表
CREATE TABLE archive.old_orders AS
SELECT * FROM main.orders WHERE order_date < '2023-01-01';
-- 跨库 JOIN
SELECT u.name, o.order_id, o.amount
FROM main.users u
JOIN archive.old_orders o ON u.id = o.user_id;
-- 附加更多数据库
ATTACH DATABASE 'reports.db' AS reports;
ATTACH DATABASE ':memory:' AS tempdb;
-- 查看所有已附加的数据库
SELECT * FROM pragma_database_list;
-- main|/path/to/mydb.db
-- archive|/path/to/archive.db
-- temp|/path/to/temp.db (SQLite 内部临时数据库)
-- reports|/path/to/reports.db
-- tempdb|(内存数据库)
-- 使用附加数据库名限定表
INSERT INTO tempdb.report_cache (query_name, result_json)
VALUES ('monthly_summary', '{"total": 12345}');
-- 分离数据库
DETACH DATABASE archive;
-- DETACH DATABASE tempdb;5. 性能优化
SQLite 虽然小巧,但通过合理的配置和编码实践,可以在大多数场景下获得非常出色的性能。性能优化的核心思路是:减少磁盘 I/O、增加缓存命中率、减少锁争用、以及设计高效的查询计划。本节将介绍 SQLite 性能调优的核心手段,包括 PRAGMA 参数调优、批量操作优化、索引设计以及数据库维护。
5.1 PRAGMA 优化
PRAGMA 是 SQLite 的"控制面板",通过设置 PRAGMA 值可以显著影响数据库的行为和性能。这些设置大多是连接级别的——同一个数据库文件的不同连接可以有不同的 PRAGMA 配置。理解每个 PRAGMA 的作用,是 SQLite 性能调优的第一步。
选择正确的 PRAGMA 参数,可以将数据库的读写性能提升数倍甚至数十倍。以下是最重要的优化 PRAGMA 及其推荐值:
-- 推荐的生产环境 PRAGMA 设置(连接级别)
PRAGMA journal_mode = WAL; -- 预写日志模式,提升并发
PRAGMA synchronous = NORMAL; -- 安全性/速度平衡(1)
PRAGMA cache_size = -8000; -- 缓存 8MB(负数表示 KB)
PRAGMA page_size = 4096; -- 页面大小 4KB(必须在创建数据库时设置)
PRAGMA temp_store = MEMORY; -- 临时表存储在内存中
PRAGMA mmap_size = 268435456; -- 内存映射 256MB
PRAGMA busy_timeout = 5000; -- 等待 5 秒而不是立即返回忙碌
PRAGMA foreign_keys = ON; -- 启用外键约束PRAGMA 详解:
| PRAGMA | 选项 | 说明 |
|---|---|---|
journal_mode |
DELETE / TRUNCATE / PERSIST / WAL / MEMORY / OFF |
日志模式。WAL 通常最佳。OFF 无崩溃恢复能力。 |
synchronous |
OFF (0) / NORMAL (1) / FULL (2) / EXTRA (3) |
同步级别。OFF 最快但可能损坏数据库;NORMAL 安全性/速度平衡;FULL 最安全。 |
cache_size |
正整数(页数)或负整数(KB) | SQLite 缓存页数。-8000 = 8MB。越大读性能越好。 |
page_size |
512, 1024, 2048, 4096, 8192, 16384, 32768, 65536 | 数据库页面大小。必须在首次建库时设置,4KB 或 8KB 通用。 |
temp_store |
DEFAULT (0) / FILE (1) / MEMORY (2) |
临时表存储位置。MEMORY 更快。 |
mmap_size |
字节数 | 内存映射大小。减少 I/O 调用。0 禁用。 |
busy_timeout |
毫秒 | 连接忙时等待时间。避免立即返回 SQLITE_BUSY。 |
foreign_keys |
ON / OFF |
外键约束开关。默认关闭以保持向后兼容。 |
auto_vacuum |
NONE (0) / FULL (1) / INCREMENTAL (2) |
自动回收空间。默认关闭。 |
case_sensitive_like |
ON / OFF |
LIKE 运算符是否大小写敏感。默认不敏感。 |
完整优化示例:
import sqlite3
def get_optimized_connection(db_path: str) -> sqlite3.Connection:
"""获取优化配置的 SQLite 连接"""
conn = sqlite3.connect(db_path)
conn.execute("PRAGMA journal_mode = WAL")
conn.execute("PRAGMA synchronous = NORMAL")
conn.execute("PRAGMA cache_size = -64000") # 64MB 缓存
conn.execute("PRAGMA temp_store = MEMORY")
conn.execute("PRAGMA busy_timeout = 5000")
conn.execute("PRAGMA foreign_keys = ON")
return conn5.2 批量插入优化
SQLite 单个 INSERT 语句的开销主要在事务提交上——每次提交都需要将数据刷新到磁盘,而磁盘 I/O 是数据库操作中最慢的环节。将多个插入包裹在同一个事务中,可以分摊事务提交的开销,性能可提升数十到数百倍。
import sqlite3
import time
conn = sqlite3.connect(':memory:')
conn.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)")
# ❌ 慢:每次 INSERT 自动提交
start = time.time()
for i in range(10000):
conn.execute(f"INSERT INTO test VALUES ({i}, 'value-{i}')")
print(f"逐条插入耗时: {time.time() - start:.3f}秒")
# 约 2-5 秒
# ✅ 快:手动事务包裹
conn.execute("DELETE FROM test")
start = time.time()
conn.execute("BEGIN TRANSACTION")
for i in range(10000):
conn.execute(f"INSERT INTO test VALUES ({i}, 'value-{i}')")
conn.commit()
print(f"事务包裹插入耗时: {time.time() - start:.3f}秒")
# 约 0.05-0.1 秒(快 20-50 倍)更多批量插入技巧:
# 使用 executemany(更快)
data = [(i, f'value-{i}') for i in range(10000)]
start = time.time()
conn.execute("BEGIN TRANSACTION")
conn.executemany("INSERT INTO test VALUES (?, ?)", data)
conn.commit()
print(f"executemany 耗时: {time.time() - start:.3f}秒")
# 约 0.03-0.05 秒
# 最大速度:PRAGMA 调优 + WAL + 事务
conn.execute("PRAGMA synchronous = OFF") # 牺牲安全性换取速度
conn.execute("PRAGMA journal_mode = MEMORY") # 内存日志
conn.execute("PRAGMA locking_mode = EXCLUSIVE") # 独占锁
data = [(i, f'value-{i}') for i in range(100000)]
start = time.time()
conn.execute("BEGIN TRANSACTION")
conn.executemany("INSERT INTO test VALUES (?, ?)", data)
conn.commit()
print(f"极速导入 10 万行: {time.time() - start:.3f}秒")
# 约 0.2-0.5 秒5.3 索引使用
索引是数据库查询性能的基石。合理使用索引可以将查询速度从全表扫描(O(n))提升到索引查找(O(log n)),对于百万级数据的表来说,性能差距可能在数千倍以上。
SQLite 的索引实现基于 B-Tree 数据结构,每个索引在数据库文件中都是一个独立的 B-Tree。创建索引的同时也在创建一棵新的树,因此索引会占用额外的磁盘空间,并略微降低写入性能(每次写入需要同时更新表和索引)。
-- 创建索引的基本原则
CREATE INDEX idx_orders_user ON orders(user_id); -- 外键列
CREATE INDEX idx_orders_status ON orders(status); -- 经常 WHERE
CREATE INDEX idx_orders_date ON orders(order_date DESC); -- 经常排序
CREATE INDEX idx_orders_user_date ON orders(user_id, order_date); -- 复合索引
-- 索引的选择性
-- 选择性 = COUNT(DISTINCT col) / COUNT(*)
-- 选择性越高,索引效果越好
-- 覆盖索引:包含查询所需所有列的索引
-- 这样查询可以只扫描索引而不访问表数据
CREATE INDEX idx_orders_summary ON orders(user_id, status, order_date, amount);
-- 此时以下查询只需扫描索引
SELECT order_date, amount
FROM orders
WHERE user_id = 100 AND status = 'completed'
ORDER BY order_date;
-- 分析查询计划
EXPLAIN QUERY PLAN
SELECT * FROM orders WHERE user_id = 100;
-- 输出:SEARCH orders USING INDEX idx_orders_user (user_id=?)
EXPLAIN QUERY PLAN
SELECT * FROM orders WHERE status = 'pending';
-- 如果 status 选择性不高,可能:SCAN orders索引注意事项:
-- 1. LIKE 索引使用规则
-- 前缀通配才可能使用索引
SELECT * FROM users WHERE email LIKE 'alice%'; -- 可能使用索引
SELECT * FROM users WHERE email LIKE '%example.com'; -- 无法使用索引
-- 2. 函数包裹会阻止索引使用
SELECT * FROM users WHERE LOWER(email) = 'alice@example.com'; -- 无法使用索引
-- 改为存储时统一小写
-- 3. OR 条件
SELECT * FROM users WHERE email = 'a@b.com' OR username = 'alice'; -- 可能用两个索引
-- 如果两个列都有索引,SQLite 可以用 INDEX 合并
-- 4. IS NULL 索引
-- SQLite 索引包含 NULL 值,IS NULL 条件能使用索引
-- 5. 多列索引的顺序
CREATE INDEX idx_a_b_c ON t(a, b, c);
-- 能使用索引的场景:a, a+b, a+b+c
-- 不能使用索引的场景:b, c, b+c5.4 VACUUM
SQLite 在删除数据或更新数据后,数据库文件大小不会自动减小。被删除的行所占用的空间被标记为"空闲",标记为数据库内部复用,但文件在磁盘上的大小不会缩小。VACUUM 命令通过重建整个数据库文件的方式,回收所有未使用的空间,并重新整理索引以提升访问效率。
当你的数据库经历频繁的删除和更新操作时,定期的 VACUUM 维护可以有效控制文件体积并提升性能。
-- 全量重建(会阻塞读写)
VACUUM;
-- 增量 VACUUM(需要设置 auto_vacuum)
PRAGMA auto_vacuum = INCREMENTAL;
-- 手动回收空间(以页面为单位)
PRAGMA incremental_vacuum(100); -- 回收 100 页
-- 查看数据库碎片情况
PRAGMA page_count; -- 总页数
PRAGMA freelist_count; -- 空闲页数(可回收的页)
-- 计算碎片率
-- 碎片率 = freelist_count * 100 / page_count
-- 碎片率 > 20% 时建议 VACUUM
-- VACUUM 的替代方案:导出再导入
sqlite3 mydb.db ".dump" | sqlite3 compacted.db
mv compacted.db mydb.dbVACUUM 注意事项:
- VACUUM 需要约等于数据库文件大小的额外磁盘空间
- VACUUM 会重建所有索引
- VACUUM 期间表是只读的
- 频繁 VACUUM 不可取,回收超过 20% 空闲页时再做
5.5 完整性能优化清单
import sqlite3
def apply_performance_pragmas(conn: sqlite3.Connection, is_read_only: bool = False):
"""应用全套性能优化 PRAGMA"""
# 并发与安全
conn.execute("PRAGMA journal_mode = WAL")
conn.execute("PRAGMA synchronous = NORMAL")
conn.execute("PRAGMA busy_timeout = 5000")
# 缓存与内存
conn.execute("PRAGMA cache_size = -64000") # 64MB
conn.execute("PRAGMA temp_store = MEMORY")
conn.execute("PRAGMA mmap_size = 268435456") # 256MB
# 约束
conn.execute("PRAGMA foreign_keys = ON")
# 只读连接可进一步优化
if is_read_only:
conn.execute("PRAGMA query_only = ON")
conn.execute("PRAGMA cache_spill = OFF")6. 实际应用场景
SQLite 的最大优势在于它的通用性和适应性。从航天器的飞控系统到手机的社交应用,从浏览器的 WebAssembly 到企业的数据分析管道,SQLite 几乎无处不在。本节将通过完整的代码示例,展示 SQLite 在五个最重要的应用场景中的实际用法。
6.1 移动端(Flutter / Android / iOS)
在移动端,SQLite 是本地数据存储的标准方案。几乎所有的主流移动应用都使用 SQLite 来存储结构化数据——通讯录、短信、聊天记录、待办事项、离线缓存等。Android 和 iOS 系统均内置了 SQLite 引擎。
Flutter 使用 sqflite
// pubspec.yaml
// dependencies:
// sqflite: ^2.3.0
// path_provider: ^2.1.0
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init();
static Database? _database;
DatabaseHelper._init();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB('app.db');
return _database!;
}
Future<Database> _initDB(String filePath) async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, filePath);
return await openDatabase(
path,
version: 1,
onCreate: _createDB,
onUpgrade: _upgradeDB,
// 优化设置
readOnly: false,
singleInstance: true,
);
}
Future<void> _createDB(Database db, int version) async {
await db.execute('''
CREATE TABLE notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime')),
updated_at TEXT DEFAULT (datetime('now', 'localtime'))
)
''');
await db.execute('''
CREATE INDEX idx_notes_created ON notes(created_at DESC)
''');
}
Future<int> insertNote(String title, String content) async {
final db = await database;
return await db.insert('notes', {
'title': title,
'content': content,
});
}
Future<List<Map<String, dynamic>>> getAllNotes() async {
final db = await database;
return await db.query('notes', orderBy: 'created_at DESC');
}
}Android(Kotlin + Room)
// build.gradle.kts
// implementation("androidx.room:room-runtime:2.6.1")
// annotationProcessor("androidx.room:room-compiler:2.6.1")
import androidx.room.*
@Entity(tableName = "tasks")
data class Task(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "completed") val completed: Boolean = false,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
)
@Dao
interface TaskDao {
@Query("SELECT * FROM tasks ORDER BY created_at DESC")
suspend fun getAllTasks(): List<Task>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: Task): Long
@Update
suspend fun updateTask(task: Task)
@Delete
suspend fun deleteTask(task: Task)
}
@Database(entities = [Task::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"tasks_database"
).build()
INSTANCE = instance
instance
}
}
}
}6.2 桌面应用(Electron)
桌面应用是 SQLite 的另一个重要战场。许多知名的 Electron 应用(如 VS Code、Slack、Discord、Notion)都使用 SQLite 来管理本地数据。相比于在用户机器上部署 MySQL 或 PostgreSQL(这几乎不可能也不需要),SQLite 是桌面应用本地数据存储的理想选择——它不需要任何服务进程,不会增加部署复杂度,而且性能对单用户场景来说绰绰有余。
以下是一个使用 better-sqlite3(Node.js 中最快的 SQLite 绑定库)构建的桌面应用数据库类的完整示例:
// Electron + better-sqlite3
const Database = require('better-sqlite3');
const path = require('path');
const { app } = require('electron');
class AppDatabase {
constructor() {
const dbPath = path.join(app.getPath('userData'), 'app.db');
this.db = new Database(dbPath);
this._init();
}
_init() {
// 启用 WAL 模式以获得更好的并发性能
this.db.pragma('journal_mode = WAL');
this.db.pragma('foreign_keys = ON');
this.db.exec(`
CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_documents_project
ON documents(project_id);
`);
}
// 准备语句缓存 —— 提升重复查询性能
_prepare(sql) {
if (!this._stmtCache) this._stmtCache = new Map();
if (!this._stmtCache.has(sql)) {
this._stmtCache.set(sql, this.db.prepare(sql));
}
return this._stmtCache.get(sql);
}
getProjects() {
return this._prepare('SELECT * FROM projects ORDER BY updated_at DESC').all();
}
createProject(name, description) {
const info = this._prepare(
'INSERT INTO projects (name, description) VALUES (?, ?)'
).run(name, description);
return info.lastInsertRowid;
}
// 事务操作
createDocumentWithTransaction(projectId, title, content) {
const transaction = this.db.transaction(() => {
const info = this._prepare(
'INSERT INTO documents (project_id, title, content) VALUES (?, ?, ?)'
).run(projectId, title, content);
this._prepare(
'UPDATE projects SET updated_at = datetime(\'now\') WHERE id = ?'
).run(projectId);
return info.lastInsertRowid;
});
return transaction();
}
close() {
this.db.close();
}
}
module.exports = AppDatabase;6.3 嵌入式设备
在嵌入式领域,SQLite 几乎是事实上的标准数据库。物联网设备、工业控制器、智能家居中枢、车载信息娱乐系统——这些资源受限的环境正是 SQLite 大放异彩的地方。嵌入式设备通常只有几 MB 到几十 MB 的 RAM 和有限的闪存存储,SQLite 的极小体积和精细的资源控制完全适配这类场景。
以下是一个使用 C 语言在嵌入式 Linux 设备上操作 SQLite 的完整示例,包含了针对嵌入式环境的优化配置:
// C 语言嵌入式 SQLite 示例(ESP32 / ARM Linux)
#include <sqlite3.h>
#include <stdio.h>
int main() {
sqlite3 *db;
char *err_msg = NULL;
// 1. 打开数据库(存储在闪存或 SD 卡)
int rc = sqlite3_open("/data/sensor.db", &db);
if (rc != SQLITE_OK) {
fprintf(stderr, "无法打开数据库: %s\n", sqlite3_errmsg(db));
return 1;
}
// 2. 嵌入式环境优化 PRAGMA
const char *pragmas[] = {
"PRAGMA journal_mode = WAL", // 减少写入放大
"PRAGMA synchronous = OFF", // 承受掉电风险时关闭同步
"PRAGMA cache_size = -2000", // 2MB 缓存(嵌入式够用)
"PRAGMA temp_store = MEMORY", // 临时数据放内存
"PRAGMA page_size = 512", // 小页面节省闪存写入
"PRAGMA auto_vacuum = INCREMENTAL", // 增量回收空间
};
for (int i = 0; i < sizeof(pragmas)/sizeof(pragmas[0]); i++) {
rc = sqlite3_exec(db, pragmas[i], 0, 0, &err_msg);
if (rc != SQLITE_OK) {
fprintf(stderr, "PRAGMA 错误: %s\n", err_msg);
sqlite3_free(err_msg);
}
}
// 3. 创建表
const char *sql_create =
"CREATE TABLE IF NOT EXISTS sensor_data ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" sensor_id INTEGER NOT NULL,"
" temperature REAL,"
" humidity REAL,"
" pressure REAL,"
" recorded_at TEXT DEFAULT (datetime('now'))"
");";
rc = sqlite3_exec(db, sql_create, 0, 0, &err_msg);
if (rc != SQLITE_OK) {
fprintf(stderr, "建表错误: %s\n", err_msg);
sqlite3_free(err_msg);
}
// 4. 批量插入(事务包裹)
sqlite3_exec(db, "BEGIN TRANSACTION", 0, 0, 0);
sqlite3_stmt *stmt;
sqlite3_prepare_v2(db,
"INSERT INTO sensor_data (sensor_id, temperature, humidity, pressure) "
"VALUES (?, ?, ?, ?)",
-1, &stmt, 0);
// 模拟 100 条传感器读数
for (int i = 0; i < 100; i++) {
sqlite3_bind_int(stmt, 1, 1);
sqlite3_bind_double(stmt, 2, 25.0 + (i % 10) * 0.5);
sqlite3_bind_double(stmt, 3, 60.0 + (i % 20));
sqlite3_bind_double(stmt, 4, 1013.25 + (i % 5));
sqlite3_step(stmt);
sqlite3_reset(stmt);
}
sqlite3_exec(db, "COMMIT", 0, 0, 0);
sqlite3_finalize(stmt);
// 5. 定期 VACUUM 控制闪存磨损
// sqlite3_exec(db, "VACUUM", 0, 0, 0);
sqlite3_close(db);
return 0;
}嵌入式设备上的最佳实践:
- 使用 WAL 模式:减少随机写入,延长闪存寿命
- 批量事务:将 N 个 INSERT 合并到一个事务中,写入量减少 N 倍
- 控制页面大小:闪存块大小通常为 4KB,但小页面可减少写入放大
- 定期维护:设定阈值自动 VACUUM(如写入 10000 条后)
- 电量安全:使用
PRAGMA synchronous = OFF时需评估掉电风险
6.4 测试数据库
在软件测试领域,SQLite 的内存数据库模式是一个不可多得的利器。使用内存数据库进行单元测试,有以下几个显著优势:第一,测试速度极快——所有数据操作都在内存中完成,没有磁盘 I/O 开销;第二,每次测试都是独立环境——setUp 中创建全新的数据库,tearDown 中自动销毁,不同测试用例之间不会互相污染;第三,零配置——不需要外部数据库服务器,不需要安装数据库驱动,不需要管理测试数据文件。
# Python 单元测试示例
import sqlite3
import unittest
class TestUserRepository(unittest.TestCase):
def setUp(self):
"""每个测试方法前执行:创建内存数据库"""
self.conn = sqlite3.connect(':memory:')
self.conn.execute("PRAGMA foreign_keys = ON")
self.conn.executescript('''
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE
);
CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
);
''')
def tearDown(self):
"""每个测试方法后执行:关闭连接"""
self.conn.close()
def test_insert_user(self):
self.conn.execute(
"INSERT INTO users (username, email) VALUES (?, ?)",
('alice', 'alice@example.com')
)
cursor = self.conn.execute("SELECT * FROM users WHERE username = ?", ('alice',))
user = cursor.fetchone()
self.assertIsNotNone(user)
self.assertEqual(user[1], 'alice')
def test_unique_username(self):
self.conn.execute(
"INSERT INTO users (username, email) VALUES (?, ?)",
('bob', 'bob@example.com')
)
with self.assertRaises(sqlite3.IntegrityError):
self.conn.execute(
"INSERT INTO users (username, email) VALUES (?, ?)",
('bob', 'bob2@example.com')
)
def test_foreign_key_cascade(self):
# 插入用户
self.conn.execute(
"INSERT INTO users (username, email) VALUES (?, ?)",
('charlie', 'charlie@example.com')
)
user_id = self.conn.execute(
"SELECT id FROM users WHERE username = ?", ('charlie',)
).fetchone()[0]
# 插入文章
self.conn.execute(
"INSERT INTO posts (user_id, title) VALUES (?, ?)",
(user_id, '测试文章')
)
# 验证级联删除(需要设置 PRAGMA foreign_keys = ON)
self.conn.execute("DELETE FROM users WHERE id = ?", (user_id,))
posts = self.conn.execute(
"SELECT * FROM posts WHERE user_id = ?", (user_id,)
).fetchall()
self.assertEqual(len(posts), 0)
if __name__ == '__main__':
unittest.main()测试数据库的最佳实践:
# 使用 pytest fixture 自动管理生命周期
import pytest
import sqlite3
@pytest.fixture
def db():
"""提供测试用内存数据库"""
conn = sqlite3.connect(':memory:')
conn.execute("PRAGMA foreign_keys = ON")
# 执行迁移
conn.executescript("""
CREATE TABLE users (...);
CREATE TABLE posts (...);
""")
yield conn
conn.close()
def test_create_user(db):
db.execute("INSERT INTO users (username) VALUES ('test')")
count = db.execute("SELECT COUNT(*) FROM users").fetchone()[0]
assert count == 16.5 Web 端(sql.js 在浏览器中运行 SQLite)
随着 WebAssembly 技术的发展,SQLite 现在可以在浏览器中完整运行。sql.js 是一个使用 Emscripten 将 SQLite C 代码编译为 WebAssembly 的库,它允许你在 JavaScript 中调用完整的 SQLite C API,在浏览器中创建一个功能完整的数据库引擎。这意味着你可以构建完全离线可用的 Web 应用,数据完全存储在浏览器的内存中。
sql.js 的神奇之处在于:它编译的是完整的 SQLite 代码,因此 SQLite 的所有功能——包括事务、触发器、视图、索引、JSON 函数等——在浏览器中都是可用的。唯一的限制是部分操作系统级别的功能(如 WAL 模式)无法在浏览器沙箱中实现。
<!-- 浏览器端 HTML + sql.js 示例 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>sql.js 浏览器数据库演示</title>
<script src="https://cdn.jsdelivr.net/npm/sql.js@1.11/dist/sql-wasm.js"></script>
</head>
<body>
<h1>浏览器内 SQLite 数据库</h1>
<div>
<button onclick="initDB()">初始化数据库</button>
<button onclick="queryData()">查询数据</button>
<button onclick="exportDB()">导出数据库文件</button>
<button onclick="importDB()">导入数据库文件</button>
<input type="file" id="fileInput" accept=".db" style="display:none">
</div>
<pre id="output" style="background:#f4f4f4; padding:16px; margin-top:16px; font-size:14px;"></pre>
<script>
let db = null;
function log(msg) {
document.getElementById('output').textContent += msg + '\n';
}
async function initDB() {
const SQL = await initSqlJs({
locateFile: file => `https://cdn.jsdelivr.net/npm/sql.js@1.11/dist/${file}`
});
db = new SQL.Database();
// 执行迁移
db.run(`
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
)
`);
// 插入示例数据
db.run("INSERT INTO todos (title) VALUES ('学习 SQLite')");
db.run("INSERT INTO todos (title) VALUES ('学习 sql.js')");
db.run("INSERT INTO todos (title) VALUES ('写一篇技术文章')");
log('✅ 数据库已初始化');
queryData();
}
function queryData() {
if (!db) {
log('❌ 请先初始化数据库');
return;
}
const stmt = db.prepare("SELECT * FROM todos ORDER BY created_at DESC");
log('\n📋 待办事项列表:');
while (stmt.step()) {
const row = stmt.getAsObject();
log(` [${row.id}] ${row.title} — ${row.completed ? '✅ 已完成' : '⬜ 待完成'} (${row.created_at})`);
}
stmt.free();
}
function exportDB() {
if (!db) return;
const data = db.export();
const blob = new Blob([data], {type: 'application/x-sqlite3'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'browser_data.db';
a.click();
URL.revokeObjectURL(url);
log('💾 数据库已导出为 browser_data.db');
}
function importDB() {
document.getElementById('fileInput').click();
}
document.getElementById('fileInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async function() {
const SQL = await initSqlJs({
locateFile: file => `https://cdn.jsdelivr.net/npm/sql.js@1.11/dist/${file}`
});
const buffer = new Uint8Array(this.result);
db = new SQL.Database(buffer);
log('📂 已导入数据库文件');
queryData();
};
reader.readAsArrayBuffer(file);
});
</script>
</body>
</html>sql.js 的典型应用场景:
- 离线优先的 Web 应用:PWA 应用在浏览器端存储和处理数据
- 数据格式转换工具:CSV ↔ JSON ↔ SQLite 的纯浏览器转换
- 在线 SQL 学习平台:用户在浏览器中编写和测试 SQL
- 静态网站的数据查询:预先生成 SQLite 数据库,sql.js 在浏览器端查询
- 数据验证和预览:上传数据库文件,在浏览器中进行验证
sql.js 的局限:
- 数据库完全在内存中,刷新页面数据丢失(需手动持久化)
- 不支持 WAL 模式(WebAssembly 环境限制)
- 文件大小受浏览器内存限制
总结
SQLite 以简洁、可靠、零配置的设计理念,成为了嵌入式数据库领域的标杆。它不是一个"玩具数据库",而是一个经受了数十亿设备检验的生产级数据库引擎。
选择 SQLite 的核心三问:
- 你的应用是单机还是分布式? — 单机选 SQLite,分布式选 MySQL/PostgreSQL
- 并发写入量有多大? — 低到中等并发选 SQLite
- 部署复杂度要求? — 越简单越好,选 SQLite
快速上手建议:
- 安装
sqlite3CLI 工具 - 用 Python 标准库的
sqlite3模块练习 - 掌握
PRAGMA journal_mode = WAL和事务批量写入 - 为常用查询建立索引
- 根据场景选择 FTS5 全文搜索或 JSON 查询
SQLite 的座右铭是 "Think small, do big"——它体积虽小,却承载着从航天器到智能手机的无数关键数据。掌握 SQLite,是每一位开发者值得投入的技能。