다음을 통해 공유


null 의미 체계 쿼리

소개

SQL 데이터베이스는 C#의 부울 논리와 다르게 비교를 수행할 때 값이 세 개인 논리(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

두 열이 모두 null을 허용하는 경우 쿼리 4 및 5는 패턴을 보여 줍니다. <> 작업은 == 작업보다 더 복잡하며 더 느릴 수 있는 쿼리를 생성한다는 점에 유의해야 합니다.

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은 다음과 같습니다(인수 중 하나가 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 허용 열을 비교하는 것보다 더 간단하고 빠릅니다. 가능하면 항상 열을 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#과 동일한 의미를 갖지 않으며 예상과 다른 결과를 생성할 수 있습니다. 해당 모드를 사용하는 경우 주의해야 합니다.