什么是 SQLite?无服务器的基于文件的数据库的优点、局限性和实际用途

摘要

SQLite 是一种基于文件的关系数据库,它在应用程序进程中运行,无需运行单独的服务器。因为“轻”字,很容易让人误会它是一个简单的缓存,但它是一个实际的 SQL 数据库,具有事务、索引、连接和 WAL 等功能,广泛应用于移动应用、桌面应用、嵌入式设备、测试环境和小型 Web 服务。然而,同时写入处理、类型规则、外键设置和网络文件系统等方面必须与服务器型数据库不同。

目录

我们先来了解一下什么是SQLite

SQLite sqlite3 该库直接包含在应用程序中并运行。 嵌入式SQL数据库引擎没看到。应用程序不是像 MySQL 或 PostgreSQL 那样通过 TCP 连接到单独的数据库服务器进程,而是在同一进程中调用 SQLite 库,并将数据存储在单个数据库文件中。

官方文档将 SQLite 描述为“独立、无服务器、零配置、事务性 SQL 数据库引擎”。如果我们从实际的角度来解释这个表达式,则如下。

特征 意义 实践中的优势
Serverless 没有单独的数据库服务器 部署、设置和操​​作负担低
Self-contained 大多数功能都包含在小型库中 易于放入移动/桌面/CLI
Zero-configuration 安装后几乎不需要服务器设置 非常适合测试、本地开发和示例应用程序
Transactional ACID 事务支持 虽然它是一个文件数据库,但原子更改是可以预期的
Public domain 公共领域许可 在商业/个人项目中使用负担轻

先前组织的 由西瓜数据库撰写SQLite 也经常作为内部存储出现。有很多结构,例如 WatermelonDB、Android Room 和 iOS Core Data 的 SQLite 后端,“用户使用库,但将实际存储留给 SQLite 文件”。

SQLite 运行良好的情况以及应避免的情况

选择 SQLite 时最重要的标准是“数据是否围绕一个应用程序实例或一台设备流动?” SQLite 非常适合作为单个文件管理的本地数据库,但不适合多个服务器同时处理高写入负载的中央数据库角色。

当 SQLite 适合时

  • 移动应用程序的本地存储
  • 保存桌面应用程序、Electron 应用程序和 CLI 工具的设置/状态
  • 物联网和嵌入式设备的本地数据记录
  • 测试数据库、演示应用程序、本地开发环境
  • 具有读密集型或低写竞争性的小型服务
  • 当数据需要作为单个文件移动时,例如应用程序文件格式

特别是,即使网络连接丢失,应用程序内部的数据通常也需要能够继续读取和写入。此时,SQLite非常适合离线优先的结构,因为它没有服务器依赖性,文件备份也相对简单。

何时仔细查看 SQLite

  • 多个服务器实例想要同时写入同一个数据库文件的结构
  • 每秒持续经历许多写入事务的中央服务。
  • 需要DB服务器访问控制、用户权限和网络访问管理的环境
  • 您想要将数据库文件放置在网络文件系统(例如 NFS/SMB)上的环境。
  • 复杂的操作监控、复制和故障转移至关重要的服务

这并不意味着 SQLite 是一个“小而弱的数据库”。问题是角色。当用作本地文件数据库时,它的功能非常强大,但如果您尝试替换 PostgreSQL 或 MySQL 所扮演的中央服务器数据库角色,那么锁定、备份、操作权限和并发性方面的限制很快就会显现出来。

核心操作:文件、事务、类型、WAL

一个数据库文件

SQLite 数据库通常是 .sqlite, .db 它被保存为一个文件。表、索引、触发器和视图都包含在该文件中。因此,备份可能看起来像是简单的文件复制,但在复制正在运行的 DB 文件时,还必须考虑事务状态和 WAL 文件。

如果运行过程中需要安全备份,可以使用SQLite CLI .backup 最好使用命令或在线备份API。

sqlite3 app.db ".backup 'backup-$(date +%Y%m%d).db'"

事务和并发

