Azure Cosmos DB for NoSQL でのサブクエリ

適用対象: NoSQL

サブクエリは、Azure Cosmos DB for NoSQL 内の別のクエリ内に入れ子になったクエリです。 サブクエリは "内部クエリ" または "内部 SELECT" とも呼ばれます。 サブクエリを含むステートメントは通常、"外部クエリ" と呼ばれます。

サブクエリの種類

サブクエリには、主に次の 2 種類があります。

  • 相関: 外部クエリからの値を参照するサブクエリ。 サブクエリは、外部クエリが処理する行ごとに 1 回評価されます。
  • 非相関: 外部クエリから独立しているサブクエリ。 外部クエリに依存せずに単独で実行できます。

Note

Azure Cosmos DB は相関サブクエリのみをサポートします。

サブクエリは、返される行と列の数に基づいてさらに分類できます。 次の 3 種類があります。

  • テーブル: 複数の行と複数の列を返します。
  • 複数値: 複数の行と 1 つの列を返します。
  • スカラー: 1 つの行と 1 つの列を返します。

Azure Cosmos DB for NoSQL のクエリは常に 1 つの列 (単純な値または複雑な項目のどちらか) を返します。 したがって、複数値サブクエリとスカラー サブクエリのみを適用できます。 複数値サブクエリは、関係式として FROM 句でのみ使用できます。 スカラー サブクエリは、SELECT または WHERE 句でスカラー式として、あるいは FROM 句で関係式として使用できます。

複数値サブクエリ

複数値サブクエリは項目のセットを返し、常に FROM 句内で使用されます。 次の目的に使用されます。

  • JOIN (自己結合) 式を最適化する。
  • コストが高い式を 1 回評価して複数回参照する。

自己結合式を最適化する

複数値サブクエリは、WHERE 句内のすべての "クロス結合" の後ではなく、それぞれの select-many 式の後に述語をプッシュすることによって JOIN 式を最適化できます。

次のクエリを考えてみます。

SELECT VALUE
    COUNT(1)
FROM
    products p
JOIN 
    t in p.tags
JOIN 
    q in p.onHandQuantities
JOIN 
    s in p.warehouseStock
WHERE 
    t.name IN ("winter", "fall") AND
    (q.quantity BETWEEN 0 AND 10) AND
    NOT s.backstock

このクエリの場合、インデックスは、name"winter" または "fall" のいずれかのタグを持ち、少なくとも 1 つの quantity0 から 10 までの間で、少なくとも 1 つのウェアハウスの backstockfalse であるすべての項目と一致します。 ここでの JOIN 式は、あらゆるフィルターが適用される前に、一致する項目ごとに、tagsonHandQuantitieswarehouseStock の各配列の全項目の "外積" を実行します。

WHERE 句はその後、<c, t, n, s> タプルごとにフィルター述語を適用します。 たとえば、一致する項目で、3 つの配列のそれぞれに 10 個の項目があるとした場合、1 x 10 x 10 x 10 (つまり、1,000) タプルに展開されます。 ここでサブクエリを使用すると、次の式と結合する前に、結合された配列項目をフィルターで除外するために役立ちます。

このクエリは前のものと同等ですが、サブクエリを使用します。

SELECT VALUE
    COUNT(1)
FROM
    products p
JOIN 
    (SELECT VALUE t FROM t IN p.tags WHERE t.name IN ("winter", "fall"))
JOIN 
    (SELECT VALUE q FROM q IN p.onHandQuantities WHERE q.quantity BETWEEN 0 AND 10)
JOIN 
    (SELECT VALUE s FROM s IN p.warehouseStock WHERE NOT s.backstock)

tags 配列の 1 つの項目のみがフィルターに一致し、nutrients 配列と servings 配列の両方に 5 つの項目があるとします。 JOIN 式は、最初のクエリの 1,000 項目とは異なり、1 x 1 x 5 x 5 (25) 項目に展開されます。

1 回評価して複数回参照する

サブクエリは、ユーザー定義関数 (UDF)、複雑な文字列、算術式などのコストが高い式でクエリを最適化するために役立ちます。 サブクエリを JOIN 式と一緒に使用することで、式を 1 回しか評価しないが複数回参照するという処理ができます。

次の UDF (getTotalWithTax) が定義されているとします。

function getTotalWithTax(subTotal){
  return subTotal * 1.25;
}

次のクエリは UDF getTotalWithTax を複数回実行します。

SELECT VALUE {
    subtotal: p.price,
    total: udf.getTotalWithTax(p.price)
}
FROM
    products p
WHERE
    udf.getTotalWithTax(p.price) < 22.25

