gpt4 book ai didi

c# - 表值参数插入性能不佳

转载 作者:搜寻专家 更新时间:2023-10-30 21:41:16 25 4
gpt4 key购买 nike

我实现了 TVP+SP 插入策略,因为我需要插入大量行(可能同时插入),同时能够获得一些信息作为返回,如 Id和东西。最初我使用 EF 代码优先方法来生成数据库结构。我的实体:

设施组

public class FacilityGroup
{
public int Id { get; set; }

[Required]
public string Name { get; set; }

public string InternalNotes { get; set; }

public virtual List<FacilityInstance> Facilities { get; set; } = new List<FacilityInstance>();
}

设施实例

public class FacilityInstance
{
public int Id { get; set; }

[Required]
[Index("IX_FacilityName")]
[StringLength(450)]
public string Name { get; set; }

[Required]
public string FacilityCode { get; set; }

//[Required]
public virtual FacilityGroup FacilityGroup { get; set; }

[ForeignKey(nameof(FacilityGroup))]
[Index("IX_FacilityGroupId")]
public int FacilityGroupId { get; set; }

public virtual List<DataBatch> RelatedBatches { get; set; } = new List<DataBatch>();

public virtual HashSet<BatchRecord> BatchRecords { get; set; } = new HashSet<BatchRecord>();
}

批记录

public class BatchRecord
{
public long Id { get; set; }

//todo index?
public string ItemName { get; set; }

[Index("IX_Supplier")]
[StringLength(450)]
public string Supplier { get; set; }

public decimal Quantity { get; set; }

public string ItemUnit { get; set; }

public string EntityUnit { get; set; }

public decimal ItemSize { get; set; }

public decimal PackageSize { get; set; }

[Index("IX_FamilyCode")]
[Required]
[StringLength(4)]
public string FamilyCode { get; set; }

[Required]
public string Family { get; set; }

[Index("IX_CategoryCode")]
[Required]
[StringLength(16)]
public string CategoryCode { get; set; }

[Required]
public string Category { get; set; }

[Index("IX_SubCategoryCode")]
[Required]
[StringLength(16)]
public string SubCategoryCode { get; set; }

[Required]
public string SubCategory { get; set; }

public string ItemGroupCode { get; set; }

public string ItemGroup { get; set; }

public decimal PurchaseValue { get; set; }

public decimal UnitPurchaseValue { get; set; }

public decimal PackagePurchaseValue { get; set; }

[Required]
public virtual DataBatch DataBatch { get; set; }

[ForeignKey(nameof(DataBatch))]
public int DataBatchId { get; set; }

[Required]
public virtual FacilityInstance FacilityInstance { get; set; }

[ForeignKey(nameof(FacilityInstance))]
[Index("IX_FacilityInstance")]
public int FacilityInstanceId { get; set; }

[Required]
public virtual Currency Currency { get; set; }

[ForeignKey(nameof(Currency))]
public int CurrencyId { get; set; }
}

数据批处理

public class DataBatch
{
public int Id { get; set; }

[Required]
public string Name { get; set; }

public DateTime DateCreated { get; set; }

public BatchStatus BatchStatus { get; set; }

public virtual List<FacilityInstance> RelatedFacilities { get; set; } = new List<FacilityInstance>();

public virtual HashSet<BatchRecord> BatchRecords { get; set; } = new HashSet<BatchRecord>();
}

然后是我的SQL Server相关代码,TVP结构:

CREATE TYPE dbo.RecordImportStructure 
AS TABLE (
ItemName VARCHAR(MAX),
Supplier VARCHAR(MAX),
Quantity DECIMAL(18, 2),
ItemUnit VARCHAR(MAX),
EntityUnit VARCHAR(MAX),
ItemSize DECIMAL(18, 2),
PackageSize DECIMAL(18, 2),
FamilyCode VARCHAR(4),
Family VARCHAR(MAX),
CategoryCode VARCHAR(MAX),
Category VARCHAR(MAX),
SubCategoryCode VARCHAR(MAX),
SubCategory VARCHAR(MAX),
ItemGroupCode VARCHAR(MAX),
ItemGroup VARCHAR(MAX),
PurchaseValue DECIMAL(18, 2),
UnitPurchaseValue DECIMAL(18, 2),
PackagePurchaseValue DECIMAL(18, 2),
FacilityCode VARCHAR(MAX),
CurrencyCode VARCHAR(MAX)
);

插入存储过程:

CREATE PROCEDURE dbo.ImportBatchRecords (
@BatchId INT,
@ImportTable dbo.RecordImportStructure READONLY
)
AS
SET NOCOUNT ON;

DECLARE @ErrorCode int
DECLARE @Step varchar(200)

--Clear old stuff?
--TRUNCATE TABLE dbo.BatchRecords;