SQLite 支持事务。通过将多个 SQL 语句分组到一个工作单元中,您可以在它们过早失败时进行回滚。

BEGIN IMMEDIATE;

UPDATE accounts
SET balance = balance - 10000
WHERE id = 1;

UPDATE accounts
SET balance = balance + 10000
WHERE id = 2;

COMMIT;

关于并发的重要一点是 可以同时进行多次读取,但写入同一个数据库文件基本上一次集中在一个写入器上。重点是。尽管 WAL 模式可以减少读写冲突,但它无法为写入争用非常高的工作负载提供无限解决方案。

沃尔玛模式

预写日志记录 (WAL) 是一种首先将更改记录到单独的 WAL 文件的方法。官方文档解释说,WAL 在大多数场景下都可以更快,并以读取器不阻塞写入器、写入器不阻塞读取器的方式提供更好的并发性。

PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA busy_timeout = 5000;

上述设置在应用程序开发中经常使用,但不一定在所有环境下都是正确的答案。数据库文件旁边的WAL app.db-wal, app.db-shm 您可以创建文件,但在网络文件系统或多进程分布结构中需要小心。

灵活的类型系统和 STRICT 表

SQLite 与 PostgreSQL 或 MySQL 不同,后者的列类型严格限制值。正如官方文档中所表达的,SQLite 有一个附加到值本身的类型。 动态类型系统使用 。例如 INTEGER根据情况声明为的列还可能包含不同类型的值。

这种灵活性有时很方便,但在实践中,您可能会想,“为什么字符串位于数字列中?”它可能会产生相同的错误。如果您需要更严格的类型检查,您可以使用 STRICT 你可以考虑一下这个表:

CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  email TEXT NOT NULL UNIQUE,
  age INTEGER CHECK (age >= 0),
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
) STRICT;

养成为每个连接打开外键的习惯是安全的。

SQLite 支持外键约束,但从官方文档来看,出于向后兼容性的原因,不要采用默认值并在每个连接的基础上使用它们。 PRAGMA foreign_keys = ON;指定是一个安全的习惯。

PRAGMA foreign_keys = ON;

CREATE TABLE authors (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL
);

CREATE TABLE posts (
  id INTEGER PRIMARY KEY,
  author_id INTEGER NOT NULL,
  title TEXT NOT NULL,
  FOREIGN KEY (author_id) REFERENCES authors(id)
);

每当您在 Node.js、Python、React Native、Android 等中创建新的 SQLite 连接时,您需要确保应用此设置。即使您使用 ORM 或包装器库,如果您不检查实际的连接选项,外键的行为也可能与预期不同。

立即运行的 SQLite 基本示例

macOS 和许多 Linux 发行版都有 sqlite3 CLI 已包含在内,或者可以使用包管理器轻松安装。

sqlite3 blog.db

让我们创建一个简单的表并向其中放入一些数据。

PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;

CREATE TABLE IF NOT EXISTS notes (
  id INTEGER PRIMARY KEY,
  title TEXT NOT NULL,
  body TEXT NOT NULL,
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
) STRICT;

INSERT INTO notes (title, body)
VALUES ('SQLite 테스트', '서버 없이 파일 하나로 저장되는 로컬 DB 예제입니다.');

SELECT id, title, created_at
FROM notes
ORDER BY id DESC;

如果你想从 CLI 一次性运行它,你可以像这样使用它:

sqlite3 blog.db <<'SQL'
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;

CREATE TABLE IF NOT EXISTS notes (
  id INTEGER PRIMARY KEY,
  title TEXT NOT NULL,
  body TEXT NOT NULL,
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
) STRICT;

INSERT INTO notes (title, body)
VALUES ('SQLite 테스트', '서버 없이 파일 하나로 저장되는 로컬 DB 예제입니다.');

SELECT id, title, created_at
FROM notes
ORDER BY id DESC;
SQL

在Python中,标准库 sqlite3您可以单独使用它。

import sqlite3

