SQLite 是一个嵌入式关系型数据库引擎。
它不需要独立服务进程,不需要安装配置,甚至整个数据库就是一个文件。
它由 C 语言编写,源码完全开放(Public Domain),具备轻量、可靠、零运维等特点。
从 iPhone 上的聊天记录、浏览器缓存,到大量 IoT 设备和移动应用,本地数据存储背后几乎都能看到 SQLite 的身影。
但在 .NET 圈子里,SQLite 的评价却始终两极分化。
做单元测试时,很多人喜欢用 SQLite 内存库替代 SQL Server:
不用搭环境 启动极快 跑测试方便
可一旦到了生产环境,尤其是 Web API、高并发写入、持久化场景,大家的第一反应往往是:
“SQLite 扛不住吧?”
“那不就是个玩具数据库?”
问题是——
你真的了解 SQLite 吗?
很多开发者对 SQLite 的认知,还停留在“本地缓存”“单机 Demo”“测试替身”。
但事实上:
SQLite 支持事务 支持 WAL 支持索引、触发器、视图 支持 JSON、窗口函数 甚至已经被用于大量生产级应用
它不是“简化版 MySQL”,而是一个真正完整的关系型数据库。
今天这篇文章,就从 .NET 开发者视角,深入聊透 SQLite:
NuGet 包到底该怎么选 WAL 模式为什么能提升并发 什么场景适合它,什么场景必须换库
看完后,你对 SQLite 的印象,大概率会彻底改观。
🔗 官网:
https://www.sqlite.org
⚡ 三步跑起来:安装 → 连接 → CRUD
SQLite 在 .NET 里没有内置支持,必须通过第三方 ADO.NET 提供程序。当前唯一稳妥的选择是 Microsoft.Data.Sqlite——微软官方维护,内嵌跨平台 sqlite3 二进制(通过 SQLitePCLRaw),无需手动部署 DLL。
至于 System.Data.SQLite?它已经多年未维护,在 .NET 6+ 项目中极容易因运行时标识不匹配报 DllNotFoundException 或 BadImageFormatException。除非你还在维护 .NET Framework 4.6.1 以下的老项目,否则新项目一律用 Microsoft.Data.Sqlite。
dotnet add package Microsoft.Data.Sqlite
最简示例——创建数据库、建表、插入数据:
using Microsoft.Data.Sqlite;// 1. 打开连接(文件不存在会自动创建)using var conn = new SqliteConnection("Data Source=app.db");conn.Open();// 2. 建表using var cmd = conn.CreateCommand();cmd.CommandText = @"CREATE TABLE IF NOT EXISTS Users ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT NOT NULL, Age INTEGER CHECK(Age >= 0), CreatedAt TEXT DEFAULT (datetime('now')))";cmd.ExecuteNonQuery();// 3. 插入数据(参数化查询,防止 SQL 注入)cmd.CommandText = "INSERT INTO Users (Name, Age) VALUES (@name, @age)";cmd.Parameters.Clear();cmd.Parameters.AddWithValue("@name", "张三");cmd.Parameters.AddWithValue("@age", 28);cmd.ExecuteNonQuery();// 4. 查询cmd.CommandText = "SELECT COUNT(*) FROM Users";var count = (long)cmd.ExecuteScalar();Console.WriteLine($"用户数: {count}");
三步走完:安装 NuGet 包 → 打开连接 → 执行 SQL。路径自动创建、参数自动绑定,不需要装任何系统级依赖。
🧬 .NET 生态里的 SQLite 工具链:一张表看懂各种 NuGet 包
SQLite 是部署量最大的数据库引擎,嵌入式、零配置、单文件。但 .NET 生态里围绕它的包五花八门,初次接触很容易选错。
UseSqlite 即可使用 | |||
大多数场景:Microsoft.Data.Sqlite + EF Core / Dapper 就够用了。如果高并发写入,再加一个 EntityFrameworkCore.Sqlite.Concurrency。
💥 为什么你的 SQLite 一并发就崩?
这可能是 SQLite 被误解最深的环节。很多人用了默认配置,一上来多个线程同时写,直接报 SQLITE_BUSY,然后下结论:“SQLite 不适合生产环境。”
问题不出在 SQLite 本身,而出在你没开WAL 模式。
SQLite 默认使用 DELETE 日志模式,写入时对整个数据库文件加排他锁,写阻塞读、读阻塞写。同一时间只允许一个写入操作。多个线程抢着写,直接抛 database is locked,这不是 Bug,是默认设置太保守了。
WAL(Write-Ahead Logging) 是解决这个问题的关键:读操作不阻塞写操作,写操作不阻塞读操作。WAL 模式下写入并发可达 100-200 QPS,混合读写负载轻松支持 1000+ QPS——对初创公司和中小项目来说,完全够用了。
启用 WAL 只需一行 PRAGMA:
using var conn = new SqliteConnection("Data Source=app.db");conn.Open();// 在连接打开后立即执行using var cmd = conn.CreateCommand();cmd.CommandText = "PRAGMA journal_mode = WAL;";cmd.ExecuteNonQuery();
WAL 模式具有持久性——第一次执行后,数据库文件永久保持 WAL 模式,不需要每次连接都重新设置。在 MAUI 等资源受限场景中,采用 WAL 模式并调整连接参数是解决 SQLITE_IOERR 最有效的策略。
🧠 三层优化,从能用到能打
WAL 模式只是第一层。真正扛住生产环境,还需要事务和 PRAGMA 的组合拳。
第一层:批量插入用事务包起来
单条 INSERT 默认自动提交,100 条就是 100 次磁盘刷写。放到同一个显式事务里,性能提升 5–10 倍。
using var tx = conn.BeginTransaction();var cmd = conn.CreateCommand();cmd.Transaction = tx; // 这句必须有,否则事务不生效cmd.CommandText = "INSERT INTO Users (Name, Age) VALUES (@name, @age)";for (int i = 0; i < 1000; i++){ cmd.Parameters.Clear(); cmd.Parameters.AddWithValue("@name", $"用户{i}"); cmd.Parameters.AddWithValue("@age", 20 + i % 50); cmd.ExecuteNonQuery();}tx.Commit();
关键细节:事务中的所有 command 必须共用同一个 connection 实例,且 command.Transaction 必须显式赋值——少写这句,事务完全无效。
第二层:PRAGMA 微调磁盘行为
WAL 只是日志模式的开关,搭配以下 PRAGMA,性能再上一档:
using var cmd = conn.CreateCommand();cmd.CommandText = @" PRAGMA journal_mode = WAL; -- 读写不互斥 PRAGMA synchronous = NORMAL; -- 平衡安全与性能(默认 FULL 会每次刷盘) PRAGMA busy_timeout = 60000; -- 写锁等待 60 秒,不立即抛异常 PRAGMA cache_size = -65536; -- 增大缓存,减少磁盘 I/O PRAGMA temp_store = MEMORY; -- 临时表存内存";cmd.ExecuteNonQuery();
Web API 场景下,这套配置可以把“并发一写就崩”降到“偶尔排队等待”。
第三层:数据库重建后必须跑 ANALYZE
SQLite 的查询优化器依赖统计信息来做索引选择。数据库从零建完、批量 insert 了大量数据后,如果没有跑 ANALYZE(或 PRAGMA optimize),查询计划可能极差。
有用户反馈:应用首次运行时,每 10 万条记录的 upsert 耗时约 300ms;重启应用后,同样的操作耗时膨胀到 3500ms,整整慢了 10 倍以上。最终发现是统计信息过时导致的查询计划退化。解决方案很简单——在数据库初始化完成后跑一次 PRAGMA optimize:
using var cmd = conn.CreateCommand();cmd.CommandText = "PRAGMA optimize;";cmd.ExecuteNonQuery();
这对数据库首次初始化后的查询性能有显著影响。
🛡️ 生产环境还要注意什么?
连接字符串路径必须绝对路径
SQLite 数据库就是一个文件,路径写错 = 创建空库 / 操作了别的文件。相对路径在 IDE 调试时可能正常,发布到 Docker 容器或生产环境后行为完全不同。
//推荐:显式构造绝对路径var dataPath = Path.Combine(AppContext.BaseDirectory, "data", "app.db");var conn = new SqliteConnection($"Data Source={dataPath}");// 不推荐:相对路径风险大var conn = new SqliteConnection("Data Source=app.db");
连接不要长期持有
SQLite 没有连接池,并发写入能力弱。读多写少的场景下可以复用同一个 SqliteConnection 实例,但不能跨线程共享,用完之后及时 Dispose。
外键默认不开启
SQLite 的设计中,外键约束默认是关闭的。如果需要级联删除等外键行为,连接打开后需立刻执行:
cmd.CommandText = "PRAGMA foreign_keys = ON;";cmd.ExecuteNonQuery();
这一步很多人都漏掉,结果发现 DELETE 不会触发 CASCADE,还以为是 Bug。
异步不要在文档构建里写
SQLite 不支持真正的异步 I/O。如果在连接打开期间执行 await 延迟操作(如 HTTP 调用),后续对同一数据的写入可能因为连接状态不一致而失败。
内存模式是双刃剑
Data Source=:memory: 可以在内存中运行数据库,关闭即消失,适合单元测试。但它不支持 WAL 模式,因为没有磁盘文件。单元测试时记得提前种数据。
🎯 什么时候不该用 SQLite?
SQLite 不是万能的。遇到以下情况,果断换 PostgreSQL / SQL Server:
高并发写入场景:写入并发 > 200 QPS,WAL 模式也扛不住,其他线程会排队等待
需要网络访问的架构:多个服务实例需要同时读写同一份数据,SQLite 文件级锁是瓶颈
大事务场景:单次事务超过 100MB,传统回滚日志模式可能比 WAL 还快
行级安全或复杂权限:需要 GRANT/REVOKE 级别的权限控制
🎯 最终选型速查表
SQLite 不是“玩具数据库”,而是一把被严重低估的瑞士军刀。
它设计精巧、部署极简、性能足够,是全球部署最广的嵌入式数据库。把它用好,不是“打开连接、写几句 SQL”那么简单——WAL 模式、事务粒度、PRAGMA 微调、索引统计、并发模型,每一个环节都直接影响它在生产环境的稳定性。
当你真正掌握这些的时候,你会发现:原来很多场景根本不需要上 PostgreSQL,一个管好用的 SQLite,完全能扛住。