INSERT INTO dbo.BatchRecords (
ItemName,
Supplier,
Quantity,
ItemUnit,
EntityUnit,
ItemSize,
PackageSize,
FamilyCode,
Family,
CategoryCode,
Category,
SubCategoryCode,
SubCategory,
ItemGroupCode,
ItemGroup,
PurchaseValue,
UnitPurchaseValue,
PackagePurchaseValue,
DataBatchId,
FacilityInstanceId,
CurrencyId
)
OUTPUT INSERTED.Id
SELECT
ItemName,
Supplier,
Quantity,
ItemUnit,
EntityUnit,
ItemSize,
PackageSize,
FamilyCode,
Family,
CategoryCode,
Category,
SubCategoryCode,
SubCategory,
ItemGroupCode,
ItemGroup,
PurchaseValue,
UnitPurchaseValue,
PackagePurchaseValue,
@BatchId,
--FacilityInstanceId,
--CurrencyId
(SELECT TOP 1 f.Id from dbo.FacilityInstances f WHERE f.FacilityCode=FacilityCode),
(SELECT TOP 1 c.Id from dbo.Currencies c WHERE c.CurrencyCode=CurrencyCode)
FROM @ImportTable;

最后是我的快速、仅测试解决方案,用于在 .NET 端执行这些内容。

public class BatchRecordDataHandler : IBulkDataHandler<BatchRecordImportItem>
{
public async Task<int> ImportAsync(SqlConnection conn, SqlTransaction transaction, IEnumerable<BatchRecordImportItem> src)
{
using (var cmd = new SqlCommand())
{
cmd.CommandText = "ImportBatchRecords";
cmd.Connection = conn;
cmd.Transaction = transaction;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandTimeout = 600;

var batchIdParam = new SqlParameter
{
ParameterName = "@BatchId",
SqlDbType = SqlDbType.Int,
Value = 1
};

var tableParam = new SqlParameter
{
ParameterName = "@ImportTable",
TypeName = "dbo.RecordImportStructure",
SqlDbType = SqlDbType.Structured,
Value = DataToSqlRecords(src)
};

cmd.Parameters.Add(batchIdParam);
cmd.Parameters.Add(tableParam);

cmd.Transaction = transaction;

using (var res = await cmd.ExecuteReaderAsync())
{
var resultTable = new DataTable();
resultTable.Load(res);

var cnt = resultTable.AsEnumerable().Count();

return cnt;
}
}
}

private IEnumerable<SqlDataRecord> DataToSqlRecords(IEnumerable<BatchRecordImportItem> src)
{
var tvpSchema = new[] {
new SqlMetaData("ItemName", SqlDbType.VarChar, SqlMetaData.Max),
new SqlMetaData("Supplier", SqlDbType.VarChar, SqlMetaData.Max),
new SqlMetaData("Quantity", SqlDbType.Decimal),
new SqlMetaData("ItemUnit", SqlDbType.VarChar, SqlMetaData.Max),
new SqlMetaData("EntityUnit", SqlDbType.VarChar, SqlMetaData.Max),
new SqlMetaData("ItemSize", SqlDbType.Decimal),
new SqlMetaData("PackageSize", SqlDbType.Decimal),
new SqlMetaData("FamilyCode", SqlDbType.VarChar, SqlMetaData.Max),
new SqlMetaData("Family", SqlDbType.VarChar, SqlMetaData.Max),
new SqlMetaData("CategoryCode", SqlDbType.VarChar, SqlMetaData.Max),
new SqlMetaData("Category", SqlDbType.VarChar, SqlMetaData.Max),
new SqlMetaData("SubCategoryCode", SqlDbType.VarChar, SqlMetaData.Max),
new SqlMetaData("SubCategory", SqlDbType.VarChar, SqlMetaData.Max),
new SqlMetaData("ItemGroupCode", SqlDbType.VarChar, SqlMetaData.Max),
new SqlMetaData("ItemGroup", SqlDbType.VarChar, SqlMetaData.Max),
new SqlMetaData("PurchaseValue", SqlDbType.Decimal),
new SqlMetaData("UnitPurchaseValue", SqlDbType.Decimal),
new SqlMetaData("PackagePurchaseValue", SqlDbType.Decimal),
new SqlMetaData("FacilityInstanceId", SqlDbType.VarChar, SqlMetaData.Max),
new SqlMetaData("CurrencyId", SqlDbType.VarChar, SqlMetaData.Max),
};

var dataRecord = new SqlDataRecord(tvpSchema);

foreach (var importItem in src)
{
dataRecord.SetValues(importItem.ItemName,
importItem.Supplier,
importItem.Quantity,
importItem.ItemUnit,
importItem.EntityUnit,
importItem.ItemSize,
importItem.PackageSize,
importItem.FamilyCode,
importItem.Family,
importItem.CategoryCode,
importItem.Category,
importItem.SubCategoryCode,
importItem.SubCategory,
importItem.ItemGroupCode,
importItem.ItemGroup,
importItem.PurchaseValue,
importItem.UnitPurchaseValue,
importItem.PackagePurchaseValue,
importItem.FacilityCode,
importItem.CurrencyCode);

yield return dataRecord;
}
}
}

