为何Npgsql v3及以上版本中NpgsqlDataReader读取速度大幅变慢?
将PostgreSQL v9升级至v14时,同步把.NET Framework 4.6.1项目的Npgsql从2.2.7升级到4.1.12,发现执行存储过程并通过NpgsqlDataReader读取20000行数据时,v3+版本耗时是v2.2.7的10倍(v2.2.7需10秒,v3+需140秒,连接PostgreSQL v9或v14耗时无差异)。已尝试调整连接字符串Read Buffer Size、使用ExecuteAsync/ReadAsync方法,均无明显改善。相关代码如下:
using Npgsql; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Data; namespace UpgradeV2.Sample { internal class SampleRequest { private static string connectionstring = "server=myhostname;port=5432;database=mydatabase;user id=myuser;password=mypassword;enlist=true;pooling=false;minpoolsize=1;maxpoolsize=100;timeout=1024;commandtimeout=3000;"; public static IEnumerable<DataClass> GetData(long id) { var parameters = new Dictionary<string, object> { {"i_id", (int)id} }; using (NpgsqlConnection _npgsqlConnection = new NpgsqlConnection(connectionstring)) { var command = new NpgsqlCommand { CommandType = CommandType.StoredProcedure, Connection = _npgsqlConnection, CommandText = "public.function_returning_lots_of_data" }; foreach (var parameter in parameters) { command.Parameters.Add(new NpgsqlParameter { ParameterName = parameter.Key, Value = parameter.Value }); } using (var dbReader = command.ExecuteReader()) { var dataList = new Collection<DataClass>(); // to complete this while loop with npgsql v2 takes 10 seconds // to complete this while loop with npgsql v3-v6 takes 140 seconds while (dbReader.Read()) { if (dbReader["id"] == DBNull.Value) { continue; } DataClass item = new DataClass() { id = dbReader.GetFieldValue<int>("id"), //And about 20 other Fields read using GetFieldValue }; dataList.Add(item); } return dataList; } } } } }
解决方案
替换
GetFieldValue<T>为批量读取
Npgsql v3+对GetFieldValue<T>的类型检查逻辑做了调整,频繁调用单字段读取会累积大量开销。改用GetValues一次性读取整行数据,再映射到实体:while (dbReader.Read()) { var idOrdinal = dbReader.GetOrdinal("id"); if (dbReader.IsDBNull(idOrdinal)) continue; var values = new object[dbReader.FieldCount]; dbReader.GetValues(values); DataClass item = new DataClass() { id = (int)values[idOrdinal], // 其他字段通过对应索引取值转换 }; dataList.Add(item); }也可以使用Dapper等ORM工具自动映射实体,进一步减少手动读取的性能损耗。
开启二进制格式传输
Npgsql v2.2.7默认启用二进制格式传输数据,而v3+默认关闭。在连接字符串中添加UseBinaryFormat=true,大幅降低数据序列化与反序列化的开销:server=myhostname;port=5432;database=mydatabase;user id=myuser;password=mypassword;enlist=true;pooling=false;minpoolsize=1;maxpoolsize=100;timeout=1024;commandtimeout=3000;UseBinaryFormat=true;明确指定参数类型
手动创建NpgsqlParameter时,显式设置NpgsqlDbType可以避免Npgsql自动推断类型的额外开销:command.Parameters.Add(new NpgsqlParameter { ParameterName = parameter.Key, Value = parameter.Value, NpgsqlDbType = NpgsqlDbType.Integer });启用连接池
当前连接字符串设置pooling=false,每次请求都新建连接会带来显著开销。建议改为pooling=true,并根据业务调整minpoolsize和maxpoolsize,减少连接建立的耗时。升级Npgsql版本
Npgsql 4.1.12存在部分性能缺陷,建议升级到4.1.x系列的最新补丁版本(如4.1.16),或兼容.NET Framework 4.6.1的5.x/6.x版本,新版本修复了多个性能相关问题。
内容的提问来源于stack exchange,提问作者Code Ninja

