O mnieBlogGitHub

Potęga podzapytań w Cosmos DB

30 January, 2020 - 2 min read

Cosmos DB Query Playground to interaktywne środowisko do eksperymentowania z kwerendami do Cosmos DB. Jako dane testowe zawiera ono bazę produktów spożywczych wraz z informacjami o ich wartościach odżywczych. Przykładowy dokument wygląda mniej-więcej tak:

{
  "food": {
    "id": "14203",
    "description": "Coffee, instant, regular, powder, half the caffeine",
    "tags": [
      {
        "name": "coffee"
      },
      {
        "name": "instant"
      },
      ...
      // pomijam resztę
    ],
    "foodGroup": "Beverages",
    "nutrients": [
      {
        "id": "204",
        "description": "Total lipid (fat)",
        "nutritionValue": 0.5,
        "units": "g"
      },
      {
        "id": "205",
        "description": "Carbohydrate, by difference",
        "nutritionValue": 73.18,
        "units": "g"
      },
      ...
      // pomijam resztę
    ]
    ...
    // pomijam resztę
  }
}

Posłużmy się tym "typem" danych jako przykładem dla zobrazowania, jak bardzo mogą się różnić koszty zapytania.

Załóżmy, że chcemy wybrać wszystkie produkty, które zawierają 1g lub więcej kofeiny. A zatem musimy dla każdego produkty wyszukać w liście nutrients elementu o description równym "Caffeine" oraz nutritionValue >= 1000.

Podejście "łopatologiczno-proceduralne" może nas zaprowadzić do pomysłu, żeby dodać sobie user-defined-function, która "opakuje" te warunki, powiedzmy coś takiego:

function hasHighLevelOfCaffeine(food) {
  return food.nutrients.some(
    n => n.description == "Caffeine" && n.nutritionValue >= 1000
  )
}

Wówczas główne zaptanie się "upraszcza":

SELECT food FROM food WHERE udf.hasHighLevelOfCaffeine(food)

Problem z tym zapytaniem jest jednak taki, że zżera ono sporo RU. Na potrzeby obliczeń zrobiłem sobie przykładową bazę, gdzie miałem niecałe 2000 produktów i tylko jeden z "wysokim poziomem kofeiny". Dla takich dancyh zwykły select zeżarł ponad 600 RUs. Co gorsza - im więcej dokumentów w kolekcji, tym koszt będzie jeszcze większy. Wynika to prawdopodobnie z tego, że Cosmos nie potrafi sobie "zoptymalizować" zapytania z użyciem UDF, operującej na podkolekcji.

Spróbujmy zatem do problemu podejść inaczej. Korzystając z dokumentacji możemy spróbować użyć podzapytania - tak byśmy przecież postąpili w "zwykłym SQL-u". (Mamy tu relację jeden-do-wielu, więc za pomocą EXISTS i podzapytania moglibyśmy to ograć.) W Cosmosie spraw wygląda podobnie, z tą różnicą, że używamy JOIN i podzapytania. Finalnie query wygląda tak:

SELECT
food
FROM food
JOIN (SELECT VALUE n FROM n IN food.nutrients
WHERE
	n.description = 'Caffeine' and n.nutritionValue >= 1000)

Dla wspomnianej wyżej "benchmarkowej" bazy z 2000 produktów - to zapytanie "kosztowało" tylko 2 RU. I to w zasadzie niezależnie od ilości dokumentów.

Ciekawe, prawda? :)

© 2020, Built with Gatsby