Null 의미체계를 쿼리합니다

소개

SQL 데이터베이스는 C#의 부울 논리와는 달리 비교를 수행할 때 3값 논리(true, false, null)에서 작동합니다. 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 검사가 필요하지 않습니다. 두 번째 쿼리에서 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]

세 번째 쿼리는 null 검사를 도입합니다. NullableIntnull일 때 비교 Id <> NullableIntnull를 생성하며, 이는 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는 두 열이 모두 nullable일 때 패턴을 표시합니다. <> 작업이 == 작업보다 더 복잡하고 잠재적으로 느린 쿼리를 생성한다는 점을 주목할 가치가 있습니다.

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)

함수에서 nullable 값 처리

SQL의 많은 함수는 일부 인수가 있는 null 경우에만 결과를 반환할 수 있습니다 null. EF Core는 이를 활용하여 보다 효율적인 쿼리를 생성합니다. 아래 쿼리는 최적화를 보여 줍니다.

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

생성된 SQL은 다음과 같습니다(함수의 인수 중 하나가 null일 경우, 함수 자체도 null이 되므로 SUBSTRING 함수를 평가할 필요가 없습니다.)

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 허용 열을 비교하는 것보다 더 간단하고 빠릅니다. 가능하면 열을 non-nullable로 표시하는 것이 좋습니다.

  • 같음(==)을 확인하는 것은 쿼리가 결과를 구분 !=null 할 필요가 없으므로 비같음(false)을 확인하는 것보다 더 간단하고 빠릅니다. 가능하면 동일성 비교를 사용합니다. 그러나 단순히 비교를 부정하는 == 것은 사실상 !=과 같아 성능이 향상되지 않습니다.

  • 일부 경우에는 열에서 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)))

두 번째 쿼리에서 String1 열에서 null 결과가 명시적으로 필터링됩니다. EF Core는 String1 비교하는 동안 열을 null을 허용하지 않는 것으로 안전하게 처리하여 더 간단한 쿼리를 생성할 수 있습니다.

관계형 null 의미 체계 사용

null 비교 보정을 사용하지 않도록 설정하고 관계형 null 의미 체계를 직접 사용할 수 있습니다. 이 작업은 메서드 내 UseRelationalNulls(true) 의 옵션 작성기에서 메서드를 호출 OnConfiguring 하여 수행할 수 있습니다.

new SqlServerDbContextOptionsBuilder(optionsBuilder).UseRelationalNulls();

경고

관계형 null 의미 체계를 사용하는 경우 LINQ 쿼리는 더 이상 C#에서와 동일한 의미를 갖지 않으며 예상과 다른 결과를 생성할 수 있습니다. 이 모드를 사용할 때는 주의해야 합니다.