Microsoft Fabric 中的 Cosmos DB 中的索引

Cosmos DB 是一个与架构无关的数据库,可用于循环访问应用程序,而无需处理架构或索引管理。 这也称为 读取架构,这意味着当将数据写入数据库时,Cosmos DB 不会对数据强制实施架构。 相反,在读取或查询数据时,架构将投影到应用程序中定义的类中。

Microsoft Fabric 中的 Cosmos DB 中的索引旨在提供快速灵活的查询性能,无论数据如何发展。 默认情况下,Cosmos DB 会自动为容器中的所有项为每个属性编制索引,而无需定义任何架构或配置辅助索引。

概念树

每次在容器中存储项时,项的内容都投影为 JSON 文档,然后转换为树表示形式。 此转换意味着,该项的每个属性都在树中以节点的形式呈现。 伪根节点被创建为项的所有第一级属性的父级。 叶节点包含项带有的实际标量值。

例如,请看以下项:

{
  "locations": [
    { 
      "country": "Germany", 
      "city": "Berlin" 
    },
    { 
      "country": "France", 
      "city": "Paris" 
    }
  ],
  "headquarters": { 
    "country": "Belgium", 
    "employees": 250 
  },
  "exports": [
    { 
      "city": "Moscow" 
    },
    { 
      "city": "Athens" 
    }
  ]
}

此概念树表示示例 JSON 项:

  • locations
    • 0
      • country: Germany
      • city: Berlin
    • 1
      • country: France
      • city: Paris
  • headquarters
    • country: Belgium
    • employees: 250
  • exports
    • 0
      • city: Moscow
    • 1
      • city: Athens

Cosmos DB 中项的树表示关系图。

显示具有三个分支的根节点的树图:“位置”、“总部”和“导出”。 “位置”拆分为两个编号节点,每个节点具有两个与位置相关的子节点(“德国或柏林”和“法国或巴黎”)。 “总部”位于“比利时”,并拥有“250”名“雇员”。 “导出”拆分为两个编号节点,每个节点都有一个“城市”子节点(“莫斯科”和“雅典”)。

请注意数组在树中的编码方式:数组中的每个条目都获取一个中间节点,该节点标有数组中该条目的索引。 例如,第一个条目是 0 ,第二个条目是 1

属性路径

Cosmos DB 将项转换为树,因为它允许系统使用这些树中的路径引用属性。 若要获取属性的路径,可从根节点到该属性来遍历树,并将每个遍历的节点的标签连接起来。

下面是之前描述的示例项中每个属性的路径:

路径 价值
/locations/0/country "Germany"
/locations/0/city "Berlin"
/locations/1/country "France"
/locations/1/city "Paris"
/headquarters/country "Belgium"
/headquarters/employees 250
/exports/0/city "Moscow"
/exports/1/city "Athens"

当写入项时,Cosmos DB 有效地为每个属性的路径及其对应的值编制索引。

索引类型

Cosmos DB 目前支持四种类型的索引。

定义索引策略时,可以配置这些索引类型。

范围索引

范围索引基于已排序的树形结构。 这是默认索引类型,在定义索引策略时不需要指定。 范围索引类型用于:

  • 相等查询:

    SELECT
      *
    FROM
      container c
    WHERE
      c.property = 'value'
    
    SELECT
      *
    FROM
      container c
    WHERE
      c.property IN ("value1", "value2", "value3")
    
  • 数组元素上的相等匹配

    SELECT
      *
    FROM
      container c
    WHERE
      ARRAY_CONTAINS(c.tags, "tag1")
    
  • 范围查询:

    SELECT
      *
    FROM
      container c
    WHERE
      c.property > 0
    

    注释

    适用于 ><>=<=!=

  • 检查属性是否存在:

    SELECT
      *
    FROM
      container c
    WHERE
      IS_DEFINED(c.property)
    
  • 字符串系统函数:

    SELECT
      *
    FROM
      container c
    WHERE
      CONTAINS(c.property, "value")
    
    SELECT
      *
    FROM
      container c
    WHERE
      STRINGEQUALS(c.property, "value")
    
  • ORDER BY 查询:

    SELECT
      *
    FROM
      container c
    ORDER BY
      c.property
    
  • JOIN 查询:

    SELECT
      d
    FROM
      container c
    JOIN
      d IN c.properties
    WHERE
      d = 'value'
    

