Azure Cosmos DB for NoSQL 中的子查询

适用范围: NoSQL

子查询是嵌套在 Azure Cosmos DB for NoSQL 中的另一个查询中的查询。 子查询也称为内部查询内部 SELECT。 包含子查询的语句通常称为外部查询

子查询的类型

有两种主要类型的子查询:

  • “相关”:引用外部查询中值的子查询。 将针对外部查询处理的每个行求值该子查询一次。
  • “非相关”:独立于外部查询的子查询。 它可以独立运行,而不依赖于外部查询。

注意

Azure Cosmos DB 仅支持相关子查询。

可以根据子查询返回的行数和列数进一步分类子查询。 有三种类型:

  • “表格”:返回多个行和多个列。
  • “多值”:返回多个行和单个列。
  • “标量”:返回单个行和单个列。

Azure Cosmos DB for NoSQL 中的查询始终返回单个列(简单值或复杂项)。 因此,仅适用多值子查询和标量子查询。 只能在 FROM 子句中将多值子查询用作关系表达式。 可以在 SELECTWHERE 子句中将标量子查询用作标量表达式,或者在 FROM 子句中用作关系表达式。

多值子查询

多值子查询返回一组项,始终在 FROM 子句中使用。 它们用于:

  • 优化 JOIN(自联接)表达式。
  • 求值高开销的表达式一次,并多次引用。

优化自联接表达式

多值子查询可以通过在 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”的标记、至少一个 quantity010 之间以及至少一个 backstockfalse 的仓库的项目。 此处的 JOIN 表达式将在应用任何筛选器之前,为每个匹配项执行 tagsonHandQuantitieswarehouseStock 数组的所有项的叉积计算

WHERE 子句将对每个 <c, t, n, s> 元组应用筛选谓词。 例如,如果一个匹配项在三个数组中的每一个数组中都有 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 数组中只有一个项与筛选器相匹配,而 quantity 和 stock 数组各有 5 个项。 然后,JOIN 表达式将扩展为 1 x 1 x 5 x 5 (25) 项,而不是第一个查询中的 1,000 项。

求值一次,引用多次

子查询可以帮助优化包含高开销表达式(例如,用户定义的函数 (UDF)、复杂字符串或算术表达式)的查询。 可以结合 JOIN 表达式使用子查询,以求值该表达式一次,但引用它多次。

假设你已经定义了以下 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 一次:

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 表达式始终生成单个行。

模拟与外部引用数据的联接

你可能经常需要引用极少发生变化的静态数据,例如度量单位。 最好不要复制查询中每个项的静态数据。 避免这种复制可以节省存储空间,并通过减小单个项大小来提高写入性能。 可以使用子查询通过静态引用数据的集合来模拟内部联接语义。

例如,请考虑以下一组度量值:

名称 “乘数” 基础单位
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

标量子查询

标量子查询表达式是求值为单个值的子查询。 标量子查询表达式的值是该子查询的投影(SELECT 子句)值。 可以在标量表达式有效的多个位置使用标量子查询表达式。 例如,可以在 SELECTWHERE 子句中的任一表达式内使用标量子查询。

不过,使用标量子查询并不总是有助于优化查询。 例如,将标量子查询作为自变量传递给系统定义的函数或用户定义的函数并不能在减少资源单位 (RU) 消耗量与延迟方面带来好处。

标量子查询可以进一步分类为:

  • 简单表达式标量子查询
  • 聚合标量子查询

简单表达式标量子查询

简单表达式标量子查询属于相关子查询,其中的某个 SELECT 子句不包含任何聚合表达式。 这些子查询不能提供优化方面的优势,因为编译器会将其转换为一个较大的简单表达式。 内部与外部查询之间没有相关的上下文。

作为第一个示例,请考虑此普通查询。

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": "aaaaaaaa-0000-1111-2222-bbbbbbbbbbbb",
    "name": "Winter glove"
  },
  {
    "id": "bbbbbbbb-1111-2222-3333-cccccccccccc"
  },
  {
    "id": "cccccccc-2222-3333-4444-dddddddddddd"
  }
]

聚合标量子查询

聚合标量子查询在其投影或筛选器中包含一个求值为单个值的聚合函数。

作为第一个示例,请考虑具有以下字段的项。

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

以下子查询在其投影中包含单个聚合函数表达式。 此查询计算每个项的所有标记。

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

由于查询引擎不区分布尔表达式和其他任何标量表达式,因此,可以同时在 SELECTWHERE 子句中使用 EXISTS。 此行为不同于 T-SQL,其中布尔表达式仅限于筛选器。

如果 EXISTS 子查询返回的单个值为 undefined,则 EXISTS 的计算结果为 false。 例如,考虑以下不返回任何内容的查询。

SELECT VALUE
    undefined

如果使用 EXISTS 表达式和前面的查询作为子查询,则表达式返回 false

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

如果省略上述子查询中的 VALUE 关键字,则子查询的计算结果为具有单个空对象的数组。

SELECT
    undefined
[
  {}
]

此时,EXISTS 表达式的求值结果为 true,因为对象 ({}) 从技术上退出。

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

ARRAY_CONTAINS 的一个常见用例是根据数组中是否存在某个项来筛选项。 在本例中,我们将检查 tags 数组是否包含名为“outerwear”的项。

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
    }
  ]
}

现在,请考虑以下查询,该查询基于每个项中数组中的 typequantityOnHand 属性进行筛选。

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"
  }
]