简介:面向 .NET 开发人员的《System.Data.SQLite 数据库详细介绍》PDF 文档,系统讲解 SQLite 的 .NET 增强版用法。System.Data.SQLite 将 SQLite 引擎与 ADO.NET 2.0 接口封装在一起,无需额外安装 .NET Framework 即可在 Windows、Linux、移动端等环境运行,特别适合轻量级、独立部署的小型系统。文档从 SQLite 基础讲起,涵盖事务、触发器、复杂查询及宽松类型检查等特性,并介绍 System.Data.SQLite 在 VS2005/VS2008 及 Entity Framework 中的集成方式,读者可按步骤在服务器资源管理器添加数据连接,像操作 SQL Server 一样管理表数据。随后结合一个 Excel 分析案例说明其适用场景,并给出通用封装类:可创建数据库文件、返回 DataTable/DataReader、执行增删改、获取聚合值、列出所有表,统一使用参数化 SQL 防止注入,便于直接复用到项目中。资源为单份 PDF,大小约 300KB,已有 209 人学习,适合希望快速上手 System.Data.SQLite 的初学者,以及需要离线查阅资料、评估数据库选型的开发者。
1. System.Data.SQLite:.NET 应用里跑 SQLite 的那座桥
某项目组接手过一套运行多年的设备采集程序,本地攒着几十万条温度记录。最初用的服务型数据库,客户现场没有部署环境,装实例、配防火墙、开远程访问,前前后后折腾了两天,最后还是因为网络策略翻车。后来把存储层换成 System.Data.SQLite——整个数据库变成一个.db文件,程序拷过去就能用。
System.Data.SQLite 不是独立数据库,而是 .NET 世界接入 SQLite 的 ADO.NET 数据提供程序:它把原生 SQLite 引擎封装成 .NET 能调用的程序集,让你用SQLiteConnection、SQLiteCommand这些熟悉类型操作一个文件数据库。它解决的是「不想装服务、又要结构化查询」的中间地带需求,适合桌面工具、采集程序、小型站点,也适合任何希望部署清单里只多一个文件的从业者。下面从选型讲起,落到参数设置,再聊几个高频坑。
2. 选型之前先看内部结构:托管代码包着原生引擎
2.1 它为什么是「半托管」组件
很多人第一次接触 System.Data.SQLite,以为它是纯 C# 实现的数据库。实际不是——它拆成两层。
System.Data.SQLite.dll:负责实现 ADO.NET 规范,暴露SQLiteConnection、SQLiteCommand这些类型。这一层是托管程序集,编译时一般选 AnyCPU 也能通过。SQLite.Interop.dll:原生 C 库,真正执行 SQL、维护 B-Tree、处理事务。System.Data.SQLite 通过 P/Invoke 调用它。
这两层的分工决定了部署时必须保证「托管的 dll 和对应位数的原生 dll 同时存在」。第 5 章的翻车现场,多数出在这条依赖链上。
如果你只想验证数据,也可以用命令行工具直接操作同一个文件,两边读写是兼容的,因为底层引擎同一颗。这带来一个实用习惯:程序里某个查询结果不对,先把.db文件拷出来用命令行执行同一句 SQL,很容易分清是数据问题还是封装问题。参数上与原生 SQLite 一致,所以网上的PRAGMA、锁相关的经验在 System.Data.SQLite 里同样适用——这也是它相比其他封装最大的好处:问题能直接对应到原生文档。
2.2 与周边几个封装怎么选
生态里有几个容易混的驱动。新项目用 EF Core 时,很多人会优先看微软官方的 Microsoft.Data.Sqlite;但如果是老项目、或者需要完整 ADO.NET 特性(DataAdapter、DataSet、设计时支持),System.Data.SQLite 更顺手。这张表我经常直接甩给同事。
| 对比点 | System.Data.SQLite | Microsoft.Data.Sqlite | Mono.Data.Sqlite |
|---|---|---|---|
| ADO.NET 完整度 | 完整:Connection、Command、DataAdapter、DataSet 都齐 | 偏轻量,主要为 EF Core 准备 | 完整但偏老 |
| 原生引擎 | 内置 SQLite 原生库并随包分发 | 依赖 SQLitePCLRaw 提供原生库 | 依赖系统安装的 SQLite |
| 典型场景 | 桌面工具、复杂 ADO.NET 代码、老项目升级 | 新 .NET Web、EF Core 项目 | 单声道时代的遗留项目 |
| 维护状态 | 长期维护 | 官方维护 | 基本停滞 |
我一般给的建议是:不确定时优先用 System.Data.SQLite,它覆盖的 API 面最大,以后想换 ORM 也方便;如果项目明确只要轻量查询,用 Microsoft.Data.Sqlite 也不迟。重点不是你选哪个,而是别在项目写到一半时从 A 驱动换到 B 驱动,连接字符串和参数类型细节都有差异,迁移成本比想象中高。
2.3 什么项目别选它
嵌入式不等于万能。遇到这三种情况,我会主动说服团队放弃:
- 多个进程高频同时写同一个库文件。SQLite 的写入锁是文件级加表级的,写并发一旦上来,
SQLITE_BUSY会频繁出现,调一堆参数不如换服务型数据库。 - 数据量预期达到几十 GB。备份、VACUUM、一致性检查都会明显变慢,这个时候服务型数据库的运维优势才体现出来。
- 需要细粒度权限、用户体系、审计日志。SQLite 全都没有,硬要自己实现等于绕远路。
反过来,单写多读、写入量可控(每秒几十次以内)、部署环境复杂(内网、离线、多台机器拷贝),这套方案非常省心。需求对得上,再看实现。
3. 从 NuGet 到第一个能跑的程序:全套 CRUD
3.1 引包与第一次初始化
常见做法是装System.Data.SQLite.Core包。这个包会带上运行所需的最小内容:托管 dll 加原生 interop。不带 Core 的完整包还包含设计时组件、命令行工具等,体积更大,普通程序用 Core 更整洁。
在项目目录执行安装命令:
dotnet add package System.Data.SQLite.Core然后创建数据库文件并打开连接。SQLite 有个特点:默认情况下Data Source指向的文件不存在时会自动创建空文件。第一次跑通时我喜欢把这句话写进注释里,避免有人建表前满世界找文件。
using System.Data.SQLite; var conn = new SQLiteConnection("Data Source=app.db;Version=3;"); conn.Open(); // 建表语句,IF NOT EXISTS 保证重复执行不报错 using var cmd = conn.CreateCommand(); cmd.CommandText = @" CREATE TABLE IF NOT EXISTS device_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_no TEXT NOT NULL, temp REAL NOT NULL, created_at TEXT DEFAULT (datetime('now')) );"; cmd.ExecuteNonQuery(); conn.Close();逻辑说明:先创建连接对象,再调用Open()建立对数据库文件的访问。ExecuteNonQuery()用于执行 DDL 这类不返回结果集的语句,建表就是典型场景。AUTOINCREMENT让 id 稳定自增,后续按主键定位更新、删除最省事。
参数说明:Version=3指定使用 SQLite 3.x 格式,旧教程里常见。实际新版驱动默认已经是 3,标准的Data Source=app.db就够了;写出来是照顾从老代码拷贝来的同学,防止你抄到一半发现少了参数心里不踏实。相对路径是相对于进程当前工作目录,不是程序集所在目录,这点后面有坑。
3.2 插入、查询、更新的一条龙演示
using var conn2 = new SQLiteConnection("Data Source=app.db;Version=3;"); conn2.Open(); // 参数化插入,避免拼接 SQL 带来的注入和数据格式问题 using var insert = conn2.CreateCommand(); insert.CommandText = "INSERT INTO device_log(device_no, temp) VALUES(@device_no, @temp);"; insert.Parameters.AddWithValue("@device_no", "D-10086"); insert.Parameters.AddWithValue("@temp", 36.5); int affected = insert.ExecuteNonQuery(); // 查询:返回多行用 DataReader using var query = conn2.CreateCommand(); query.CommandText = @" SELECT id, device_no, temp, created_at FROM device_log WHERE temp > @threshold ORDER BY id DESC LIMIT 10;"; query.Parameters.AddWithValue("@threshold", 35.0); using var reader = query.ExecuteReader(); while (reader.Read()) { Console.WriteLine($"{reader["id"]} | {reader["device_no"]} | {reader["temp"]}"); } // 更新:按主键定位 using var update = conn2.CreateCommand(); update.CommandText = "UPDATE device_log SET temp = @temp WHERE id = @id;"; update.Parameters.AddWithValue("@temp", 37.0); update.Parameters.AddWithValue("@id", 1); update.ExecuteNonQuery();逻辑说明:插入和更新都走ExecuteNonQuery,返回值是影响行数;查询用ExecuteReader,返回的是只读前向的 DataReader。读取循环里不能同时用同一个连接执行其他命令,否则会报「连接已有一个打开的 DataReader」,正确做法是把数据先拷进对象列表,再关闭 reader。
参数说明:AddWithValue的第一个参数要和 SQL 里的@参数名完全一致,大小写不敏感。注意@temp传 36.5(double)会映射成 REAL,传字符串会映射成 TEXT,类型写错是隐蔽的数据问题,后续temp > @threshold这类比较会碰壁。@id传整数 1 时会映射为 INTEGER,和主键匹配。
3.3 DataAdapter + DataTable:老项目里最常见的读取方式
如果你的程序还在用DataSet那套老代码,或者结果要交给报表组件绑定,直接用SQLiteDataAdapter填表:
var dt = new DataTable(); using var adapter = new SQLiteDataAdapter( "SELECT id, device_no, temp FROM device_log;", conn2); adapter.Fill(dt); // 修改内存数据后,用命令生成器回写(注意:这是笨办法) using var cb = new SQLiteCommandBuilder(adapter); adapter.Update(dt);这节给维护老项目的同学:SQLiteCommandBuilder会自动生成 INSERT、UPDATE、DELETE 语句,但它是逐条命令回写,上万行数据更新时性能很差。我的习惯是:数据量大时拿 DataTable 只做展示,写库还是走 3.2 的循环加事务(见 4.2),别拿 Builder 偷懒。如果只是只读场景,连SQLiteCommandBuilder都不需要,它只在调用Update时才发挥作用
4. 连接字符串、事务与并发:参数决定上限
4.1 连接字符串常用参数表
连接字符串是 System.Data.SQLite 里信息密度最高、也最容易被复制粘贴出问题的地方。下表是我实际项目里逐个检查过的配置项:
| 参数 | 示例 | 作用与注意 |
|---|---|---|
| Data Source | Data Source=D:\data\app.db | 数据库文件路径;相对路径相对当前工作目录 |
| Version | Version=3 | 新版可省略,保留无害 |
| Cache | Cache=Shared | 同一进程内共享缓存,多连接读同一文件时减少磁盘 IO |
| Pooling | Pooling=True | 是否启用连接池;桌面程序一般保持默认 |
| Default Timeout | Default Timeout=30 | 命令默认超时,与锁等待相关 |
| Read Only | Read Only=True | 只读打开,适合报表、备份场景 |
| Password | Password=change-me | 是否加密,注意默认原生库不带加密实现 |
| FailIfMissing | FailIfMissing=True | 文件不存在时报错,默认自动创建 |
| Foreign Keys | Foreign Keys=True | 启用外键约束;SQLite 默认关闭外键检查 |
两处容易踩的坑:一是Data Source使用相对路径时,ASP.NET 程序的工作目录和.db文件目录经常不一致,建议用绝对路径或程序动态计算路径;二是期望Password加密却发现文件依然能被其他工具打开,原因在第 5 章专门讲。
连接池方面,System.Data.SQLite 的连接池按连接字符串维度维护。桌面程序通常不关心它,但 Web 程序如果每条请求都新建连接,Pooling=True能明显减少打开文件的开销。代价是连接被复用时要留意事务状态——连接回池前必须已提交或回滚,否则下一个使用者会继承到一个未结束的事务。
4.2 事务与批量写入的正确姿势
写入性能是新手最容易感知差距的地方。直接循环单条 INSERT 会有两笔开销:每次写入开启一次隐式事务并同步落盘;虽然没有显式事务,但每次提交都触发一次磁盘同步。把 N 条插入放进一个显式事务,能合并成一次提交,写入量上万时差距往往是数量级的。
using var bulkConn = new SQLiteConnection("Data Source=app.db;"); bulkConn.Open(); using var bulkTxn = bulkConn.BeginTransaction(); using var bulkCmd = bulkConn.CreateCommand(); bulkCmd.Transaction = bulkTxn; // 命令必须挂到事务上,否则不生效 bulkCmd.CommandText = "INSERT INTO device_log(device_no, temp) VALUES(@device_no, @temp);"; var rnd = new Random(); for (int i = 0; i < 100000; i++) { bulkCmd.Parameters.Clear(); bulkCmd.Parameters.AddWithValue("@device_no", "D-" + i.ToString("D5")); bulkCmd.Parameters.AddWithValue("@temp", 30.0 + rnd.NextDouble() * 10); bulkCmd.ExecuteNonQuery(); } bulkTxn.Commit();逻辑说明:这段是十万行插入的骨架。bulkCmd.Transaction = bulkTxn这一行漏掉的话,命令会在事务外执行,批量退化回逐条提交。循环里每次清空参数再重新添加,避免残留上一个循环的参数值。全部插入完成后一次性Commit()。
参数说明:BeginTransaction()在 System.Data.SQLite 中返回SQLiteTransaction,默认隔离级别对绝大多数场景足够。如果中间抛异常,要 catch 后执行Rollback(),否则连接释放时挂着未完成事务,可能阻塞其他写入。批量大小建议以十万为上限分批提交,事务太大时回滚开销也大。
关于AddWithValue的一个补充:它对参数类型的推断基于传入值的 CLR 类型。批量循环里如果temp一会儿是 double、一会儿是 float,SQLite 会按第一次的声明类型存储,后面类型不一致的数据可能被静默转换。批量写入时最好统一类型,或者显式创建SQLiteParameter并指定DbType。
4.3 并发读写:锁、超时与 WAL
System.Data.SQLite 和原生 SQLite 的并发模型一致:同一时刻只有一个写者,读可以并发。默认的 rollback journal 模式下,写事务开始时申请排它锁,期间其他连接连读都可能被挡住,表现是代码里偶发「database is locked」。处理顺序我按三步走:
先给连接字符串加Default Timeout=30,让命令在锁等待时愿意多等几秒而不是立刻抛异常。再对关键数据库执行:
PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=5000;WAL模式把写操作改成追加日志的方式,读写不再互相长时间阻塞;synchronous=NORMAL在 WAL 下仍能保持不错的崩溃安全性,同时减少每次提交的磁盘同步开销;busy_timeout=5000让连接遇到锁时最多等 5 秒,避免瞬时写撞车导致的「database is locked」。
如果对程序来说,一次写入都等不起(十几毫秒的写延迟都会造成体验问题),那说明并发请求已经超出 SQLite 的舒适区,该考虑换数据库而不是继续调参。这个判断要在设计阶段做,等线上翻车再换存储,成本很高。
5. 避坑指南:System.Data.SQLite 的 5 个翻车现场
下面五条是我实际项目里踩过的坑,每条按现象、原因、解决三段写。
5.1 发布后报「未能加载 SQLite.Interop.dll」
现象:本机调试正常,把 publish 出来的程序拷到别的机器,一Open()就抛DllNotFoundException,提示找不到SQLite.Interop.dll。
原因:引用 NuGet 包时,原生 interop 是按x86、x64两个子目录分发的。默认发布动作有时候只把托管 dll 拷出去,没把这两个子目录带上。
解决:发布后检查输出目录里是否有x86\SQLite.Interop.dll和x64\SQLite.Interop.dll;没有就手动从 NuGet 包缓存里拷贝到对应目录。更好的做法是在发布流程里加一步验证:在干净环境跑一次连接测试,确保部署包完整。
5.2 试图加载格式不正确的程序(32/64 位错位)
现象:报BadImageFormatException,常见于「平台目标 = AnyCPU」但只带了其中一个位数的 interop 文件。64 位开发机上没事,换成 32 位测试机就崩。
原因:AnyCPU 程序在 64 位系统上以 64 位进程运行,会去找x64\SQLite.Interop.dll;在 32 位系统上以 32 位运行,去找x86\下的版本。缺任一个,.NET 加载原生库时就会对不上位数。
解决:要么发布时同时保留x86和x64两个目录;要么在项目属性里把平台目标固定成实际运行环境的 x86 或 x64。后者省事但会把兼容性问题藏起来。我推荐前者,因为同一份部署包面对的客户机器位数不可控。
5.3 加了 Password 却等于没加密
现象:连接字符串里写了Password=123456,程序能正常打开数据库,但把.db文件拷到另一台机器,用任何 SQLite 工具直接打开,明文全在。
原因:System.Data.SQLite 的官方免费构建没有启用加密扩展(需要 SQLITE_HAS_CODEC 编译选项)。这种构建里,Password=xxx会被忽略,连接正常但没加密。这个行为很隐蔽——程序不报错,容易让人误以为数据已经受保护。
解决:先明确需求。只是防手滑打开,就给管理者讲清楚它是明文文件;如果确实要加密,需要确认当前使用的原生库构建支持加密,不能只看连接字符串参数。另一个务实方案是对敏感字段做字段级加密,这样即使文件被人拷走,核心数据也是密文。
5.4 SQLITE_BUSY 频繁出现:先查长事务
现象:程序跑一段时间后开始报SQLITE_BUSY: database is locked,集中在多个页面同时触发写操作的时间段。重试几次能好,日志里错误时间没有规律。
原因:默认 journal 模式下,写事务会把整个数据库文件的写权限锁住,两个写请求时间上重叠一点就会互撞。另一个常见来源是长事务:某处BeginTransaction()后迟迟没提交,中间夹了个网络请求或耗时计算,把锁挂太久。
解决:把连接字符串统一加上Default Timeout=30,然后对库执行PRAGMA journal_mode=WAL;。代码层面检查所有事务,写完整 try/catch/finally,保证提交或回滚一定会执行。另外把写入入口串行化——用一个独占锁对象包住所有写路径,比反复重试隐式冲突干净得多。
5.5 跨线程共享同一个连接:诡异的间歇性报错
现象:桌面程序里用Task.Run在不同线程查数,偶发异常:有时报「对象已关闭」,有时报「连接尚未打开」。复现概率不高,重启就好,非常玄学。
原因:SQLite 本身有线程模式,System.Data.SQLite 默认允许跨线程使用但同一时刻只能一个线程操作。多个线程在同一个连接上并发调用时,内部状态互相覆盖,错误就变成随机出现。这类问题最难查,因为不固定报错在某一行。
解决:给每条线程分配独立连接;或者维护一个连接队列,按线程分配、用完归还。连接字符串里开Pooling=True时,System.Data.SQLite 自己维护池,但不要手动把同一个连接对象同时交给两个线程。长期习惯是:绝不在 lambda 闭包里直接捕获外层的SQLiteConnection。
6. 调优与验证:把 System.Data.SQLite 用到恰到好处
6.1 用 VACUUM 和 integrity_check 做体检
数据库文件增删改一段时间后,SQLite 文件里会留下空闲页,文件大小不一定减小。要真正压缩体积,需要在没有活动事务时执行VACUUM。这条命令会重写整个库文件,耗时随数据量线性增长,建议放在维护窗口或程序启动空闲时执行。
同时在程序里保留一条体检命令:
using var checkConn = new SQLiteConnection("Data Source=app.db;"); checkConn.Open(); using var checkCmd = checkConn.CreateCommand(); checkCmd.CommandText = "PRAGMA integrity_check;"; Console.WriteLine(checkCmd.ExecuteScalar()); // 正常输出 okintegrity_check返回ok说明库结构一致;如果输出损坏信息,优先用备份恢复而不是继续读取。它逐页验证 B-Tree 和链表,是比较可靠的体检手段。小库秒出结果,大库执行前要评估耗时。
6.2 WAL 模式下的备份姿态
很多人以为直接复制.db文件就是备份,在 WAL 模式下会漏数据——尚未合并进主库文件的日志事务丢了。备份前先执行PRAGMA wal_checkpoint(TRUNCATE);把日志落盘并清空,再复制文件才是完整的。更稳的做法是在程序里用 System.Data.SQLite 提供的在线备份方法,它能处理活动连接,不用先关库。备份文件恢复后先跑一遍integrity_check再投入使用。
6.3 固定流程
我自己的固定流程是:每个SQLiteConnection都用using包裹,有异常先看.db文件和日志,改完参数跑一遍integrity_check收尾,备份前确认 WAL checkpoint。这套习惯帮我避过了大部分低级的部署错误。希望帮到你。
本文还有配套的精品资源,点击获取