효율적인 쿼리

효율적으로 쿼리하는 것은 인덱스, 관련 엔터티 로드 전략 등 광범위한 주제를 다루는 방대한 주제입니다. 이 섹션에서는 쿼리를 더 빠르게 만들기 위한 몇 가지 일반적인 테마와 사용자가 일반적으로 발생하는 문제를 자세히 설명합니다.

인덱스 제대로 사용

쿼리가 빠르게 실행되는지 여부의 주요 결정 요소는 적절한 경우 인덱스를 제대로 활용할지 여부입니다. 데이터베이스는 일반적으로 대량의 데이터를 보유하는 데 사용되며 전체 테이블을 트래버스하는 쿼리는 일반적으로 심각한 성능 문제의 원본입니다. 지정된 쿼리에서 인덱스 사용 여부를 즉시 알 수 없으므로 인덱싱 문제를 쉽게 파악할 수 없습니다. 예시:

// Matches on start, so uses an index (on SQL Server)
var posts1 = context.Posts.Where(p => p.Title.StartsWith("A")).ToList();
// Matches on end, so does not use the index
var posts2 = context.Posts.Where(p => p.Title.EndsWith("A")).ToList();

인덱싱 문제를 파악하는 좋은 방법은 먼저 느린 쿼리를 정확히 파악한 다음 데이터베이스에서 가장 좋아하는 도구를 통해 쿼리 계획을 검사하는 것입니다. 이 작업을 수행하는 방법에 대한 자세한 내용은 성능 진단 페이지를 참조하세요. 쿼리 계획은 쿼리가 전체 테이블을 트래버스하는지 아니면 인덱스를 사용하는지 여부를 표시합니다.

일반적으로 인덱스를 사용하거나 이와 관련된 성능 문제를 진단하는 특별한 EF 지식은 없습니다. 인덱스와 관련된 일반 데이터베이스 지식은 EF를 사용하지 않는 애플리케이션과 마찬가지로 EF 애플리케이션과 관련이 있습니다. 다음은 인덱스를 사용할 때 유의해야 할 몇 가지 일반적인 지침을 나열합니다.

  • 인덱스는 쿼리 속도를 높일 수 있지만 업데이트를 최신 상태로 유지해야 하므로 업데이트 속도가 느려집니다. 필요하지 않은 인덱스를 정의하지 말고 인덱스 필터를 사용하여 인덱스를 행의 하위 집합으로 제한하여 이 오버헤드를 줄이는 것이 좋습니다.
  • 복합 인덱스는 여러 열을 필터링하는 쿼리 속도를 높일 수 있지만 순서에 따라 모든 인덱스의 열을 필터링하지 않는 쿼리 속도를 높일 수도 있습니다. 예를 들어 A 열과 B 열의 인덱스는 A 및 B에 의한 쿼리 필터링과 A로만 필터링하는 쿼리의 속도를 향상시키지만 B를 통해서만 쿼리 필터링 속도를 높일 수는 없습니다.
  • 쿼리가 열을 통해 식을 기준으로 필터링하는 경우(예: price / 2) 간단한 인덱스를 사용할 수 없습니다. 그러나 식에 대해 저장된 지속형 열을 정의하고 이를 통해 인덱싱을 만들 수 있습니다. 일부 데이터베이스는 식 인덱스를 지원하며, 식별로 쿼리 필터링 속도를 높이기 위해 직접 사용할 수 있습니다.
  • 다양한 데이터베이스를 사용하면 인덱스를 다양한 방식으로 구성할 수 있으며, 대부분의 경우 EF Core 공급자는 Fluent API를 통해 이러한 인덱스를 노출합니다. 예를 들어 SQL Server 공급자를 사용하면 인덱스가 클러스터되어 있는지 여부를 구성하거나 채우기 인수를 설정할 수 있습니다. 자세한 내용은 데이터 공급자의 설명서를 확인하십시오.

필요한 프로젝트 전용 속성

EF Core를 사용하면 엔터티 인스턴스를 쉽게 쿼리한 다음 코드에서 해당 인스턴스를 사용할 수 있습니다. 그러나 엔터티 인스턴스를 쿼리하면 데이터베이스에서 필요한 것보다 더 많은 데이터를 끌어올 수 있습니다. 다음을 고려하십시오.