with sqlite3.connect("blog.db") as conn:
    conn.execute("PRAGMA foreign_keys = ON")
    conn.execute("PRAGMA busy_timeout = 5000")
    conn.execute("""
        CREATE TABLE IF NOT EXISTS notes (
            id INTEGER PRIMARY KEY,
            title TEXT NOT NULL,
            body TEXT NOT NULL,
            created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
        ) STRICT
    """)
    conn.execute(
        "INSERT INTO notes (title, body) VALUES (?, ?)",
        ("SQLite 테스트", "파라미터 바인딩으로 SQL 인젝션을 피합니다.")
    )
    rows = conn.execute("SELECT id, title FROM notes ORDER BY id DESC").fetchall()

print(rows)

这里重要的部分是字符串格式化,而不是直接组装 SQL。 ? 重点是使用了参数。仅仅因为 SQLite 是本地数据库并不意味着 SQL 注入的风险就消失了。

实用的最佳实践

1.清除连接初始化SQL

SQLite 有许多针对每个连接的设置。在启动应用程序或创建连接时将初始化 SQL 收集到一处可以减少由于环境差异而导致的错误。

PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
PRAGMA journal_mode = WAL;

步, journal_mode是保存在DB文件中的设置,根据执行环境的不同,结果可能会有所不同,所以最好也检查一下实际的返回值。

PRAGMA journal_mode;

2. 保持写事务简短

在 SQLite 中,长写入事务可能会导致其他写入操作等待。建议仅将数据库更改所需的最少部分捆绑到事务中,而不是在事务中包含文件下载、网络调用和图像处理等缓慢操作。

3.根据查询模式创建索引

索引设计在 SQLite 中也很重要。考虑为经常搜索或排序的列建立索引。 EXPLAIN QUERY PLAN检查是否实际使用过。

CREATE INDEX IF NOT EXISTS idx_notes_created_at
ON notes (created_at DESC);

EXPLAIN QUERY PLAN
SELECT id, title
FROM notes
WHERE created_at >= '2026-01-01'
ORDER BY created_at DESC;

4. 团队确定日期/时间存储格式。

SQLite 没有单独的专用日期类型。通常,您选择以下选项之一:ISO-8601 字符串、Unix 时间整数或儒略日数字。如果应用程序没有一起设置时区规则,则排序、比较和显示时可能会出现混乱。

在实践中,很容易理解如何存储基于 UTC 的 ISO-8601 字符串,并在屏幕上显示时将其转换为用户的时区。

5. SQLite工具优先于文件复制进行备份

当您的应用程序使用数据库时进行简单的文件复制可能会丢失 WAL 文件或检查点状态。运行期间备份 .backup 或者,首先查看 SQLite Online Backup API。

sqlite3 app.db ".backup backup.db"
sqlite3 backup.db "PRAGMA integrity_check;"

常见的误解和错误

“SQLite 是一个用于测试的玩具数据库”

不。 SQLite 用于非常广泛的应用程序,包括浏览器、移动操作系统、应用程序和嵌入式设备。但其用途与服务器型DB不同。它在本地、嵌入式、面向读取和面向单个文件的问题上很强。

“如果开启WAL,所有并发写入问题都将得到解决。”

WAL 有助于减少读者和作者的冲突。但是,对于同一数据库文件的写入争用较高的结构仍然存在限制。您可能需要将写入操作序列化到队列,使事务更短,或者重新考虑是否真的需要中央数据库。

“自动增量必须始终添加到整数主键”

SQLite官方文档是 AUTOINCREMENT会产生额外的 CPU、内存、磁盘和 I/O 开销,除非绝对必要,否则应避免使用。大多数时候 INTEGER PRIMARY KEY仅此而已。 AUTOINCREMENT仅当您有特殊需要避免重复使用以前使用过的 ROWID 时才应进行审查。

-- 대부분의 경우 충분합니다.
CREATE TABLE items (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL
);

-- 이전 ROWID 재사용 방지 요구가 있을 때만 신중히 검토합니다.
CREATE TABLE audit_events (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  message TEXT NOT NULL
);

