Tipps zur Abfrageleistung für Azure Cosmos DB-SDKs

GILT FÜR: SQL-API

Azure Cosmos DB ist eine schnelle und flexible verteilte Datenbank mit nahtloser Skalierung, garantierter Latenz und garantiertem Durchsatz. Die Skalierung Ihrer Datenbank mit Azure Cosmos DB erfordert weder aufwendige Änderungen an der Architektur noch das Schreiben von komplexem Code. Zentrales Hoch- und Herunterskalieren ist ebenso problemlos möglich wie ein einzelner API-Aufruf. Weitere Informationen finden Sie unter Bereitstellen von Standarddurchsatz (manuell) für einen Azure Cosmos-Container und Bereitstellen von Standarddurchsatz (manuell) für eine Datenbank in Azure Cosmos DB.

Reduzieren von Abfrageplanaufrufen

Zum Ausführen einer Abfrage muss ein Abfrageplan erstellt werden. Dies stellt im Allgemeinen eine Netzwerkanforderung an das Azure Cosmos DB-Gateway dar, wodurch die Latenz des Abfragevorgangs erhöht wird. Es gibt zwei Möglichkeiten, diese Anforderung zu entfernen und die Latenz des Abfragevorgangs zu reduzieren:

Nutzung der lokalen Abfrageplangenerierung

Das SQL SDK enthält die native Datei „ServiceInterop.dll“, um Abfragen lokal zu analysieren und zu optimieren. „ServiceInterop.dll“ wird nur auf der Windows x64-Plattform unterstützt. Bei den folgenden Anwendungstypen wird standardmäßig die 32-Bit-Hostverarbeitung verwendet. Um die Hostverarbeitung auf die 64-Bit-Verarbeitung umzustellen, führen Sie je nach Typ Ihrer Anwendung die folgenden Schritte aus:

  • Bei ausführbaren Anwendungen können Sie die Host-Verarbeitung ändern, indem Sie das Plattformziel im Fenster Projekteigenschaften auf der Registerkarte Erstellen auf x64 setzen.

  • Bei VSTest-basierten Testprojekten können Sie die Hostverarbeitung ändern, indem Sie in der Menüoption Visual Studio Test die Option Test>Testeinstellungen>Default Processor Architecture as X64 (Standardprozessorarchitektur als x64) auswählen.

  • Bei lokal bereitgestellten ASP.NET-Webanwendungen ändern Sie die Hostverarbeitung, indem Sie unter Extras>Optionen>Projekte und Projektmappen>Webprojekte die Option 64-Bit-Version von IIS Express für Websites und Projekte verwenden aktivieren.

  • Für ASP.NET-Webanwendungen, die in Azure bereitgestellt werden, können Sie die Hostverarbeitung ändern, indem Sie im Azure-Portal unter Anwendungseinstellungen die 64-Bit-Plattform auswählen.

Hinweis

Standardmäßig ist bei neuen Visual Studio-Projekten Beliebige CPU festgelegt. Es wird empfohlen, Ihr Projekt auf x64 festzulegen, damit es nicht zu x86 wechselt. Ein Projekt mit der Einstellung Beliebige CPU kann schnell zu x86 wechseln, wenn eine Abhängigkeit hinzugefügt wird, die nur bei x86 verfügbar ist.
Die Datei „ServiceInterop.dll“ muss sich im selben Ordner befinden, aus dem die SDK-DLL ausgeführt wird. Dies sollte nur dann von Bedeutung sein, wenn Sie DLLs manuell kopieren oder über benutzerdefinierte Build-/Bereitstellungssysteme verfügen.

Verwenden von Abfragen mit nur einer Partition

Für Abfragen, die auf einen Partitionsschlüssel abzielen, indem sie die PartitionKey-Eigenschaft in QueryRequestOptions festlegen und keine Aggregationen enthalten (einschließlich Distinct, DCount, Group By):

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey("Washington")}))
{
    // ...
}

Hinweis

Für partitionsübergreifende Abfragen muss das SDK alle vorhandenen Partitionen besuchen, um nach Ergebnissen zu suchen. Je mehr physische Partitionen der Container hat, desto langsamer sind sie tendenziell.

Vermeiden der unnötigen Neuerstellung des Iterators

