Skip to content

Nested vs Join 关系建模 ​

1. 关系型数据在 ES 中的挑战 ​

ES 是面向文档的搜索引擎,不支持关系型数据库那样的任意 JOIN,但可通过 join 字段实现受限的父子关系查询。在 ES 中建模关联关系,需要根据查询模式选择合适的方案。

2. 方案一:对象数组(Object Array) ​

最简单的方式,但会丢失对象边界:

json
PUT /orders
{
  "mappings": {
    "properties": {
      "order_id": { "type": "keyword" },
      "items": {
        "properties": {
          "product": { "type": "text" },
          "price": { "type": "integer" }
        }
      }
    }
  }
}

// 存储
PUT /orders/_doc/1
{
  "order_id": "ORD001",
  "items": [
    { "product": "手机", "price": 5000 },
    { "product": "耳机", "price": 200 }
  ]
}

问题:对象数组被扁平化存储,查询"手机 price < 300"会错误匹配(手机关联了 200)。

3. 方案二:Nested 类型 ​

保留对象边界,每个嵌套对象作为独立隐藏文档:

json
PUT /orders
{
  "mappings": {
    "properties": {
      "order_id": { "type": "keyword" },
      "items": {
        "type": "nested",
        "properties": {
          "product": { "type": "text" },
          "price": { "type": "integer" }
        }
      }
    }
  }
}

// 查询:手机且价格 ≤ 300
GET /orders/_search
{
  "query": {
    "nested": {
      "path": "items",
      "query": {
        "bool": {
          "must": [
            { "match": { "items.product": "手机" } },
            { "range": { "items.price": { "lte": 300 } } }
          ]
        }
      }
    }
  }
}

优点:保留对象边界,查询精确。 缺点:更新嵌套对象通常需要重建并重新索引根文档及其嵌套文档;嵌套对象数量影响性能。

4. 方案三:Parent-Child(Join) ​

父子文档独立存储,通过 Join Field 关联:

json
PUT /company
{
  "mappings": {
    "properties": {
      "doc_type": {
        "type": "join",
        "relations": { "department": "employee" }
      },
      "name": { "type": "text" }
    }
  }
}

// 写入父文档
PUT /company/_doc/dept_1
{
  "doc_type": { "name": "department" },
  "name": "研发部"
}

// 写入子文档:以父文档 _id 作 routing,并指定 join 关系
PUT /company/_doc/emp_1?routing=dept_1
{
  "doc_type": { "name": "employee", "parent": "dept_1" },
  "name": "张三"
}

// 查询:找出有员工叫「张三」的部门
GET /company/_search
{
  "query": {
    "has_child": {
      "type": "employee",
      "query": { "match": { "name": "张三" } }
    }
  }
}

优点:子文档可独立更新。 缺点:关联查询需要跨文档匹配,性能相对较差;父子文档必须通过 routing 落在同一分片才能正确关联。

5. 方案四:反规范化(宽表) ​

将关联数据冗余存储到一个文档中:

json
PUT /orders_flat
{
  "mappings": {
    "properties": {
      "order_id": { "type": "keyword" },
      "product_name": { "type": "text" },
      "product_price": { "type": "integer" },
      "product_category": { "type": "keyword" }
    }
  }
}

优点:查询性能通常最好。 缺点:数据冗余,更新成本高。

6. 选型决策 ​

维度ObjectNestedParent-Child反规范化
对象边界❌ 丢失✅ 保留✅ 保留✅ 保留
查询性能✅ 较好(无需数组元素间精确匹配)✅ 较好(子对象规模可控时)❌ 相对较差(有额外关联查询开销)✅ 通常最好(查询条件可由冗余字段直接满足)
更新性能✅ 较好(仍需重索引根文档)❌ 成本较高(需重索引根文档及嵌套文档)✅ 子文档可独立更新❌ 可能较高(取决于冗余数据维护范围)
数据冗余低低低高
适用场景不需要精确匹配同数组内组合条件、子对象规模可控子文档频繁独立更新查询优先

7. 最佳实践 ​

  • 对象间关系简单、需在数组元素内组合查询条件且子对象规模可控 → Nested
  • 子文档需要独立更新 → Parent-Child
  • 查询性能优先 → 反规范化(宽表)
  • 关联关系复杂 → 应用层关联
  • 每个父文档的嵌套对象数量应结合子对象数量、文档大小和查询负载评估,无通用固定阈值
  • Parent-Child 写入子文档时必须指定 routing,使其与父文档落在同一分片

Released under the MIT License.