“因为您声明了外键,所以总是会自动检查它”

外键声明和实际检查应该分开查看。每个连接 PRAGMA foreign_keys = ON;,并进行测试以确保实际上防止了约束违规。

故障案例及解决方案

案例1。 database is locked 错误重复出现

症状:在应用程序中间歇性地出现 database is locked, SQLITE_BUSY 发生错误。当多个线程或进程同时写入时,这种情况尤其常见。

主要原因

  • 长写事务会长时间持有锁。
  • 通过连接 busy_timeout太短或没有
  • 多个写入者同时访问同一个数据库文件
  • 执行缓慢的操作,例如事务中的网络调用
  • 在网络文件系统上使用 SQLite 文件

如何检查

PRAGMA journal_mode;
PRAGMA busy_timeout;
# macOS/Linux에서 DB 파일을 잡고 있는 프로세스 확인 예시
lsof app.db

解决方向

PRAGMA busy_timeout = 5000;
PRAGMA journal_mode = WAL;
  • 保持写事务简短。
  • 在应用程序中,考虑使用写入队列进行顺序处理。
  • 不要将 SQLite 文件放在网络共享文件夹中。
  • 如果多个服务器需要共享同一个数据库文件,请考虑使用服务器类型数据库,例如 PostgreSQL/MySQL,而不是 SQLite。

情况2、使用了外键,但输入了错误的数据。

症状: FOREIGN KEY已声明,但插入没有父行的数据。

原因:您很可能没有在 SQLite 连接中打开外键检查。根据官方文档,外键是 PRAGMA foreign_keys = ON;它必须在运行时通过激活并针对每个连接应用。

确认命令

PRAGMA foreign_keys;
PRAGMA foreign_key_check;

如何解决

PRAGMA foreign_keys = ON;

在您的应用程序代码中,每次创建连接时都运行初始化例程。如果您使用 ORM,则需要检查此 PRAGMA 是否应用于 ORM 的 SQLite 连接选项或事件挂钩中。

情况3、在数字列中输入一个字符串,后面排序很奇怪。

症状: INTEGER它是声明为字符串值的列,排序或比较结果与预期不同。

原因:SQLite 的基本类型系统非常灵活。您不应该认为仅通过列声明就可以强有力地防止所有类型错误。

确认命令

SELECT id, age, typeof(age)
FROM users
WHERE typeof(age) <> 'integer';

如何解决

  • 新表是 STRICT 查看表格。
  • 在输入阶段添加应用程序验证。
  • 迁移前现有数据 typeof()检查与 .
CREATE TABLE users_new (
  id INTEGER PRIMARY KEY,
  email TEXT NOT NULL UNIQUE,
  age INTEGER CHECK (age >= 0)
) STRICT;

情况4.我复制了备份文件,但恢复后部分数据丢失。

症状:运行中 .db 我只复制了文件,但最近的数据在恢复的数据库中不可见。

原因:在WAL模式下,变化是 -wal 它可能保留在文件中。仅复制正在运行的数据库文件可能不会产生一致的备份。

如何解决

sqlite3 app.db ".backup backup.db"
sqlite3 backup.db "PRAGMA integrity_check;"

或者,您的应用程序使用 SQLite Online Backup API。如果文件复制是绝对必要的,则必须先清理数据库连接并了解检查点状态,然后再继续。

结论

SQLite 是一种比它的描述所暗示的更实用的数据库:“一种使用一个文件而不需要服务器的轻量级数据库。”在数据必须靠近应用程序的情况下,例如本地应用程序、移动应用程序、嵌入式设备和测试环境,它可以显着减轻安装和操作负担。

相反,如果您需要一个可以同时处理来自多个服务器的大量写入的中央数据库,那么选择诸如 PostgreSQL 或 MySQL 之类的服务器类型数据库比强制扩展 SQLite 更安全。用好SQLite的关键是准确判断角色、初始化连接PRAGMA、短事务、理解WAL、启用外键、尽早组织备份方法。

参考资料