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 검사가 필요하지 않습니다. 두 번째 쿼리에서는 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 검사를 도입합니다. NullableInt
가 null
인 경우 비교 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을 허용하는 경우 쿼리 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을 허용하지 않음으로 표시하는 것이 좋습니다.
쿼리가
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)))
두 번째 쿼리에서는 null
결과가 String1
열에서 명시적으로 필터링됩니다. EF Core는 비교 중에 String1
열을 null을 허용하지 않음으로 안전하게 처리할 수 있으므로 더 간단한 쿼리가 생성됩니다.
관계형 null 의미 체계 사용
null 비교 보정을 사용하지 않고 관계형 null 의미 체계를 직접 사용할 수 있습니다. OnConfiguring
메서드 내의 옵션 작성기에서 UseRelationalNulls(true)
메서드를 호출하여 해당 작업을 수행할 수 있습니다.
new SqlServerDbContextOptionsBuilder(optionsBuilder).UseRelationalNulls();
경고
관계형 null 의미 체계를 사용하는 경우 LINQ 쿼리는 더 이상 C#과 동일한 의미를 갖지 않으며 예상과 다른 결과를 생성할 수 있습니다. 해당 모드를 사용하는 경우 주의해야 합니다.
.NET