
简介面向 .NET 开发者的 System.Data.SQLite 数据库详解文档系统梳理了这款 SQLite 加强版的核心价值内部集成 ADO.NET 2.0 引擎可脱离完整 .NET Framework 独立部署开发者能直接使用 SqlConnection、SqlCommand 等熟悉接口操作数据库。文档先从 SQLite 基础特性讲起涵盖单文件存储、跨平台支持、事务与复杂查询并说明其不支持行级/表级锁、适合中小型或移动受限环境再结合 VS2008 服务器资源管理器演示添加 SQLite 数据连接、建表与数据操作同时提供创建库、返回 DataTable/DataReader、执行增删改、参数化查询、获取表名等通用类代码强调以参数化 SQL 防注入。资料还以一个 Excel 多表分析案例说明其无需 Access 即可完成高效数据统计的落地价值。整份资料仅 1 个 PDF 文件大小约 300KB轻量易读目前已有 209 人学习下载适合需要在嵌入式、离线或小型系统中快速集成 SQLite 的 .NET 开发人员。1. System.Data.SQLite 到底是什么一个让人又爱又恨的本地数据库驱动做桌面工具、局域网小系统或者边缘计算网关时你大概率会遇到一个需求不想装数据库服务但又要用 SQL 管理结构化数据。System.Data.SQLite 正是为这个场景设计的——它把 SQLite 引擎封装成 .NET 开发者最熟悉的 ADO.NET 接口让你能用 Connect、Command、DataReader 这一套老伙计去操作本地文件库。它能帮你解决“零部署、单文件、跨平台”的问题也埋了不少坑平台目标选错、数据库被锁、依赖库缺失哪一个都能让人折腾半宿。这篇文章我会从连接字符串讲起一直到事务、并发、加密、在线备份把能抄作业的命令和参数都直接给出来。2. 为什么要选 System.Data.SQLite从 SQLite 到 ADO.NET 的桥接与三个核心特性2.1 标准 ADO.NET Provider 带来的统一编程模型如果你已经写过 .NET 程序访问数据库那你一定熟悉SqlConnection、OracleConnection这类对象。System.Data.SQLite 提供的SQLiteConnection同样继承自DbConnection意味着你写的通用数据访问代码可以自然地迁移到本地文件库。我一般会这样理解它只是个适配器把 SQLite 的 C 接口翻译成 .NET 的托管类型。因此任何基于DbProviderFactory的数据访问层、轻量 ORM 或者自己封装的仓储类都能直接注册使用而不必为本地数据库写一套“特殊接口”。这种统一模型还有一个额外好处学习成本低。你不需要去记一套全新的 API。建库、建表、增删改查思维方式与操作远程数据库几乎一样。但它的确不是远程数据库——连接串里不是 IP 和端口而是一个文件路径。这个差别会带来后续所有“锁”“权限”“占文件”问题的根源理解这一点很重要。2.2 单文件、零服务的本地数据库为什么适用System.Data.SQLite 背后的核心是 SQLite 引擎。SQLite 没有独立的服务进程读写就是直接操作文件。你可以在安装包里带着一个空的.db文件程序运行时就访问它也可以运行到一半用一句CREATE DATABASE的等价语法创建。这样部署时就少了一个“安装数据库”步骤也省了内存和 CPU 占用。但“零服务”不等于“免维护”。SQLite 通过文件锁控制并发所以同一时刻只允许一个写者多读可以并发。如果你的业务是单客户端桌面应用这个限制无所谓如果是多个进程同时写一个库那就要认真设计重试策略。选它之前先问自己会不会有超过两个写线程或写进程会的话我建议你先把 WAL 模式和 busy_timeout 配置好否则后面一定会遇到 nonsystem 的database is locked。2.3 功能完整性触发器、事务、JSON、加密别把 System.Data.SQLite 看成“玩具”。它支持的 SQL 特性远多于一般的嵌入式数据库比如触发器、视图、递归 CTE、窗口函数以及最近一些版本中的 JSON 函数。事务方面支持BEGIN TRANSACTION、COMMIT、ROLLBACK还有SAVEPOINT。加上 ADO.NET 的事务对象SQLiteTransaction可以很好地实现原子写入。值得一提的还有加密能力。标准 SQLite 本身不提供加密而 System.Data.SQLite 在编译选项开启时可以通过连接串里的Password参数给数据库文件加密。这是很多人选它的重要理由尤其是做一些离线加密存储的行业工具。不过要注意加密不是把整个文件磨成密文它更像页级加密需要指定密码才能打开。后文我会给一个最小加密连接串示例。3. 在 .NET 项目中引入 System.Data.SQLite最小可运行步骤与连接参数详解3.1 安装使用包管理器添加依赖我常用的做法是在项目根目录执行包管理命令把依赖加进来。假设你有一个名为SqliteDemo.csproj的类库或控制台项目打开终端并执行dotnet add package System.Data.SQLite安装完成后你的.csproj文件里会多出一个PackageReference。这一步会自动引入托管 DLL 和对应的原生运行库。有一点要特别提醒System.Data.SQLite 的包依赖原生 SQLite 引擎而这些原生库分为 x86、x64 以及不同的运行时标识。如果之后发布时不做配置默认按平台生成部署到具体机器上可能找不到正确版本。如果你使用的是传统 .NET Framework 项目也可以打开“包管理控制台”执行同样的操作。命令会帮你解析依赖。无论哪种项目装完后先做一次 Build确认没有版本冲突。我遇到过某项目里同时引用了 System.Data.SQLite Core 和完整包导致类型冲突解决办法是只保留一个包。3.2 最小连接代码从创建数据库到执行第一条命令下面是最简单的可运行代码。它会在当前目录生成一个app.db文件并创建一张user表然后插入一行数据using System.Data.SQLite; string connectionString Data Sourceapp.db;Version3;; using var conn new SQLiteConnection(connectionString); conn.Open(); using var cmd conn.CreateCommand(); cmd.CommandText CREATE TABLE IF NOT EXISTS user ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL ); cmd.ExecuteNonQuery(); cmd.CommandText INSERT INTO user (name) VALUES (张三); cmd.ExecuteNonQuery(); Console.WriteLine(OK);这段代码里Data Sourceapp.db指定数据库文件路径Version3表示使用 SQLite 3.x 文件格式。注意如果文件不存在Open()会触发数据库文件的生成这是 SQLite 的默认行为。定义表时AUTOINCREMENT会让id自增这是本地库最常见的写法。一定要记得使用using释放资源。无论是连接、命令还是DataReader不释放会造成文件句柄泄漏。尤其是在 Windows 上未关闭的连接会一直占用.db文件导致后续删除或备份失败。这个坑我遇到太多次了。3.3 连接字符串参数缓存、超时、读写锁与池化连接字符串是 System.Data.SQLite 的灵魂很多人只写一句Data Source遇到问题后不知从何下手。我实际项目里常用的几个参数如下参数名示例值作用说明Data SourceC:\data\app.db数据库文件路径也可以是:memory:内存库Version3SQLite 版本一般固定为 3PoolingTrue是否启用连接池开启后能复用物理连接但注意连接释放Busy Timeout3000等待锁释放的毫秒数设为 0 会立刻抛异常Default Timeout30ADO.NET 命令执行超时时间单位秒默认 30Cache Size8000SQLite 页缓存大小单位是 KB适当调大能减少磁盘 IOJournal ModeWAL日志模式WAL 可显著提高并发读性能PasswordPssw0rd若无参数则默认不加密设置后即开启加密以下是我在桌面端工具里常用的完整连接串string connStr Data Sourceapp.db;Version3;PoolingTrue;Busy Timeout3000;Default Timeout30;Cache Size2000;;设置Busy Timeout的意义在于当另一个连接持锁时SQLite 可以选择等待而不是立刻返回SQLITE_BUSY。Default Timeout则是命令层面的超时避免一个错误的 SQL 卡住整个界面。如果你希望提高写入并发最好再加上Journal ModeWAL。注意Pooling开启后using块内执行Close()时连接并不会真正断开而是回到池中。如果在同一线程里频繁打开关闭能省去重新初始化文件句柄的开销。但在多线程场景下池化的连接可能会把锁状态带出来所以高并发写时我通常会关闭 Pooling改为手动管理。3.4 参数化查询防止注入的基础操作直接用字符串拼接 SQL 的习惯在本地库同样危险。虽然本地库没有网络注入风险但拼接时的引号、中文转义都会让人崩溃。正确做法是用参数对象using var conn new SQLiteConnection(connStr); conn.Open(); using var cmd conn.CreateCommand(); cmd.CommandText INSERT INTO user (name) VALUES (name); cmd.Parameters.AddWithValue(name, 李四); cmd.ExecuteNonQuery();AddWithValue会自动推断参数类型但有时会把字符串推断成NVarChar导致 SQLite 类型不匹配。我一般更喜欢显式指定cmd.Parameters.Add(name, DbType.String).Value 李四;DbType.String会映射成TEXT更符合预期。注意SQLite 对参数大小写不敏感NAME也能绑到name。但为了可读性还是统一命名比较好。参数化查询同样适用于LIKE查询、日期过滤这是写生产代码的基本要求。4. 读与写事务、批量插入与并发控制的正确姿势4.1 事务与原子写入当你需要把一个业务中的多条 SQL 作为一个整体提交时事务是不可少的。System.Data.SQLite 提供了标准的事务对象用法如下using var conn new SQLiteConnection(connStr); conn.Open(); using var tx conn.BeginTransaction(); try { using var cmd conn.CreateCommand(); cmd.Transaction tx; cmd.CommandText UPDATE user SET name王五 WHERE id1; cmd.ExecuteNonQuery(); cmd.CommandText INSERT INTO user_log (action) VALUES (rename); cmd.ExecuteNonQuery(); tx.Commit(); } catch { tx.Rollback(); throw; }在这个示例里所有命令都绑定到了同一个事务对象tx上。只要有一段失败Rollback()就会把数据库恢复到事务开始前的状态。这样做的好处是不会出现“改了一半”的数据。有个容易被忽略的细节SQLite 的事务默认是DEFERRED意思是直到执行第一条写语句时才真正获取写锁。如果你的事务里先读了很久的SELECT再执行写入就有可能因为连接冲突而失败。这时可以显式指定BeginTransaction(IsolationLevel.Serializable)或直接使用BEGIN IMMEDIATE来提前拿锁。在 System.Data.SQLite 中可以通过连接串里的Default IsolationLevel调整但我一般是在高频写事务前先执行一句BEGIN IMMEDIATE。4.2 批量插入复用命令对象与事务的差别一次性插入大量数据时很多人会一条一条ExecuteNonQuery这样会有大量磁盘同步和语句解析开销。常见的两种提速方式第一种是循环内复用同一个命令对象只改参数值第二种是把所有插入放在一个事务里。我推荐两者结合。下面是一个插入一万行的例子同时使用事务和命令复用using var conn new SQLiteConnection(connStr); conn.Open(); using var tx conn.BeginTransaction(); using var cmd conn.CreateCommand(); cmd.Transaction tx; cmd.CommandText INSERT INTO user (name) VALUES (name); var p cmd.CreateParameter(); p.ParameterName name; p.DbType DbType.String; cmd.Parameters.Add(p); for (int i 0; i 10000; i) { p.Value user_ i; cmd.ExecuteNonQuery(); } tx.Commit();这个循环之所以快是因为 SQLite 只需要解析一次INSERT语句后续只是替换绑定参数。配合事务一万行通常能在数百毫秒内完成。如果没有事务每条那条语句都默认启动一个隐式事务慢不说还容易积累日志文件。另一种更极端的方式是用SQLiteCommand带有INSERT OR REPLACE或使用预处理语句。但对于绝大多数场景上面的写法已经足够。真正瓶颈往往不在于 SQLite而在于你逐条构造字符串的循环效率。4.3 并发读写WAL 模式与 busy_timeout 的配合SQLite 的默认回滚日志模式在写入时需要独占锁读与写互斥。如果你有一个后台线程持续写而主线程要读很快就会出现“database is locked”。改善办法是开启 WALWrite-Ahead Logging模式。你可以在连接串中加Journal ModeWAL也可以直接执行 PRAGMAusing var conn new SQLiteConnection(connStr); conn.Open(); using var cmd conn.CreateCommand(); cmd.CommandText PRAGMA journal_modeWAL;; cmd.ExecuteScalar();开启 WAL 后写操作会先写入独立的-wal文件读操作直接读主文件和 WAL 的缓存快照不再阻塞。这样读与写可以并发但仍然只有一份写锁。busy_timeout还是需要的用于处理写与写之间的竞争。在多进程场景下WAL 需要所有连接都在同一模式下工作混用会变回旧模式。请确保所有连接串保持一致。另外WAL 模式会产生额外的-wal文件和-shm文件部署时要把它们也考虑进去。如果只拷贝主.db文件会造成数据不一致。5. System.Data.SQLite 实战避坑5 个高频翻车现场与排查方案5.1 运行时报错未能加载文件或程序集 System.Data.SQLite现象程序编译通过一运行就抛System.IO.FileNotFoundException提示无法加载System.Data.SQLite.dll或其依赖项。原因大部分时候是平台目标不匹配。System.Data.SQLite 包含原生代码如果你在 x64 系统上生成的是AnyCPU而项目运行在 32 位环境或者反过来都可能导致加载失败。另外包依赖的 VC 运行库没有部署到目标机器也很常见。解决先检查你的项目平台目标。我一般的做法是把目标平台设为x64同时确保发布的文件夹里有对应的x64子目录和原生 DLL。如果是 32 位系统就设为x86。对于 .NET Core / .NET 5 项目还要确认运行时标识是否正确。如果还报错用Dependencies窗口检查缺失的原生组件。5.2 数据库被锁定database is locked现象写入或偶尔读取时抛出SQLiteException: database is locked且不是偶发删除重试也没有效果。原因最常见是另一个连接的写事务没有提交或没有释放。SQLite 的锁机制与连接生命周期强相关尤其是连接池开启时池化的连接如果还占着事务那锁就没有被释放。解决先确保所有写操作都在事务里且有try/finally释放。把ConnectionString里的Busy Timeout调大到 3000 甚至更长。同时开启 WAL 模式避免读与写互相阻塞。如果多个进程都要写考虑改成“单写进程 队列”的架构这是最彻底的办法。5.3 中文路径或中文内容乱码现象连接串里给的是C:\数据\app.db打开时报错或者写入中文后读出来是乱码。原因System.Data.SQLite 内部使用 UTF-8 存储文本但连接串中的路径解析和 ADO.NET 字符串编码处理有时不一致。乱码更多与代码中的字符串编码有关比如从旧的非 UTF-8 系统读取了 GBK 编码的文本。解决路径中尽量不要放中文把数据库放在纯英文目录。如果业务必须用中文路径可以在连接串里使用URIfile://格式或对路径做 UrlEncode。对于文本内容我始终建议在代码层明确使用 UTF-8。在读取时禁止把字节数组转成Encoding.Default统一用Encoding.UTF8处理。乱码根源通常是“写是 UTF-8读是 GBK”这种不对称。5.4 连接泄漏导致文件被占用现象程序运行一段时间后无法删除或备份数据库文件甚至后续连接都变慢。原因某个连接对象没有被Close或Dispose。连接池开启时连接的物理句柄可能仍然存活只是逻辑上关闭并不会真正释放文件。解决坚持使用using或try/finally。如果用了PoolingTrue可以在连接串里加PoolingFalse来彻底关闭池化强制每次物理释放代价是性能下降。也可以定期调用SQLiteConnection.ClearPool(connection)清理池中连接。还有一个排查技巧在 Windows 上用进程资源管理器查看哪个进程打开了.db文件通常能找到泄漏位置。5.5 在 .NET Core 下找不到原生库现象在开发环境运行正常但发布成独立部署后目标机器上提示DllNotFoundException。原因System.Data.SQLite 的原生运行库没有随发布包含进去。.NET Core 的发布策略对非托管 DLL 有不同的处理方式默认可能不会复制SQLite.Interop.dll到输出目录。这个文件是真正的原生引擎缺失就无法工作。解决在.csproj中显式配置内容包含这些原生库或者使用runtime identifier发布。我常用的做法是直接让项目包含x86、x64和arm64目录下的SQLite.Interop.dll并设置为“复制到输出目录”。如果使用自包含发布要把运行标识与目标机器匹配。还有一种更省心的方式完全使用独立实现比如Microsoft.Data.Sqlite它同样基于 SQLite但包内直接带原生库部署时没那么多麻烦。不过如果你已经用了很多 System.Data.SQLite 的 API 或加密功能换门是有成本。6. 进一步用 System.Data.SQLite 做备份、迁移与性能验证6.1 密码加密一个连接串让本地库变成私有仓库如果你希望数据库文件被别人拿走也无法读取System.Data.SQLite 可以加密码。连接串里多一个参数string connStr Data Sourcesecret.db;PasswordMyPss;;对一个尚未加密的库执行Open()并执行任意一条命令后再关闭连接文件就带上加密标记。之后每次打开都必须使用相同的密码。注意加密只在包编译时开启了加密扩展才可用。使用该功能时请确保你部署的目标环境支持。6.2 在线备份用 BackupDatabase 避免文件复制冲突直接拷贝正在被写操作的.db文件往往得到不一致数据。System.Data.SQLite 提供了在线备份方法可以在不打断业务的情况下把数据库备份到另一个文件using var source new SQLiteConnection(connStr); await source.OpenAsync(); using var backup new SQLiteConnection(Data Sourcebackup.db;Version3;); await backup.OpenAsync(); source.BackupDatabase(backup, main, main);这段代码把source的 main 数据库备份到backup连接所指向的backup.db。执行过程中SQLite 会协调锁保证备份是事务一致的。对于每天定时备份的场景这个方式比我见过很多人用的“复制文件”可靠得多。6.3 性能验证一个极简的读写测试脚本在调整参数后我会用一个简单的计时器验证效果。比如测试一万次同步写入耗时var sw System.Diagnostics.Stopwatch.StartNew(); using var conn new SQLiteConnection(connStr); conn.Open(); using var tx conn.BeginTransaction(); using var cmd conn.CreateCommand(); cmd.Transaction tx; cmd.CommandText INSERT INTO perf (val) VALUES (val); cmd.Parameters.Add(val, DbType.Int64); for (int i 0; i 10000; i) { cmd.Parameters[val].Value i; cmd.ExecuteNonQuery(); } tx.Commit(); sw.Stop(); Console.WriteLine($耗时: {sw.ElapsedMilliseconds} ms);这个测试可以让你直观看到缓存、WAL 和事务带来的差异。我平时优化某一个模块时习惯先跑这个基准再改参数再跑一次用数字说话。最后分享一个我的习惯接手任何一个用 System.Data.SQLite 的项目我会先检查三件事——连接串是否写了Busy Timeout、是否开启了WAL、每个连接是否都会及时释放。把这三件事做对项目基本就稳了一半。加密与在线备份则更像保险等你真的碰到丢数据或拷库失败时会觉得它们值得一开始就做。希望帮到你。本文还有配套的精品资源点击获取