SQL 加密列后查询性能下降

SQL query performance degradation after encrypting column

我有一个带有 entity framework 6.1.2 的 .net framework 4.7.2 应用程序。该应用程序使用一个 azure sql 数据库,其中有一个 table 和加密数据。 我们使用了 sql 服务器的 Always Encrypt 功能来加密这些数据。数据库架构如下所示。

tableOrder字段Description为nvarchar(100),Note字段Description为nvarchar(100),并使用Always Encrypt(确定性加密)加密。这两列都可以为空,并且在它们上面有一个非聚集索引。两个 table 都有几千万条记录。

我们的代码中有以下查询:var user = DbContext.Set<User>().FirstOrDefault(u => u.Notes.Any(n => n.Description == value));此查询以前运行具有正常的性能成本(几十毫秒) 加密我们数据库中的字段。这在加密后完全改变了。执行时间改为几十秒

我重建了所有索引,但它改变了 nothing.The 上面的代码语句从 entity framework 翻译成如下内容:

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 中的索引,并且执行了 Clustered Index Scan。这就是查询性能差的原因。

棘手的部分是,如果我们删除 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))) )
            

如果我们对订单 table 执行完全相同的查询,该订单具有与 Note 完全相同的架构,但其描述字段未加密,则性能和执行计划在所有情况下都符合预期。

我看到了以下相关问题,但它们并未涉及加密列。他们指的是默认 entity framework 行为。 1

因此,问题是:在上述情况下数据加密(始终加密)有什么影响,我们如何克服它?

我怀疑这与加密无关,而与此 catch-all 子句有关:

AND (([Note].[Description] = @p__linq__0) OR ((@p__linq__0 IS NULL)))

这样的 catch-all 子句是尝试在存储过程中使用“可选”参数的一种常见但不幸的方式。不幸的是,他们 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);
}

这就是 EF 在 SQL 中复制 C# 空值比较语义的方式。这是可选的,我总是将其关闭。

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)))

DbContextConfiguration.UseDatabaseNullSemantics

在您的 DbContext 构造函数中只需设置:

this.Configuration.UseDatabaseNullSemantics = true;

在EF Core中,配置是在OptionsBuilder上完成的,例如

optionsBuilder.UseSqlServer(constr, o => o.UseRelationalNulls());

最终问题与数据加密无关。 实际问题来自于大数据迁移后数据库的统计数据已过时。我们使用 EXEC sp_updatestats 更新了它们,之后使用预期的执行计划和预期的性能执行了查询。