☰
C#操作SQLite3从入门到工程实践:增删改查、事务与WAL模式详解
2026/10/11 15:19:45 网站建设 项目流程

简介:面向.NET初学者的C# SQLite3增删改查Demo,通过完整可运行示例演示轻量级数据库的接入流程,覆盖连接配置、建表操作、数据插入、结果查询、记录更新与删除等增删改查核心环节,适用于桌面应用、工具软件以及移动端的本地离线存储场景。资源打包为rar压缩包,大小约32.56MB,共175个文件,核心内容包括System.Data.SQLite.dll等40个动态链接库、9个C#工程源码文件、14个targets与14个xml工程配置、6个nupkg依赖包,外加1个db数据库样例,工程源码与运行依赖齐全,下载解压后即可在Visual Studio中编译运行。目前已有2256人学习下载。Demo还重点展示了参数化查询防SQL注入、BeginTransaction事务提交与回滚、操作完成后及时关闭连接释放资源等工程化写法,并将增删改查封装为辅助类,便于直接复制到自身项目中按需调整。通过连接字符串配置、建表与命令执行等完整调用链,开发者可以清晰理解SQLite3在C#中的使用方式,适合希望快速落地SQLite3本地数据库的.NET开发者学习参考。

1. C# SQLite3增删改查Demo:本地数据存储最务实的入口

做C#上位机或者桌面工具,早晚会撞到“数据得存下来”这道坎。SQLite3是一个不需要安装服务端的嵌入式数据库,整个数据库就是一个单文件,官方驱动一个NuGet包就能引进来。这份Demo把数据库增删改查整个闭环拆成了可以直接调用的C#代码,从建表、插入、查询到更新、删除,每一步都和实际项目贴得很近。适合刚开始接触SQLite3的C#开发,也适合给WinForm、WPF项目快速落地一个本地存储模块。读完之后你手里会有一套改改就能用的CRUD模板,不是零散的知识点,是能直接拖进项目跑的东西。

2. 选型与初始化:为什么是SQLite3,连接字符串怎么配

2.1 嵌入式数据库的选型理由与适用场景

在C#这边做本地数据持久化,最常见的三个方向是SQL Server、Access和SQLite3。三者差异不是功能强弱,而是部署模型和应用场景。SQL Server是服务型数据库,功能最全,但安装实例、维护账号权限、管理端口这些成本对单机工具来说太重。Access是老牌桌面数据库,胜在简单,但驱动在32位和64位进程里频繁踩坑,文件锁也让人头疼。SQLite3是嵌入式数据库,不需要独立服务进程,数据库就是一个.db文件,驱动以DLL形式嵌入程序。

方案部署成本单文件并发能力典型场景
SQL Server高,需要服务端和运维否强多客户端、服务端集中管理
Access中,驱动兼容性问题多是弱旧版桌面程序、小型工具
SQLite3低,DLL随程序走是中,配合WAL可用单机应用、上位机本地缓存

我的习惯很直接:凡是单机桌面程序、上位机数据采集的本地缓冲、工具软件的配置存档,优先SQLite3。特别是C#上位机场景,设备采集数据往往高频写入本地,隔一段时间再同步到服务器,SQLite3在这种读写模式下非常合适。单文件也让部署和排障变得简单——程序出问题,直接把.db文件拷回来就是一手证据。需要说明的是,SQLite3并不适合高并发写入的服务端场景,它是为嵌入式场景设计的,别指望它替代SQL Server。

2.2 NuGet包引入与连接字符串初始化

我用的是Microsoft.Data.Sqlite,这是.NET基金会维护的官方驱动。相比老牌的System.Data.SQLite,它更贴近现代.NET接口风格,接口长得像ADO.NET,从别的数据库迁移过来学习成本低。在Visual Studio里通过NuGet包管理器搜索Microsoft.Data.Sqlite,安装对应你项目目标框架的版本即可。我一般还会同时安装SQLitePCLRaw.bundle_e_sqlite3,它负责提供原生SQLite引擎,避免程序在精简系统上找不到原生组件。

