一、 核心设计原则:反规范化(Denormalization)
这是 ES 中最重要、最常用、也是性能最高的关联设计方法。
1. 是什么?
将业务关联的多个实体(表)的数据,在写入 ES 之前,合并到一个文档中。换句话说,用空间换时间,用数据冗余避免昂贵的查询时关联。
2. 何时使用?
- 一对多关系(例如:一篇博客文章和它的所有评论)。
- 多对一关系(例如:多个订单都属于同一个用户)。
- 关联数据不频繁变更,或者变更后对实时性要求不高。
3. 案例:电商订单模型(Order - Product)
在关系数据库中,我们有 orders
表和 products
表,通过 product_id
关联。
在 ES 中,我们将其反规范化为一个 orders
索引。
json
// RDBMS 结构
orders: [id, user_id, total_amount, created_at]
order_items: [id, order_id, product_id, quantity, price]
products: [id, name, category, description]
// ES Denormalized 映射设计
PUT /orders
{
"mappings": {
"properties": {
"order_id": { "type": "keyword" },
"user_id": { "type": "keyword" },
"total_amount": { "type": "float" },
"created_at": { "type": "date" },
"items": { // 将订单项和商品信息直接内嵌
"type": "nested", // 使用nested类型保证数组对象的独立性
"properties": {
"item_id": { "type": "keyword" },
"quantity": { "type": "integer" },
"price": { "type": "float" },
// 以下是反规范化的产品信息
"product_id": { "type": "keyword" },
"product_name": { "type": "text", "fields": { "raw": { "type": "keyword" } } },
"product_category": { "type": "keyword" }
}
}
}
}
}
4. 优缺点:
- 优点:查询性能极致。一次查询即可获取所有所需数据,无需额外关联。
- 缺点 :
- 数据冗余 :商品信息(如
product_name
)会在每个包含它的订单中重复存储。 - 更新困难:如果商品名称变更,需要更新所有包含该商品的订单文档。通常通过后续的异步作业(如 Spark、Logstash)来执行这种大规模更新。
- 数据冗余 :商品信息(如
二、 应用端关联(Application-side Joins)
1. 是什么?
由应用程序而非 ES 来执行关联逻辑。通常分为两步:
-
第一个查询从 ES 中获取一组结果(例如 IDs)。
-
根据第一步的结果,发起第二个查询到 ES 或数据库,获取关联数据。
2. 何时使用?
- 关联的实体完全独立,更新非常频繁。
- 关联数据不需要在每次查询时都返回(例如,只在详情页才需要展示用户信息)。
- 你可以接受较高的查询延迟(两次网络往返)。
3. 案例:用户评论系统(Comment - User)
comments
索引存储评论内容,包含一个user_id
。users
索引存储用户详情(姓名、头像等)。
json
// 第一步:查询评论
GET /comments/_search
{
"query": { "match": { "post_id": "123" } },
"_source": ["content", "created_at", "user_id"] // 只取评论内容和用户ID
}
// 应用端:从返回的 hits 中提取所有 user_id (e.g., [101, 202])
// 第二步:根据 user_id 批量获取用户信息
GET /users/_mget
{
"ids": ["101", "202"]
}
// 最后,在应用端将用户信息"缝合"到评论数据中并返回给客户端。
4. 优缺点:
- 优点:数据模型清晰,无冗余,易于更新。
- 缺点:查询延迟高(N+1 查询问题),对应用程序逻辑有依赖。
三、 父子关联(Parent-Child Join)
(注:已在上一节详细介绍,此处简要回顾)
1. 是什么?
在同一索引内,通过 join
类型字段建立文档间的父子关系。父子文档是独立的,但必须路由到同一分片。
2. 何时使用?
- 父子数据更新非常频繁 且需要独立更新(反规范化不适用)。
- 子文档数量巨大,反规范化会使父文档变得臃肿。
- 你能接受其显著的性能开销。
3. 案例:问答系统(Question - Answer)
一个问题(父)对应大量回答(子),回答频繁新增。
json
PUT /faq
{
"mappings": {
"properties": {
"qa_relationship": {
"type": "join",
"relations": { "question": "answer" }
},
"body": { "type": "text" },
"user": { "type": "keyword" }
}
}
}
4. 优缺点:
- 优点:父子数据独立,更新灵活。
- 缺点:性能差,内存消耗大,使用复杂(需处理路由)。
四、 宽表预关联(Pre-joined Wide Tables)
1. 是什么?
在数据写入 ES 之前,通过 ETL(Extract, Transform, Load)过程,在数据源头(如数据仓库、业务数据库)完成所有需要的关联查询,生成一张包含所有所需字段的"宽表",再整批导入 ES。
2. 何时使用?
- 数据主要用于只读的分析和搜索(如报表、看板、大屏)。
- 数据更新有明确的批处理窗口(如每天凌晨更新一次)。
- 关联逻辑非常复杂,无法用上述方案简单实现。
3. 案例:电商数据分析宽表
json
PUT /product_analytics_wide
{
"mappings": {
"properties": {
"product_id": { "type": "keyword" },
"product_name": { "type": "keyword" },
"category_name": { "type": "keyword" },
"brand_name": { "type": "keyword" },
"total_sales_volume": { "type": "long" }, // 预聚合好的数据
"avg_rating": { "type": "float" }, // 预聚合好的数据
"first_sale_date": { "type": "date" }
// ... 几十个其他维度/指标字段
}
}
}
这张宽表可能由 products
, categories
, brands
, orders
, order_items
, reviews
等多张表关联和聚合后生成。
4. 优缺点:
- 优点:查询性能达到巅峰,非常适合 OLAP 场景。
- 缺点:数据延迟高,完全失去了实时性,ETL 流程复杂。
五、 架构师决策指南:如何选择?
面对一个关联需求,作为架构师,你可以遵循以下决策流程:
性能最优,最符合ES哲学] B -- 是 --> D{子数据量是否巨大
且需独立更新?} D -- 是 --> E[谨慎选择:父子关联
需接受其性能开销和复杂度] D -- 否 --> F[选择:应用端关联
保持数据独立,由应用层处理] G[分析型/只读场景] --> H[选择:宽表预关联
在ETL阶段完成关联]
总结:Elasticsearch 关联设计哲学
- 第一选择永远是反规范化:这是最符合 ES 设计理念的方式,能带来极致的性能体验。不要害怕数据冗余。
- 如果反规范化不行,考虑应用端关联:这保持了数据的独立性,将复杂度转移到了应用层,是一种务实的解决方案。
- 父子关联是最后的逃生舱口:仅在万不得已时(数据量大、更新频繁且必须独立)使用,并务必进行充分的性能压测。
- 为分析场景设计宽表:如果业务是只读的、滞后的分析需求,那么在数据进入 ES 之前就完成所有关联(ETL),向 ES 提供最简单的扁平宽表。