foreach (var blog in context.Blogs)
{
    Console.WriteLine("Blog: " + blog.Url);
}

이 코드에는 실제로 각 블로그의 Url 속성만 필요하지만 전체 블로그 엔터티가 페치되고 불필요한 열이 데이터베이스에서 전송됩니다.

SELECT [b].[BlogId], [b].[CreationDate], [b].[Name], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]

Select를 사용하여 프로젝션할 열을 EF에 알려서 최적화할 수 있습니다.

foreach (var blogName in context.Blogs.Select(b => b.Url))
{
    Console.WriteLine("Blog: " + blogName);
}

결과 SQL은 필요한 열만 다시 가져옵니다.

SELECT [b].[Url]
FROM [Blogs] AS [b]

둘 이상의 열을 프로젝트해야 하는 경우 원하는 속성을 사용하여 C# 익명 형식으로 프로젝트합니다.

이 기술은 읽기 전용 쿼리에 매우 유용하지만 EF의 변경 내용 추적은 엔터티 인스턴스에서만 작동하므로 가져온 블로그를 업데이트해야 하는 경우 상황이 더 복잡해집니다. 수정된 블로그 인스턴스를 연결하고 변경된 속성을 EF에 알려 전체 엔터티를 로드하지 않고 업데이트를 수행할 수 있지만, 이는 가치가 없을 수 있는 고급 기술입니다.

결과 집합 크기 제한

기본적으로 쿼리는 필터와 일치하는 모든 행을 반환합니다.

var blogsAll = context.Posts
    .Where(p => p.Title.StartsWith("A"))
    .ToList();

반환되는 행 수는 데이터베이스의 실제 데이터에 따라 달라지므로 데이터베이스에서 로드되는 데이터의 양, 결과에 의해 얼마나 많은 메모리가 차지되는지, 이러한 결과를 처리할 때 생성되는 추가 부하(예: 네트워크를 통해 사용자 브라우저로 전송)를 알 수 없습니다. 결정적으로 테스트 데이터베이스에는 데이터가 거의 없으므로 테스트하는 동안 모든 것이 잘 작동하지만 쿼리가 실제 데이터에서 실행되기 시작하고 많은 행이 반환되면 성능 문제가 갑자기 나타납니다.

결과적으로 일반적으로 결과 수를 제한하는 생각을 할 가치가 있습니다.

var blogs25 = context.Posts
    .Where(p => p.Title.StartsWith("A"))
    .Take(25)
    .ToList();

최소한 UI는 데이터베이스에 더 많은 행이 있을 수 있음을 나타내는 메시지를 표시할 수 있습니다(그리고 다른 방식으로 검색할 수 있음). 본격적인 솔루션은 페이지 매김을 구현합니다. 여기서 UI는 한 번에 특정 수의 행만 표시하고 사용자가 필요에 따라 다음 페이지로 진행할 수 있도록 합니다. 이 작업을 효율적으로 구현하는 방법에 대한 자세한 내용은 다음 섹션을 참조하세요.

효율적인 페이지 매김

페이지 매김은 결과를 한 번에 검색하는 것이 아니라 페이지로 검색하는 것을 의미합니다. 이는 일반적으로 사용자가 결과의 다음 또는 이전 페이지로 이동할 수 있는 사용자 인터페이스가 표시되는 큰 결과 집합에 대해 수행됩니다. 데이터베이스를 사용하여 페이지 매김을 구현하는 일반적인 방법은 SkipTake 연산자(SQL의 OFFSETLIMIT)를 사용하는 것입니다. 직관적인 구현이지만 매우 비효율적입니다. 한 번에 한 페이지를 이동할 수 있는 페이지 매김의 경우(임의의 페이지로 이동하지 않고) 대신 키 집합 페이지 매김을 사용하는 것이 좋습니다.

자세한 내용은 페이지 매김에 대한 설명서 페이지를 참조하세요.