Wenn alle Abfrageergebnisse von der aktuellen Komponente genutzt werden, müssen Sie den Iterator nicht mit der Fortsetzung für jede Seite neu erstellen. Ziehen Sie es immer vor, die Abfrage vollständig zu leeren, es sei denn, die Paginierung wird von einer anderen aufrufenden Komponente gesteuert:

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey("Washington")}))
{
    while (feedIterator.HasMoreResults) 
    {
        foreach(MyItem document in await feedIterator.ReadNextAsync())
        {
            // Iterate through documents
        }
    }
}

Optimieren des Parallelitätsgrades

Optimieren Sie bei Abfragen die Eigenschaft MaxConcurrency in QueryRequestOptions, um die besten Konfigurationen für Ihre Anwendung zu ermitteln, insbesondere, wenn Sie partitionsübergreifende Abfragen (ohne Filter für den Partitionsschlüsselwert) ausführen. MaxConcurrency steuert die maximale Anzahl von parallelen Aufgaben, d. h. das Maximum von Partitionen, die parallel besucht werden. Wenn Sie den Wert auf -1 festlegen, kann das SDK die optimale Parallelität automatisch festlegen.

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { 
        PartitionKey = new PartitionKey("Washington"),
        MaxConcurrency = -1 }))
{
    // ...
}

Annahme:

  • D = Standardwert, maximale Anzahl paralleler Aufgaben (= Gesamtzahl der Prozessoren auf dem Clientcomputer)
  • P = Vom Benutzer angegebene maximale Anzahl paralleler Aufgaben
  • N = Anzahl der Partitionen, auf die zur Beantwortung einer Abfrage zugegriffen werden muss

Es folgen die Auswirkungen auf das Verhalten der parallelen Abfragen bei unterschiedlichen Werten für P.

  • (P == 0) => serieller Modus
  • (P == 1) => maximal eine Aufgabe
  • (P > 1) => minimale Anzahl (P, N) paralleler Aufgaben
  • (P < 1) => minimale Anzahl (N, D) paralleler Aufgaben

Optimieren der Seitengröße

Wenn Sie eine SQL-Abfrage stellen, werden die Ergebnisse bei einem zu großen Resultset segmentiert zurückgegeben. Ergebnisse werden standardmäßig in Blöcken mit je 100 Elementen oder 1 MB zurückgegeben (je nachdem, welcher Grenzwert zuerst erreicht wird).

Hinweis

Die MaxItemCount-Eigenschaft sollte nicht nur für die Paginierung verwendet werden. Ihr Hauptverwendungszweck ist die Verbesserung der Leistung von Abfragen durch Reduzierung der maximalen Anzahl von Elementen, die auf einer einzigen Seite zurückgegeben werden.

Sie können die Seitengröße auch mithilfe der verfügbaren Azure Cosmos DB-SDKs festlegen. Mit der MaxItemCount-Eigenschaft in QueryRequestOptions können Sie die maximale Anzahl von Elementen festlegen, die beim Enumerationsvorgang zurückgegeben werden. Wenn MaxItemCount auf „-1“ festgelegt ist, ermittelt das SDK automatisch den optimalen Wert abhängig von der Größe des Dokuments. Beispiel:

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { 
        PartitionKey = new PartitionKey("Washington"),
        MaxItemCount = 1000}))
{
    // ...
}

Beim Ausführen einer Abfrage werden die resultierenden Daten in einem TCP-Paket gesendet. Wenn Sie einen zu niedrigen Wert für MaxItemCount angeben, ist die zum Senden der Daten im TCP-Paket erforderliche Anzahl der Roundtrips hoch, was sich auf die Leistung auswirkt. Wenn Sie nicht sicher sind, welchen Wert Sie für die MaxItemCount-Eigenschaft festlegen sollen, empfiehlt es sich, den Wert auf „-1“ festzulegen und den Standardwert vom SDK auswählen zu lassen.

Die Puffergröße optimieren

Parallele Abfragen sind so konzipiert, dass Ergebnisse vorab abgerufen werden, während der Client den aktuellen Batch der Ergebnisse verarbeitet. Dieser Vorababruf führt zu einer Verbesserung der allgemeinen Latenz einer Abfrage. Durch die Eigenschaft MaxBufferedItemCount in QueryRequestOptions wird die Anzahl von vorab abgerufenen Ergebnissen begrenzt. Wenn Sie MaxBufferedItemCount auf die erwartete Anzahl von zurückgegebenen Ergebnissen (oder eine höhere Zahl) festlegen, ist der Vorteil durch das vorherige Abrufen für die Abfrage am größten. Wenn Sie diesen Wert auf „-1“ festlegen, kann das System die Anzahl der zu puffernden Elemente automatisch festlegen.

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { 
        PartitionKey = new PartitionKey("Washington"),
        MaxBufferedItemCount = -1}))
{
    // ...
}