次に示すのは、UDF を 1 回しか実行しない同等のクエリです。

SELECT VALUE {
    subtotal: p.price,
    total: totalPrice
}
FROM
    products p
JOIN
    (SELECT VALUE udf.getTotalWithTax(p.price)) totalPrice
WHERE
    totalPrice < 22.25

ヒント

JOIN 式の外積の動作に留意してください。 UDF 式が undefined と評価される可能性がある場合、値を直接返すのではなくサブクエリからのオブジェクトを返すことによって、JOIN 式の結果が常に 1 行になることを保証してください。

外部参照データとの結合を模倣する

"測定単位" のように、ほとんど変更されない静的データを参照することが必要になる場合がよくあります。 クエリ内の各項目の静的データを重複しないようにするのが理想的です。 この重複を回避して個々の項目のサイズを小さく保つことで、ストレージを節約し、書き込みパフォーマンスを高めることができます。 サブクエリを使用して、静的参照データのコレクションとの内部結合セマンティクスを模倣することができます。

たとえば、次のような測定のセットを考えます。

名前 乗数 基本単位
ng ナノグラム 1.00E-09 グラム
µg マイクログラム 1.00E-06 グラム
mg ミリグラム 1.00E-03 グラム
g グラム 1.00E+00 グラム
kg キログラム 1.00E+03 グラム
Mg メガグラム 1.00E+06 グラム
Gg ギガグラム 1.00E+09 グラム

次のクエリは、このデータとの結合を模倣し、単位の名前を出力に追加します。

SELECT
    s.id,
    (s.weight.quantity * m.multiplier) AS calculatedWeight,
    m.unit AS unitOfWeight
FROM
    shipments s
JOIN m IN (
    SELECT VALUE [
        {unit: 'ng', name: 'nanogram', multiplier: 0.000000001, baseUnit: 'gram'},
        {unit: 'µg', name: 'microgram', multiplier: 0.000001, baseUnit: 'gram'},
        {unit: 'mg', name: 'milligram', multiplier: 0.001, baseUnit: 'gram'},
        {unit: 'g', name: 'gram', multiplier: 1, baseUnit: 'gram'},
        {unit: 'kg', name: 'kilogram', multiplier: 1000, baseUnit: 'gram'},
        {unit: 'Mg', name: 'megagram', multiplier: 1000000, baseUnit: 'gram'},
        {unit: 'Gg', name: 'gigagram', multiplier: 1000000000, baseUnit: 'gram'}
    ]
)
WHERE
    s.weight.units = m.unit

スカラー サブクエリ

スカラー サブクエリ式は、1 つの値に評価されるサブクエリです。 スカラー サブクエリ式の値は、サブクエリのプロジェクション (SELECT 句) の値です。 スカラー式が有効な多くの場面で、スカラー サブクエリ式を使用できます。 たとえば、SELECT 句と WHERE 句両方の任意の式でスカラー サブクエリを使用できます。

スカラー サブクエリの使用が常にクエリの最適化に寄与するとは限りません。 たとえば、スカラー サブクエリをシステムまたはユーザー定義関数に引数として渡しても、リソース単位 (RU) 消費または待機時間を削減する上でのメリットはありません。

スカラー サブクエリは、さらに次のように分類できます。

  • 単純式スカラー サブクエリ
  • 集計スカラー サブクエリ

単純式スカラー サブクエリ

単純式スカラー サブクエリは、集約式を含まない SELECT 句を持つ相関サブクエリです。 これらのサブクエリは、コンパイラによって 1 つのより大きな単純式に変換されるため、最適化のベネフィットをもたらしません。 内部クエリと外部クエリの間に相関コンテキストはありません。

最初の例として、この単純なクエリを考えてみましょう。

SELECT
    1 AS a,
    2 AS b

単純式スカラー サブクエリを使用して、このクエリを書き直すことができます。

SELECT
    (SELECT VALUE 1) AS a, 
    (SELECT VALUE 2) AS b

どちらのクエリも同じ出力を生成します。

[
  {
    "a": 1,
    "b": 2
  }
]

次のクエリ例では、一意識別子とプレフィックスを単純式スカラー サブクエリとして連結します。

SELECT 
    (SELECT VALUE Concat('ID-', p.id)) AS internalId
FROM
    products p

この例では単純式スカラー サブクエリを使用して、各項目の関連フィールドのみを返します。 クエリは各項目に対して何かを出力しますが、サブクエリ内のフィルターを満たしている場合に、射影フィールドのみが含まれます。

SELECT
    p.id,
    (SELECT p.name WHERE CONTAINS(p.name, "glove")).name
FROM
    products p