范围索引可用于标量值(字符串或数字)。 新建容器的默认索引策略会对任何字符串或数字强制使用范围索引。

注释

ORDER BY 单个属性排序的子句 始终 需要范围索引,如果它引用的路径没有范围索引,则失败。 同样, ORDER BY 按多个属性排序的查询 始终 需要复合索引。

空间索引

空间 索引支持对地理空间对象(如点、线条、多边形和多多边形)进行高效的查询。 这些查询使用ST_DISTANCEST_WITHINST_INTERSECTS关键字。 下面是使用空间索引类型的一些示例:

  • 地理空间距离查询:

    SELECT
      *
    FROM
      container c
    WHERE
      ST_DISTANCE(c.property, { "type": "Point", "coordinates": [0.0, 10.0] }) < 40
    
  • 在查询的地理空间:

    SELECT
      *
    FROM
      container c
    WHERE
      ST_WITHIN(c.property, {"type": "Point", "coordinates": [0.0, 10.0] })
    
  • 地理空间相交查询:

    SELECT
      *
    FROM
      container c
    WHERE
      ST_INTERSECTS(c.property, { 'type':'Polygon', 'coordinates': [[ [31.8, -5], [32, -5], [31.8, -5] ]]  })  
    

空间索引可在格式正确的 GeoJSON 对象上使用。 目前支持点、线串、多边形和多面。

复合索引

对多个字段执行操作时,组合索引可提高效率。 复合索引类型用于:

  • 对多个属性的 ORDER BY 查询:

    SELECT
      *
    FROM
      container c
    ORDER BY
      c.property1,
      c.property2
    
  • 使用筛选器和 ORDER BY 的查询。 如果在 ORDER BY 子句中添加筛选器属性,则这些查询可使用组合索引。

    SELECT
      *
    FROM
      container c
    WHERE
      c.property1 = 'value'
    ORDER BY
      c.property1,
      c.property2
    
  • 对两个或多个属性进行查询,其中至少有一个属性使用相等筛选器:

    SELECT
      *
    FROM
      container c
    WHERE
      c.property1 = 'value' AND
      c.property2 > 'value'
    

只要一个筛选器谓词使用其中一种索引类型,查询引擎就会先评估该索引类型,然后再扫描其余部分。 例如,如果有 SQL 查询,例如 SELECT * FROM c WHERE c.department = "Information Technology" and CONTAINS(c.team, "Pilot")

  • 此查询首先对使用索引的 department = "Information Technology" 条目应用筛选器。 然后,它会通过后续管道处理所有 department = "Information Technology" 条目,以进行 CONTAINS 筛选器谓词的评估。

  • 使用执行完全扫描的函数时,可以加快查询速度并避免对容器的全面扫描 CONTAINS。 可以添加更多使用索引的筛选器谓词来加快这些查询的速度。 筛选子句的顺序并不重要。 查询引擎将确定哪些谓词更具选择性,并相应地运行查询。

矢量索引

