读取存储过程结果行前获取总行数输出参数遇空引用异常求助
解决存储过程输出参数在DataReader关闭前读取的空引用异常问题
你遇到的问题核心在于SQL Server输出参数的返回时机——输出参数和返回值只有在整个批处理执行完成、所有结果集都被客户端读取完毕后,才会被服务器发送给客户端。当DataReader还处于打开状态时,@TotalRows参数的最终值还没被返回,此时读取它会得到null,自然触发"未将对象引用设置到对象实例"的异常。
下面给你两种可行的解决方案,你可以根据导出场景的实际需求选择:
方案1:读取完DataReader后再获取输出参数
这种方案不需要修改现有存储过程,只需要调整C#代码的执行顺序:先完整读取所有导出数据,再读取输出参数。
C#示例代码
using (SqlConnection conn = new SqlConnection("你的数据库连接字符串")) { conn.Open(); using (SqlCommand cmd = new SqlCommand("[dbo].[test]", conn)) { cmd.CommandType = CommandType.StoredProcedure; // 配置输出参数 var totalRowsParam = cmd.Parameters.Add("@TotalRows", SqlDbType.Int); totalRowsParam.Direction = ParameterDirection.Output; int exportedCount = 0; using (SqlDataReader reader = cmd.ExecuteReader()) { // 循环读取所有导出数据 while (reader.Read()) { // 处理每行数据(比如写入文件、缓存等) exportedCount++; // 临时显示已导出数量:"已导出{exportedCount}行" } } // DataReader关闭后,才能正确获取输出参数的值 int totalRows = (int)totalRowsParam.Value; Console.WriteLine($"导出完成:已导出{exportedCount}/{totalRows}行"); } }
为什么这个方案有效?
当你关闭DataReader后,客户端会通知服务器所有结果集已处理完毕,此时服务器才会把输出参数的最终值发送过来,你就能正常读取到@TotalRows的正确数值了。
方案2:用双结果集提前获取总行数(更适合进度显示)
如果你需要在导出开始前就显示完整进度(比如"已导出100/100000行"),可以修改存储过程,先返回总行数作为第一个结果集,再返回导出数据作为第二个结果集。这样你在处理数据前就能拿到总行数,实时更新进度。
修改后的存储过程
SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE PROCEDURE [dbo].[test] AS BEGIN SET NOCOUNT ON BEGIN TRANSACTION -- 执行插入操作(确保不重复导出的逻辑) -- 出错则回滚事务 -- 第一步:先计算总行数,作为第一个结果集返回 SELECT COUNT(*) AS TotalRows FROM ( -- 完全复用你原来的导出查询逻辑 SELECT ... FROM ... WHERE ... UNION SELECT ... FROM ... WHERE ... GROUP BY ... ) AS TempResult -- 第二步:返回导出的数据 SELECT ... FROM ... WHERE ... UNION SELECT ... FROM ... WHERE ... GROUP BY ... -- 执行更新操作 -- 出错则回滚事务 COMMIT TRANSACTION END
对应的C#代码
using (SqlConnection conn = new SqlConnection("你的数据库连接字符串")) { conn.Open(); using (SqlCommand cmd = new SqlCommand("[dbo].[test]", conn)) { cmd.CommandType = CommandType.StoredProcedure; int totalRows = 0; int exportedCount = 0; using (SqlDataReader reader = cmd.ExecuteReader()) { // 读取第一个结果集:总行数 if (reader.Read()) { totalRows = reader.GetInt32(reader.GetOrdinal("TotalRows")); Console.WriteLine($"开始导出,共{totalRows}行"); } // 切换到第二个结果集:导出数据 reader.NextResult(); // 实时处理数据并更新进度 while (reader.Read()) { // 处理每行数据 exportedCount++; Console.WriteLine($"已导出{exportedCount}/{totalRows}行"); } } Console.WriteLine($"导出完成:已导出{exportedCount}/{totalRows}行"); } }
注意事项
- 总行数查询和导出数据都在同一个事务里,不会出现中间数据被修改导致总行数与实际导出数不一致的问题。
- 如果导出查询非常复杂,
COUNT(*)可能会带来一定性能开销,但合理的索引可以把这个影响降到最低。
额外提醒
不管用哪种方案,都要确保存储过程里的事务逻辑完善:任何步骤出错时都要正确回滚事务,避免数据不一致。
内容的提问来源于stack exchange,提问作者mulllhausen