Das vorherige Abrufen funktioniert unabhängig vom Parallelitätsgrad, und es ist nur ein Puffer für die Daten aller Partitionen vorhanden.

Nächste Schritte

Weitere Informationen über Leistung mit dem .NET SDK:

Reduzieren von Abfrageplanaufrufen

Zum Ausführen einer Abfrage muss ein Abfrageplan erstellt werden. Dies stellt im Allgemeinen eine Netzwerkanforderung an das Azure Cosmos DB-Gateway dar, wodurch die Latenz des Abfragevorgangs erhöht wird.

Zwischenspeichern von Abfrageplänen

Der Abfrageplan für eine Abfrage, die auf eine einzelne Partition abzielt, wird auf dem Client zwischengespeichert. Dadurch entfällt nach dem ersten Aufruf die Notwendigkeit, das Gateway abzufragen, um den Abfrageplan abzurufen. Der Schlüssel für den zwischengespeicherten Abfrageplan ist die SQL-Abfragezeichenfolge. Sie müssen sicherstellen, dass Ihre Abfrage parametrisiert ist. Wenn dies nicht der Fall ist, resultiert die Cachesuche des Abfrageplans häufig in einem Cachefehler, da es unwahrscheinlich ist, dass die Abfragezeichenfolge der einzelnen Aufrufe identisch ist. Das Zwischenspeichern von Abfrageplänen ist standardmäßig für Java SDK ab Version 4.20.0 sowie für Spring Data Cosmos SDK ab Version 3.13.0 aktiviert.

Verwenden parametrisierter Abfragen für eine einzelne Partition

Bei parametrisierten Abfragen, die mit setPartitionKey in CosmosQueryRequestOptions auf einen Partitionsschlüssel festgelegt sind und keine Aggregationen enthalten (einschließlich Distinct, DCount, Group By), kann der Abfrageplan vermieden werden:

CosmosQueryRequestOptions options = new CosmosQueryRequestOptions();
options.setPartitionKey(new PartitionKey("Washington"));

ArrayList<SqlParameter> paramList = new ArrayList<SqlParameter>();
paramList.add(new SqlParameter("@city", "Seattle"));
SqlQuerySpec querySpec = new SqlQuerySpec(
        "SELECT * FROM c WHERE c.city = @city",
        paramList);

//  Sync API
CosmosPagedIterable<MyItem> filteredItems = 
    container.queryItems(querySpec, options, MyItem.class);

//  Async API
CosmosPagedFlux<MyItem> filteredItems = 
    asyncContainer.queryItems(querySpec, options, MyItem.class);

Hinweis

Für partitionsübergreifende Abfragen muss das SDK alle vorhandenen Partitionen besuchen, um nach Ergebnissen zu suchen. Je mehr physische Partitionen der Container hat, desto langsamer sind sie tendenziell.

Optimieren des Parallelitätsgrades

Bei parallelen Abfragen werden mehrere Partitionen parallel abgefragt. Die Daten eines individuell partitionierten Containers werden in Bezug auf die Abfrage aber seriell abgerufen. Verwenden Sie also setMaxDegreeOfParallelism in CosmosQueryRequestOptions, um den Wert auf die Anzahl der Partitionen festzulegen, über die Sie verfügen. Falls Ihnen die Anzahl von Partitionen nicht bekannt ist, können Sie setMaxDegreeOfParallelism auf einen hohen Wert festlegen. Das System wählt für den maximalen Grad an Parallelität dann den minimalen Wert aus (Anzahl von Partitionen, Benutzereingabe). Wenn Sie den Wert auf -1 festlegen, kann das SDK die optimale Parallelität automatisch festlegen.