관계형 데이터베이스에서 모든 관련 엔터티는 단일 쿼리에 조인을 도입하여 로드됩니다.

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Post] AS [p] ON [b].[BlogId] = [p].[BlogId]
ORDER BY [b].[BlogId], [p].[PostId]

일반적인 블로그에 여러 관련 게시물이 있는 경우 이러한 게시물들의 행은 블로그의 정보를 중복합니다. 이 중복은 이른바 "데카르트 급증" 문제를 일으킵니다. 로드되는 일대다 관계가 늘어나면 중복된 데이터 양이 증가하여 애플리케이션의 성능에 부정적인 영향을 줄 수 있습니다.

EF를 사용하면 별도의 쿼리를 통해 관련 엔터티를 로드하는 "분할 쿼리"를 사용하여 이 효과를 방지할 수 있습니다. 자세한 내용은 분할 및 단일 쿼리에 대한 설명서를 읽어보세요.

참고 항목

분할 쿼리의 현재 구현은 각 쿼리에 대한 왕복을 실행합니다. 향후 이를 개선하고 모든 쿼리를 단일 왕복으로 실행할 계획입니다.

이 섹션을 계속하기 전에 관련 엔터티에 대한 전용 페이지를 읽는 것이 좋습니다.

관련 엔터티를 처리할 때 일반적으로 로드해야 하는 사항을 미리 알고 있습니다. 일반적인 예는 모든 게시물과 함께 특정 블로그 집합을 로드하는 것입니다. 이러한 시나리오에서는 EF가 필요한 모든 데이터를 한 라운드트립으로 가져올 수 있도록 항상 즉시 로드를 사용하는 것이 좋습니다. 필터링된 포함 기능을 사용하면 로드할 관련 엔터티를 제한하면서 즉시 로드 프로세스를 유지하므로 단일 왕복에서 수행할 수 있습니다.

using (var context = new BloggingContext())
{
    var filteredBlogs = context.Blogs
        .Include(
            blog => blog.Posts
                .Where(post => post.BlogId == 1)
                .OrderByDescending(post => post.Title)
                .Take(5))
        .ToList();
}

다른 시나리오에서는 주 엔터티를 가져오기 전에 필요한 관련 엔터티를 알 수 없습니다. 예를 들어 블로그를 로드할 때 해당 블로그의 게시물에 관심이 있는지 여부를 알기 위해 다른 데이터 원본(웹 서비스)을 참조해야 할 수 있습니다. 이러한 경우 명시적 또는 지연 로드를 사용하여 관련 엔터티를 별도로 가져오고 블로그의 게시물 탐색을 채울 수 있습니다. 이러한 메서드는 즉시 로드되는 것이 아니라 데이터베이스에 대한 추가 왕복이 필요합니다. 이는 속도 저하의 근원입니다. 특정 시나리오에 따라 추가 왕복을 실행하고 필요한 게시물만 선택적으로 가져오는 대신 항상 모든 게시물을 로드하는 것이 더 효율적일 수 있습니다.

지연 로드 주의

지연 로드는 EF Core가 코드에서 액세스할 때 데이터베이스에서 관련 엔터티를 자동으로 로드하기 때문에 데이터베이스 논리를 작성하는 매우 유용한 방법처럼 보입니다. 이렇게 하면 필요하지 않은 관련 엔터티(예: 명시적 로드)가 로드되지 않으며 프로그래머가 관련 엔터티를 모두 처리할 필요가 없는 것처럼 보입니다. 그러나 지연 로드는 특히 애플리케이션을 느리게 할 수 있는 불필요한 추가 왕복을 생성하는 경향이 있습니다.

다음을 고려하십시오.

foreach (var blog in context.Blogs.ToList())
{
    foreach (var post in blog.Posts)
    {
        Console.WriteLine($"Blog {blog.Url}, Post: {post.Title}");
    }
}