[
  {
    "id": "03230",
    "name": "Winter glove"
  },
  {
    "id": "03238"
  },
  {
    "id": "03229"
  }
]

集計スカラー サブクエリ

集計スカラー サブクエリは、1 つの値に評価されるそのプロジェクションまたはフィルターに集計関数が含まれるサブクエリです。

最初の例として、次のフィールドを持つ項目を考えてみましょう。

{
  "name": "Snow coat",
  "inventory": [
    {
      "location": "Redmond, WA",
      "quantity": 50
    },
    {
      "location": "Seattle, WA",
      "quantity": 30
    },
    {
      "location": "Washington, DC",
      "quantity": 25
    }
  ]
}

次に示すのは、1 つの集計関数式がそのプロジェクションに含まれるサブクエリです。 このクエリでは、各項目のすべてのタグがカウントされます。

SELECT
    p.name,
    (SELECT VALUE COUNT(1) FROM i IN p.inventory) AS locationCount
FROM
    products p
[
  {
    "name": "Snow coat",
    "locationCount": 3
  }
]

次に、フィルターを含む同じサブクエリを示します。

SELECT
    p.name,
    (SELECT VALUE COUNT(1) FROM i IN p.inventory WHERE ENDSWITH(i.location, "WA")) AS washingtonLocationCount
FROM
    products p
[
  {
    "name": "Snow coat",
    "washingtonLocationCount": 2
  }
]

次に示すのは、複数の集計関数式が含まれる別のサブクエリです。

SELECT
    p.name,
    (SELECT
        COUNT(1) AS locationCount,
        SUM(i.quantity) AS totalQuantity
    FROM i IN p.inventory) AS inventoryData
FROM
    products p
[
  {
    "name": "Snow coat",
    "inventoryData": {
      "locationCount": 2,
      "totalQuantity": 75
    }
  }
]

最後に示すのは、プロジェクションとフィルターの両方に集計サブクエリが含まれるクエリです。

SELECT
    p.name,
    (SELECT VALUE AVG(q.quantity) FROM q IN p.inventory WHERE q.quantity > 10) AS averageInventory
FROM
    products p
WHERE
    (SELECT VALUE COUNT(1) FROM i IN p.inventory WHERE i.quantity > 10) >= 1
[
  {
    "name": "Snow coat",
    "averageInventory": 35
  }
]

このクエリのより最適な記述方法は、サブクエリで結合し、SELECT 句と WHERE 句の両方でサブクエリの別名を参照することです。 このクエリのほうが効率的なのは、JOIN ステートメントの中でサブクエリを実行するだけでよく、プロジェクションとフィルターの両方で実行する必要がないからです。

SELECT
    p.name,
    inventoryData.inventoryAverage
FROM
    products p
JOIN
    (SELECT 
        COUNT(1) AS inventoryCount, 
        AVG(i.quantity) as inventoryAverage 
    FROM i IN p.inventory 
    WHERE i.quantity > 10) AS inventoryData
WHERE
    inventoryData.inventoryCount >= 1

EXISTS 式

Azure Cosmos DB for NoSQL のクエリ エンジンでは、EXISTS 式がサポートされています。 この式は、Azure Cosmos DB for NoSQL に組み込まれている集計スカラー サブクエリです。 EXISTS は、サブクエリ式を取り、サブクエリが行を返す場合は true を返します。 それ以外の場合は、 falseを返します。

クエリ エンジンはブール式と他のスカラー式を区別しないため、SELECT 句と WHERE 句の両方で EXISTS を使用できます。 この動作は、ブール式がフィルターのみに制限されている T-SQL とは異なります。

EXISTS サブクエリにより undefined の 1 つの値が返された場合、EXISTS は false に評価されます。 たとえば、何も返されない次のクエリを考えてみましょう。

SELECT VALUE
    undefined

EXISTS 式と前のクエリをサブクエリとして使用する場合、式は false を返します。

SELECT
    EXISTS (SELECT VALUE undefined)
[
  {
    "$1": false
  }
]

前のサブクエリの VALUE キーワードを省略すると、サブクエリは 1 つの空のオブジェクトを持つ配列に評価されます。

SELECT
    undefined
[
  {}
]

その時点で、オブジェクト ({}) が技術的に存在するため、EXISTS 式は true に評価されます。

SELECT 
    EXISTS (SELECT undefined) 
[
  {
    "$1": true
  }
]

ARRAY_CONTAINS の一般的なユース ケースでは、配列内の項目の存在によって項目をフィルター処理します。 この場合は、"outerwear" という名前の項目が tags 配列に含まれているかどうかチェックしています。

SELECT
    p.name,
    p.tags
FROM
    products p