using Microsoft.Data.Sqlite; // 构建连接字符串,Data Source指定数据库文件路径 string connectionString = "Data Source=app.db;DefaultTimeout=30;"; using var connection = new SqliteConnection(connectionString); connection.Open(); // 验证连接是否可用,SELECT 1 是最轻量的探活语句 using var cmd = connection.CreateCommand(); cmd.CommandText = "SELECT 1"; var result = cmd.ExecuteScalar(); Console.WriteLine($"连接测试结果: {result}");

这段代码做了三件事:定义连接字符串、打开连接、执行一个SELECT 1验证连接可用。Data Source=app.db表示数据库文件在当前工作目录下,如果文件不存在,SQLite会自动创建。注意using var保证连接对象在方法结束时自动释放,这是避免连接泄漏的关键写法,后面避坑章节会详细展开。

连接字符串里几个常用参数需要说明一下。Mode=ReadWriteCreate是默认值,意思是以读写方式打开,文件不存在就创建;如果只想只读打开,改成Mode=ReadOnly。Cache=Shared可以开启共享缓存,某些跨线程场景用得着。DefaultTimeout=30设置默认命令超时时间,单位是秒,推荐在生产代码里显式写出来,告诉排障的人超时阈值在哪。

string connStr = "Data Source=app.db;Mode=ReadWriteCreate;Cache=Shared;DefaultTimeout=30;";

这行配置是我在项目里最常用的连接字符串模板。Cache=Shared在多线程共享同一个连接时能减少数据库锁冲突的概率,但前提是线程之间不能在同一个连接上同时执行写入,否则还是要自己加锁。DefaultTimeout也是给SQLite的锁等待兜底,写入冲突时线程会阻塞等待而不是立刻抛异常。

2.3 初始化安全:路径处理与启动自检

初始化还有一个容易被忽略的点:数据库文件路径。工作目录变了,app.db就可能落到你意想不到的地方。我一般会显式指定路径,用Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "app.db")并把目录创建好,而不是依赖相对路径。否则程序装了服务或者换了启动目录,连接日志里全是"unable to open database file"。

// 显式指定数据库目录,防止工作目录变化导致文件位置漂移 string dataDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data"); Directory.CreateDirectory(dataDir); string dbPath = Path.Combine(dataDir, "app.db"); string connStr = $"Data Source={dbPath};Cache=Shared;DefaultTimeout=30;"; using var conn = new SqliteConnection(connStr); conn.Open(); // 启动自检:确认数据库可读写并校验完整性 using var cmd = conn.CreateCommand(); cmd.CommandText = "PRAGMA integrity_check;"; var integrity = cmd.ExecuteScalar(); if (integrity.ToString() != "ok") { throw new Exception($"数据库完整性检查失败: {integrity}"); }

Directory.CreateDirectory不会在目录已存在时报错,可以放心调用。PRAGMA integrity_check是SQLite自带的完整性校验命令,返回ok说明文件结构正常。这段启动自检放在程序入口处,数据库文件出问题的时候能在第一时间暴露,而不是等到查询报错才去排查,这个习惯帮我省了不少上线后的尴尬时间。

3. 核心CRUD实操:增删改查四类操作的完整代码与参数说明

3.1 建表与插入:主键策略和写入的正确姿势

SQLite3基本操作里,建表是第一步。这个Demo里的表结构用了一个自增主键id,跟实际项目里设备数据采集的场景很贴合。SQLite的INTEGER PRIMARY KEY本身就会自增,不需要额外加AUTOINCREMENT。加上AUTOINCREMENT之后,SQLite会额外维护一个序列,保证自增值永不重用,代价是多一点写开销。如果数据量不大、业务上不需要“删掉最大ID后不重复使用”,用INTEGER PRIMARY KEY就够了。