矢量索引可在使用 系统函数执行矢量搜索时提高效率VECTORDISTANCE。 使用向量索引时,矢量搜索的延迟较低、吞吐量更高,RU 消耗量更少。 Cosmos DB 支持大小在 4,096 维以下的任何矢量嵌入(文本、图像、多模式等)。

  • ORDER BY 矢量搜索查询:

    SELECT TOP 10
      *
    FROM
      container c
    ORDER BY
      VECTORDISTANCE(c.vector1, c.vector2)
    
  • 矢量搜索查询中相似性分数的投影:

    SELECT TOP 10
      c.name,
      VECTORDISTANCE(c.vector1, c.vector2) AS score
    FROM
      container c
    ORDER BY
      VECTORDISTANCE(c.vector1, c.vector2)
    
  • 基于相似性分数的范围筛选器。

    SELECT TOP 10
      *
    FROM
      container c
    WHERE
      VECTORDISTANCE(c.vector1, c.vector2) > 0.8
    ORDER BY
      VECTORDISTANCE(c.vector1, c.vector2)
    

重要

创建后,矢量策略和矢量索引是不可变的。 若要进行更改,请创建新的集合。

索引使用情况

查询引擎可采用 5 种方式来评估查询筛选器,按效率从高到低排序:

  • 索引查找
  • 精确索引扫描
  • 扩展索引扫描
  • 完全索引扫描
  • 完全扫描

索引属性路径时,查询引擎会尽可能高效地自动使用索引。 除了索引新的属性路径外,无需配置任何内容即可优化查询使用索引的方式。 查询的请求单位 (RU) 费用是索引使用量的 RU 费用与加载项的 RU 费用之和。

下表总结了 Cosmos DB 中使用索引的不同方式:

Lookup 类型 DESCRIPTION 常见示例 从索引使用情况收取费用 从事务数据存储加载项的费用
索引查找 只读取所需的索引值,并且只从事务数据存储中加载匹配项 相等筛选器,IN 每个相等筛选器的费用相同 根据查询结果中的项数增加
精确索引扫描 索引值的二进制搜索,并且只从事务数据存储中加载匹配项 范围比较(>、<、<= 或 >=),StartsWith 与索引查找相比,根据索引属性的基数略有增加 根据查询结果中的项数增加
扩展索引扫描 索引值的优化搜索(但比二进制文搜索的效率低),并且只从事物数据存储中加载匹配项 StartsWith(不区分大小写的),StringEquals(不区分大小写) 根据索引属性的基数略有增加 根据查询结果中的项数增加
完整索引扫描 读取一组非重复的索引值,并且只从事务数据存储中加载匹配项 Contains、EndsWith、RegexMatch、LIKE 根据索引属性的基数呈线性增加 根据查询结果中的项数增加
完全扫描 从事务数据存储加载所有项 Upper、Lower 根据容器中的项数增加

编写查询时,应采用尽可能有效使用索引的筛选谓词。 例如,如果 StartsWithContains 都适合你的用例,应选择 StartsWith,因为它将执行精确索引扫描,而不是完全索引扫描。

索引使用情况详细信息

小窍门

本部分介绍有关查询如何使用索引的更多详细信息。 学习如何开始使用 Cosmos DB 时,不需要此级别的详细信息,但为好奇的用户详细记录。 我们将参考在本文档前面分享的示例项:

请考虑以下两个示例项:

[
  {
    "id": 1,
    "locations": [
      { "country": "Germany", "city": "Berlin" },
      { "country": "France", "city": "Paris" }
    ],
    "headquarters": { "country": "Belgium", "employees": 250 },
    "exports": [
      { "city": "Moscow" },
      { "city": "Athens" }
    ]
  },
  {
    "id": 2,
    "locations": [
      { "country": "Ireland", "city": "Dublin" }
    ],
    "headquarters": { "country": "Belgium", "employees": 200 },
    "exports": [
      { "city": "Moscow" },
      { "city": "Athens" },
      { "city": "London" }
    ]
  }
]

Cosmos DB 使用倒排索引。 索引的工作原理是将每个 JSON 路径映射到包含该值的一组项中。 对于容器,项 ID 映射跨多个不同的索引页表示。 以下是包含两个示例项的容器的倒排索引示例示意图:

