
最近被问得最多的问题之一就是C#怎么往PostgreSQL里高效写数据尤其在用SqlSugar这个ORM的时候。说实话这个组合真的很配SqlSugar对国内外各种主流数据库都友好PostgreSQL又免费又能扛数据量在上位机、ERP、物联网采集这类场景里越来越常见。我自己的项目里跑过每天几十万条传感器记录基本就是靠SqlSugar的批量插入加事务稳稳地落库没出过幺蛾子。下面这些内容不是把官方文档复读一遍而是我实际踩完坑之后整理出来的实操笔记适合刚开始用C#加SqlSugar访问PostgreSQL的朋友也适合已经在用但偶尔被连接串、类型映射折腾得不轻的老手。1. 为什么是SqlSugar加PostgreSQL先想清楚再动手1.1 这个组合到底解决了什么问题先说我接触最多的一个场景上位机采集。工控机每50毫秒要从设备那边读一次扭矩、温度、振动这类数据读完之后第一反应就是扔进数据库。早期有人图省事直接拼字符串Insert听上去也能跑但数据量一旦上来就原形毕露SQL语句拼接容易错参数注入先不说光是把几千条数据一条条循环插进去就能把UI线程卡死还会频繁触发数据库日志写盘整个上位机直接变成PPT。用SqlSugar之后这些问题被收拢到三个很舒服的点上。第一实体映射简单表结构对应类字段对应属性建表、插入、查询都不用再手写一大堆DbCommand。第二批量插入是天然支持的不用像原生Npgsql那样手动封装Copy协议。第三它内置事务控制一批设备数据要么全进库要么全回滚不会出现一半成功一半失败这种让人头皮发麻的脏数据。至于数据库选PostgreSQL我更愿意把它理解为长期主义的选择。工业数据和业务数据有一个共同特点写完了基本不怎么改但是要一直查、一直分析。PostgreSQL的JSONB字段能直接存设备回传的原始报文数组、范围类型对时间段统计也很友好配合时序类查询不输给很多商业数据库。最关键是部署没有任何授权成本买台普通工控机就能装业务规模上来了再迁移或者做只读备库路子很宽。1.2 版本选择和下载层面的实在建议很多刚入门的朋友一上来就问“PostgreSQL下载哪个版本”。我的建议很简单生产环境用官方安装包装16或者更新一点的17稳定版别折腾什么便携版。便携版虽然看起来绿色免安装但背后经常要手动配服务、配环境变量出问题更难排查。除非你是离线内网环境、或者只想在本机快速验证一下语法否则老老实实用官方安装包最省事。SqlSugar这边要分清一个颗粒度如果你用的是.NET 6/7/8这种跨平台项目NuGet里搜SqlSugarCore这是专门为.Net Core和.NET 5准备的包。如果你还守着.NET Framework 4.x的老项目那就得上SqlSugar不带Core或者官方给的dll引用。这个区别挺关键装错包会导致运行时不断报程序集加载失败尤其是在WinForm上位机项目里特别普遍。组件版本的大致对应关系我列在下面方便大家做技术方案的时候心里有数项目组件推荐版本说明PostgreSQL16.x / 17.x建议用官方安装包生产环境选择稳定版SqlSugarCore5.1.4.x 及以上当前主流的跨平台版本随NuGet更新Npgsql8.xSqlSugar依赖的PostgreSQL驱动一般自动引入.NET8.0 LTS长期支持版本适合新项目起步这里多说一句Npgsql的小版本和PostgreSQL服务端版本不需要严格一一对应它走的是标准v3协议老客户端连新服务端一般也没问题。但是别去手动换一个老旧Npgsql那样很容易和SqlSugar内部的版本冲突。2. 环境准备与前置配置把连接和映射一次配明白2.1 NuGet依赖安装与项目结构建议新建一个控制台项目或者上位机项目之后最直接的一步就是打开包管理控制台输入Install-Package SqlSugarCore或者直接在NuGet管理器里搜索SqlSugarCore找到那个蓝色图标包安装即可。装完之后顺手看一眼依赖项正常情况下会自动带出Npgsql。如果项目里本身有旧版Npgsql建议统一升级到同一个大版本避免出现“找到了Npgsql但版本不是预期版本”之类的加载异常。我习惯把数据有关的代码单独拆一层不要全部堆在窗体事件里。比如建一个DbHelper.cs或者Repository文件夹里面统一管SqlSugarClient的初始化。这样后续要换连接串、加Aop日志、做多库切换都只改一个地方。很多半路出家的项目就是因为到处new SqlSugarClient最后连事务都跨实例了排查起来特别难受。2.2 连接串怎么写才对几个参数逐一说清楚SqlSugar连接PostgreSQL的配置块是长这样的var db new SqlSugarClient(new ConnectionConfig { ConnectionString Host127.0.0.1;Port5432;Databasetestdb;Usernamepostgres;Passwordyour_password;, DbType DbType.PostgreSQL, IsAutoCloseConnection true, MoreSettings new ConnMoreSettings() { PgSqlIsAutoToLower true } });这段代码里Host和Port是最容易出问题的两个参数。Host写数据库服务器地址连本机就是127.0.0.1或者localhost如果数据库跑在隔壁机器或者云服务器上一定别漏了防火墙放行5432端口否则你程序报的往往是“connection timeout”而不是密码错误那种明确的提示排查方向很容易歪掉。Database是具体库名PostgreSQL对数据库名大小写敏感建库时如果是小写testdb连接串里就别写成TestDb。Username和Password不用多说默认超级用户是postgres密码在安装数据库时设定忘了密码的话要去改pg_hba.conf这个放到后面问题排查部分细说。重点讲一下MoreSettings里的PgSqlIsAutoToLower。PostgreSQL有一个很反直觉的行为对于不加双引号的表名、字段名会自动折叠成小写。如果你的C#实体属性起的是DeviceNo、AcquireTime这种帕斯卡命名法建表或者查询时SqlSugar生成的SQL如果没做处理数据库会把它变成deviceno、acquiretime然后跟你真实建表的小写字段名对上还好对不上就报“column does not exist”。开启PgSqlIsAutoToLower true之后SqlSugar会在生成SQL时把实体映射的小写形式统一处理好不用你在每个属性上手工写特性。这个开关强烈建议一上来就打开不然等到项目里几百个字段的时候再改那才叫一个酸爽。2.3 建表语句与实体映射的正确姿势PostgreSQL建表很灵活我用得最多的是BIGSERIAL自增主键和JSONB扩展字段。一个典型的设备采集数据表可以这样建CREATE TABLE IF NOT EXISTS device_sensor_data ( id BIGSERIAL PRIMARY KEY, device_no VARCHAR(50) NOT NULL, torque_value DECIMAL(10,2) NULL, acquire_time TIMESTAMP NOT NULL DEFAULT now(), raw_data JSONB NULL );对应到C#实体类我推荐用SugarTable和SugarColumn把映射关系显式标出来[SugarTable(device_sensor_data)] public class DeviceSensorData { [SugarColumn(IsPrimaryKey true, IsIdentity true)] public long Id { get; set; } [SugarColumn(ColumnName device_no)] public string DeviceNo { get; set; } [SugarColumn(ColumnName torque_value)] public decimal? TorqueValue { get; set; } [SugarColumn(ColumnName acquire_time)] public DateTime AcquireTime { get; set; } [SugarColumn(ColumnName raw_data, ColumnDataType jsonb)] public string RawData { get; set; } }可能有朋友觉得既然开了PgSqlIsAutoToLower为什么还要写ColumnName这里面的门道是自动转小写处理的是“SqlSugar生成SQL时的大小写统一”但如果你表里的字段都是小写下划线风格而实体属性叫DeviceNo即使转成小写也只会得到deviceno并不是你想要的device_no。所以最稳妥的方法就是实体属性用C#习惯的帕斯卡命名然后通过ColumnName显式指定数据库列名。这样SqlSugar能正确映射代码可读性也不差。ColumnDataType jsonb这个属性非常关键。PostgreSQL里的JSONB列在插入字符串参数时如果不显式告诉Npgsql它是jsonb常见的报错是“column raw_data is of type jsonb but expression is of type text”。你只要在实体上标注了这一列的数据类型SqlSugar生成SQL时会带上类型转换这个坑就算提前绕过去了。3. 核心实现单条插入、批量插入与事务控制3.1 最基础的单条插入和自增ID返回先从一个最直接的例子开始。采集到一条设备数据需要落库同时要拿到新插入记录的自增主键方便后面做关联。代码写起来非常简单var sensorData new DeviceSensorData { DeviceNo PF6000-01, TorqueValue 23.45m, AcquireTime DateTime.Now, RawData System.Text.Json.JsonSerializer.Serialize(new { alarm false, mode 1 }) }; long newId db.Insertable(sensorData).ExecuteReturnIdentity();ExecuteReturnIdentity()会直接返回数据库生成的BIGSERIAL自增值。这里有一个小细节如果返回的是0或者-1先检查实体主键有没有标IsIdentity true同时确认数据库字段确实是BIGSERIAL而不是普通BIGINT。很多人在CodeFirst建表时把主键写成了普通long然后拿不到自增ID误以为ORM有问题其实根子在设计表结构那一步。如果只是插入不需要拿自增ID可以用ExecuteCommand()它返回受影响行数。对于日志型数据也更建议用批量插入而不是单条插入。3.2 批量插入的高效写法以及性能对比的直观感受真实的上位机采集绝不可能一条一条插动不动就是几千条。SqlSugar的批量插入API非常直白var list new ListDeviceSensorData(); for (int i 0; i 10000; i) { list.Add(new DeviceSensorData { DeviceNo PF6000-01, TorqueValue 10 i * 0.01m, AcquireTime DateTime.Now.AddMilliseconds(i * 100), RawData {\seq\: i } }); } int affectedRows db.Insertable(list).ExecuteCommand();这一句看起来简单背后最关键的是SqlSugar把批量插入拼成了带参数化的多值INSERT既避免了SQL注入又减少了网络往返。我做过一次直观测试一万条数据用循环单条插耗时基本在七八秒换成Insertable(list)批量插入大概一秒出头如果再用db.FastestDeviceSensorData().BulkCopy(list)走PostgreSQL底层COPY协议一秒钟不到就能完成。这里注意BulkCopy是真正的“快”但也不是无脑用。它要求目标表和实体结构高度匹配插入过程中只要有一条数据不符合约束整批就会报错回滚。所以我一般这样分配常规业务数据用Insertable(list).ExecuteCommand()追求极致性能且数据质量可控的场合用Fastest批量复制。另外不同版本的SqlSugar在API命名上有点差异老版本可能是UsePgSqlBulkCopy新版本叫Fastest用的时候以NuGet上的智能提示为准。3.3 事务与异常回滚的正确姿势如果一次操作涉及多张表的写入比如设备上报的同时要更新设备状态还可能要写一条设备报警记录那就必须放在一个事务里。SqlSugar的事务封装得很方便我常用的写法是var result db.UseTran(() { db.Insertable(deviceStatus).ExecuteCommand(); db.Insertable(sensorData).ExecuteCommand(); // 假设中途有条件不满足可以主动抛异常触发回滚 if (sensorData.TorqueValue 1000) { throw new Exception(扭矩超限整批数据回滚); } return new { StatusId deviceStatus.Id, DataId sensorData.Id }; }); if (result.IsSuccess) { Console.WriteLine(事务提交成功); } else { Console.WriteLine($事务失败{result.ErrorMessage}); }有几个非常容易踩的细节必须重点提醒。第一UseTran出来的result即使失败lambda里已经执行过的数据操作也会自动回滚不需要你手动再写一遍删除SQL。第二lambda里要使用同一个db实例千万别在里面又重新new SqlSugarClient否则事务上下文就对不上了外层的回滚根本管不住新实例的操作这是最常见的“事务失效”原因。第三不要在lambda里手动调用db.Close()或者db.Ado.Connection.Dispose()SqlSugar自己管理连接释放手动介入反而容易把连接状态弄坏。4. 进阶细节时间、Guid、JSONB这些特别容易翻车的点4.1 时间字段的时区和精度问题PostgreSQL的timestamp分两种不带时区的timestamp和带时区的timestamptz。SqlSugar的默认映射通常用timestamp这样本地时间存进去是什么就是什么查出来也是什么。可一旦表被人为改成了timestamptz你又传了一个DateTime.NowNpgsql会按照服务器所在时区去解释这个时间出现差8小时这种经典问题。我的解决思路很简单除非有跨时区需求否则建表统一用timestamp without time zone。如果不得不面对timestamptz就在SQL层面显式指定sensorData.AcquireTime DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc);或者直接在连接串上加TimeZoneUTC让客户端和服务端在一个时区认知下工作。这个配置很多教程不会提但在分布式部署、服务器在云上的环境里经常是隐患。4.2 Guid、decimal、字符串长度这几个隐藏规则PostgreSQL的uuid列对应C#的Guid这个Mapping比较自然直接赋Guid.NewGuid()就行。怕就怕有人图省事把主键设计成uuid然后在程序里传一个字符串去插入结果报错提示“column is of type uuid but expression is of type character varying”。这种问题不是SqlSugar的问题是类型没对齐把实体的属性声明成Guid就解决了。decimal类型映射到PG的numericSqlSugar默认会保留小数位数。如果实体是decimal数据库列是numeric(10, 4)插入时传了超过4位小数的数可能会被四舍五入也可能报数值溢出关键是检查你代码里的小数位数是否和列精度匹配。常规做法是把金额、扭矩这类数据都在实体里用decimal?避免用double存财务敏感信息。字符串长度也是高频坑。实体里不写Length属性不代表无限长如果你手动建的表是varchar(50)C#里却塞进来一个超长的设备编号执行的时候直接报“value too long for type character varying(50)”。所以批量插入之前最好先做一下字段长度校验或者数据库层面用text类型彻底避开这个限制。4.3 开启Aop日志SQL无处可藏我调试SqlSugar插入问题的时候第一件事就是打开Aop日志把每条执行的SQL和参数打印出来。这个动作能省掉至少一半的排查时间db.Aop.OnLogExecuting (sql, pars) { Console.WriteLine(sql); foreach (var p in pars) { Console.WriteLine($ {p.ParameterName}:{p.Value}); } };开启之后你会清楚看到SqlSugar到底生成了什么SQL参数是不是合理表名有没有变成小写JSONB列到底有没有加类型转换。有一次我发现批量插入的性能突然变差打开日志一看SqlSugar没有走多值INSERT而是拆成了单条循环原因是我传入的List里混入了Care列属性导致映射走了别的分支。没有日志这种问题你翻半天源码都不一定找得到。所以这个配置建议从一开始就在DbHelper的初始化里加好上线后可以关掉或者降到Debug级别省得刷屏。5. 常见问题速查表与排查技巧实录5.1 连接失败类问题汇总异常现象常见原因解决建议connection timeoutHost、Port不通或防火墙拦截用Telnet测试5432端口检查云安全组password authentication failed密码错误或pg_hba.conf限制重置密码核对连接串的Passworddatabase xxx does not exist数据库名大小写不匹配PostgreSQL库名有大小写敏感性改成创建时的完整名称无法绑定5432端口postgresql.conf未监听外部地址设置listen_addresses *并重启服务This connection has been closed连接池被提前关闭或连接串重复使用确认IsAutoCloseConnection且正确复用db实例连接问题最怕的就是含糊其辞看一眼就猜我建议遇到这类问题先做两件事第一用命令行工具pg_isready看服务状态第二用psql在命令行里直接连一次相同连接串能连上再怀疑程序代码的问题。很多时候根本不是C#侧的问题而是数据库服务压根没起来或者登录认证没放行。5.2 插入性能与事务问题速查现象可能原因解决思路一万条数据要插入好几秒单条循环INSERT改成Insertable(list)批量插入批量插入仍然很慢网络延迟或字段过多用Fastest走底层COPY协议减少交互次数事务莫名其妙回滚lambda里使用了不同db实例确保UseTran前后都使用同一个SqlSugarClient插入时卡死事务未提交锁表检查是否有长事务使用短事务及时提交性能问题一定要结合日志来定位我最常见的“假慢”其实是主键冲突导致的异常回滚程序线程在那里反复重试表面上像是插入慢实际是数据问题。可以先在没有数据约束的临时表里测速度再逐步加上索引和约束观察变化。5.3 类型映射与实体配置问题速查异常现象常见原因解决建议column is of type jsonb but expression is of type text实体映射缺了ColumnDataType给属性标[SugarColumn(ColumnDataType jsonb)]表名/字段名找不到PG大小写折叠统一小写命名或开PgSqlIsAutoToLower自增ID取不到主键不是自增确认数据库用BIGSERIAL实体标IsIdentitytrueGuid转string报错uuid列赋了字符串使用Guid类型属性插入超长字符串报错varchar长度不足改为text或者代码里提前截断遇到类型报错时最有效的方法依然是看Aop日志通常SQL会把参数类型列出来一眼就能看出是NpgsqlDbType不匹配。SqlSugar在参数化这一层做得已经比较完善多数报错根子还是在实体定义和库表结构对不对齐上。5.4 我个人的排查顺序和避坑心得最后分享一点真正靠时间换来的经验。配置新环境时我永远是先跑通一个最小示例只建一张单表只插一条数据连接串、实体、插入代码全部保持最简单。最小示例跑通了再逐步加批量、事务、JSONB、多表关联。这样一旦出问题定位范围非常小不会整个项目混在一起无从下手。核心理念就是“减少变量”。很多新手上来就套一个大而全的框架出了问题不知道是连接串的错还是实体的错还是SqlSugar版本的错。我在实际项目里还养成了另外一个习惯每个SqlSugarClient在初始化后先调用一次db.DbMaintenance.CreateDatabase()和db.CodeFirst.InitTablesDeviceSensorData()做自检。开发环境下这样可以自动建库建表省去手动维护SQL脚本的麻烦生产环境则要关掉自动建表避免误操作。这个组合一旦跑顺后续做数据上报、历史查询、报表统计都会很舒服。尤其是在C#上位机里拿着SqlSugar写批量插入再配合PostgreSQL的JSONB存设备原始报文整套链路非常清爽不用为了性能天天写原生SQL。如果你正在这个方向上摸索建议先把我上面提到的最小示例完整跑一遍然后把Aop日志打开再往自己的业务场景里加复杂度。数据库操作这件事七分靠设计三分靠代码搞清楚了底层是怎么映射的很多坑自然而然就绕开了。