导入实体结构:

public class BatchRecordImportItem
{
public string ItemName { get; set; }

public string Supplier { get; set; }

public decimal Quantity { get; set; }

public string ItemUnit { get; set; }

public string EntityUnit { get; set; }

public decimal ItemSize { get; set; }

public decimal PackageSize { get; set; }

public string FamilyCode { get; set; }

public string Family { get; set; }

public string CategoryCode { get; set; }

public string Category { get; set; }

public string SubCategoryCode { get; set; }

public string SubCategory { get; set; }

public string ItemGroupCode { get; set; }

public string ItemGroup { get; set; }

public decimal PurchaseValue { get; set; }

public decimal UnitPurchaseValue { get; set; }

public decimal PackagePurchaseValue { get; set; }

public int DataBatchId { get; set; }

public string FacilityCode { get; set; }

public string CurrencyCode { get; set; }
}

请不要介意最后的无用读者,实际上并没有做太多。因此,如果没有读者插入 2.5kk 行,则需要大约 26 分钟,而 SqlBulkCopy花了大约 6+- 分钟。我做错了什么吗?我正在使用 IsolationLevel.Snapshot如果这很重要。使用SQL Server 2014,自由更改数据库结构和索引。

UPD 1


完成了@Xedni 描述的一些调整/改进尝试,具体而言:

  1. 将所有没有最大长度的字符串字段限制为某个固定长度
  2. VARCHAR(MAX) 更改了所有 TVP 成员至 VARCHAR(*SomeValue*)
  3. 为 FacilityInstance->FacilityCode 添加了唯一索引
  4. 为 Curreency->CurrencyCode 添加了唯一索引
  5. 尝试将 WITH RECOMPILE 添加到我的 SP
  6. 尝试使用 DataTable而不是 IEnumerable<SqlDataRecord>
  7. 尝试将数据分批放入更小的桶中,每次 SP 执行 50k 和 100k,而不是 2.5kk

我的结构现在是这样的:

CREATE TYPE dbo.RecordImportStructure 
AS TABLE (
ItemName VARCHAR(4096),
Supplier VARCHAR(450),
Quantity DECIMAL(18, 2),
ItemUnit VARCHAR(2048),
EntityUnit VARCHAR(2048),
ItemSize DECIMAL(18, 2),
PackageSize DECIMAL(18, 2),
FamilyCode VARCHAR(16),
Family VARCHAR(512),
CategoryCode VARCHAR(16),
Category VARCHAR(512),
SubCategoryCode VARCHAR(16),
SubCategory VARCHAR(512),
ItemGroupCode VARCHAR(16),
ItemGroup VARCHAR(512),
PurchaseValue DECIMAL(18, 2),
UnitPurchaseValue DECIMAL(18, 2),
PackagePurchaseValue DECIMAL(18, 2),
FacilityCode VARCHAR(450),
CurrencyCode VARCHAR(4)
);

不幸的是,到目前为止没有明显的性能提升,和以前一样是 26-28 分钟


UPD 2
检查了执行计划——指数是我的祸根? EXE_PLAN


UPD 3
已添加 OPTION (RECOMPILE);在我的 SP 结束时,获得了轻微的提升,现在坐在 ~25m 处 2.5kk

最佳答案

你可以设置traceflag 2453 :

FIX: Poor performance when you use table variables in SQL Server 2012 or SQL Server 2014

When you use a table variable in a batch or procedure, the query is compiled and optimized for the initial empty state of table variable. If this table variable is populated with many rows at runtime, the pre-compiled query plan may no longer be optimal. For example, the query may be joining a table variable with nested loop since it is usually more efficient for small number of rows. This query plan can be inefficient if the table variable has millions of rows. A hash join may be a better choice under such condition. To get a new query plan, it needs to be recompiled. Unlike other user or temporary tables, however, row count change in a table variable does not trigger a query recompile. Typically, you can work around this with OPTION (RECOMPILE), which has its own overhead cost. The trace flag 2453 allows the benefit of query recompile without OPTION (RECOMPILE). This trace flag differs from OPTION (RECOMPILE) in two main aspects. (1) It uses the same row count threshold as other tables. The query does not need to be compiled for every execution unlike OPTION (RECOMPILE). It would trigger recompile only when the row count change exceeds the predefined threshold. (2) OPTION (RECOMPILE) forces the query to peek parameters and optimize the query for them. This trace flag does not force parameter peeking.

You can turn on trace flag 2453 to allow a table variable to trigger recompile when enough number of rows are changed. This may allow the query optimizer to choose a more efficient plan

关于c# - 表值参数插入性能不佳,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49395844/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com