EF Core Azure Cosmos DB 공급자를 사용하여 쿼리

쿼리 기본 사항

EF Core LINQ 쿼리는 다른 데이터베이스 공급자와 동일한 방식으로 Azure Cosmos DB에 대해 실행될 수 있습니다. 예시:

public class Session
{
    public Guid Id { get; set; }
    public string Category { get; set; }

    public string TenantId { get; set; } = null!;
    public Guid UserId { get; set; }
    public int SessionId { get; set; }
}

var stringResults = await context.Sessions
    .Where(
        e => e.Category.Length > 4
            && e.Category.Trim().ToLower() != "disabled"
            && e.Category.TrimStart().Substring(2, 2).Equals("xy", StringComparison.OrdinalIgnoreCase))
    .ToListAsync();

참고 항목

Azure Cosmos DB 공급자는 다른 공급자와 동일한 LINQ 쿼리 집합을 변환하지 않습니다. 예를 들어 문서 간 쿼리는 데이터베이스에서 지원되지 않으므로 EF Include() 연산자는 Cosmos에서 지원되지 않습니다.

파티션 키

분할의 이점은 관련 데이터가 있는 파티션에 대해서만 쿼리를 실행하여 비용을 절감하고 더 빠른 결과 속도를 보장하는 것입니다. 파티션 키를 지정하지 않는 쿼리는 모든 파티션에서 실행되며 비용이 많이 들 수 있습니다.

EF 9.0부터 EF는 LINQ 쿼리의 Where 연산자에서 파티션 키 비교를 자동으로 검색하고 추출합니다. 계층적 파티션 키로 구성된 Session 엔터티 형식에 대해 다음 쿼리를 실행합니다.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Session>()
        .HasPartitionKey(b => new { b.TenantId, b.UserId, b.SessionId })
}

var tenantId = "Microsoft";
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");
var username = "scott";

var sessions = await context.Sessions
    .Where(
        e => e.TenantId == tenantId
             && e.UserId == userId
             && e.SessionId > 0
             && e.Username == username)
    .ToListAsync();

EF에서 생성된 로그를 검사하면 이 쿼리가 다음과 같이 실행되는 것을 볼 수 있습니다.

Executed ReadNext (166.6985 ms, 2.8 RU) ActivityId='312da0d2-095c-4e73-afab-27072b5ad33c', Container='test', Partition='["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c"]', Parameters=[]
SELECT VALUE c
FROM root c
WHERE ((c["SessionId"] > 0) AND CONTAINS(c["Username"], "a"))

이러한 로그에서 다음을 확인할 수 있습니다.

  • 처음 두 비교(TenantIdUserId)가 해제되어 WHERE 절이 아닌 ReadNext "파티션"에 표시됩니다. 즉, 쿼리가 해당 값의 하위 파티션에서만 실행됩니다.
  • SessionId 또한 계층적 파티션 키의 일부이지만 같음 비교 대신 보다 큰 연산자(>)를 사용하므로 해제할 수 없습니다. 일반 속성과 마찬가지로 WHERE 절의 일부입니다.
  • Username은 파티션 키의 일부가 아닌 일반 속성이므로 WHERE 절에도 남아 있습니다.

일부 파티션 키 값이 제공되지 않더라도 계층적 파티션 키를 사용하면 여전히 처음 두 속성에 해당하는 하위 파티션만 대상으로 지정할 수 있습니다. 이는 단일 파티션을 대상으로 지정하는 것만큼 효율적이지는 않지만(세 가지 속성 모두에 의해 식별됨) 모든 파티션을 대상으로 지정하는 것보다 훨씬 효율적입니다.

Where 연산자의 파티션 키 속성을 참조하는 대신 WithPartitionKey 연산자를 사용하여 명시적으로 지정할 수 있습니다.

var sessions = await context.Sessions
    .WithPartitionKey(tenantId, userId)
    .Where(e => e.SessionId > 0 && e.Username.Contains("a"))
    .ToListAsync();

이는 위의 쿼리와 동일한 방식으로 실행되며, 쿼리에서 파티션 키를 보다 명시적으로 지정하려는 경우 더 바람직할 수 있습니다. 9.0 이전의 EF 버전에서는 WithPartitionKey를 사용해야 할 수 있습니다. 로그를 주시하여 쿼리가 예상대로 파티션 키를 사용하고 있는지 확인합니다.

지점 읽기

Azure Cosmos DB는 SQL을 통한 강력한 쿼리를 허용하지만 이러한 쿼리는 비용이 많이 들 수 있습니다. 또한 Cosmos DB는 id 속성과 전체 파티션 키를 모두 알 수 있을 때 사용할 수 있는 지점 읽기를 지원합니다. 이러한 지점 읽기는 특정 파티션의 특정 문서를 직접 식별하고 매우 효율적이며 비용을 절감하면서 실행할 수 있습니다. 가능한 한 포인트 읽기를 최대한 활용하는 방식으로 시스템을 설계하는 것이 좋습니다. 자세한 내용은 Cosmos DB 설명서를 참조하세요.