Es ist wichtig zu beachten, dass sich für parallele Abfragen die größten Vorteile ergeben, wenn die Daten in Bezug auf die Abfrage gleichmäßig auf alle Partitionen verteilt werden. Wenn der partitionierte Container so partitioniert ist, dass sich alle Daten bzw. die meisten Daten, die von einer Abfrage zurückgegeben werden, auf einigen wenigen Partitionen befinden (schlimmstenfalls nur auf einer Partition), kann dies die Leistung beeinträchtigen.

CosmosQueryRequestOptions options = new CosmosQueryRequestOptions();
options.setPartitionKey(new PartitionKey("Washington"));
options.setMaxDegreeOfParallelism(-1);

// Define the query

//  Sync API
CosmosPagedIterable<MyItem> filteredItems = 
    container.queryItems(querySpec, options, MyItem.class);

//  Async API
CosmosPagedFlux<MyItem> filteredItems = 
    asyncContainer.queryItems(querySpec, options, MyItem.class);

Annahme:

  • D = Standardwert, maximale Anzahl paralleler Aufgaben (= Gesamtzahl der Prozessoren auf dem Clientcomputer)
  • P = Vom Benutzer angegebene maximale Anzahl paralleler Aufgaben
  • N = Anzahl der Partitionen, auf die zur Beantwortung einer Abfrage zugegriffen werden muss

Es folgen die Auswirkungen auf das Verhalten der parallelen Abfragen bei unterschiedlichen Werten für P.

  • (P == 0) => serieller Modus
  • (P == 1) => maximal eine Aufgabe
  • (P > 1) => minimale Anzahl (P, N) paralleler Aufgaben
  • (P == -1) => minimale Anzahl (N, D) paralleler Aufgaben

Optimieren der Seitengröße

Wenn Sie eine SQL-Abfrage stellen, werden die Ergebnisse bei einem zu großen Resultset segmentiert zurückgegeben. Ergebnisse werden standardmäßig in Blöcken mit je 100 Elementen oder 4 MB zurückgegeben (je nachdem, welcher Grenzwert zuerst erreicht wird).

Sie können den Parameter pageSize in iterableByPage() für die synchrone API und byPage() für die asynchrone API verwenden, um eine Seitengröße zu definieren:

//  Sync API
Iterable<FeedResponse<MyItem>> filteredItemsAsPages =
    container.queryItems(querySpec, options, MyItem.class).iterableByPage(continuationToken,pageSize);

for (FeedResponse<MyItem> page : filteredItemsAsPages) {
    for (MyItem item : page.getResults()) {
        //...
    }
}

//  Async API
Flux<FeedResponse<MyItem>> filteredItemsAsPages =
    asyncContainer.queryItems(querySpec, options, MyItem.class).byPage(continuationToken,pageSize);

filteredItemsAsPages.map(page -> {
    for (MyItem item : page.getResults()) {
        //...
    }
}).subscribe();

Die Puffergröße optimieren

Parallele Abfragen sind so konzipiert, dass Ergebnisse vorab abgerufen werden, während der Client den aktuellen Batch der Ergebnisse verarbeitet. Diese Art des Abrufs führt zu einer Verbesserung der Latenz einer Abfrage. setMaxBufferedItemCount in CosmosQueryRequestOptions begrenzt die Anzahl vorab abgerufener Ergebnisse. Wenn Sie „setMaxBufferedItemCount“ auf die erwartete Anzahl zurückgegebener Ergebnisse (oder eine höhere Anzahl) festlegen, ist der Vorteil durch das vorherige Abrufen für die Abfrage am größten. Dies kann allerdings zu einem erhöhtem Arbeitsspeicherbedarf führen. Wenn Sie diesen Wert auf „0“ festlegen, kann das System die Anzahl der zu puffernden Elemente automatisch festlegen.

CosmosQueryRequestOptions options = new CosmosQueryRequestOptions();
options.setPartitionKey(new PartitionKey("Washington"));
options.setMaxBufferedItemCount(-1);

// Define the query

//  Sync API
CosmosPagedIterable<MyItem> filteredItems = 
    container.queryItems(querySpec, options, MyItem.class);

//  Async API
CosmosPagedFlux<MyItem> filteredItems = 
    asyncContainer.queryItems(querySpec, options, MyItem.class);

Das vorherige Abrufen funktioniert unabhängig vom Parallelitätsgrad, und es ist nur ein Puffer für die Daten aller Partitionen vorhanden.

Nächste Schritte

Weitere Informationen über Leistung mit dem Java-SDK: