查詢 Null 語意

簡介

當執行比較時,SQL 資料庫會以 3 值邏輯 ( truefalsenull ) 運作,而不是 C# 的布林邏輯。 將 LINQ 查詢轉譯為 SQL 時,EF Core 會嘗試藉由引進查詢某些元素的額外 Null 檢查來補償差異。 為了說明這一點,讓我們定義下列實體:

public class NullSemanticsEntity
{
    public int Id { get; set; }
    public int Int { get; set; }
    public int? NullableInt { get; set; }
    public string String1 { get; set; }
    public string String2 { get; set; }
}

併發出數個查詢:

var query1 = context.Entities.Where(e => e.Id == e.Int);
var query2 = context.Entities.Where(e => e.Id == e.NullableInt);
var query3 = context.Entities.Where(e => e.Id != e.NullableInt);
var query4 = context.Entities.Where(e => e.String1 == e.String2);
var query5 = context.Entities.Where(e => e.String1 != e.String2);

前兩個查詢會產生簡單的比較。 在第一個查詢中,這兩個數據行不可為 Null,因此不需要 Null 檢查。 在第二個查詢中, NullableInt 可以包含 null ,但 Id 不可為 Null;相較于 null 結果的非 Null 產生 null ,作業會篩選掉 WHERE 。 因此,也不需要額外的條款。

SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2]
FROM [Entities] AS [e]
WHERE [e].[Id] = [e].[Int]

SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2]
FROM [Entities] AS [e]
WHERE [e].[Id] = [e].[NullableInt]

第三個查詢引進 Null 檢查。 nullNullableInt 是比較 Id <> NullableInt 會產生 null 時,這會依 WHERE 作業篩選掉。 不過,從布林邏輯的觀點來看,這個案例應該傳回做為結果的一部分。 因此,EF Core 會新增必要的檢查,以確保這一點。

SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2]
FROM [Entities] AS [e]
WHERE ([e].[Id] <> [e].[NullableInt]) OR [e].[NullableInt] IS NULL

當這兩個數據行都可為 Null 時,查詢四和五會顯示模式。 值得注意的是, <> 作業會產生比 == 作業更複雜(且可能較慢)的查詢。

SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2]
FROM [Entities] AS [e]
WHERE ([e].[String1] = [e].[String2]) OR ([e].[String1] IS NULL AND [e].[String2] IS NULL)

SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2]
FROM [Entities] AS [e]
WHERE (([e].[String1] <> [e].[String2]) OR ([e].[String1] IS NULL OR [e].[String2] IS NULL)) AND ([e].[String1] IS NOT NULL OR [e].[String2] IS NOT NULL)

在函式中處理可為 Null 的值

SQL 中的許多函式只有在部分引數為 null 時,才能傳回 null 結果。 EF Core 會利用這項功能來產生更有效率的查詢。 下列查詢說明優化:

var query = context.Entities.Where(e => e.String1.Substring(0, e.String2.Length) == null);

產生的 SQL 如下所示(我們不需要評估函 SUBSTRING 式,因為它只有在其中一個引數為 null 時才會是 null。

SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2]
FROM [Entities] AS [e]
WHERE [e].[String1] IS NULL OR [e].[String2] IS NULL

優化也可用於使用者定義函式。 如需詳細資訊,請參閱 使用者定義函數對應 頁面。

撰寫高效能查詢

  • 比較不可為 Null 的資料行比比較可為 Null 的資料行更簡單且更快。 請考慮盡可能將資料行標示為不可為 Null。

  • 檢查是否相等 == ()比檢查不相等 != 更為簡單且更快,因為查詢不需要區分 nullfalse 結果。 盡可能使用相等比較。 不過,只是否定 == 比較實際上與 != 相同,因此不會產生效能改善。

  • 在某些情況下,可以藉由明確篩選 null 出資料行中的值來簡化複雜的比較,例如,當沒有任何 null 值存在,或這些值與結果無關時。 請考慮下列範例:

var query1 = context.Entities.Where(e => e.String1 != e.String2 || e.String1.Length == e.String2.Length);
var query2 = context.Entities.Where(
    e => e.String1 != null && e.String2 != null && (e.String1 != e.String2 || e.String1.Length == e.String2.Length));

這些查詢會產生下列 SQL:

SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2]
FROM [Entities] AS [e]
WHERE ((([e].[String1] <> [e].[String2]) OR ([e].[String1] IS NULL OR [e].[String2] IS NULL)) AND ([e].[String1] IS NOT NULL OR [e].[String2] IS NOT NULL)) OR ((CAST(LEN([e].[String1]) AS int) = CAST(LEN([e].[String2]) AS int)) OR ([e].[String1] IS NULL AND [e].[String2] IS NULL))

SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2]
FROM [Entities] AS [e]
WHERE ([e].[String1] IS NOT NULL AND [e].[String2] IS NOT NULL) AND (([e].[String1] <> [e].[String2]) OR (CAST(LEN([e].[String1]) AS int) = CAST(LEN([e].[String2]) AS int)))

在第二個查詢中, null 結果會明確地從 String1 資料行篩選掉。 EF Core 可以在比較期間安全地將資料 String1 行視為不可為 Null,因而產生更簡單的查詢。

使用關聯式 Null 語意

可以停用 Null 比較補償,並直接使用關聯式 Null 語意。 這可以透過在 方法內的選項產生器上 OnConfiguring 呼叫 UseRelationalNulls(true) 方法來完成:

new SqlServerDbContextOptionsBuilder(optionsBuilder).UseRelationalNulls();

警告

使用關聯式 Null 語意時,您的 LINQ 查詢不再具有與 C# 相同的意義,而且可能會產生與預期不同的結果。 使用此模式時請小心。