路径 价值 项标识符列表
/locations/0/country Germany [1]
/locations/0/country Ireland [2]
/locations/0/city Berlin [1]
/locations/0/city Dublin [2]
/locations/1/country France [1]
/locations/1/city Paris [1]
/headquarters/country Belgium [1, 2]
/headquarters/employees 200 [2]
/headquarters/employees 250 [1]

倒排索引具有 2 个重要属性:

  • 对于给定路径,值按升序排序。 因此,查询引擎可轻松地从索引中提供 ORDER BY

  • 对于给定路径,查询引擎可扫描一组非重复的可能值,以确定存在结果的索引页。

查询引擎使用倒排索引的方式有以下 4 种:

索引查找

请考虑下列查询:

SELECT
  location
FROM
  location IN company.locations
WHERE
  location.country = 'France'

查询谓词(筛选任何位置的区域为“法国”的项)将匹配此处调用的路径:

  • locations
    • 1
      • country: France

与 Cosmos DB 中项的树表示形式中的特定路径匹配的遍历(搜索)关系图。

显示具有三个分支的根节点的树图:“位置”、“总部”和“导出”。 “位置”拆分为两个编号节点,每个节点具有两个与位置相关的子节点(“德国/柏林”和“法国/巴黎”)。 “总部”位于“比利时”,并拥有“250”名“雇员”。 “导出”拆分为两个编号节点,每个节点都有一个“城市”子节点(“莫斯科”和“雅典”)。 突出了“位置”、“1”、“地点”和“法国”的路径。

由于此查询具有相等筛选器,因此在遍历此树后,我们可以快速识别包含查询结果的索引页。 在这种情况下,查询引擎将读取包含项 1 的索引页。 索引查找是使用索引最有效的方式。 通过索引查找,我们只读取必要的索引页,并且只加载查询结果中的项。 因此,无论总数据量是多少,索引查找的查找时间都很短,并且 RU 费用低。

精确索引扫描

请考虑下列查询:

SELECT
  *
FROM
  company
WHERE
  company.headquarters.employees > 200

查询谓词(对具有超过 200 名员工的项进行筛选)可通过 headquarters/employees 路径的精确索引扫描进行评估。 执行精确索引扫描时,查询引擎首先对一组非重复的可能值进行二进制搜索,来查找 200 路径的值 headquarters/employees 的位置。 由于每个路径的值都是按升序排序的,因此查询引擎可轻松执行二进制搜索。 查询引擎找到值 200 后,将开始读取所有剩余的索引页(按升序反向)。

查询引擎可执行二进制搜索来避免扫描不必要的索引页,因此精确索引扫描的延迟和 RU 费用与索引查找操作相当。

扩展索引扫描

请考虑下列查询:

SELECT
  *
FROM
  company
WHERE
  STARTSWITH(company.headquarters.country, "United", true)

查询谓词(筛选总部位于位置以不区分大小写的“United”开头的项目)可以通过扩展headquarters/country路径上的索引扫描来评估。 执行扩展索引扫描的操作具有一些优化,可帮助避免扫描每个索引页,但比精确索引扫描的二进制搜索略贵。

例如,在评估不区分大小写的 StartsWith 时,查询引擎将检查索引中是否有大写值和小写值混用的情况。 此优化使查询引擎能够避免读取大多数索引页。 不同的系统函数具有不同的优化,它们可用于避免读取每个索引页,因此我们可将其大致分类为扩展索引扫描。

完全索引扫描

请考虑下列查询:

SELECT
  *
FROM
  company
WHERE
  CONTAINS(company.headquarters.country, "United")

查询谓词(对总部位于包含“United”的位置的项目进行筛选)可通过 headquarters/country 路径的索引扫描进行评估。 与精确索引扫描不同,完全索引扫描将始终扫描一组非重复的可能值,以确定存在结果的索引页。 在这种情况下,索引上会运行 CONTAINS。 索引扫描的索引查找时间和 RU 费用会随着路径基数的增加而增加。 换句话说,查询引擎需要扫描的可能的非重复值越多,执行完全索引扫描的延迟和 RU 费用就越高。