// 执行建表语句,IF NOT EXISTS 保证重复初始化不会报错 string createSql = @" CREATE TABLE IF NOT EXISTS device_data ( id INTEGER PRIMARY KEY, device_id TEXT NOT NULL, temperature REAL NOT NULL DEFAULT 0, humidity REAL, record_time TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) )"; using (var cmd = connection.CreateCommand()) { cmd.CommandText = createSql; cmd.ExecuteNonQuery(); } // 插入一条设备数据,参数化写法避免SQL注入 using (var cmd = connection.CreateCommand()) { cmd.CommandText = @" INSERT INTO device_data (device_id, temperature, humidity) VALUES ($deviceId, $temperature, $humidity)"; cmd.Parameters.AddWithValue("$deviceId", "sensor_001"); cmd.Parameters.AddWithValue("$temperature", 26.5); cmd.Parameters.AddWithValue("$humidity", 58.2); int rows = cmd.ExecuteNonQuery(); Console.WriteLine($"插入影响行数: {rows}"); }

CREATE TABLE IF NOT EXISTS保证连接初始化时重复执行也不会报错,这在程序多次启动的场景下很实用。record_time字段用datetime('now', 'localtime')直接写入本地时间,比在C#代码里取DateTime.Now.ToString()再拼字符串要干净,时区处理统一交给SQLite。插入时我用$deviceId这样的参数名,这是Microsoft.Data.Sqlite支持的参数格式,也可以写@deviceId,但要注意命令文本里的参数名必须和Parameters.AddWithValue里的一致,连$符号都要带上。

ExecuteNonQuery()返回值是受影响的行数。插入一条数据正常情况下返回1,如果返回0或者负数,说明SQL语句没有匹配到任何数据或者触发了约束,需要检查参数值。这里temperature定义为NOT NULL DEFAULT 0,即使某条数据没填温度,也不会因为空值导致查询端崩溃。humidity没有加NOT NULL,这是故意的——有些采集设备没有湿度传感器,留空比填0更符合业务语义。

3.2 查询与更新:DataReader读取和参数化写法

查询是增删改查里最见功底的。SQLite3查询结果有两种读取方式:一种是ExecuteScalar拿单个值,适合COUNT、SUM这类聚合;一种是ExecuteReader拿结果集,适合逐行处理。这个Demo里展示的是后者,贴近实际数据展示场景。

// 按设备ID查询最近100条记录 using (var cmd = connection.CreateCommand()) { cmd.CommandText = @" SELECT id, device_id, temperature, humidity, record_time FROM device_data WHERE device_id = $deviceId ORDER BY id DESC LIMIT 100"; cmd.Parameters.AddWithValue("$deviceId", "sensor_001"); using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { long id = reader.GetInt64(0); string deviceId = reader.GetString(1); double temperature = reader.GetDouble(2); string recordTime = reader.GetString(4); Console.WriteLine($"{id} | {deviceId} | {temperature} | {recordTime}"); } } }

ORDER BY id DESC按自增主键倒序排列,最新数据排在最前面。LIMIT 100控制返回行数,避免一次查询把全表拖进内存。GetInt64(0)的0是列的序号,从0开始;也可以用reader.GetInt64(reader.GetOrdinal("id"))按列名取值,可读性更好但略慢一点。对于SQLite的INTEGER主键,驱动里对应的是long类型,用GetInt32在数据量超过21亿时会翻车,这个习惯建议从一开始就养成。

更新操作跟插入的结构很接近,关键是WHERE条件要写明白:

// 根据ID更新温度和湿度,WHERE条件必须精确到唯一记录 using (var cmd = connection.CreateCommand()) { cmd.CommandText = @" UPDATE device_data SET temperature = $temperature, humidity = $humidity WHERE id = $id"; cmd.Parameters.AddWithValue("$temperature", 27.1); cmd.Parameters.AddWithValue("$humidity", 59.0); cmd.Parameters.AddWithValue("$id", 1); int rows = cmd.ExecuteNonQuery(); Console.WriteLine($"更新影响行数: {rows}"); }

