SQLite 使用指南:轻量级嵌入式数据库

2024-09-14 后端

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-dev

Linux(CentOS/RHEL/Fedora):

sudo yum install sqlite
# 或 dnf
sudo dnf install sqlite

macOS:

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:27

Python 中使用

Python 标准库内置了 sqlite3 模块,无需额外安装:

import sqlite3
print(sqlite3.sqlite_version)
# 输出:3.46.0

Node.js 中使用

npm install better-sqlite3
# 或者
npm install sql.js  # 浏览器端可用

Go 中使用

go mod init myapp
go get github.com/mattn/go-sqlite3

Rust 中使用

# 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
-- 输出数据库文件路径

-- 查看当前配置
.show

2.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()。

3.4 窗口函数(3.25+)

自 SQLite 3.25.0 起支持窗口函数,3.28.0 起支持 EXCLUDE 子句。

3.5 UPSERT 操作

SQLite 支持 INSERT ... ON CONFLICT(3.24.0+)和 INSERT OR REPLACE 两种 UPSERT 语法。

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;
-- 输出:wal

WAL 模式的优势:

  • 并发读写:一个写入者不会阻塞读取者。读取者和写入者可以同时工作。
  • 写入性能更好:批量写入时速度更快。
  • 读性能略微提升:读操作不阻塞,总体吞吐量更高。

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 通过创建虚拟表的方式工作:

JSON 查询(3.9.0+)

SQLite 内置了强大的 JSON 函数,无需额外扩展。

4.5 附加数据库

ATTACH DATABASE 是 SQLite 中一个非常实用的功能,它允许你在同一个数据库连接中同时打开和操作多个数据库文件。这意味着你可以跨库进行 JOIN 查询、跨库复制数据、甚至将内存数据库与磁盘数据库结合使用。这个功能在数据迁移、分库管理、以及多数据源整合的场景中极为有用。

当你打开一个 SQLite 数据库时,该数据库自动被命名为 main。通过 ATTACH DATABASE 附加的数据库会被赋予你指定的别名。


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 conn

5.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+c

5.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.db

VACUUM 注意事项:

  • 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

Android(Kotlin + Room)

6.2 桌面应用(Electron)

桌面应用是 SQLite 的另一个重要战场。许多知名的 Electron 应用(如 VS Code、Slack、Discord、Notion)都使用 SQLite 来管理本地数据。相比于在用户机器上部署 MySQL 或 PostgreSQL(这几乎不可能也不需要),SQLite 是桌面应用本地数据存储的理想选择——它不需要任何服务进程,不会增加部署复杂度,而且性能对单用户场景来说绰绰有余。

以下是一个使用 better-sqlite3(Node.js 中最快的 SQLite 绑定库)构建的桌面应用数据库类的完整示例:

6.3 嵌入式设备

在嵌入式领域,SQLite 几乎是事实上的标准数据库。物联网设备、工业控制器、智能家居中枢、车载信息娱乐系统——这些资源受限的环境正是 SQLite 大放异彩的地方。嵌入式设备通常只有几 MB 到几十 MB 的 RAM 和有限的闪存存储,SQLite 的极小体积和精细的资源控制完全适配这类场景。

以下是一个使用 C 语言在嵌入式 Linux 设备上操作 SQLite 的完整示例,包含了针对嵌入式环境的优化配置:

嵌入式设备上的最佳实践:

  • 使用 WAL 模式:减少随机写入,延长闪存寿命
  • 批量事务:将 N 个 INSERT 合并到一个事务中,写入量减少 N 倍
  • 控制页面大小:闪存块大小通常为 4KB,但小页面可减少写入放大
  • 定期维护:设定阈值自动 VACUUM(如写入 10000 条后)
  • 电量安全:使用 PRAGMA synchronous = OFF 时需评估掉电风险

6.4 测试数据库

在软件测试领域,SQLite 的内存数据库模式是一个不可多得的利器。使用内存数据库进行单元测试,有以下几个显著优势:第一,测试速度极快——所有数据操作都在内存中完成,没有磁盘 I/O 开销;第二,每次测试都是独立环境——setUp 中创建全新的数据库,tearDown 中自动销毁,不同测试用例之间不会互相污染;第三,零配置——不需要外部数据库服务器,不需要安装数据库驱动,不需要管理测试数据文件。

测试数据库的最佳实践:

# 使用 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 == 1

6.5 Web 端(sql.js 在浏览器中运行 SQLite)

随着 WebAssembly 技术的发展,SQLite 现在可以在浏览器中完整运行。sql.js 是一个使用 Emscripten 将 SQLite C 代码编译为 WebAssembly 的库,它允许你在 JavaScript 中调用完整的 SQLite C API,在浏览器中创建一个功能完整的数据库引擎。这意味着你可以构建完全离线可用的 Web 应用,数据完全存储在浏览器的内存中。

sql.js 的神奇之处在于:它编译的是完整的 SQLite 代码,因此 SQLite 的所有功能——包括事务、触发器、视图、索引、JSON 函数等——在浏览器中都是可用的。唯一的限制是部分操作系统级别的功能(如 WAL 模式)无法在浏览器沙箱中实现。

sql.js 的典型应用场景:

  • 离线优先的 Web 应用:PWA 应用在浏览器端存储和处理数据
  • 数据格式转换工具:CSV ↔ JSON ↔ SQLite 的纯浏览器转换
  • 在线 SQL 学习平台:用户在浏览器中编写和测试 SQL
  • 静态网站的数据查询:预先生成 SQLite 数据库,sql.js 在浏览器端查询
  • 数据验证和预览:上传数据库文件,在浏览器中进行验证

sql.js 的局限:

  • 数据库完全在内存中,刷新页面数据丢失(需手动持久化)
  • 不支持 WAL 模式(WebAssembly 环境限制)
  • 文件大小受浏览器内存限制

总结

SQLite 以简洁、可靠、零配置的设计理念,成为了嵌入式数据库领域的标杆。它不是一个"玩具数据库",而是一个经受了数十亿设备检验的生产级数据库引擎。

选择 SQLite 的核心三问:

  1. 你的应用是单机还是分布式? — 单机选 SQLite,分布式选 MySQL/PostgreSQL
  2. 并发写入量有多大? — 低到中等并发选 SQLite
  3. 部署复杂度要求? — 越简单越好,选 SQLite

快速上手建议:

  1. 安装 sqlite3 CLI 工具
  2. 用 Python 标准库的 sqlite3 模块练习
  3. 掌握 PRAGMA journal_mode = WAL 和事务批量写入
  4. 为常用查询建立索引
  5. 根据场景选择 FTS5 全文搜索或 JSON 查询

SQLite 的座右铭是 "Think small, do big"——它体积虽小,却承载着从航天器到智能手机的无数关键数据。掌握 SQLite,是每一位开发者值得投入的技能。


参考资源