☰
C#用SqlSugar高效批量写入PostgreSQL:从连串到事务的实战指南
2026/10/5 7:24:47 网站建设 项目流程

最近被问得最多的问题之一,就是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 = "Host=127.0.0.1;Port=5432;Database=testdb;Username=postgres;Password=your_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 List<DeviceSensorData>(); 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.Fastest<DeviceSensorData>().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.Now,Npgsql会按照服务器所在时区去解释这个时间,出现差8小时这种经典问题。

我的解决思路很简单:除非有跨时区需求,否则建表统一用timestamp without time zone。如果不得不面对timestamptz,就在SQL层面显式指定:

sensorData.AcquireTime = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc);

或者直接在连接串上加TimeZone=UTC,让客户端和服务端在一个时区认知下工作。这个配置很多教程不会提,但在分布式部署、服务器在云上的环境里经常是隐患。

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的numeric,SqlSugar默认会保留小数位数。如果实体是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限制重置密码,核对连接串的Password
database "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,实体标IsIdentity=true
Guid转string报错uuid列赋了字符串使用Guid类型属性
插入超长字符串报错varchar长度不足改为text或者代码里提前截断

遇到类型报错时,最有效的方法依然是看Aop日志,通常SQL会把参数类型列出来,一眼就能看出是NpgsqlDbType不匹配。SqlSugar在参数化这一层做得已经比较完善,多数报错根子还是在实体定义和库表结构对不对齐上。

5.4 我个人的排查顺序和避坑心得

最后分享一点真正靠时间换来的经验。配置新环境时,我永远是先跑通一个最小示例:只建一张单表,只插一条数据,连接串、实体、插入代码全部保持最简单。最小示例跑通了,再逐步加批量、事务、JSONB、多表关联。这样一旦出问题,定位范围非常小,不会整个项目混在一起无从下手。

核心理念就是“减少变量”。很多新手上来就套一个大而全的框架,出了问题不知道是连接串的错,还是实体的错,还是SqlSugar版本的错。我在实际项目里还养成了另外一个习惯:每个SqlSugarClient在初始化后,先调用一次db.DbMaintenance.CreateDatabase()和db.CodeFirst.InitTables<DeviceSensorData>()做自检。开发环境下这样可以自动建库建表,省去手动维护SQL脚本的麻烦;生产环境则要关掉自动建表,避免误操作。

这个组合一旦跑顺,后续做数据上报、历史查询、报表统计都会很舒服。尤其是在C#上位机里,拿着SqlSugar写批量插入,再配合PostgreSQL的JSONB存设备原始报文,整套链路非常清爽,不用为了性能天天写原生SQL。如果你正在这个方向上摸索,建议先把我上面提到的最小示例完整跑一遍,然后把Aop日志打开,再往自己的业务场景里加复杂度。数据库操作这件事,七分靠设计,三分靠代码,搞清楚了底层是怎么映射的,很多坑自然而然就绕开了。

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

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

立即咨询