例如,请考虑两个属性:towncountry。 town 的基数是 5,000,country 的基数是 200。 下面是两个示例查询,每个查询都有一个系统函数 CONTAINS ,它对 town 属性执行完全索引扫描。 第一个查询使用的请求单位(RU)多于第二个查询,因为城镇基数高于 country

SELECT
  *
FROM
  container c
WHERE
 CONTAINS(c.town, "Red", false)
SELECT
  *
FROM
  c
WHERE
  CONTAINS(c.country, "States", false)

完全扫描

在某些情况下,查询引擎可能无法使用索引评估查询筛选器。 在这种情况下,为了评估查询筛选器,查询引擎需要从事务存储中加载所有的项。 完全扫描不使用索引,而且其 RU 费用根据总数据大小呈线性增加。 幸运的是,很少有需要完全扫描的操作。

没有定义的矢量索引的矢量搜索查询

如果未定义矢量索引策略并使用 VECTORDISTANCE 子句中的 ORDER BY 系统函数,则此查询将生成完全扫描,并且 RU 费用高于你定义的向量索引策略。 类似地,如果将 VECTORDISTANCE 暴力布尔值设置为 true,并且没有为矢量路径定义 flat 索引,则会进行完全扫描。

具有复杂筛选表达式的查询

在前面的示例中,我们只考虑到具有简单筛选表达式的查询(例如,只具有单个相等或范围筛选器的查询)。 实际上,大多数查询都具有更复杂的筛选表达式。

请考虑下列查询:

SELECT
  *
FROM
  company
WHERE
  company.headquarters.employees = 200 AND CONTAINS(company.headquarters.country, "United")

要执行此查询,查询引擎必须对 headquarters/employeesheadquarters/country 分别执行索引查找和完全索引扫描。 查询引擎具有内部启发法,用于尽可能高效地评估查询筛选表达式。 在这种情况下,查询引擎通过首先执行索引查找来避免读取不必要的索引页。 例如,如果只有 50 个项与相等筛选器匹配,则查询引擎只需要在包含这 50 个项的索引页上评估 CONTAINS。 无需对整个容器执行完全索引扫描。

标量聚合函数的索引使用率

具有聚合函数的查询必须以独占方式依赖索引才能使用它。

在某些情况下,索引会返回假正。 例如,在 CONTAINS 索引上求值时,索引中的匹配项数可能超过查询结果数。 查询引擎会加载所有索引匹配项,评估已加载的项上的筛选器,并且只返回正确的结果。

对于大多数查询,加载假正索引匹配项不会对索引利用率产生任何显著影响。

例如,考虑以下查询:

SELECT
  *
FROM
  company
WHERE
  CONTAINS(company.headquarters.country, "United")

系统 CONTAINS 函数可能会返回一些误报匹配,因此查询引擎需要验证每个加载的项是否与筛选器表达式匹配。 在此示例中,查询引擎可能只需要加载额外的几个项,因此对索引利用率和 RU 费用的影响很小。

但是,具有聚合函数的查询必须以独占方式依赖索引才能使用它。 例如,考虑使用具有 COUNT 聚合的以下查询:

SELECT
  COUNT(1)
FROM
  company
WHERE
  CONTAINS(company.headquarters.country, "United")

与在第一个示例中一样, CONTAINS 系统函数可能会返回一些误报匹配。 但与 SELECT * 查询不同,COUNT 查询无法通过评估已加载项上的筛选表达式来验证所有索引匹配项。 COUNT 查询必须以独占方式依赖索引,因此如果筛选表达式可能返回假正匹配项,查询引擎将采用完全扫描。

具有以下聚合函数的查询必须以独占方式依赖索引,因此评估某些系统函数需要采用完全扫描。