
1. 项目背景为什么我盯上了本地Db数据库选型这段时间在重构一个基于 .NET 8 的桌面客户端项目技术栈是 WPF MVVM数据规模不算大单机使用为主偶尔会有少量并发写入。项目早期用的是 SQLite跑了一段时间之后发现几个麻烦事部署时要额外打包原生 dll不同操作系统下的兼容性要单独测EF Core 映射部分类型时还有不少限制。于是我开始认真考虑一个问题——.NET 本地数据库到底怎么选才最省心。先说清楚我这里说的“本地Db数据库”指的是什么。它和 SQL Server、MySQL 这类服务型数据库完全不是一个路数服务型数据库要装服务、要配连接、要管账号权限而本地嵌入式数据库是把数据库引擎以库文件形式直接链接进你的程序进程里数据落在一个本地文件上程序启动就能用不需要额外的服务进程。.NET 生态里这类方案其实不少常见的有 SQLite经由 EF Core 或 Microsoft.Data.Sqlite 接入、LiteDB、VistaDB、VistaDB 的轻量版等再加上 SQL Server Express LocalDB 这种半嵌入式方案。适合来看这篇文章的人我猜大概有这么几类一是做 WinForms / WPF 桌面应用、正在纠结本地数据存储方案的开发者二是做 .NET MAUI 跨平台移动应用想在离线场景下用本地库的朋友三是从 .NET Framework 老项目往 .NET 6/8 迁移需要顺便重新评估存储层的同学。这篇文章我会把 SQLite、LiteDB、VistaDB、LocalDB 这几个方案放在同一个天平上称一称给出一套可以直接照抄的选型思路。2. 本地数据库方案全景与选型坐标2.1 一个都不能少主流候选方案速览动手选型之前我给自己定了个规矩先摸清候选池再谈对比。下面这几个方案是我筛选后的最终名单每一个都在 .NET 生态里有一定群众基础。方案本质数据存储形态适合的应用类型许可证SQLite EF CoreC语言编写的嵌入式关系库单个 .db/.sqlite 文件通用业务系统、分析型工具公有领域SQLite Microsoft.Data.Sqlite同上的轻量驱动层同上简单数据访问、迁移过渡公有领域LiteDB纯 C# 实现的 NoSQL 文档库单个 .db 文件配置存储、原型、中小型应用MITVistaDB纯 C# 实现的关系库单个 .vdb5 文件WinForms / WPF 传统桌面应用商业授权SQL Server Express LocalDB微软官方的轻量 SQL Server文件型数据库由服务进程管理开发环境、小型生产免费RavenDB Embedded文档库的嵌入式模式文件夹形式需要全文检索、复杂查询的场景商业/社区版我实际测过其中四个LiteDB 和 VistaDB 是近期专门搭了 Demo 跑过的RavenDB Embedded 我只在文档层面研究过没有纳入最终横评原因是它对于纯本地单文件需求来说还是偏重。LocalDB 严格讲不算嵌入式它有一个后台服务在管文件但它和 Visual Studio 集成得非常好单独拎出来讲有它的价值。2.2 选型坐标四个维度定生死我在选型时不会先看性能报表那是最后一步。我自己的评估顺序很固定总共四个维度每一个都会直接影响项目后续的开发和运维体验。第一是部署复杂度。桌面应用最怕什么最怕用户装完程序跑不起来。SQLite 在 .NET 里用 Microsoft.Data.Sqlite 驱动时Windows 下需要带上 e_sqlite3.dll 原生库Linux 下需要 libsqlite3macOS 下类似。这个 dll 如果版本和运行时对不上启动就会报 DllNotFoundException。LiteDB 和 VistaDB 没有这个问题因为它们就是纯托管代码一个 NuGet 包搞定没有原生依赖这点在 .NET MAUI 场景里简直是救命稻草。第二是数据模型贴合度。如果你的数据是强关系型的比如订单、明细、账务这种需要 join、事务、外键约束那关系库是正路SQLite 和 VistaDB 都在第一梯队。如果你的数据更像文档比如配置项、JSON 快照、日志记录字段结构经常变LiteDB 用 BsonDocument 存取会舒服得多它内部就是文档模型加字段不用改表结构。第三是并发模型。这里的并发主要指多进程并发。SQLite 默认的并发策略是文件锁一个进程写的时候另一个进程的写入会等到超时或者直接报 database is locked。LiteDB 在并发上也类似它有一个不成熟的多线程处理机制但同一时刻只能有一个写事务。VistaDB 对多用户并发支持好些行级锁做得比较到位。如果你的应用只有一个进程并发问题基本不用太担心如果是多进程SQLite 需要开 WAL 模式并且合理设置 busy_timeout。第四是生态和迁移成本。EF Core 是目前 .NET 数据访问的事实标准SQLite 有最完善的 EF Core 支持包括迁移migration、LINQ 查询翻译、事务等。LiteDB 有两个版本的 API老的 LiteDB 4.x 和新出的 LiteDB 5.xAPI 改动很大网上一些老教程会误导人。VistaDB 也支持 EF Core 6/8但它属于商业产品网上社区资料相对少。LocalDB 用的是标准 SQL Server 的 T-SQL 方言如果你的团队本来就熟 SQL Server迁移到 LocalDB 几乎没有学习成本但这货没法用于生产环境多用户部署。2.3 为什么我不直接无脑推荐 SQLite我承认 SQLite 是这个领域的人气王社区活跃度最高Stack Overflow 上的问题几乎都能搜到答案。但我恰好在实际项目中遇到几个问题让我对它保持一份清醒。第一个问题是类型映射。EF Core 里 SQLite 对 decimal 的处理有点特殊默认情况下 decimal 在 SQLite 里会被映射为 TEXT 存储EF Core 的 SQLite provider 为了保持精度这导致老数据库里如果存的 decimal 字段格式不规范查询出来反序列化时可能出现精度丢失。举个我踩过的例子项目里有个金额字段用了 decimal存进去是 12.3400EF Core 读出来时会按当前线程的区域设置解析如果用户的系统区域设置是德语区小数点用逗号那读取就会直接抛 FormatException。第二个问题是跨平台原生库的坑。我做过一个 WPF 工具的自动更新发布的时候一切正常结果有用户反馈装完打不开。查了半天是杀毒软件把 e_sqlite3.dll 当可疑文件隔离了因为那个 dll 是原生代码没有微软签名。后来我改成用 SQLitePCLRaw.bundle_e_sqlite3 包并走 provider 初始化才稳定下来。第三个问题是并发控制需要一点经验。单机应用如果只开一个进程SQLite 很稳可你要是做一个托盘工具把数据库服务和 UI 拆成两个进程那就必须精调 WAL 模式、busy_timeout、连接池。我见过有人直接抄网上的连接字符串也不看 journal_mode跑几天就出现 database is locked然后怪 SQLite 不靠谱。所以结论不是“SQLite 不行”而是“SQLite 需要你用对姿势”。它仍然是通用关系型本地存储的最佳默认选项之一但你必须知道它的边界在哪里。3. 核心技术点拆解每个方案的看家本领与致命短板3.1 SQLite 在 .NET 中的正确打开方式SQLite 在 .NET 中接入的标配组合是 Microsoft.Data.Sqlite EF Core。如果你只做简单的键值存储不想引入 EF Core 那一套直接只用 Microsoft.Data.Sqlite 就够了它的 API 风格和 ADO.NET 很像上手非常快。如果你走 EF Core 路线有几个细节必须注意。连接字符串里有一项很重要Data Sourceappdata.db;CacheShared;Foreign KeysTrue;PoolingTrueCacheShared表示同一个进程内多个连接共享同一个 SQLite 缓存减少文件锁冲突。Foreign KeysTrue必须在连接字符串或连接初始化时开启SQLite 默认不检查外键约束很多新手在这里翻车。PoolingTrue从 Microsoft.Data.Sqlite 6.0 开始支持开启后连接复用减少打开关闭文件的开销。还有一个必须养成的习惯启用 WAL 模式。在 EF Core 的OnConfiguring或者首次连接打开后执行using var connection new SqliteConnection(Data Sourceappdata.db); connection.Open(); using var command connection.CreateCommand(); command.CommandText PRAGMA journal_modeWAL; PRAGMA busy_timeout5000;; command.ExecuteNonQuery();WAL 模式最大的好处是读操作不阻塞写操作写操作也不阻塞读操作对桌面应用来说体感很关键。busy_timeout5000是指在遇到文件锁时最多等待 5 秒超过就报错你可以根据场景调大调小。我自己实测过一个有点反直觉的现象SQLite 在单文件大批量写入时如果禁用事务逐条 insert耗时可能是启用事务的 10 倍以上。所以写批量数据的代码我永远长这样using var transaction await context.Database.BeginTransactionAsync(); try { foreach (var item in items) { context.Items.Add(item); } await context.SaveChangesAsync(); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; }3.2 LiteDB纯 C# 文档库的甜与痛LiteDB 是我最近玩得比较多的方案因为它解决了 SQLite 在部署上的一个痛点无原生依赖。全部 C# 实现发布目录只有托管 dll 和一个数据文件这在 .NET MAUI 和 WinForms 里非常省心。它的 API 设计最接近 MongoDB 的体验。比如定义实体public class User { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } }插入和查询using var db new LiteDatabase(users.db); var col db.GetCollectionUser(users); col.Insert(new User { Name 张三, Email zhangsanexample.com }); var user col.FindOne(x x.Email zhangsanexample.com);如果你存的是纯文档类数据配置、日志、快照、规则引擎的条件表达式等LiteDB 真的很爽字段结构变化不需要迁移脚本直接把新的 BsonDocument 塞进去就行。但它有几个坑我提前给你踩了。第一是 5.x 版本的 API 变化巨大网上大量教程基于 4.xLiteDatabase的构造函数参数、Include的写法都有变化照着老教程写你会编译不过去。第二是并发能力一般官方文档明确说支持多线程读但单实例写事务在同一时间只能有一个多进程同时打开同一个文件是不推荐的。第三是可恢复性弱如果程序在写过程中崩溃文件损坏的风险比 SQLite 高所以一定要定期备份数据文件写入频率高的场景要小心。3.3 VistaDB老牌商业方案还值不值得选VistaDB 是 .NET 圈子里的老面孔了从 .NET Framework 时代就存在纯 C# 实现、单文件、支持行级锁对传统关系的支持比 LiteDB 完整得多。它的一个招牌特性是支持加密数据库直接就内建了加密算法而 SQLite 要加密要么用 SEE收费要么用 SQLCipher开源但编译麻烦VistaDB 在商业授权里直接把这个能力打包了。我实际用它跑过一个库存管理 Demo和 EF Core 的集成走的是正规 provider 流程迁移命令dotnet ef migrations add也能正常跑事务、外键、存储过程是的它支持类存储过程的语法都齐全。如果你的应用是传统 MIS 系统比如进销存、财务、固定资产管理数据结构强关联、对数据安全有合规要求VistaDB 是一个下限很高的选择。缺点也很明显商业授权费对个人开发者和预算敏感的小团队是一道坎社区资料数量完全没法跟 SQLite 比你遇到一个细节问题可能搜遍全网也找不到答案它在 Linux/macOS 上的支持虽然官方说可以跑但主要场景还是 Windows 桌面。3.4 LocalDB被忽视的“半个嵌入”选项LocalDB 是微软官方提供的一个轻量 SQL Server 实例它不是进程内嵌入式库而是由一个后台服务按需启动的独立进程。它最大的价值在于和 SQL Server 完全兼容的 T-SQL以及 Visual Studio 原生工具链的无缝衔接。如果你做一个桌面工具目标用户是内部员工且机器上可能装了 SQL Server Management Studio那么 LocalDB 非常合适。你直接用(localdb)\MSSQLLocalDB作为服务器地址连接字符串和 SQL Server 几乎一样Server(localdb)\MSSQLLocalDB;DatabaseAppData;Trusted_ConnectionTrue;但 LocalDB 有个硬伤它本质上是开发友好型实例不适合分发给最终用户。首先目标机器必须装有 SQL Server LocalDB 运行时几十 MB 的安装包其次同一个实例能同时打开的数据库数有限制另外 LocalDB 的实例默认不会开机自启第一次连接时会有几百毫秒的启动延迟。这些对于正式的桌面客户端分发场景都是减分项。4. 实操指南一套完整的数据访问层设计方案选型不能停留在表格对比上我把我最近一个 WPF 项目里实际跑通的数据访问层设计方案拿出来你照着改就能用。这个方案虽然用的是 SQLite EF Core但整个设计思路对其他几个库同样适用。4.1 项目结构规划我习惯把数据访问层独立成一个类库项目不掺和 UI 逻辑。目录结构大致如下src/ App.Domain/ // 实体类、枚举、接口 App.Infrastructure/ // EF Core 上下文、仓储实现、迁移 App.Wpf/ // 视图层Domain 层只放 POCO 实体不引用任何 EF Core 的 dll。Infrastructure 层才引入 EF Core 相关包。这样做的目的是保持实体纯净以后要是换存储方案不会牵连 UI 层改动。4.2 DbContext 配置要点我的AppDbContext长这样public class AppDbContext : DbContext { public DbSetUser Users { get; set; } public DbSetOrder Orders { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var connectionString new SqliteConnectionStringBuilder { DataSource Path.Combine(AppContext.BaseDirectory, appdata.db), Cache SqliteCacheMode.Shared, Pooling true, ForeignKeys true }.ToString(); optionsBuilder.UseSqlite(connectionString); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityUser(entity { entity.ToTable(users); entity.HasKey(x x.Id); entity.Property(x x.CreatedAt).HasDefaultValueSql(CURRENT_TIMESTAMP); }); } }注意几个细节DataSource我用的是AppContext.BaseDirectory也就是程序运行目录。有人喜欢用Environment.SpecialFolder.ApplicationData这样数据库文件放在用户目录下升级程序时不会被覆盖多用户共用一台电脑时数据能隔离。取舍取决于你的产品形态。还有一个容易被忽略的点SQLite 的DateTime默认存成 TEXT 格式形如2024-06-19 14:30:00如果你希望统一成 ISO 8601 带时区需要在连接字符串里加DateTimeFormatISO8601。4.3 自动迁移的开机流程桌面应用最忌讳在用户电脑上手动跑迁移命令。我启动流程里加了一段自动执行Database.Migrate()的逻辑public static void InitializeDatabase() { using var context new AppDbContext(); context.Database.Migrate(); }Migrate()会自动检查数据库文件是否存在不存在则创建然后按迁移历史把架构建好。首次启动时会多花一两秒钟之后都是毫秒级。但这里有个两种场景要分开如果你的数据库文件是程序自己创建的直接Migrate()没问题如果是从旧版本程序升级上来的就要小心Migrate()和已有数据的兼容性。我遇到过一次字段类型变更导致迁移失败后来在迁移代码里手动写了数据转换的 SQL 才搞定。升级场景最好先备份旧库然后跑迁移再验证数据量。4.4 类型安全的仓储封装我不主张每个实体都写一套仓储但我会封装一个通用的IRepositoryT内部只做最基本的事复杂查询直接走IQueryable交给上层组合。public interface IRepositoryT where T : class { TaskT? GetByIdAsync(int id); TaskIEnumerableT GetAllAsync(ExpressionFuncT, bool? predicate null); Task AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(T entity); Taskint SaveChangesAsync(); }实现里就包一下DbSetT和DbContext.SaveChangesAsync不做花活。真正的复杂查询用 LINQ 在 UI 层直接写配合 AsNoTracking 提高只读性能。4.5 连接生命周期管理EF Core 的 DbContext 是工作单元模式桌面应用里建议遵循“一个请求一个上下文”的思路。WPF 的 ViewModel 在构造函数里注入一个上下文工厂比如public class UserListViewModel { private readonly IDbContextFactoryAppDbContext _factory; public UserListViewModel(IDbContextFactoryAppDbContext factory) { _factory factory; } public async Task LoadUsersAsync() { await using var context await _factory.CreateDbContextAsync(); var users await context.Users.AsNoTracking().ToListAsync(); // 绑定 UI 数据 } }用IDbContextFactory而不是直接注入 DbContext可以避免跨操作共享同一个上下文导致的缓存陈旧、线程安全问题。注册服务时记得services.AddDbContextFactoryAppDbContext(options options.UseSqlite(...));5. 常见问题与排查技巧实录5.1 数据库文件被占用无法删除或复制这个坑太经典了。WPF 程序在调试时经常崩溃调试器还没有完全释放资源然后你想删掉数据库文件重新来结果提示“文件正在被另一个进程使用”。排查思路按顺序来先确认程序是否真的退出了看任务管理器的进程列表然后确认是否有数据库连接没有释放比如用了using但没包await using、连接池里的连接还挂着最后检查是否有后台任务没取消。如果只想快速解决可以在程序退出时执行SqliteConnection.ClearAllPools()如果是 Litedb 就确保Dispose()了实例。5.2 database is locked 的诱因与解法这个几乎每个用 SQLite 的人都会遇到。常见的触发场景是多个连接同时在写、一个写事务还没结束另一个连接又尝试写、长事务卡住了文件锁。我的标准做法是三步开启 WAL 模式并把 busy_timeout 设到 3000~5000 毫秒。写操作统一走同一个连接串并且尽量集中批次操作不要一条一条开连接。配置连接池确保池内连接数合理默认是 5 到 10不要自己 new 一堆连接。还有一种隐蔽情况EF Core 的一个SaveChangesAsync走了异步但你在事务里同步调用另一个查询由于 SQLite 的锁机制是写事务持有独占锁这个同步查询会被卡住直到超时。解决办法是把所有操作都改成异步链。5.3 中文乱码怎么解决数据存取没有问题但读取回来显示乱码十有八九是数据库文件的编码与你程序的期望不一致。SQLite 默认以 UTF-8 编码存储文本Microsoft.Data.Sqlite 默认也是 UTF-8正常情况下不应该乱码。如果你是从老版本 .NET Framework 项目迁移过来的老库文件可能是 UTF-16Unicode编码这时需要在连接字符串里显式设置Data Sourcelegacy.db;DefaultUtf16True;再不行就写一段读取脚本把文本数据先读成字节再用Encoding.Unicode.GetString()转出来重新写回 UTF-8。5.4 文件损坏怎么办SQLite 的容错性已经不错了但任何数据库都挡不住断电、杀毒软件干预、磁盘异常。我的安全阀是三个地方同时上一是定期把数据文件复制到备份目录频率根据写入量定二是关键写入操作不要怕麻烦开启事务并设置 WAL三是提早在代码里捕获SQLiteException提示用户数据库损坏并引导恢复备份总比直接闪退体面。LiteDB 用户要更谨慎一点它的崩溃恢复机制没有 SQLite 成熟我建议在每次写操作密集的任务完成后立即 flush 并关闭数据库而不是长期保持打开。5.5 性能从多少毫秒优化到多少毫秒最后分享一组我在同一台开发机上跑出来的实际数字数据量 10 万条场景SQLiteLiteDBVistaDB全表扫描无索引210ms320ms280ms主键单条查询1ms2ms1ms批量插入 5000 条单事务680ms550ms720ms数据库文件大小8.6MB12.4MB9.3MB批量插入 LiteDB 最快全表扫描 SQLite 最快主键查询其实都很快。单机桌面应用根本打不到性能瓶颈别把性能当作主要选型依据部署复杂度和数据模型贴合度才是优先考虑的。6. 选型决策路径从需求到方案的映射方法论我把这几周折腾下来的一套判断方法整理成可复用的步骤不管你是新项目还是老项目迁移都可以按这个顺序走。6.1 第一步先分清你的数据是“关系型”还是“文档型”这个分法决定了你是选 SQLite/VistaDB 还是 LiteDB。关系型数据的特点是有明确的实体关联比如订单有明细、用户有角色、账套有科目你需要 join 查询、外键约束、事务一致性保证。文档型数据的特点是结构松散、字段动态、多为配置文件、缓存快照。判断标准很简单你写查询的时候是“这个用户关联了哪些订单”这种关系思维多还是“取这个配置项的内容”这种直取思维多。前者用关系库后者用文档库。有人非要用 LiteDB 存强关系数据也不是不行但联接查询写起来很别扭最后大概率会后悔。6.2 第二步盘点你的部署环境与发布方式如果是纯 Windows 桌面WinForms/WPF三个主流方案都可以选这时看团队熟悉度和是否需要加密。如果是 .NET MAUI 跨平台Android/iOS/Windows强烈建议优先考虑纯托管方案的 LiteDB原生依赖少打包链路最简单。如果目标是 Linux 服务器上的单机服务SQLite 是最稳的选择。如果目标用户是企业内部网且已经依赖 SQL Server 生态LocalDB 可能是最平滑的过渡方案。6.3 第三步评估数据安全和恢复要求这里有个容易被人忽视的地方。SQLite 本身没有内建加密你想对数据库文件加密要么用 SQLCipher社区版要自己编译商业授权要钱要么在应用层做字段加密。VistaDB 内建加密对数据敏感型传统 MIS 系统是加分项。LiteDB 从 5.x 开始支持ConnectionString里的Password参数但加密强度比较基础只能挡住文件被直接拷贝打开的情况防不了专业取证。如果你的应用承载的是用户核心资产比如财务数据我建议不管选谁都在应用层叠加 AES 对称加密字段加密不要把宝全押在数据库的加密机制上。6.4 第四步看你和团队的查询语言偏好团队如果都是老 SQL 手子上来就能写 join、group by、havingSQLite 几乎没有学习成本。如果团队更习惯 LINQ 一把梭LiteDB 的 fluent API 也够舒服。如果你对 SQL Server 的 T-SQL 方言很熟直接上 LocalDB 可以免学习成本。选型的时候不要高估团队的适应能力一个全组都熟的方案比一个网上口碑更好但没人用过的高冷方案靠谱得多。7. 迁移实战从 SQLite 迁到 LiteDB 的踩坑记录选型这件事有时候不是一次能定死的。项目做了一半发现数据模型越来越像文档之前的强关系设计根本用不上几个 join于是我把一个模块从 SQLite 迁到了 LiteDB。过程比想象中麻烦把值得注意的点写下来。7.1 实体模型的变化处理SQLite 里我原来是这样的一张表CREATE TABLE configs ( id INTEGER PRIMARY KEY AUTOINCREMENT, [key] TEXT NOT NULL UNIQUE, [value] TEXT NULL, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP );迁移到 LiteDB 之后我面临一个问题LiteDB 的Id局限性很强。默认情况下如果你用int Id插入时如果Id为 0它会自动赋自增值如果是 string Id就需要用Id Guid.NewGuid().ToString()。我的配置项没有严格的自增需求最终用了 string 类型public class ConfigItem { public string Id { get; set; } Guid.NewGuid().ToString(); public string Key { get; set; } public string Value { get; set; } public DateTime UpdatedAt { get; set; } }这里有个迁移时的坑SQLite 的自增 id 在语义上常常用于关联外键如果你把表迁移到 LiteDB 后想保留 id需要把原来的整型 id 绑定成实体的 Id 属性。LiteDB 只识别Id属性名或标记了[BsonId]的属性不要试图保留原来的id字段同时另建主键那只会给自己找不自在。7.2 数据导出导入脚本我写了一个控制台工具做数据迁移逻辑很简单从 SQLite 读出来再插到 LiteDB全程开启事务分批插入。这里有个性能经验LiteDB 批量插入时一次插几千条和一次插一条性能差一个数量级所以务必用InsertBulk。using var db new LiteDatabase(new ConnectionString { Filename new.db }); var col db.GetCollectionConfigItem(configs); var items ReadFromSqlite(); // 从旧库读取 col.InsertBulk(items);7.3 查询语法的重写这个是最花时间的。原来 EF Core 里用 LINQ 写var value await context.Configs .Where(c c.Key key) .Select(c c.Value) .FirstOrDefaultAsync();LiteDB 里对应的写法是var config col.FindOne(c c.Key key); string? value config?.Value;看起来区别不大但一旦查询带排序、分页、聚合两者的表达式语义还是有差距。比如分组统计LiteDB 需要用col.Query().GroupBy(...)配合聚合表达式没有 EF Core 的 LINQ 翻译器帮你兜底。迁移前务必把所有查询梳理一遍分类标注哪些是简单查询、哪些是复杂查询心理预期要放平。7.4 迁移后的体检清单迁移完不能直接删旧库我会按这个清单做体检数据条数一致新旧库各跑一遍 count。抽样比对每条记录的字段值写个 diff 工具。跑一遍原有自动化测试重点看日期格式、浮点数精度。测试异常关闭后能否重新打开观察文件大小是否异常增长。我这次迁移最后发现一个问题SQLite 里存的日期是2024-06-19 14:30:00这种 TEXT 格式LiteDB 里自动转成了DateTime类型读取正常但有一个老数据的时间是空字符串LiteDB 反序列化时直接丢了一个异常。最后在读取函数里加了一个兜底判断才搞定。老数据的脏值问题永远比你想的多。8. 一点真实感受搞完这一轮选型和迁移我有个挺深的体会本地数据库选型没有银弹每个方案都是带着它的“性格”来的。SQLite 像一匹血统纯正但需要驯服的马能力强、生态好可你不管好原生依赖和并发模式它随时给你撂挑子。LiteDB 像个灵巧的年轻人部署省心、API 亲切但在数据安全上你需要额外操心。VistaDB 像稳重的中年人功能齐全、自带安全能力但你需要为这份稳定付费。最实用的建议就一句先画清楚你的数据模型和部署目标再选方案别本末倒置。如果实在拿不定主意先用 SQLite 起步因为它是这几个方案里最容易被替换掉的——EF Core 的抽象层让你后续迁移到其他关系库的成本相对可控。但如果你同时预感到数据模型会越来越“文档化”那从一开始就用 LiteDB 反而能省一次迁移。最后分享一个我常用的判断技巧选型时做一个 1 小时的原型不是写 hello world而是把你项目里最复杂的那条查询原样写一遍再模拟一次批量写入加备份恢复。哪个方案在这个原型里让你皱眉最少往往就是那个陪你走完整条路的选择。