이전 섹션에서는 EF가 보다 효율적인 쿼리를 위해 Where 절에서 파티션 키 비교를 식별하고 추출하여 처리를 관련 파티션으로만 제한하는 것을 살펴보았습니다. 한 단계 더 나아가 쿼리에 id 속성도 제공할 수 있습니다. 다음 쿼리를 살펴보겠습니다.

var session = await context.Sessions.SingleAsync(
    e => e.Id == someId
         && e.TenantId == tenantId
         && e.UserId == userId
         && e.SessionId == sessionId);

이 쿼리에서는 Id 속성 값(Cosmos DB id 속성에 매핑됨)뿐만 아니라 모든 파티션 키 속성에 대한 값도 제공됩니다. 또한 쿼리에 대한 추가 구성 요소는 없습니다. 이러한 모든 조건이 충족되면 EF는 쿼리를 지점 읽기로 실행할 수 있습니다.

Executed ReadItem (46 ms, 1 RU) ActivityId='d7391311-2266-4811-ae2d-535904c42c43', Container='test', Id='9', Partition='["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",10.0]'

쿼리가 효율적인 지점 읽기로 실행되었음을 나타내는 ReadItem을 확인합니다. SQL 쿼리는 관련되어 있지 않습니다.

파티션 키 추출과 마찬가지로 EF 9.0에서 이 메커니즘을 크게 개선했습니다. 이전 버전은 지점 읽기를 안정적으로 검색하고 사용하지 않습니다.

페이지 매김

참고 항목

이 기능은 EF Core 9.0에서 도입되었으며 실험적입니다. 얼마나 유용했는지, 피드백이 있는 경우 알려주세요.

페이지 매김은 결과를 한 번에 검색하지 않고 페이지 단위로 검색하는 것을 말하며, 일반적으로 사용자 인터페이스가 표시되어 사용자가 결과의 페이지를 탐색할 수 있는 대규모 결과 집합에 대해 수행됩니다.

데이터베이스로 페이지 매김을 구현하는 일반적인 방법은 SkipTake LINQ 연산자(SQL의 OFFSETLIMIT)를 사용하는 것입니다. 페이지 크기의 결과가 10개인 경우 다음과 같이 세 번째 페이지를 EF Core로 가져올 수 있습니다.

var position = 20;
var nextPage = context.Session
    .OrderBy(s => s.Id)
    .Skip(position)
    .Take(10)
    .ToList();

아쉽게도 이 기술은 매우 비효율적이며 쿼리 비용을 상당히 증가시킬 수 있습니다. Cosmos DB는 연속 토큰을 사용하여 쿼리 결과를 페이지 매김하기 위한 특별한 메커니즘을 제공합니다.

CosmosPage firstPage = await context.Sessions
    .OrderBy(s => s.Id)
    .ToPageAsync(pageSize: 10, continuationToken: null);

string continuationToken = firstPage.ContinuationToken;
foreach (var session in firstPage.Values)
{
    // Display/send the sessions to the user
}

LINQ 쿼리를 ToListAsync로 종료하거나 이와 유사한 방식으로 종료하는 대신, 모든 페이지에서 최대 10개의 항목(데이터베이스에 항목이 더 적을 수 있음)을 가져오도록 지시하는 ToPageAsync 메서드를 사용합니다. 첫 번째 쿼리이므로 처음부터 결과를 가져오고 null을 연속 토큰으로 전달하려고 합니다. ToPageAsync는 연속 토큰과 페이지의 값(최대 10개 항목)을 노출하는 CosmosPage를 반환합니다. 프로그램은 일반적으로 연속 토큰과 함께 해당 값을 클라이언트에 보냅니다. 이렇게 하면 나중에 쿼리를 다시 시작하고 더 많은 결과를 가져올 수 있습니다.

이제 사용자가 UI에서 "다음" 단추를 클릭하여 다음 10개 항목을 요청한다고 가정해 보겠습니다. 그러면 다음과 같이 쿼리를 실행할 수 있습니다.

CosmosPage nextPage = await context.Sessions.OrderBy(s => s.Id).ToPageAsync(10, continuationToken);
string continuationToken = nextPage.ContinuationToken;
foreach (var session in nextPage.Values)
{
    // Display/send the sessions to the user
}

동일한 쿼리를 실행하지만 이번에는 첫 번째 실행에서 받은 연속 토큰을 전달합니다. 이렇게 하면 Cosmos DB에서 중단된 쿼리를 계속하고 다음 10개 항목을 가져오도록 지시합니다. 마지막 페이지를 가져오고 더 이상 결과가 없으면 연속 토큰은 null이 되고, "다음" 단추가 회색으로 표시될 수 있습니다. 이 페이지 매김 방법은 SkipTake를 사용하는 것에 비해 매우 효율적이고 비용 효과적입니다.

