问题描述
我有一个带有实体框架6.1.2的.net框架4.7.2应用程序。该应用程序使用Azure sql数据库,该数据库除其他外还具有带加密数据的表。 我们已经使用sql server的Always Encrypt功能来加密这些数据。数据库架构如下所示。
表Order中的字段Description为nvarchar(100),Note中的字段Description为nvarchar(100),并使用Always Encrypt加密(确定性加密)。两列都是可为空的,并且它们上都具有非聚集索引。这两个表都有几千万条记录。
我们的代码中包含以下查询:var user = DbContext.Set<User>().FirstOrDefault(u => u.Notes.Any(n => n.Description == value));
该查询以前以正常的性能成本(几十毫秒)运行
加密数据库中的字段。加密后,这种情况完全改变了。执行时间更改为几十秒。
我重建了所有索引,但没有任何改变。上面的代码语句从实体框架转换为类似的东西
DECLARE @p__linq__0 nvarchar(100) = 'some text'
SELECT *
FROM [User]
WHERE EXISTS (SELECT 1 AS [C1]
FROM [Note]
WHERE ([User].[Id] = [Note].[UserId]) AND (([Note].[Description] = @p__linq__0) OR (([Note].[Description] IS NULL) AND (@p__linq__0 IS NULL))) )
其在数据库中的执行计划如下。从该计划中可以明显看出,没有使用Description中的索引,而是执行了聚集索引扫描。这就是查询性能差的原因。
棘手的部分是,如果我们删除where子句的OR (([Note].[Description] IS NULL) AND (@p__linq__0 IS NULL)))
部分,查询的执行将立即发生,并且执行计划是预期的执行计划(见下文)
DECLARE @p__linq__0 nvarchar(100) = 'some text'
SELECT *
FROM [User]
WHERE EXISTS (SELECT 1 AS [C1]
FROM [Note]
WHERE ([User].[Id] = [Note].[UserId]) AND ([Note].[Description] = @p__linq__0) )
最有趣的是,如果我们移除([Note].[Description] IS NULL)
!在where子句的一部分中,性能在较差的状态下会再次下降,查询的执行计划是前一个。
DECLARE @p__linq__0 nvarchar(100) = 'some text'
SELECT *
FROM [User]
WHERE EXISTS (SELECT 1 AS [C1]
FROM [Note]
WHERE ([User].[Id] = [Note].[UserId]) AND (([Note].[Description] = @p__linq__0) OR ((@p__linq__0 IS NULL))) )
如果我们对具有与Note完全相同的架构的Order表执行相同的精确查询,但其Description字段未加密,则在所有情况下性能和执行计划都是预期的。
我已经看到以下相关问题,但它们未涉及加密列。它们指的是默认实体框架行为。 1 2
因此,问题是:在上述情况下,数据加密(始终加密)会产生什么影响,我们将如何克服?
解决方法
这是EF在SQL中复制C#空比较语义的方式。这是可选的,我总是将其关闭。
for X in range(1,397):
dirname = os.path.join('.','Beta' + str(X),'')
output = os.path.join('.','output.pdf')
DbContextConfiguration.UseDatabaseNullSemantics
只需在您的DbContext构造函数中设置:
Gets or sets a value indicating whether database null semantics are exhibited when comparing two operands,both of which are potentially nullable. The default value is false.
For example (operand1 == operand2) will be translated as:
(operand1 = operand2)
if UseDatabaseNullSemantics is true,respectively
(((operand1 = operand2) AND (NOT (operand1 IS NULL OR operand2 IS NULL))) OR ((operand1 IS NULL) AND (operand2 IS NULL)))
在EF Core中,配置是在OptionsBuilder上完成的,例如
this.Configuration.UseDatabaseNullSemantics = true;
,
最后,该问题与数据加密无关。
实际的问题是由于大数据迁移后数据库的统计信息已过时。我们使用EXEC sp_updatestats
更新了它们,然后使用预期的执行计划和预期的性能执行了查询。
我怀疑这与加密无关,并且与这个catch-all
子句无关:
AND (([Note].[Description] = @p__linq__0) OR ((@p__linq__0 IS NULL)))
此类包罗万象的子句是尝试在存储过程中使用“可选”参数的常见但不幸的方式。不幸的是,他们cause performance issues就是这样。服务器在第一次运行查询时缓存执行计划,并在后续调用中重用它。 不需要需要查询的特定列的执行计划与执行该查询的计划有很大不同。
如果第一次调用使用null
作为参数,则生成的执行计划将不使用覆盖该列的任何索引-为什么要这么做?
在使用诸如EF之类的ORM时,尤其是在涉及LINQ时,不需要这些技巧。
我怀疑EF查询(未发布)包含这样的Where
调用:
.Where(note=> note.Description == text || text ==null);
您可以链接LINQ调用,这意味着您只能在需要时添加条件。 catch-all
调用可以替换为:
if (text != null)
{
query=query.Where(note => note.Description == text);
}