WHERE id = $id只更新明确指定的一条记录。实际项目里经常犯的错是UPDATE语句漏掉WHERE,结果全表都被改了。执行ExecuteNonQuery后务必要判断rows,如果大于1就要警觉是不是WHERE条件不够精确。执行更新前可以先跑一条同条件SELECT,确认影响范围再执行更新,这是个便宜又有效的习惯。

3.3 删除操作:物理删除与软删除的取舍

删除操作的写法本身很简单:

// 按ID物理删除记录 using (var cmd = connection.CreateCommand()) { cmd.CommandText = "DELETE FROM device_data WHERE id = $id"; cmd.Parameters.AddWithValue("$id", 1); int rows = cmd.ExecuteNonQuery(); Console.WriteLine($"删除影响行数: {rows}"); }

但删除策略值得多说两句。DELETE是物理删除,数据一旦执行,在没有备份的情况下基本找不回来。我在上位机数据采集项目里常见做法是软删除:加一个deleted字段,默认0,删除时把deleted置为1,查询条件统一加WHERE deleted = 0。好处是可以恢复误删数据,而且在排查数据问题时能看到完整历史。

// 软删除:标记deleted字段为1,代替物理删除 using (var cmd = connection.CreateCommand()) { cmd.CommandText = "UPDATE device_data SET deleted = 1 WHERE id = $id"; cmd.Parameters.AddWithValue("$id", 1); cmd.ExecuteNonQuery(); }

软删除的代价是每次查询都要记得过滤deleted字段,忘了就出现“删了还在”的诡异现象。如果表数据有明确的生命周期、不需要恢复,物理删除反而更省心。这个取舍没有标准答案,我一般按数据的业务价值来定:设备采集的原始数据软删除,临时缓存数据物理删除。

3.4 分页查询与LIMIT/OFFSET边界

分页查询在CRUD里也常被问到。SQLite的LIMIT/OFFSET用法几乎和MySQL一致:

SELECT id, device_id, temperature, record_time FROM device_data WHERE deleted = 0 ORDER BY id DESC LIMIT 20 OFFSET 40;

OFFSET 40表示跳过前40条,配合LIMIT 20实现第三页的效果。数据量大了之后OFFSET越深越慢,因为SQLite要扫描并丢弃前面的行。真正到了几万行以上的分页,我会改用游标式分页写法:

// 游标式分页:传入上一页最后一条记录的id,性能稳定 string pageSql = @" SELECT id, device_id, temperature, humidity, record_time FROM device_data WHERE deleted = 0 AND id < $lastId ORDER BY id DESC LIMIT 20"; cmd.Parameters.AddWithValue("$lastId", lastIdFromPreviousPage);

这里id < $lastId替代了OFFSET,SQLite可以直接走主键索引定位到游标位置,不需要扫描被跳过的行。页码越深,这个写法的优势越明显。如果是桌面应用的数据列表,配合“加载更多”按钮,体验比传统分页好得多。

4. 避坑排查:C# SQLite3中的五个高频翻车点

4.1 database is locked:并发写引发的经典报错

现象:程序运行一阵子后,写入操作随机抛出SQLiteException: database is locked,有时候重启程序就好了,过一会又出现。

原因:SQLite默认的日志模式是DELETE模式,同一时间只允许一个进程执行写操作。上位机场景里,采集线程和UI线程同时对数据库写入,或者两个连接分别开了事务没及时提交,都会触发锁。还有一种隐蔽情况:一个连接对象没释放,事务一直悬挂,后续所有写操作都会排队超时。

解决:先确保所有连接都用using释放,事务一定在finally里提交或回滚。然后给命令设置超时时间,cmd.CommandTimeout = 10;,让线程等待锁而不是立刻抛异常。更高层的做法是换成WAL日志模式,执行一次PRAGMA journal_mode=WAL;后,读和写可以并发,写与写之间冲突概率大幅下降。我通常在建库初始化时就把WAL模式持久化打开。

4.2 中文路径打不开数据库

现象:数据库文件放在中文目录下,连接能创建,执行SQL时报错或直接抛出SqliteException,英文路径下一切正常。