Cosmos DB의 페이지 매김에 대한 자세한 내용은 이 페이지를 참조하세요.

참고 항목

Cosmos DB는 이전 페이지 매김을 지원하지 않으며 총 페이지 또는 항목 수를 제공하지 않습니다.

현재 ToPageAsync는 Cosmos 특정이 아닌 보다 일반적인 EF 페이지 매김 API로 대체될 수 있으므로 실험적이라는 주석이 추가되었습니다. 현재 API를 사용하면 컴파일 경고(EF9102)가 생성되지만 안전해야 합니다. 향후 변경 시 API 셰이프에 약간의 조정이 필요할 수 있습니다.

FindAsync

FindAsync는 기본 키로 엔터티를 가져오고 엔터티가 이미 로드되고 컨텍스트에 의해 추적될 때 데이터베이스 왕복을 방지하는 데 유용한 API입니다.

관계형 데이터베이스에 익숙한 개발자는 Id 속성 등으로 구성된 엔터티 형식의 기본 키에 익숙합니다. EF Cosmos DB 공급자를 사용하는 경우 기본 키에는 JSON id 속성에 매핑된 속성 외에도 파티션 키 속성이 포함됩니다. Cosmos DB에서 서로 다른 파티션에 동일한 JSON id 속성이 있는 문서를 포함할 수 있으므로 결합된 id와 파티션 키만이 컨테이너의 단일 문서를 고유하게 식별할 수 있습니다.

public class Session
{
    public Guid Id { get; set; }
    public string PartitionKey { get; set; }
    ...
}

var mySession = await context.FindAsync(id, pkey);

계층적 파티션 키가 있는 경우 모든 파티션 키 값을 구성된 순서대로 FindAsync에 전달해야 합니다.

참고 항목

컨텍스트에서 엔터티를 이미 추적할 수 있고 데이터베이스 왕복을 방지하려는 경우에만 FindAsync를 사용합니다. 그렇지 않으면 데이터베이스에서 엔터티를 로드해야 하는 경우 둘 사이에 성능 차이가 없는 SingleAsync를 사용하면 됩니다.

SQL 쿼리

쿼리는 SQL로 직접 작성할 수도 있습니다. 예시:

var rating = 3;
_ = await context.Blogs
    .FromSql($"SELECT VALUE c FROM root c WHERE c.Rating > {rating}")
    .ToListAsync();

이 쿼리로 인해 다음 쿼리가 실행됩니다.

SELECT VALUE s
FROM (
    SELECT VALUE c FROM root c WHERE c.Angle1 <= @p0
) s

FromSql은 EF 9.0에 도입되었습니다. 이전 버전에서는 FromSqlRaw를 대신 사용할 수 있지만, 해당 방법은 SQL 삽입 공격에 취약하다는 점에 유의하세요.

SQL 쿼리에 대한 자세한 내용은 SQL 쿼리에 대한 관계형 설명서를 참조하세요. 대부분의 콘텐츠는 Cosmos 공급자와도 관련이 있습니다.

함수 매핑

이 섹션에서는 Azure Cosmos DB 공급자로 쿼리할 때 어떤 .NET 메서드 및 멤버가 어떤 SQL 함수로 변환되는지 보여 줍니다.

날짜 및 시간 함수

.NET SQL 추가된 위치
DateTime.UtcNow GetCurrentDateTime()
DateTimeOffset.UtcNow GetCurrentDateTime()
dateTime.Year1 DateTimePart("yyyy", dateTime) EF Core 9.0
dateTimeOffset.Year1 DateTimePart("yyyy", dateTimeOffset) EF Core 9.0
dateTime.AddYears(years)1 DateTimeAdd("yyyy", dateTime) EF Core 9.0
dateTimeOffset.AddYears(years)1 DateTimeAdd("yyyy", dateTimeOffset) EF Core 9.0

1 다른 구성 요소 멤버도 번역됩니다(월, 일...).

숫자 함수

.NET SQL 추가된 위치
double.DegreesToRadians(x) RADIANS(@x) EF Core 8.0
double.RadiansToDegrees(x) DEGREES(@x) EF Core 8.0
EF.Functions.Random() RAND()
Math.Abs(value) ABS(@value)
Math.Acos(d) ACOS(@d)
Math.Asin(d) ASIN(@d)
Math.Atan(d) ATAN(@d)
Math.Atan2(y, x) ATN2(@y, @x)
Math.Ceiling(d) CEILING(@d)
Math.Cos(d) COS(@d)
Math.Exp(d) EXP(@d)
Math.Floor(d) FLOOR(@d)
Math.Log(a, newBase) LOG(@a, @newBase)
Math.Log(d) LOG(@d)
Math.Log10(d) LOG10(@d)
Math.Pow(x, y) POWER(@x, @y)
Math.Round(d) ROUND(@d)
Math.Sign(value) SIGN(@value)
Math.Sin(a) SIN(@a)
Math.Sqrt(d) SQRT(@d)
Math.Tan(a) TAN(@a)
Math.Truncate(d) TRUNC(@d)

