ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#操作SQLite从入门到实战:连接、事务、并发与踩坑指南

C#操作SQLite从入门到实战:连接、事务、并发与踩坑指南 1. 从一次“数据库文件打不开”的现场说起前两天帮朋友排查一个C#的小工具现象很典型程序在开发机上跑得好好的拷到客户那边就报“unable to open database file”。查了一圈路径没写错文件也存在最后发现是客户把程序放在了一个没有写权限的目录里SQLite压根没法创建临时文件。这种问题光靠搜索引擎是搜不出答案的得真踩过坑才知道。C#连SQLite听起来是个老话题网上教程一大把但大部分要么只贴几个方法名要么直接扔一段代码让人复制完全没讲背后的设计逻辑。这篇不打算那么干。我会从连接字符串的构造、基础增删改查的套路到事务、并发、参数化这些绕不开的细节一层层拆开讲。中间会穿插一些实操中才能碰到的坑比如类型映射、路径权限、连接池副作用这些常规教程不会提的东西。适合三种人看刚入门的C#新手想找一个能直接跑的方案写上位机或者桌面工具的老手想查漏补缺还有那些被SQLite坑过、想搞清楚“为什么”的人。SQLite在C#生态里最典型的应用场景是本地单机软件——配置存储、日志落盘、离线数据缓存、上位机的运行参数记录。它不需要安装服务不需要账号密码就是一个文件挪走就能带走部署成本几乎为零。但也正因为“太简单”很多人反而会在一些基础环节上栽跟头。下面从选型开始把整个链路捋一遍。2. 环境准备与驱动选型为什么推荐Microsoft.Data.Sqlite2.1 两个主流驱动怎么选C#里操作SQLite绕不开两个包System.Data.SQLite和Microsoft.Data.Sqlite。前者出道早功能全自带一个完整的ADO.NET实现还能做加密后者是微软官方维护的轻量专门为.NET Core和.NET 5设计的。我的建议很直接新项目一律用Microsoft.Data.Sqlite。理由有三个微软官方在维护和EF Core的集成最顺畅包体积小依赖少发布的时候不拖泥带水API风格更现代写起来顺手。System.Data.SQLite也不是没有价值如果你需要SQLite的加密扩展或者要用到一些冷门特性它仍然是唯一选择。但对绝大多数应用场景微软官方包足够而且踩坑的几率低得多。安装方式Visual Studio里打开“管理NuGet程序包”搜索“Microsoft.Data.Sqlite”装最新稳定版就行。或者用包管理器控制台Install-Package Microsoft.Data.Sqlite这里补一个细节包版本和.NET版本有对应关系。如果你还在用.NET Framework 4.x需要装2.x版本的Microsoft.Data.Sqlite.NET Core 3.1以上才能用最新版。装之前看一眼项目目标框架免得装完编译报一堆类型冲突。2.2 可视化工具DB Browser for SQLite够用吗光有代码不够调试SQLite的时候我强烈建议配一个可视化工具。我用得最顺的是DB Browser for SQLite也就是常说的DB4S。免费、开源、跨平台Windows和Linux都能跑。这个工具能干嘛直接打开数据库文件浏览表结构、看数据、执行SQL语句、导出数据还能可视化编辑表结构。最常用的场景是程序写进去的数据怀疑有问题的时候用DB4S打开看一眼是不是真的写进去了死盯代码效率高得多。另一个高频场景是改表结构。SQLite早期版本的ALTER TABLE能力极弱想删一列都费劲。DB4S有个“修改表”的图形化入口它会自动帮你重建表结构省去手写一堆迁移SQL的麻烦。2.3 路径与权限第一个隐藏坑环境准备好以后第一件事不是写代码而是搞清楚文件路径怎么给。SQLite的数据库文件路径天然就是连接字符串的一部分看起来很简单但路径有讲究var connectionString Data Sourcemydatabase.db;这个相对路径写法工作目录是程序启动时所在目录。开发环境没问题但发布之后程序可能被放在任务计划程序、Windows服务、或者别的进程拉起的环境里工作目录会变。典型故障明明文件生成在程序目录下却报找不到文件。原因很简单——工作目录变了。经验做法连接字符串里的路径写成绝对路径或者基于程序集位置拼接的路径。var baseDir AppContext.BaseDirectory; // 程序集所在目录 var dbPath Path.Combine(baseDir, app_data, mydatabase.db); var connectionString $Data Source{dbPath};再补一个权限细节SQLite运行时不只是读写数据库文件本身还要在同一目录下创建-journal或-wal文件用于事务日志和预写日志。这意味着程序不仅要有数据库文件的读写权限还要有所在目录的创建文件权限。开篇提到的“unable to open database file”多半就是这个原因。部署到生产环境时先确认运行账户对这个目录有完全控制权别把程序放在Program Files这类受保护目录下还不做权限调整。3. 连接管理和基础增删改查从第一个连接开始3.1 连接字符串与连接生命周期连接字符串看起来最简单的写法Data Sourcexxx.db但真实场景往往还要加几个参数。常用参数参数作用建议值Data Source数据库文件路径必填Mode连接模式ReadWriteCreate是默认能读能写不存在就创建ReadWriteCreate / ReadOnlyCache连接缓存模式Shared能让多个连接共享同一缓存默认即可Password加密数据库的密码前提是SQLite编译了加密扩展非必须Foreign Keys是否启用外键约束建议True连接创建和释放直接用using块托管using var connection new SqliteConnection(connectionString); connection.Open(); // 你的操作 // 离开作用域自动Dispose连接自动关闭为什么要强调“用完即关”SQLite是文件型数据库连接本质上就是一个文件句柄加一堆内部状态。连接开太多不放轻则内存和句柄泄漏重则影响其他进程对这个文件的访问。虽然SQLite做了很多并发保护但不要挑战它的底线。有人喜欢在程序里维护一个全局唯一的静态连接省得反复开关。我试过短期没问题时间一长就出怪事连接状态错乱、事务脏数据、文件锁不释放。SQLite的最佳实践恰恰是“短连接”——每次操作开一个连接用完成立刻关这是它性能最稳的使用方式。连接池的开销并不大正常情况下每次Open也就是微秒级到毫秒级的事完全没必要为了省这个时间引入状态管理的复杂度。3.2 建表与插入理解SqliteCommand的工作方式建表和增删改查核心对象就三个SqliteConnection、SqliteCommand、SqliteDataReader。先看建表using var connection new SqliteConnection(connectionString); connection.Open(); var createSql CREATE TABLE IF NOT EXISTS device_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_name TEXT NOT NULL, temperature REAL NOT NULL, record_time TEXT NOT NULL ); using var command connection.CreateCommand(); command.CommandText createSql; command.ExecuteNonQuery();建表SQL和别的数据库差不多。有几个SQLite特有的细节INTEGER PRIMARY KEY AUTOINCREMENT自增主键的标准写法SQLite没有专门的日期时间类型一般用TEXT存ISO格式字符串REAL对应C#的double或float。插入数据最忌讳的是拼字符串。新手常犯的错误// 反面教材 var sql $INSERT INTO device_records (device_name, temperature, record_time) VALUES ({name}, {temp}, {time});这在SQLite里同样有SQL注入风险。比如name是abc); DROP TABLE device_records;--整个表就没了。更重要的是拼字符串还会导致SQLite无法复用SQL语句的执行计划性能白白损耗。正确做法是用参数化using var connection new SqliteConnection(connectionString); connection.Open(); using var command connection.CreateCommand(); command.CommandText INSERT INTO device_records (device_name, temperature, record_time) VALUES ($name, $temp, $time); command.Parameters.AddWithValue($name, deviceName); command.Parameters.AddWithValue($temp, temperature); command.Parameters.AddWithValue($time, DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss)); command.ExecuteNonQuery();参数名用$或前缀都可以SQLite都认。建议统一用$可以避免某些驱动里被解析成变量的歧义。3.3 查询与读取DataReader的正确打开方式查询数据用ExecuteReader拿到SqliteDataReader然后遍历using var connection new SqliteConnection(connectionString); connection.Open(); using var command connection.CreateCommand(); command.CommandText SELECT id, device_name, temperature, record_time FROM device_records; command.Parameters.AddWithValue($minTemp, 25.0); command.CommandText WHERE temperature $minTemp; using var reader command.ExecuteReader(); while (reader.Read()) { var id reader.GetInt32(0); var name reader.GetString(1); var temp reader.GetDouble(2); var time reader.GetString(3); Console.WriteLine(${id} - {name} - {temp} - {time}); }这里有几个容易犯糊涂的地方GetInt32、GetString这类方法要求索引对应的列类型必须匹配否则抛异常。比如列是INTEGER你用GetString去想拿到“123”对不起报错。真要拿通用值用reader[columnName]返回的是object再自己转换。另一个重点是Read()方法每调用一次指针移动一行数据是顺序读的不能跳行想回头读得重新查询。读取大批量数据的时候不要用DataTable——那会把全部数据加载进内存。直接用DataReader一条条消费内存占用小且速度快这是ADO.NET生态里被过度遗忘的好习惯。3.4 更新与删除别忘了一个关键细节更新删除的写法逻辑上一样都是ExecuteNonQueryusing var command connection.CreateCommand(); command.CommandText UPDATE device_records SET temperature $temp WHERE id $id; command.Parameters.AddWithValue($temp, 26.5); command.Parameters.AddWithValue($id, 1); command.ExecuteNonQuery();这里补一个SQLite特有的行为DELETE和UPDATE对参数的使用同样应该参数化原因和INSERT完全一样。另一个小知识ExecuteNonQuery的返回值是受影响的行数。判断删除或更新是否真的命中直接检查返回值即可不用再额外查询一遍——这招在处理“按条件删除但行可能不存在”的热点问题上很省事。3.5 万能查询帮手写好一个迷你工具类写上位机或工具类程序时最简单高效的封装是做一个“查询Template”方法public static ListT QueryListT(string sql, FuncSqliteDataReader, T mapper, params SqliteParameter[] parameters) { var result new ListT(); using var connection new SqliteConnection(_connectionString); connection.Open(); using var command connection.CreateCommand(); command.CommandText sql; if (parameters ! null) command.Parameters.AddRange(parameters); using var reader command.ExecuteReader(); while (reader.Read()) result.Add(mapper(reader)); return result; }用起来就是var list QueryList(SELECT * FROM device_records, r new DeviceRecord { Id r.GetInt32(0), Name r.GetString(1), Temperature r.GetDouble(2) });这套思路参考了Dapper这类微ORM的映射逻辑但不用引第三方包轻巧直接。写日志、读配置、查历史记录一块代码全搞定。如果你愿意还能进一步做泛型加表达式树不过那就是另一个深度话题了这个迷你版在大多数场景下已经能把代码量砍掉一大半。4. 进阶实操事务、并发、类型异常与防坑指南4.1 事务到底怎么用批量操作为王SQLite的事务是一条分界线——不会事务就只能做“玩具级”应用用好了才能承接真正的业务数据。事务有两个核心价值原子性和批量提交性能。原子性好理解一个事务内的操作要么全部成功要么全部回滚。比如要同时更新设备状态表和写入一条日志第二条失败时第一条不能留着。用事务包裹using var connection new SqliteConnection(connectionString); connection.Open(); using var transaction connection.BeginTransaction(); using var command connection.CreateCommand(); command.Transaction transaction; command.CommandText INSERT INTO device_records (device_name, temperature, record_time) VALUES ($name, $temp, $time); try { for (int i 0; i 1000; i) { command.Parameters.Clear(); command.Parameters.AddWithValue($name, $device_{i}); command.Parameters.AddWithValue($temp, 20 i); command.Parameters.AddWithValue($time, DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss)); command.ExecuteNonQuery(); } transaction.Commit(); } catch { transaction.Rollback(); throw; }这里有个重要细节同一个SqliteCommand可以循环使用但每次都要清空再添加参数或者直接给参数重新赋值。command.Parameters[$name].Value $device_{i};这种赋值方式更快省去频繁Add的开销。性能方面事务是批量插入的救星。默认情况下SQLite每次ExecuteNonQuery都自动开启一个隐式事务每次都要做磁盘同步fsync那才叫慢。把1000条插入包在一个事务里实测速度能提升两个数量级——从秒级降到毫秒级。原理是磁盘IO从每次插入一次变成整个事务一次代价是事务期间内存占用会高一点但对几千条数据来说影响可以忽略。注意事务提交前SqliteConnection不能关闭连接关闭会自动回滚未提交事务。这一条最容易被忽略——很多人写完代码发现数据“不见了”多半就是早把连接关掉了。4.2 SQLite并发模型谁在锁谁SQLite的并发和MySQL、SQL Server完全是两个世界。它走的是文件级锁同一时刻只能有一个事务在写数据库严格说是同一个数据库文件。实践中的取舍多线程并发读没问题WAL模式预写日志下读不阻塞写一读一写并发WAL模式下也没问题两个写事务并发无论什么模式只能排队而且超时默认会报“database is locked”。怎么办两条路第一条开启WAL模式。执行一句SQL就能开启PRAGMA journal_modeWAL;这条命令开启后SQLite的读写并发能力大幅提升。注意它改变的是数据库文件的持久属性改动一次后续所有连接默认都带WAL属性除非再次改回DELETE模式。第二条控制业务写法短事务、快事务、串行化写操作。所有写操作走同一把业务锁比如用C#的SemaphoreSlim(1,1)包住写入逻辑。这不是SQLite的缺陷而是“本地文件型数据库”的固有特性。认识到这一点架构设计上就不会把SQLite当作高并发服务器数据库来用了。4.3 类型映射对照C#与SQLite之间的数据转换SQLite宣称“动态类型”但驱动层会做类型检查表定义的声明类型仍会影响读写行为。我整理了常用对照表SQLite类型C#推荐类型说明INTEGERlong / int读出来用GetInt64或GetInt32REALdouble / floatGetDoubleTEXTstringGetStringBLOBbyte[]GetFieldValuebyte[]NULLDBNull / 可空类型IsDBNull判断最容易踩的坑是INTEGER读出长整型。SQLite的INTEGER存储本身是64位驱动默认返回long但如果代码里用GetInt32读一个超过2^31的大数会抛OverflowException。反过来插入int值没问题SQLite会自动扩展为INTEGER。稳妥做法不确定数值范围时读出来一律用Convert.ToInt32(reader[column])做转换它能内部处理类型兼容问题。另一个常见坑SQLite没有专门的DateTime类型存TEXT用字符串。读写时格式不一致可能引发解析异常。我在工具里习惯统一用yyyy-MM-dd HH:mm:ss格式简单直观避免驱动自动转换的小数点格式问题。4.4 参数化的隐藏价值不只是防注入参数化除了防SQL注入还有一个容易被忽略的好处——类型保真。看这个例子command.Parameters.AddWithValue($time, DateTime.Now);驱动会按DateTime类型绑定参数自动格式化成SQLite能识别的文本。但如果你直接拼字符串var sql $INSERT ... VALUES ({DateTime.Now});ToString的格式受系统区域语言影响上线前好好的上线后换台电脑格式变了数据存进去就坏。参数化把这个隐患彻底消化掉了。同理带小数点的数值拼字符串还会遇到小数点符号是“.”还是“,”的问题——参数化一律不存在。4.5 查询大量数据时的内存控制举个例子需要从SQLite里读10万条传感器数据然后做一次简单统计。不推荐的做法var dt new DataTable(); dbAdapter.Fill(dt); // 一次性全载入内存推荐做法double sum 0; int count 0; using var reader command.ExecuteReader(); while (reader.Read()) { sum reader.GetDouble(1); count; }DataReader是流式读取逐行消费内存占用恒定DataTable则是先攒到内存里量大时内存飙到几百MB也是轻轻松松。日常工具类应用里这条经验比什么花哨的ORM技巧都管用。4.6 并发写冲突处理与重试机制“database is locked”是SQLite实操中最常见的运行时异常尤其是不同进程同时写同一个库文件的时候。处理思路很简单捕获异常稍后重试。写一个带重试的执行器public static void ExecuteWithRetry(Action action, int maxRetryCount 5) { for (int i 0; i maxRetryCount; i) { try { action(); return; } catch (SqliteException ex) when (ex.SqliteErrorCode 5) // SQLITE_BUSY { Thread.Sleep(50 * (i 1)); } } }这里SqliteErrorCode 5对应的是SQLITE_BUSY即数据库被锁。重试间隔用线性退避第一次等50毫秒第二次100毫秒逐步变长。实测在WAL模式下多进程写冲突概率大幅下降重试基本在第一次就成功。5. 常见报错与排查技巧遇到问题别再瞎猜5.1 高频异常速查表现象根本原因排查/解决unable to open database file目录无写权限/路径错误检查运行账户对目录的写权限改用绝对路径no such table: xxx表确实不存在或连接到的数据库文件不对用DB4S打开确认文件确认Data Source路径确认建表SQL已执行database is locked并发写冲突或长事务未提交加重试机制、开启WAL模式、缩短事务时间file is not a database打开了一个不是SQLite格式的文件文件损坏或路径指向了非数据库文件比如日志文件SqliteException: constraint failed违反约束如UNIQUE冲突NOT NULL冲突检查插入/更新数据是否满足表结构约束找不到问题但数据没写入可能连接被提前关闭导致事务回滚确保Commit、确保连接的using块包含整个事务生命周期5.2 “no such table”的高频迷案这个错误很有迷惑性。程序明明执行过建表语句但每次查询都报这张表不存在。最常见的原因有两个一是连接到了不同的文件。比如建表时用的连接是相对路径app.db查询时用的是拼接出来的绝对路径两个路径实际指向不同文件。开发时注意在连接字符串构造处加日志打印把实际路径打出来看一眼十次里有八次能秒定位。二是建表的ExecuteNonQuery根本没执行成功。常见于把建表逻辑放在某个条件分支里或者建表语句有语法错误但被吞掉。排查办法把建表SQL复制到DB4S里手动执行一次如果报错优先检查SQL语法。5.3 命令无法识别你的环境可能连错了有时候问题压根不在SQLite本身而是环境不对。网上经常看到类似“sqlite3无法识别”的报错——这不怪SQLite是系统没装命令行工具或者没配环境变量。解决路径很简单Windows下去SQLite官网下载sqlite-tools-win-x64压缩包解压后把目录加进PATH或者干脆用DB4S图形化管理不需要命令行。这跟C#开发经常碰到的“dotnet命令无法识别”是一个道理工具链没有装好而已别去怀疑代码逻辑。5.4 数据文件损坏怎么抢救SQLite也会损坏概率不高但一坏就头大。特别是程序中途崩溃、磁盘写满、断电的场景可能把数据库文件搞坏。处理优先顺序先备份。任何操作前先把.db文件复制一份到安全位置用DB4S打开看能不能读能读就导出为SQL文件或CSV如果打不开在命令行里执行sqlite3 corrupted.db .recover | sqlite3 recovered.dbSQLite自带recover命令能把能读的内容尽可能恢复到新文件4. 如果还是不行.dump命令强制导出文本SQL再导入新库。实测经验日常备份才是根治手段。写个简单任务定时把数据库文件复制到备份目录成本极低收益极高。别等数据没了再想对策。5.5 调试技巧用一条日志看清全局我调试SQLite程序时习惯在关键位置打印SQL和参数值Console.WriteLine($SQL: {command.CommandText}); foreach (SqliteParameter p in command.Parameters) Console.WriteLine(${p.ParameterName} {p.Value});别看这土办法简单它能瞬间暴露两类问题SQL写错、参数绑错。特别是从别的数据库迁移过来的SQL语法差异经常在这里现原形。调试完再删掉日志代码就行或者用条件编译包在#if DEBUG里让正式发布不输出。5.6 跨平台部署的额外注意C#的跨平台能力这些年确实强了但SQLite在不同平台上的行为还是有细微差别。Windows下路径分隔符是\Linux/macOS下是/最好用Path.Combine统一处理。大小写敏感性在Linux上不可忽略文件名和表名的大小写错误在Windows上可能侥幸通过Linux上直接报错。部署到Linux服务器时先确认libe_sqlite3依赖已经安装否则运行时会报找不到native库的错误。一个更隐蔽的问题SMB网络共享上使用SQLite。文件锁在网络文件系统上有时候不生效两个客户端同时写容易损坏数据库。这是一个“设计层面就不该做”的事SQLite定位是本地数据库不要把网络共享上的SQLite文件当作多机共享数据库来用。6. 完整案例分析一个设备数据采集与查询程序6.1 需求从采集到展示我做一个具体的案例帮你把前面所有知识串起来。场景很常见一个上位机程序定时从一个传感器读取温湿度数据存入SQLite提供按时间范围查询和统计功能。这个案例覆盖了连接管理、参数化写数据、事务批量插入、查询与统计。整套代码可以直接拆开用。6.2 建库与初始化using System; using Microsoft.Data.Sqlite; namespace SQLiteDemo { class Program { static string _baseDir AppContext.BaseDirectory; static string _connStr $Data Source{Path.Combine(_baseDir, sensor_data.db)}; static void Main(string[] args) { InitializeDatabase(); // 模拟写入25条数据 InsertSensorData(25); // 查询最近5条 QueryLatest(5); // 统计平均值 AverageTemperature(); } static void InitializeDatabase() { using var conn new SqliteConnection(_connStr); conn.Open(); using var cmd conn.CreateCommand(); cmd.CommandText CREATE TABLE IF NOT EXISTS sensor_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, sensor_name TEXT NOT NULL, temperature REAL NOT NULL, humidity REAL NOT NULL, record_time TEXT NOT NULL ); cmd.ExecuteNonQuery(); }这个初始化方法有几个细节CREATE TABLE IF NOT EXISTS保证重复运行不报错字段类型TEXT存储时间匹配SQLite推荐做法主键自增让插入更省心。6.3 事务写入与模拟采集static void InsertSensorData(int count) { using var conn new SqliteConnection(_connStr); conn.Open(); using var transaction conn.BeginTransaction(); using var cmd conn.CreateCommand(); cmd.Transaction transaction; cmd.CommandText INSERT INTO sensor_records (sensor_name, temperature, humidity, record_time) VALUES ($name, $temp, $hum, $time); var rnd new Random(); try { for (int i 0; i count; i) { cmd.Parameters.Clear(); cmd.Parameters.AddWithValue($name, sensor_a); cmd.Parameters.AddWithValue($temp, 18.5 rnd.NextDouble() * 10); cmd.Parameters.AddWithValue($hum, 40 rnd.NextDouble() * 20); cmd.Parameters.AddWithValue($time, DateTime.Now.AddMinutes(-count i).ToString(yyyy-MM-dd HH:mm:ss)); cmd.ExecuteNonQuery(); } transaction.Commit(); Console.WriteLine($成功写入 {count} 条记录); } catch { transaction.Rollback(); throw; } }每次循环cmd.Parameters.Clear()再重新Add是为了避免参数值残留也避免参数集合无限膨胀。事务包裹确保25条要么全进、要么全不进。如果一次写入更多数据上千条推荐继续维持这个结构只是把循环拆成几个批次每批次一个事务避免单个事务太大导致日志文件暴涨。6.4 查询与统计展示static void QueryLatest(int limit) { using var conn new SqliteConnection(_connStr); conn.Open(); using var cmd conn.CreateCommand(); cmd.CommandText SELECT id, sensor_name, temperature, humidity, record_time FROM sensor_records ORDER BY id DESC LIMIT $limit; cmd.Parameters.AddWithValue($limit, limit); using var reader cmd.ExecuteReader(); Console.WriteLine(--- 最近记录 ---); while (reader.Read()) { var id reader.GetInt32(0); var name reader.GetString(1); var temp reader.GetDouble(2); var hum reader.GetDouble(3); var time reader.GetString(4); Console.WriteLine($#{id} | {name} | {temp:F1}°C | {hum:F1}% | {time}); } } static void AverageTemperature() { using var conn new SqliteConnection(_connStr); conn.Open(); using var cmd conn.CreateCommand(); cmd.CommandText SELECT AVG(temperature), MIN(temperature), MAX(temperature), COUNT(*) FROM sensor_records; using var reader cmd.ExecuteReader(); if (reader.Read()) { Console.WriteLine($平均温度: {reader.GetDouble(0):F2}°C); Console.WriteLine($最低温度: {reader.GetDouble(1):F2}°C); Console.WriteLine($最高温度: {reader.GetDouble(2):F2}°C); Console.WriteLine($总记录数: {reader.GetInt32(3)}); } } } }注意ORDER BY id DESC LIMIT $limit这个写法查询最近N条记录不用先全量加载再排序高效直接。聚合函数AVG/MIN/MAX直接让SQLite算数据再多也不用把明细拉回C#内存算。这套查法在上位机场景里非常够用几分钟就能跑的秒级查询SQLite完全吃得消。7. 踩坑实录与效率提升心得7.1 一个让我记忆犹新的索引问题有一次同事反馈设备记录表才几万条数据查询一天的历史数据居然要两秒多。表结构很简单就是时间字段record_time但查询语句写的是WHERE record_time 2024-01-01 00:00:00 AND record_time 2024-01-01 23:59:59没建索引SQLite只能全表扫描。解决方案一句话CREATE INDEX idx_record_time ON sensor_records(record_time);加完再看查询直接变成几十毫秒。索引是SQLite性能的第一课。表不大不觉得一旦数据量到十万级以上索引和没索引是两个世界的体验。注意索引不要建太多它虽然加速查询但也会拖慢写操作权衡标准是“查询远多于写入的字段才值得建索引”。7.2 可视化和权限管理的小习惯用DB4S调试完记得关闭它的连接再运行程序。DB4S默认打开数据库时会持有一个连接有些模式下会把数据库锁住程序要修改写数据时就会碰到“database is locked”。不是SQLite出问题是工具占着茅坑。另一个我养成的习惯生产环境中SQLite数据库文件所在目录一律单独建一个data子目录并在部署脚本里预设写权限。这样数据库文件放在一个干净路径程序日志、临时文件各放各的排查问题也方便。这个习惯帮我避免过至少三起“目录权限不足导致程序静默失败”的事件。7.3 版本升级与迁移程序升级时SQLite表结构往往要变。SQLite的ALTER TABLE能力有限早期版本只支持改表名和加列不支持直接删列、改约束。我当时做一个工具需求是给一张老表加两列再加一个索引方案是ALTER TABLE sensor_records ADD COLUMN alarm_level TEXT; ALTER TABLE sensor_records ADD COLUMN remark TEXT; CREATE INDEX IF NOT EXISTS idx_sensor_time ON sensor_records(record_time);加列是老表升级最高效、最不破坏性的操作。如果真要删列就别偷懒把目标表数据导到临时表重建新结构把数据导回来改表名。DB4S的“修改表”功能能帮你自动完成这个Shuffle过程但我建议你在代码里也写一个类似的迁移逻辑不然生产环境的数据库迟早等你手撸SQL。7.4 自增主键的“空隙”陷阱INTEGER PRIMARY KEY AUTOINCREMENT会生成严格递增且不重复的主键但删除一行后这个ID不会被复用。有些业务会因此产生“为什么ID跳到100了表里只有30行”的困惑。解决办法看需求只是展示用不用管业务逻辑依赖连续ID那就说明表设计得重新考虑连续ID本来就不该作为主键依赖。7.5 经验把数据库当作一个日志文件说句大实话C#开发里SQLite最顺手的用法就是把数据库当成一个“结构化的日志文件”。写入频繁查询按时间切片数据定期清理。用这个思路设计代码逻辑最顺一条数据一个Insert一批数据一个事务查询走索引老数据定时DELETE。别想着在SQLite上搞复杂的关系模型那是在违背它的天性。8. 写在最后的几条个人经验这条路走了不少弯有几条经验是实实在在从生产环境的故障里换来的再说一遍事务和短连接是SQLite稳定性的两块基石。每一个单条写入都裸奔没有事务包裹出问题时数据半条不条那是常态。反过来连接开在那里不关闭文件锁慢慢失控性能一路下滑也是大家最容易忽略的慢性病。这两件事做对了SQLite在本地应用里稳如老狗。路径和权限永远是SQLite应用搬上生产环境后最先爆炸的问题。开发机跑得好好的部署就报错十有八九是这里。把路径拼接打印出来、把权限确认好能省掉一个通宵的排查时间。参数化不是可选项是必须项。它同时解决注入风险和类型保真两个问题写任何SQLite命令时请无条件使用。索引一定要建但别乱建。查询性能翻倍是它写入变慢也是它。建索引前先想清楚查询频率别一股脑每个字段都建。最后分享一个小技巧上线前花十分钟用DB4S把数据库文件整个检查一遍跑几个关键查询确认表和索引都在顺手备份一份文件。这个习惯让我避开过好几次“数据库文件损坏”“表结构没来得及建对”这种低级但致命的问题。SQLite用起来简单但越简单的东西越值得保持敬畏。
返回列表