무고해 보이는 이 코드 조각은 모든 블로그와 게시물을 반복하여 인쇄합니다. EF Core의 문 로깅을 켜면 다음이 표시됩니다.

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [b].[BlogId], [b].[Rating], [b].[Url]
      FROM [Blogs] AS [b]
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (5ms) [Parameters=[@__p_0='1'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title]
      FROM [Post] AS [p]
      WHERE [p].[BlogId] = @__p_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[@__p_0='2'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title]
      FROM [Post] AS [p]
      WHERE [p].[BlogId] = @__p_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[@__p_0='3'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title]
      FROM [Post] AS [p]
      WHERE [p].[BlogId] = @__p_0

... and so on

무슨 일이 일어나고 있는 건가요? 위의 단순 루프에 대해 이러한 모든 쿼리가 전송되는 이유는 무엇인가요? 지연 로드를 사용하면 게시물 속성에 액세스할 때만 블로그 게시물이 로드됩니다. 결과적으로 내부 foreach의 각 반복은 자체 왕복에서 추가 데이터베이스 쿼리를 트리거합니다. 따라서 초기 쿼리가 모든 블로그를 로드한 후 블로그당 다른 쿼리가 생성되어 모든 게시물이 로드됩니다. 이를 N+1 문제라고도 하며 매우 중요한 성능 문제를 일으킬 수 있습니다.

블로그의 모든 게시물이 필요하다고 가정하면 여기에 즉시 로드하는 것이 좋습니다. Include 연산자를 사용하여 로드를 수행할 수 있지만 블로그의 URL만 필요하므로 필요한 URL만 로드해야 합니다. 따라서 프로젝션을 대신 사용합니다.

foreach (var blog in context.Blogs.Select(b => new { b.Url, b.Posts }).ToList())
{
    foreach (var post in blog.Posts)
    {
        Console.WriteLine($"Blog {blog.Url}, Post: {post.Title}");
    }
}

이렇게 하면 EF Core가 게시물과 함께 모든 블로그를 단일 쿼리로 가져옵니다. 경우에 따라 분할 쿼리를 사용하여 카티시안 폭발 효과를 방지하는 것이 유용할 수도 있습니다.

Warning

지연 로드를 사용하면 실수로 N+1 문제를 트리거하는 것이 매우 쉽기 때문에 이를 방지하는 것이 좋습니다. 즉시 또는 명시적 로드는 데이터베이스 왕복이 발생할 때 소스 코드에서 매우 명확하게 합니다.

버퍼링 및 스트리밍

버퍼링은 모든 쿼리 결과를 메모리에 로드하는 것을 의미하지만 스트리밍은 EF가 매번 단일 결과를 애플리케이션에 전달하는 반면, 메모리에 전체 결과 집합을 포함하지 않습니다. 원칙적 스트리밍 쿼리의 메모리 요구 사항은 고정됩니다. 쿼리가 1행 또는 1000을 반환하는지 여부에 관계없이 동일합니다. 반면 버퍼링 쿼리는 더 많은 행이 반환되면 더 많은 메모리가 필요합니다. 큰 결과 집합을 생성하는 쿼리의 경우 이는 중요한 성능 요소일 수 있습니다.

쿼리 버퍼 또는 스트림은 평가 방법에 따라 달라집니다.

// ToList and ToArray cause the entire resultset to be buffered:
var blogsList = context.Posts.Where(p => p.Title.StartsWith("A")).ToList();
var blogsArray = context.Posts.Where(p => p.Title.StartsWith("A")).ToArray();

// Foreach streams, processing one row at a time:
foreach (var blog in context.Posts.Where(p => p.Title.StartsWith("A")))
{
    // ...
}

// AsEnumerable also streams, allowing you to execute LINQ operators on the client-side:
var doubleFilteredBlogs = context.Posts
    .Where(p => p.Title.StartsWith("A")) // Translated to SQL and executed in the database
    .AsEnumerable()
    .Where(p => SomeDotNetMethod(p)); // Executed at the client on all database results

쿼리가 몇 가지 결과만 반환하는 경우 이에 대해 걱정할 필요가 없습니다. 그러나 쿼리가 많은 수의 행을 반환할 수 있는 경우 버퍼링 대신 스트리밍에 대한 생각을 제공하는 것이 좋습니다.

참고 항목

결과에 다른 LINQ 연산자를 사용하려는 경우 ToList 또는 ToArray를 사용하지 마세요. 이 경우 모든 결과를 메모리에 버퍼링할 필요가 없습니다. 대신 AsEnumerable을 사용합니다.

EF의 내부 버퍼링

특정 상황에서 EF는 쿼리를 평가하는 방법에 관계없이 결과 집합을 내부적으로 버퍼링합니다. 이 경우 다음과 같은 두 가지가 있습니다.

  • 재시도 실행 전략이 있는 경우 이 작업은 나중에 쿼리를 다시 시도하는 경우 동일한 결과가 반환되도록 하기 위해 수행됩니다.
  • 분할 쿼리를 사용하면 MARS(다중 활성 결과 집합)가 SQL Server 활성화되지 않는 한 마지막 쿼리를 제외한 모든 결과 집합이 버퍼링됩니다. 일반적으로 여러 쿼리 결과 집합을 동시에 활성화하는 것은 불가능하기 때문입니다.

이 내부 버퍼링은 LINQ 연산자를 통해 발생하는 버퍼링 외에도 발생합니다. 예를 들어 쿼리에서 ToList를 사용하고 다시 시도 실행 전략이 있는 경우 결과 집합은 EF에서 내부적으로 한 번, ToList에 의해 한 번 메모리에 두 번 로드됩니다.

추적, 추적 없음 및 ID 확인

이 섹션을 계속하기 전에 추적 및 추적 없음에 대한 전용 페이지를 읽는 것이 좋습니다.

EF는 기본적으로 엔터티 인스턴스를 추적하므로 SaveChanges가 호출될 때 엔터티 인스턴스의 변경 내용이 검색되고 유지됩니다. 쿼리 추적의 또 다른 효과는 EF가 데이터에 대해 인스턴스가 이미 로드되었는지 감지하고 새 인스턴스를 반환하는 대신 추적된 인스턴스를 자동으로 반환한다는 것입니다. 이를 ID 확인이라고 합니다. 성능 관점에서 변경 내용 추적은 다음을 의미합니다.

  • EF는 내부적으로 추적된 인스턴스의 사전을 유지 관리합니다. 새 데이터가 로드되면 EF는 사전을 확인하여 인스턴스가 해당 엔터티의 키(ID 확인)에 대해 이미 추적되었는지 확인합니다. 사전 유지 관리 및 조회는 쿼리 결과를 로드할 때 다소 시간이 소요됩니다.
  • 로드된 인스턴스를 애플리케이션에 전달하기 전에 EF는 해당 인스턴스를 스냅샷으로 만들고 스냅샷을 내부적으로 유지합니다. SaveChanges가 호출되면 애플리케이션의 인스턴스를 스냅샷과 비교하여 유지할 변경 내용을 검색합니다. 스냅샷은 더 많은 메모리를 차지하며 스냅샷 프로세스 자체는 시간이 걸립니다. 값 비교자를 통해 다른 보다 효율적인 스냅샷 동작을 지정하거나 변경 추적 프록시를 사용하여 스냅샷 프로세스를 완전히 우회할 수 있습니다(자체 단점 집합과 함께 제공됨).

변경 내용이 데이터베이스에 다시 저장되지 않는 읽기 전용 시나리오에서는 추적 금지 쿼리를 사용하여 위의 오버헤드를 방지할 수 있습니다. 그러나 추적 없음 쿼리는 ID 확인을 수행하지 않으므로 로드된 여러 행에서 참조하는 데이터베이스 행이 다른 인스턴스로 구체화됩니다.

설명하기 위해 데이터베이스에서 많은 수의 게시물과 각 게시물에서 참조하는 블로그를 로드하고 있다고 가정합니다. 100개의 게시물이 동일한 블로그를 참조하는 경우 추적 쿼리는 ID 확인을 통해 이를 검색하고 모든 Post 인스턴스는 중복되지 않은 동일한 블로그 인스턴스를 참조합니다. 반면 추적 없음 쿼리는 동일한 블로그를 100번 복제하며 애플리케이션 코드를 그에 따라 작성해야 합니다.

다음은 각 게시물이 20개인 10개의 블로그를 로드하는 쿼리에 대한 추적 및 추적 없음 동작을 비교하는 벤치마크 결과입니다. 소스 코드는 여기에서 사용할 수 있으며, 사용자 고유의 측정을 위한 기준으로 자유롭게 사용할 수 있습니다.

메서드 NumBlogs NumPostsPerBlog 평균 오류 StdDev 중앙값 비율 RatioSD Gen 0 Gen 1 Gen 2 Allocated
AsTracking 10 20 1,414.7 us 27.20 us 45.44 us 1,405.5 1.00 0.00 60.5469 13.6719 - 380.11KB
AsNoTracking 10 20 993.3 us 24.04 us 65.40 us 966.2 us 0.71 0.05 37.1094 6.8359 - 232.89KB

마지막으로 추적 없음 쿼리를 활용한 다음 반환된 인스턴스를 컨텍스트에 연결하여 변경 내용 추적의 오버헤드 없이 업데이트를 수행할 수 있습니다. 이렇게 하면 변경 내용 추적의 부담이 EF에서 사용자에게 전송되며, 프로파일링 또는 벤치마킹을 통해 변경 내용 추적 오버헤드가 허용되지 않는 것으로 표시된 경우에만 시도해야 합니다.

SQL 쿼리 사용

경우에 따라 EF가 생성하지 않는 쿼리에 대해 더 최적화된 SQL이 존재합니다. 이는 SQL 구문이 지원되지 않는 데이터베이스와 관련된 확장이거나 EF가 아직 로 변환되지 않았기 때문에 발생할 수 있습니다. 이러한 경우 직접 SQL을 작성하면 상당한 성능 향상을 제공할 수 있으며 EF는 이 작업을 수행하는 여러 가지 방법을 지원합니다.

  • 쿼리에서 직접 SQL 쿼리를 사용합니다(예: FromSqlRaw를 통해). EF를 사용하면 일반 LINQ 쿼리를 사용하여 SQL을 통해 작성할 수 있으므로 SQL에서 쿼리의 일부만 표현할 수 있습니다. 이는 SQL을 코드베이스의 단일 쿼리에서만 사용해야 하는 경우에 유용한 기술입니다.
  • UDF(사용자 정의 함수)를 정의한 다음 쿼리에서 호출합니다. EF를 사용하면 UDF에서 TVF(테이블 반환 함수)라고 하는 전체 결과 집합을 반환할 수 있으며 DbSet를 함수에 매핑하여 다른 테이블처럼 보이게 할 수도 있습니다.
  • 데이터베이스 뷰를 정의하고 쿼리에서 쿼리합니다. 함수와 달리 뷰는 매개 변수를 허용할 수 없습니다.

참고 항목

원시 SQL은 일반적으로 EF가 원하는 SQL을 생성할 수 없는지 확인하고 지정된 쿼리가 이를 정당화할 만큼 성능이 중요한 경우 최후의 수단으로 사용해야 합니다. 원시 SQL을 사용하면 상당한 유지 관리 단점이 있습니다.

비동기 프로그래밍

일반적으로 애플리케이션을 확장 가능하도록 하려면 동기 API(예: SaveChanges 대신 SaveChangesAsync)가 아닌 항상 비동기 API를 사용하는 것이 중요합니다. 동기 API는 데이터베이스 I/O 기간 동안 스레드를 차단하여 스레드에 대한 필요성과 발생해야 하는 스레드 컨텍스트 스위치의 수를 증가합니다.

자세한 내용은 비동기 프로그래밍 페이지를 참조하세요.

Warning

동일한 애플리케이션에서 동기 및 비동기 코드를 혼합하지 마세요. 미묘한 스레드 풀 고갈 문제를 실수로 트리거하는 것은 매우 쉽습니다.

Warning

Microsoft.Data.SqlClient의 비동기 구현에는 알려진 이슈(예:#593, #601등)가 있습니다. 예기치 않은 성능 문제가 발생하는 경우 특히 큰 텍스트 또는 이진 값을 처리할 때 동기화 명령 실행을 대신 사용해 보세요.

추가 리소스

  • 효율적인 쿼리와 관련된 추가 항목은 고급 성능 항목 페이지를 참조하세요.
  • nullable 값을 비교할 때 몇 가지 모범 사례는 null 비교 설명서 페이지의 성능 섹션을 참조하세요.