
简介面向 .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.SQLiteMicrosoft.Data.SqliteMono.Data.SqliteADO.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 到第一个能跑的程序全套 CRUD3.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 Sourceapp.db;Version3;); 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 稳定自增后续按主键定位更新、删除最省事。参数说明Version3指定使用 SQLite 3.x 格式旧教程里常见。实际新版驱动默认已经是 3标准的Data Sourceapp.db就够了写出来是照顾从老代码拷贝来的同学防止你抄到一半发现少了参数心里不踏实。相对路径是相对于进程当前工作目录不是程序集所在目录这点后面有坑。3.2 插入、查询、更新的一条龙演示using var conn2 new SQLiteConnection(Data Sourceapp.db;Version3;); 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.5double会映射成 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 SourceData SourceD:\data\app.db数据库文件路径相对路径相对当前工作目录VersionVersion3新版可省略保留无害CacheCacheShared同一进程内共享缓存多连接读同一文件时减少磁盘 IOPoolingPoolingTrue是否启用连接池桌面程序一般保持默认Default TimeoutDefault Timeout30命令默认超时与锁等待相关Read OnlyRead OnlyTrue只读打开适合报表、备份场景PasswordPasswordchange-me是否加密注意默认原生库不带加密实现FailIfMissingFailIfMissingTrue文件不存在时报错默认自动创建Foreign KeysForeign KeysTrue启用外键约束SQLite 默认关闭外键检查两处容易踩的坑一是Data Source使用相对路径时ASP.NET 程序的工作目录和.db文件目录经常不一致建议用绝对路径或程序动态计算路径二是期望Password加密却发现文件依然能被其他工具打开原因在第 5 章专门讲。连接池方面System.Data.SQLite 的连接池按连接字符串维度维护。桌面程序通常不关心它但 Web 程序如果每条请求都新建连接PoolingTrue能明显减少打开文件的开销。代价是连接被复用时要留意事务状态——连接回池前必须已提交或回滚否则下一个使用者会继承到一个未结束的事务。4.2 事务与批量写入的正确姿势写入性能是新手最容易感知差距的地方。直接循环单条 INSERT 会有两笔开销每次写入开启一次隐式事务并同步落盘虽然没有显式事务但每次提交都触发一次磁盘同步。把 N 条插入放进一个显式事务能合并成一次提交写入量上万时差距往往是数量级的。using var bulkConn new SQLiteConnection(Data Sourceapp.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、一会儿是 floatSQLite 会按第一次的声明类型存储后面类型不一致的数据可能被静默转换。批量写入时最好统一类型或者显式创建SQLiteParameter并指定DbType。4.3 并发读写锁、超时与 WALSystem.Data.SQLite 和原生 SQLite 的并发模型一致同一时刻只有一个写者读可以并发。默认的 rollback journal 模式下写事务开始时申请排它锁期间其他连接连读都可能被挡住表现是代码里偶发「database is locked」。处理顺序我按三步走先给连接字符串加Default Timeout30让命令在锁等待时愿意多等几秒而不是立刻抛异常。再对关键数据库执行PRAGMA journal_modeWAL; PRAGMA synchronousNORMAL; PRAGMA busy_timeout5000;WAL模式把写操作改成追加日志的方式读写不再互相长时间阻塞synchronousNORMAL在 WAL 下仍能保持不错的崩溃安全性同时减少每次提交的磁盘同步开销busy_timeout5000让连接遇到锁时最多等 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 却等于没加密现象连接字符串里写了Password123456程序能正常打开数据库但把.db文件拷到另一台机器用任何 SQLite 工具直接打开明文全在。原因System.Data.SQLite 的官方免费构建没有启用加密扩展需要 SQLITE_HAS_CODEC 编译选项。这种构建里Passwordxxx会被忽略连接正常但没加密。这个行为很隐蔽——程序不报错容易让人误以为数据已经受保护。解决先明确需求。只是防手滑打开就给管理者讲清楚它是明文文件如果确实要加密需要确认当前使用的原生库构建支持加密不能只看连接字符串参数。另一个务实方案是对敏感字段做字段级加密这样即使文件被人拷走核心数据也是密文。5.4 SQLITE_BUSY 频繁出现先查长事务现象程序跑一段时间后开始报SQLITE_BUSY: database is locked集中在多个页面同时触发写操作的时间段。重试几次能好日志里错误时间没有规律。原因默认 journal 模式下写事务会把整个数据库文件的写权限锁住两个写请求时间上重叠一点就会互撞。另一个常见来源是长事务某处BeginTransaction()后迟迟没提交中间夹了个网络请求或耗时计算把锁挂太久。解决把连接字符串统一加上Default Timeout30然后对库执行PRAGMA journal_modeWAL;。代码层面检查所有事务写完整 try/catch/finally保证提交或回滚一定会执行。另外把写入入口串行化——用一个独占锁对象包住所有写路径比反复重试隐式冲突干净得多。5.5 跨线程共享同一个连接诡异的间歇性报错现象桌面程序里用Task.Run在不同线程查数偶发异常有时报「对象已关闭」有时报「连接尚未打开」。复现概率不高重启就好非常玄学。原因SQLite 本身有线程模式System.Data.SQLite 默认允许跨线程使用但同一时刻只能一个线程操作。多个线程在同一个连接上并发调用时内部状态互相覆盖错误就变成随机出现。这类问题最难查因为不固定报错在某一行。解决给每条线程分配独立连接或者维护一个连接队列按线程分配、用完归还。连接字符串里开PoolingTrue时System.Data.SQLite 自己维护池但不要手动把同一个连接对象同时交给两个线程。长期习惯是绝不在 lambda 闭包里直接捕获外层的SQLiteConnection。6. 调优与验证把 System.Data.SQLite 用到恰到好处6.1 用 VACUUM 和 integrity_check 做体检数据库文件增删改一段时间后SQLite 文件里会留下空闲页文件大小不一定减小。要真正压缩体积需要在没有活动事务时执行VACUUM。这条命令会重写整个库文件耗时随数据量线性增长建议放在维护窗口或程序启动空闲时执行。同时在程序里保留一条体检命令using var checkConn new SQLiteConnection(Data Sourceapp.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。这套习惯帮我避过了大部分低级的部署错误。希望帮到你。本文还有配套的精品资源点击获取