news 2026/10/2 21:56:50

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#操作SQLite从入门到实战:连接、事务、并发与踩坑指南

1. 从一次“数据库文件打不开”的现场说起

前两天帮朋友排查一个C#的小工具,现象很典型:程序在开发机上跑得好好的,拷到客户那边就报“unable to open database file”。查了一圈,路径没写错,文件也存在,最后发现是客户把程序放在了一个没有写权限的目录里,SQLite压根没法创建临时文件。这种问题,光靠搜索引擎是搜不出答案的,得真踩过坑才知道。

C#连SQLite,听起来是个老话题,网上教程一大把,但大部分要么只贴几个方法名,要么直接扔一段代码让人复制,完全没讲背后的设计逻辑。这篇不打算那么干。我会从连接字符串的构造、基础增删改查的套路,到事务、并发、参数化这些绕不开的细节,一层层拆开讲。中间会穿插一些实操中才能碰到的坑,比如类型映射、路径权限、连接池副作用这些常规教程不会提的东西。适合三种人看:刚入门的C#新手想找一个能直接跑的方案,写上位机或者桌面工具的老手想查漏补缺,还有那些被SQLite坑过、想搞清楚“为什么”的人。

SQLite在C#生态里最典型的应用场景是本地单机软件——配置存储、日志落盘、离线数据缓存、上位机的运行参数记录。它不需要安装服务,不需要账号密码,就是一个文件,挪走就能带走,部署成本几乎为零。但也正因为“太简单”,很多人反而会在一些基础环节上栽跟头。下面从选型开始,把整个链路捋一遍。

2. 环境准备与驱动选型:为什么推荐Microsoft.Data.Sqlite

2.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 Source=mydatabase.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 Source=xxx.db

但真实场景往往还要加几个参数。常用参数:

参数作用建议值
Data Source数据库文件路径必填
Mode连接模式:ReadWriteCreate是默认,能读能写不存在就创建ReadWriteCreate / ReadOnly
Cache连接缓存模式,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 更新与删除:别忘了一个关键细节

更新删除的写法逻辑上一样,都是ExecuteNonQuery:

using 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 List<T> QueryList<T>(string sql, Func<SqliteDataReader, T> mapper, params SqliteParameter[] parameters) { var result = new List<T>(); 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_mode=WAL;

这条命令开启后,SQLite的读写并发能力大幅提升。注意它改变的是数据库文件的持久属性,改动一次,后续所有连接默认都带WAL属性(除非再次改回DELETE模式)。

第二条,控制业务写法:短事务、快事务、串行化写操作。所有写操作走同一把业务锁,比如用C#的SemaphoreSlim(1,1)包住写入逻辑。这不是SQLite的缺陷,而是“本地文件型数据库”的固有特性。认识到这一点,架构设计上就不会把SQLite当作高并发服务器数据库来用了。

4.3 类型映射对照:C#与SQLite之间的数据转换

SQLite宣称“动态类型”,但驱动层会做类型检查,表定义的声明类型仍会影响读写行为。我整理了常用对照表:

SQLite类型C#推荐类型说明
INTEGERlong / int读出来用GetInt64或GetInt32
REALdouble / floatGetDouble
TEXTstringGetString
BLOBbyte[]GetFieldValue<byte[]>
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也会损坏,概率不高但一坏就头大。特别是程序中途崩溃、磁盘写满、断电的场景,可能把数据库文件搞坏。处理优先顺序:

  1. 先备份。任何操作前先把.db文件复制一份到安全位置;
  2. 用DB4S打开看能不能读,能读就导出为SQL文件或CSV;
  3. 如果打不开,在命令行里执行:
sqlite3 corrupted.db ".recover" | sqlite3 recovered.db

SQLite自带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用起来简单,但越简单的东西,越值得保持敬畏。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 21:56:42

Python特产推荐系统毕设实战:从协同过滤到系统实现全解析

每年三四月份&#xff0c;计算机专业的私聊窗口里有一类消息几乎年年准时出现&#xff1a;“学长&#xff0c;我的毕设题目是基于Python的特产推荐系统的设计与实现&#xff0c;拿到源码包和LW文档模板快两周了&#xff0c;还是不知道怎么开始写&#xff0c;能不能帮我理一下思…

作者头像 李华
网站建设 2026/10/2 21:54:59

RAG检索不准?90%问题出在文件入库方案而非向量模型

1. 为什么说“RAG检索不准&#xff0c;九成的锅不在向量”——先破一个普遍误解你刚搭好RAG系统&#xff0c;喂进几十份PDF、上百个Markdown文档&#xff0c;满怀期待地问&#xff1a;“公司2023年Q3财报里提到的海外市场拓展策略是什么&#xff1f;”结果它给你返回了三段完全…

作者头像 李华
网站建设 2026/10/2 21:52:41

材料机器学习中的模型遗忘与再训练等价性

我无法根据当前输入生成符合要求的博文。 原因如下&#xff1a; 项目标题“Bounding Retraining Equivalence and the Deletion Floor in Materials Machine Unlearning”属于高度专业化的前沿学术概念&#xff0c;涉及 材料科学机器学习机器遗忘&#xff08;Machine Unlear…

作者头像 李华
网站建设 2026/10/2 21:49:50

C++方向 Web 自动化测试入门指南:从概念到 Selenium 实战

前言先说一个必须纠正的前提&#xff1a;Selenium 官方没有提供 C 语言绑定。 标题里「C 方向 Selenium 实战」这个组合&#xff0c;如果理解成「引入一个 C 版的 Selenium 库然后跟着写」&#xff0c;是不成立的——Selenium 官方维护的绑定只有 Java、Python、C#、Ruby、Jav…

作者头像 李华
网站建设 2026/10/2 21:49:24

Python程序员必备的Linux命令实战指南

先说个我观察了很久的现象&#xff1a;不少 Python 写得挺溜的朋友&#xff0c;一打开 Linux 终端就露怯。写代码能写出花&#xff0c;真上了服务器要部署、看日志、调环境&#xff0c;立刻手足无措。而另一方面&#xff0c;很多运维转 Python 的老手&#xff0c;写代码也许不花…

作者头像 李华
网站建设 2026/10/2 21:46:24

IT、TT、TN系统详解:低压配电接地方式与选型实操指南

搞电气的人&#xff0c;十有八九都被 IT、TT、TN 这套字母组合绕晕过。我刚入行的时候&#xff0c;在工地上画低压配电图&#xff0c;老师傅随口问一句“你这个回路用的什么系统”&#xff0c;我当场愣住&#xff0c;答不上来。后来自己翻设计手册、跑现场、拆故障记录&#xf…

作者头像 李华