原因:SQLite原生层对路径字符串的解析依赖编码环境。某些环境中旧版驱动把路径按ANSI编码传给原生库,遇到中文就乱码,找不到文件。用相对路径加中文工作目录最容易触发。

解决:要么用英文路径绕开,要么把路径转成URI格式给驱动。Microsoft.Data.Sqlite支持Data Source=file:D:/数据/app.db?mode=rwc的URI写法,或者更省心的做法是启动时把数据库固定到英文目录再拼接路径。我的习惯是建库前统一Path.GetFullPath并检查目录是否存在,避免各种斜杠和相对路径的坑。

4.3 DateTime存进去再读出来变了样

现象:把DateTime.Now存进TEXT字段,读出来变成一个奇怪的格式,或者参与排序时顺序不对。

原因:SQLite本身没有时间类型,Microsoft.Data.Sqlite默认把它序列化成TEXT字符串。不同驱动序列化格式有差异,比如部分版本存成yyyy-MM-dd HH:mm:ss,另一部分存成带时区的ISO8601格式。如果不统一格式,查出来之后还得手工解析。

解决:我一般不在数据库里依赖驱动做DateTime转换,而是自己统一格式:写入时用DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),读取时用DateTime.ParseExact(recordTime, "yyyy-MM-dd HH:mm:ss", null)。也可以给连接字符串加DateTimeFormat=Ticks,这样存取的是自增Tick数,排序天然正确,但数据可读性差,排查问题时不够直白。两种方案按项目需求选,我是可读性优先。

4.4 参数名前缀不一致直接报错

现象:命令文本写WHERE id = @id,代码里Parameters.AddWithValue("@id", 1),执行时报unrecognized token或者一直查不到数据。

原因:Microsoft.Data.Sqlite的参数名其实不在乎前缀是$还是@,但命令文本里的占位符和AddWithValue的参数名必须完全一致。常见翻车是把@id传给$id,或者传的时候没写前缀。

解决:养成一个统一习惯。我在所有C#项目里统一用$前缀,命令文本和AddWithValue里都带$,因为$在SQLite里和C#字符串插值的$共存时更直观。一旦项目里出现SQL报错,先检查参数名,八成是这里的问题。

4.5 批量插入慢到怀疑人生

现象:循环执行INSERT语句插入5000条记录,耗时几十秒,CPU不高但磁盘写入不断。把同样的逻辑搬到MySQL上反而快得多。

原因:默认情况下每条INSERT都是一个独立事务,SQLite每执行一条都要做一次磁盘同步操作,把数据落盘。循环写入的每一轮都在同步,5000条就是5000次磁盘同步,慢是必然。

解决:批量写入时手动开一个事务,把所有INSERT包在事务里,最后统一提交。这样SQLite会把中间数据留在缓存里,最后一次落盘。5000条数据从几十秒压到一两秒是常态。写法在下一章事务部分给出。

5. 进阶技巧:事务批量写入与WAL模式并发优化

5.1 事务批量写入:从几十秒到一两秒

上面4.5提到的问题,解决方案就是事务。SQLite的事务机制可以把多条写操作打包成原子操作,要么全部成功,要么全部回滚。批量写入场景下,事务带来的收益不仅是正确性,更是性能数量级上的提升。

// 批量插入1万条数据,通过事务统一提交 using var connection = new SqliteConnection("Data Source=app.db"); connection.Open(); using var transaction = connection.BeginTransaction(); try { using var cmd = connection.CreateCommand(); cmd.Transaction = transaction; cmd.CommandText = @" INSERT INTO device_data (device_id, temperature, humidity) VALUES ($deviceId, $temperature, $humidity)"; for (int i = 0; i < 10000; i++) { cmd.Parameters.Clear(); // 清空上一轮参数,防止参数累积 cmd.Parameters.AddWithValue("$deviceId", $"sensor_{i % 10}"); cmd.Parameters.AddWithValue("$temperature", 20 + i * 0.01); cmd.Parameters.AddWithValue("$humidity", 50 + i % 20); cmd.ExecuteNonQuery(); } transaction.Commit(); Console.WriteLine("1万条数据批量写入完成"); } catch { transaction.Rollback(); throw; }