여기에 나열된 메서드 외에도 해당 제네릭 수학 구현 및 MathF 메서드도 번역됩니다. 예를 들어 Math.Sin, MathF.Sin, double.Sin, float.Sin 모두 SQL의 SIN 함수에 매핑합니다.

문자열 함수

.NET SQL 추가된 위치
Regex.IsMatch(input, pattern) RegexMatch(@pattern, @input) EF Core 7.0
Regex.IsMatch(input, pattern, options) RegexMatch(@input, @pattern, @options) EF Core 7.0
string.Concat(str0, str1) @str0 + @str1
string.Equals(a, b, StringComparison.Ordinal) STRINGEQUALS(@a, @b)
string.Equals(a, b, StringComparison.OrdinalIgnoreCase) STRINGEQUALS(@a, @b, true)
stringValue.Contains(value) CONTAINS(@stringValue, @value)
stringValue.Contains(value, StringComparison.Ordinal) CONTAINS(@stringValue, @value, false) EF Core 9.0
stringValue.Contains(value, StringComparison.OrdinalIgnoreCase) CONTAINS(@stringValue, @value, true) EF Core 9.0
stringValue.EndsWith(value) ENDSWITH(@stringValue, @value)
stringValue.EndsWith(value, StringComparison.Ordinal) ENDSWITH(@stringValue, @value, false) EF Core 9.0
stringValue.EndsWith(value, StringComparison.OrdinalIgnoreCase) ENDSWITH(@stringValue, @value, true) EF Core 9.0
stringValue.Equals(value, StringComparison.Ordinal) STRINGEQUALS(@stringValue, @value)
stringValue.Equals(value, StringComparison.OrdinalIgnoreCase) STRINGEQUALS(@stringValue, @value, true)
stringValue.FirstOrDefault() LEFT(@stringValue, 1)
stringValue.IndexOf(value) INDEX_OF(@stringValue, @value)
stringValue.IndexOf(value, startIndex) INDEX_OF(@stringValue, @value, @startIndex)
stringValue.LastOrDefault() RIGHT(@stringValue, 1)
stringValue.Length LENGTH(@stringValue)
stringValue.Replace(oldValue, newValue) REPLACE(@stringValue, @oldValue, @newValue)
stringValue.StartsWith(value) STARTSWITH(@stringValue, @value)
stringValue.StartsWith(value, StringComparison.Ordinal) STARTSWITH(@stringValue, @value, false) EF Core 9.0
stringValue.StartsWith(value, StringComparison.OrdinalIgnoreCase) STARTSWITH(@stringValue, @value, true) EF Core 9.0
stringValue.Substring(startIndex) SUBSTRING(@stringValue, @startIndex, LENGTH(@stringValue))
stringValue.Substring(startIndex, length) SUBSTRING(@stringValue, @startIndex, @length)
stringValue.ToLower() LOWER(@stringValue)
stringValue.ToUpper() UPPER(@stringValue)
stringValue.Trim() TRIM(@stringValue)
stringValue.TrimEnd() RTRIM(@stringValue)
stringValue.TrimStart() LTRIM(@stringValue)

기타 함수

.NET SQL 주의
collection.Contains(item) @item IN @collection
EF.Functions.CoalesceUndefined(x, y)1 x ?? y EF Core 5.0에서 추가됨
EF.Functions.IsDefined(x) IS_DEFINED(x) EF Core 5.0에서 추가됨
EF.Functions.VectorDistance(vector1, vector2)2 VectorDistance(vector1, vector2) EF Core 9.0에서 추가됨, 실험적
EF.Functions.VectorDistance(vector1, vector2, bruteForce)2 VectorDistance(vector1, vector2, bruteForce) EF Core 9.0에서 추가됨, 실험적
EF.Functions.VectorDistance(vector1, vector2, bruteForce, distanceFunction)2 VectorDistance(vector1, vector2, bruteForce, distanceFunction) EF Core 9.0에서 추가됨, 실험적

1 EF.Functions.CoalesceUndefinednull이 아닌 undefined를 병합합니다. null을 병합하려면 일반 C# ?? 연산자를 사용합니다.

2 Azure Cosmos DB에서 벡터 검색을 사용하는 방법에 대한 자세한 내용은 설명서를 참조하세요. Cosmos DB 벡터 검색은 실험적이며 API는 변경될 수 있습니다.