使用数据库设置
CREATE TYPE dbo.TableType AS TABLE (
prop1 int,
prop2 datetime2,
prop3 varchar(1000)
);
GO
CREATE OR ALTER PROC dbo.TestTableTypePerf
@Data dbo.TableType READONLY
AS
SELECT COUNT(*)
FROM @Data;
以下代码以流式传输方式传递 TVP 值。可枚举值直到调用后才被评估ExecuteScalarAsync
,并且不需要将全部 5,000,000 个元素具体化为客户端中的集合。
越来越流行的是放弃 TVP 而选择 JSON 字符串,对于 JSON 能做类似的事情吗?
using System.Data;
using Microsoft.Data.SqlClient;
using Microsoft.Data.SqlClient.Server;
const string connectionString =
@"...";
await TvpTest();
return;
static async Task TvpTest()
{
await using var conn = new SqlConnection(connectionString);
await conn.OpenAsync();
await using var cmd = new SqlCommand("dbo.TestTableTypePerf", conn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter
{
ParameterName = "@Data",
SqlDbType = SqlDbType.Structured,
TypeName = "dbo.TableType",
Value = GetEnumerableOfRandomSqlDataRecords(5_000_000)
});
Console.WriteLine($"calling ExecuteScalarAsync at {DateTime.Now:O}");
var result = await cmd.ExecuteScalarAsync();
Console.WriteLine($"writing result at {DateTime.Now:O}");
Console.WriteLine(result);
}
static IEnumerable<SqlDataRecord> GetEnumerableOfRandomSqlDataRecords(uint length)
{
SqlMetaData[] metaData =
[
new SqlMetaData("prop1", SqlDbType.Int),
new SqlMetaData("prop2", SqlDbType.DateTime2),
new SqlMetaData("prop3", SqlDbType.VarChar, 1000)
];
foreach (var dto in GetEnumerableOfRandomDto(length))
{
var record = new SqlDataRecord(metaData);
record.SetInt32(0, dto.Prop1);
record.SetDateTime(1, dto.Prop2);
record.SetString(2, dto.Prop3);
yield return record;
}
}
static IEnumerable<Dto> GetEnumerableOfRandomDto(uint length)
{
var rnd = new Random();
for (var i = 0; i < length; i++)
{
yield return new Dto(rnd.Next(1, int.MaxValue),
DateTime.Now.AddMinutes(rnd.Next(1, 10000)),
Guid.NewGuid().ToString()
);
if ((i + 1) % 100_000 == 0)
Console.WriteLine($"Generated enumerable {i + 1} at {DateTime.Now:O}");
}
}
internal record Dto(int Prop1, DateTime Prop2, string Prop3);
我已经对您的 JSON 测试进行了更高效、更简单的版本,并使用 BenchmarkDotNet(.NET 基准测试中的黄金标准)将其与 TVP 测试进行了比较。
改进:
SELECT DISTINCT
在所有列上添加了派生子查询。prop2
和的相同值创建的prop3
,以避免Random
和字符串插值的开销。TextReader
使用新的API得到的结果System.IO.Pipelines
如下:Pipe
。Stream
其传递给StreamReader
。Stream
并发症,因为它Pipe
为我们完成了这一切。TvpTest
,您不应重新创建。与地球上SqlDataRecord
的所有其他用法相反,建议重复使用。注意,比例是明确指定的。IEnumerable
SqlDataRecord
DateTime2
BenchmarkDotNet 结果如下。您可以看到,对于非常小的数据集,使用 JSON 的速度要快得多,但 TVP 在上限时速度会大幅落后。因此,最好只在数据集可能达到 1000 行时使用 TVP,因为启动成本明显较大。
我还对两个版本中的单个
Prop1 int
列进行了测试,并对每个列进行了查询SELECT MAX(Prop1) FROM
。结果很有趣:TVP 版本现在即使对于 50000 行也使用更少的内存,但 JSON 使用的内存相同,并且高行数时的速度差异也更大。进一步说明:
StreamReader
确实存在从 UTF-8 转换为 UTF-16 的缺点。我确实看到在某些情况下通过使用varbinary
并将 UTF-8Stream
直接写入服务器,然后在CAST
服务器端将其转换为varchar
并转换为,会略有改善nvarchar
,但并不一致。不幸的是,SqlClient 不接受Stream
UTF-8 字节的值varchar
。System.Text.Json
编写 UTF-16,而OPENJSON
只接受nvarchar
,因此转换时几乎肯定会损失一些效率。.nodes
各种.values
方法。在所有情况下,它们都不如其他两种方法有效。XmlReader
需要string
传入值,这意味着转换后的int
和DateTime
值会生成大量字符串。对于仅涉及字符串的用例,这仍然是一种选择。XmlSerializer
将仅使用一个XmlReader
输出。XDocument
可以生成一个XmlReader
,但是您仍然需要生成所有这些字符串。Pipe
,StreamReader
以及Stream
直接将其作为 传递并CAST
在服务器上执行。This was tested using SqlClient v6.0.1, and LocalDB 2019 RTM-CU29-GDR. I did not test using TDS v8.0, but some improvements have been made there to streaming large unknown-length parameters.
主题示例:流式传输到 SQL Server确实展示了如何从流中设置参数。
虽然
System.Text.Json
可以序列化为流,但对于我来说,是否/如何可以让它序列化为可枚举,并将可枚举的评估推迟到用作等效存储过程的参数值为止,并不清楚。不过,我确实在这个 StackOverflow 答案中找到了可能的解决方案。下面的解决方案(现在非常松散)基于此,并将对可枚举的评估推迟到调用之后
ExecuteScalarAsync
。客户端进程中的内存始终很低。参数传递代码可能仍然可以进行一些性能改进,但这不会有助于查询性能。
TVP 查询在我的计算机上以批处理模式以 DOP 4 运行,查询存储中显示的每个持续时间平均为 80 毫秒,而 JSON 查询则获得串行计划,平均持续时间 > 20 秒。但实际上,不太可能有人会将 500 万个 JSON 元素发送到 SQL Server 只是为了计算它们,在更现实的用例中,差异可能不那么明显。