这个写法的关键有两点。第一,cmd.Transaction = transaction把命令绑定到事务上,如果不设置,命令会用隐式事务,等于每条又单独提交,事务白开。第二,循环里cmd.Parameters.Clear()加AddWithValue重新添加参数,这是很多人容易漏的一步——不清空参数,上一轮的参数值会被下一轮复用,绑定对象越来越多,内存和性能都会受影响。

值得注意的一个细节:SQLite参数绑定AddWithValue重复调用同一参数名时,行为是替换旧值,但代码上显式Clear()再添加更不容易出错。实际压测里,1万条数据开启事务后耗时通常在1到2秒左右,比起不开事务时几十秒的体验,差距非常明显。如果你写入量更大,还可以进一步用Prepare()预编译语句,提前让SQLite解析SQL,循环里只改参数值,性能又上一层。

5.2 WAL日志模式:让读写并发成为可能

SQLite默认的DELETE日志模式里,写事务进行时,其他连接连读都会被阻塞。WAL(Write-Ahead Logging)模式把写入操作先追加到.wal文件,读操作仍然可以从原来的主库文件读取数据,读写互不阻塞。对上位机这种有采集线程写、界面线程读的场景非常实用。

// 开启WAL模式:PRAGMA语句返回值为wal表示切换成功 string connStr = "Data Source=app.db;Cache=Shared;DefaultTimeout=30;"; using var conn = new SqliteConnection(connStr); conn.Open(); using var cmd = conn.CreateCommand(); cmd.CommandText = "PRAGMA journal_mode=WAL;"; var mode = cmd.ExecuteScalar(); Console.WriteLine($"当前日志模式: {mode}");

ExecuteScalar返回的mode应该是wal,代表成功切换。WAL模式是持久化的,数据库文件一旦设置过WAL,下次打开仍然是WAL模式,不需要每次连接都执行一遍PRAGMA。但要注意,这个设置是跟随数据库文件的,不是跟随连接字符串;你把.db文件拷贝到另一个环境,那个环境打开时也会自动走WAL。

WAL模式下还有两个关联PRAGMA值得设一下。一个是PRAGMA synchronous=NORMAL;,配合WAL使用能在掉电时保持数据完整性,同时比FULL性能更好;另一个是PRAGMA wal_autocheckpoint=1000;,控制WAL文件每隔多少页自动合并回主库文件。默认值1000一般够用,如果写入量很大,WAL文件长得很快,可以把这个值调小,比如200,换取频繁一点的自动合并,避免WAL文件膨胀到几GB。

我不建议在Demo阶段过早调这些参数。先把WAL模式打开,synchronous保持默认FULL跑一段时间,观察WAL文件增长速度再决定要不要做调整。优化类的参数调整一定以监控数据为准,不要凭感觉,这是我在好几个项目里换来的血泪经验。

5.3 索引设计与慢查询排查

单表数据少的时候,全表扫描也无所谓。但当device_data表涨到几十万行,按device_id查询的响应时间会明显退化。SQLite的索引设计很简单,但踩过的坑也不少。

-- 为经常查询的字段创建索引 CREATE INDEX IF NOT EXISTS idx_device_data_device_id ON device_data(device_id); CREATE INDEX IF NOT EXISTS idx_device_data_record_time ON device_data(record_time);

WHERE device_id = $deviceId如果走全表扫描,耗时随数据量线性增长。加了idx_device_data_device_id索引之后,查询复杂度降到对数级。要注意的是,SQLite的复合索引有最左前缀原则,如果你经常用WHERE device_id = ? AND record_time BETWEEN ? AND ?这种组合查询,应该建复合索引(device_id, record_time),而不是两个独立索引。独立索引在复合查询时只能命中一个,另一个字段还是得回表扫描。

排查慢查询我一般用EXPLAIN QUERY PLAN:

EXPLAIN QUERY PLAN SELECT * FROM device_data WHERE device_id = 'sensor_001';

输出结果里如果看到SCAN,说明没走索引,是全表扫描;看到SEARCH device_data USING INDEX idx_device_data_device_id,说明索引生效了。这个命令比猜索引有没有生效靠谱得多,我每次优化完SQL都会执行一遍确认。

5.4 连接管理与多线程访问边界

ADO.NET有默认连接池,Microsoft.Data.Sqlite也沿用了这套机制。连接字符串相同的情况下,连接会被池化复用,所以“每次操作打开一个新连接”并不是什么大问题。但在WinForm/WPF项目里,我一直建议用一个简单的单例封装,控制并发访问:

public class SqliteHelper { private static SqliteConnection _connection; public static SqliteConnection GetConnection() { if (_connection == null) { _connection = new SqliteConnection("Data Source=app.db;Cache=Shared;DefaultTimeout=30;"); _connection.Open(); } return _connection; } }

但单例连接有一个隐藏风险:多个线程同时在这个连接上执行写入,会互相踩。SqliteConnection不是线程安全的,跨线程共享同一个连接时,写操作必须自己加锁,或者保证每个线程创建自己的连接。我一般这么分工:采集线程单独持有连接写入,UI线程用只读查询连接走WAL模式,两边各用各的,不共享同一个连接对象。这比试图用一个锁保护单例连接要干净得多。

6. 验证方法:用sqlite3命令行核对数据与索引完整性

6.1 命令行工具基本核对

程序跑完了,代码能增删改查了,怎么确认数据和预想的一致?我习惯在调试阶段开一个终端,用sqlite3命令行工具直接检查数据库文件。SQLite3安装配置教程网上很多,装好之后在命令行进入数据库文件所在目录,执行:

sqlite3 app.db .tables .headers on .mode column SELECT COUNT(*) AS total FROM device_data; SELECT * FROM device_data LIMIT 5;

.tables列出所有表,确认建表成功。.headers on加.mode column让查询结果以表格形式展示,列名对齐。SELECT COUNT(*)核对总行数,和程序里插入的数量比对;SELECT * FROM device_data LIMIT 5抽样看几条数据,检查字段值和类型对不对。这套命令我几乎每天都会用,比打开数据库客户端工具快得多。

6.2 完整性检查与索引核对

数据核对之外,还有两个命令我建议每次写完CRUD跑一遍:

PRAGMA integrity_check; PRAGMA journal_mode; .indexes device_data

PRAGMA integrity_check返回ok说明数据库文件结构完整,没有损坏。PRAGMA journal_mode确认WAL模式是否已持久化生效。.indexes device_data查看表上所有索引,确认之前建的索引确实存在。这些都是我做增删改查功能后的固定收尾动作,能拦截掉一批“程序不报错但数据库状态不对”的隐蔽问题。

如果用的是Microsoft.Data.Sqlite,还可以在程序里用代码做一次同样的验证,方便集成到自动化测试里:

using var cmd = connection.CreateCommand(); cmd.CommandText = "PRAGMA integrity_check;"; var integrity = cmd.ExecuteScalar(); if (integrity.ToString() != "ok") { throw new Exception($"数据库完整性检查失败: {integrity}"); }

这段代码放在程序启动自检里,数据库文件出问题的时候能在第一时间暴露,而不是等到查询报错才去排查。我最后一个建议是:Demo跑通之后,把连接字符串里的DefaultTimeout显式写出来,把WAL模式固定下来,然后把上面这段完整性检查放进初始化流程。这三件事做完,你的SQLite3增删改查才算真正从Demo代码变成了工程代码。

说实话,我第一次连续遇到“database is locked”时也是各种怀疑人生,最后一步步查连接泄漏、查事务悬挂,才发现问题出在自己没释放连接。从那以后我每次写完SQLite相关代码,都会强制走一遍:查连接是否using释放、查事务是否提交、用sqlite3命令行验证PRAGMA和索引。这个习惯帮我挡掉了不少上线后的尴尬问题,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询