クエリの null セマンティクス

はじめに

SQL データベースで比較が実行される場合、その動作は、C# のブール型ロジックではなく、3 値論理 (truefalsenull) に基づいたものとなります。 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);

最初の 2 つのクエリによって行われるのは、シンプルな比較です。 最初のクエリでは、両方の列が null 非許容であるため、null チェックは必要ありません。 2 番目のクエリでは、NullableIntnull を含めることはできますが、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]

3 番目のクエリには、null チェックが導入されています。 NullableIntnull である場合、比較 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

4 番目と 5 番目のクエリで示されているのは、両方の列が 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 許容値の処理

引数の一部が null である場合、SQL の関数の多くで 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)))

2 番目のクエリでは、null の結果が String1 列から明示的に除外されています。 EF Core により、比較中は String1 列が null 非許容として確実に処理されるので、クエリが簡略化されます。

リレーショナル null セマンティクスの使用

null 値の比較補正を無効にして、リレーショナル null セマンティクスを直接使用することができます。 これを行うには、OnConfiguring メソッド内のオプション ビルダー上で UseRelationalNulls(true) メソッドを呼び出します。

new SqlServerDbContextOptionsBuilder(optionsBuilder).UseRelationalNulls();

警告

リレーショナル null セマンティクスを使用すると、LINQ クエリは C# の場合と同じ意味を持たなくなり、予想とは異なる結果が生成される可能性があります。 このモードを使用する際は注意が必要です。