WHERE
    ARRAY_CONTAINS(p.tags, "outerwear")

同じクエリで EXISTS を別のオプションとして使用できます。

SELECT
    p.name,
    p.tags
FROM
    products p
WHERE
    EXISTS (SELECT VALUE t FROM t IN p.tags WHERE t = "outerwear")

さらに、ARRAY_CONTAINS がチェックできるのは、配列内のいずれかの要素に値が等しいかどうかだけです。 さらに複雑なフィルターが配列プロパティに対して必要な場合は、代わりに JOIN を使用します。

複数の項目がそれぞれ accessories 配列を含むセット内の項目の例について考えてみましょう。

{
  "name": "Unobtani road bike",
  "accessories": [
    {
      "name": "Front/rear tire",
      "type": "tire",
      "quantityOnHand": 5
    },
    {
      "name": "9-speed chain",
      "type": "chains",
      "quantityOnHand": 25
    },
    {
      "name": "Clip-in pedals",
      "type": "pedals",
      "quantityOnHand": 15
    }
  ]
}

ここで、各項目内の配列の type および quantityOnHand プロパティに基づいてフィルター処理を行う次のクエリを考えます。

SELECT
    p.name,
    a.name AS accessoryName
FROM
    products p
JOIN
    a IN p.accessories
WHERE
    a.type = "chains" AND
    a.quantityOnHand >= 10
[
  {
    "name": "Unobtani road bike",
    "accessoryName": "9-speed chain"
  }
]

コレクション内の項目ごとに、その配列要素を使用して外積を実行します。 この JOIN 操作により、配列内のプロパティをフィルター処理できます。 ただし、このクエリの RU 消費量は多大です。 たとえば、1,000 個の項目で、各配列に 100 個の項目があるとした場合、1,000 x 100 (つまり、100,000) タプルに展開されます。

EXISTS を使用すれば、このようにコストが高い外積を回避できます。 この次の例では、クエリは EXISTS サブクエリ内で配列要素をフィルター処理します。 配列要素がフィルターに一致した場合、それを射影し、EXISTS は true と評価されます。

SELECT VALUE
    p.name
FROM
    products p
WHERE
    EXISTS (SELECT VALUE 
        a 
    FROM 
        a IN p.accessories
    WHERE
        a.type = "chains" AND
        a.quantityOnHand >= 10)
[
  "Unobtani road bike"
]

クエリでは EXISTS に別名を付けて、プロジェクション内で別名を参照することもできます。

SELECT
    p.name,
    EXISTS (SELECT VALUE
        a 
    FROM 
        a IN p.accessories
    WHERE
        a.type = "chains" AND
        a.quantityOnHand >= 10) AS chainAccessoryAvailable
FROM
    products p
[
  {
    "name": "Unobtani road bike",
    "chainAccessoryAvailable": true
  }
]

ARRAY 式

ARRAY 式を使用して、クエリの結果を配列として射影することができます。 この式はクエリの SELECT 句内でのみ使用できます。

これらの例では、少なくとも次の項目を含むコンテナーがあるとします。

{
  "name": "Radimer mountain bike",
  "tags": [
    {
      "name": "road"
    },
    {
      "name": "bike"
    },
    {
      "name": "competitive"
    }
  ]
}

次の最初の例では、この式は SELECT 句内で使用されています。

SELECT
    p.name,
    ARRAY (SELECT VALUE t.name FROM t in p.tags) AS tagNames
FROM
    products p
[
  {
    "name": "Radimer mountain bike",
    "tagNames": [
      "road",
      "bike",
      "competitive"
    ]
  }
]

他のサブクエリと同様、ARRAY 式によるフィルターが可能です。

SELECT
    p.name,
    ARRAY (SELECT VALUE t.name FROM t in p.tags) AS tagNames,
    ARRAY (SELECT VALUE t.name FROM t in p.tags WHERE CONTAINS(t.name, "bike")) AS bikeTagNames
FROM
    products p
[
  {
    "name": "Radimer mountain bike",
    "tagNames": [
      "road",
      "bike",
      "competitive"
    ],
    "bikeTagNames": [
      "bike"
    ]
  }
]

配列式は、サブクエリの FROM 句の後に置くこともできます。

SELECT
    p.name,
    n.t.name AS nonBikeTagName
FROM
    products p
JOIN
    n IN (SELECT VALUE ARRAY(SELECT t FROM t in p.tags WHERE t.name NOT LIKE "%bike%"))
[
  {
    "name": "Radimer mountain bike",
    "nonBikeTagName": "road"
  },
  {
    "name": "Radimer mountain bike",
    "nonBikeTagName": "competitive"
  }
]