elasticsearch实践篇:跨表join查询补充篇

本文深入探讨了Elasticsearch中的四种关联查询方法:服务端Join、反范式宽表处理、嵌套文档和父子文档,分析了各自的适用场景、优缺点及实现方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在上一篇elasticsearch实践篇:跨表join查询中讲过2种方式,其实并不全面。ES还有2种方法来处理数据实体间的关联关系:Nested objects(嵌套文档)和Parent/child relationships(父子文档)。

1. Application-side joins(服务端Join或客户端Join)

这种方式,索引之间完全独立(利于对数据进行标准化处理,如便于上述两种增量同步的实现),由应用端的多次查询来实现近似关联关系查询。这种方法适用于第一个实体只有少量的文档记录的情况(使用ES的terms查询具有上限,默认1024,具体可在elasticsearch.yml中修改),并且最好它们很少改变。这将允许应用程序对结果进行缓存,并避免经常运行第一次查询。

2. Data denormalization(反范式) 

这种方式,通俗点就是通过字段冗余,以一张大宽表来实现粗粒度的index,这样可以充分发挥扁平化的优势。但是这是以牺牲索引性能及灵活度为代价的。使用的前提:冗余的字段应该是很少改变的;比较适合与一对少量关系的处理。当业务数据库并非采用非规范化设计时,这时要将数据同步到作为二级索引库的ES中,就很难使用上述增量同步方案,必须进行定制化开发,基于特定业务进行应用开发来处理join关联和实体拼接。

宽表处理在处理一对多、多对多关系时,会有字段冗余问题,适合“一对少量”且这个“一”更新不频繁的应用场景。宽表化处理,在查询阶段如果只需要“一”这部分时,需要进行结果去重处理(可以使用ES5.x的字段折叠特性,但无法准确获取分页总数,产品设计上需采用上拉加载分页方式)

3. Nested objects(嵌套文档) 

索引性能和查询性能二者不可兼得,必须进行取舍。嵌套文档将实体关系嵌套组合在单文档内部(类似与json的一对多层级结构),这种方式牺牲索引性能(文档内任一属性变化都需要重新索引该文档)来换取查询性能,可以同时返回关系实体,比较适合于一对少量的关系处理。 

当使用嵌套文档时,使用通用的查询方式是无法访问到的,必须使用合适的查询方式(nested query、nested filter、nested facet等),很多场景下,使用嵌套文档的复杂度在于索引阶段对关联关系的组织拼装。

4. Parent/child relationships(父子文档) 

父子文档牺牲了一定的查询性能来换取索引性能,适用于一对多的关系处理。其通过两种type的文档来表示父子实体,父子文档的索引是独立的。父-子文档ID映射存储在 Doc Values 中。当映射完全在内存中时, Doc Values 提供对映射的快速处理能力,另一方面当映射非常大时,可以通过溢出到磁盘提供足够的扩展能力。 在查询parent-child替代方案时,发现了一种filter-terms的语法,要求某一字段里有关联实体的ID列表。基本的原理是在terms的时候,对于多项取值,如果在另外的index或者type里已知主键id的情况下,某一字段有这些值,可以直接嵌套查询。具体可参考官方文档的示例:通过用户里的粉丝关系,微博和用户的关系,来查询某个用户的粉丝发表的微博列表。

父子文档相比嵌套文档较灵活,但只适用于“一对大量”且这个“一”不是海量的应用场景,该方式比较耗内存和CPU,这种方式查询比嵌套方式慢5~10倍,且需要使用特定的has_parent和has_child过滤器查询语法,查询结果不能同时返回父子文档(一次join查询只能返回一种类型的文档)。而受限于父子文档必须在同一分片上,ES父子文档在滚动索引、多索引场景下对父子关系存储和联合查询支持得不好,而且子文档type删除比较麻烦(子文档删除必须提供父文档ID)。

如果业务端对查询性能要求很高的话,还是建议使用宽表化处理的方式,这样也可以比较好地应对聚合的需求。

<think>好的,用户问的是分后如何确定查询哪个。之前已经介绍过查询条件的设计,现在这个问题更具体,是关于分后的路由机制。我需要先回忆一下之前的内容,确保不重复,同时补充新的信息。 用户可能是开发人员或者系统架构师,他们可能正在设计分方案,或者在优化现有系统。他们需要了解具体的路由方法,可能关心如何高效地定位数据所在的,避免全扫描带来的性能问题。 分路由的核心是分片键的选择和路由算法。需要解释不同的分片策略,比如哈希、范围、一致性哈希等,以及对应的路由方式。还要提到中间件的作用,比如ShardingSphere或者MyCAT,这些工具如何自动化路由。 用户可能对具体实现细节感兴趣,比如如何根据分片键计算名,或者如何处理没有分片键的查询。此外,可能需要举例说明,比如订单按user_id分,如何通过user_id找到对应的。还要注意可能遇到的陷阱,比如分片键选择不当导致数据分布不均,或者查询的问题。 需要结构清晰,分点说明,结合实例,让用户能够理解不同的路由方法及其适用场景。同时,要补充实际应用中的最佳实践,比如使用中间件还是自行实现路由逻辑,以及如何处理特殊情况。</think>分后确定查询目标的核心是通过 **分片键(Sharding Key)** 和 **路由算法** 计算数据所在的物理。以下是具体实现方式和常见策略: -- ### **一、分路由的核心逻辑** #### **1. 分片键(Sharding Key)** - **定义**:数据分的依据字段(如 `user_id`、`order_id`、`时间`)。 - **规则**:查询时必须携带分片键(或能推导出分片键),否则需扫描所有。 - **示例**: ```sql -- 订单按 user_id 分1024张查询时需明确 user_id SELECT * FROM order_ WHERE user_id = 123 AND product_id = 456; ``` #### **2. 路由算法** | **算法类型** | **原理** | **适用场景** | **优缺点** | |---|--------------------------------|------------------------------------------| | **哈希取模** | 对分片键哈希后取模(如 `hash(user_id) % 1024`) | 数据均匀分布(如用户) | ✅ 简单高效<br>❌ 扩容需重新哈希 | | **范围分片** | 按分片键范围划分(如 `0 ≤ user_id < 1000 → 1`) | 按时间或ID范围查询(如日志) | ✅ 易扩容<br>❌ 可能数据倾斜 | | **一致性哈希** | 哈希环虚拟节点分配,扩容时仅迁移部分数据 | 高频扩容缩容场景 | ✅ 扩容影响小<br>❌ 实现复杂 | | **自定义路由** | 业务自定义规则(如根据地域、商户ID映射) | 复杂业务逻辑(如多租户系统) | ✅ 灵活适配业务<br>❌ 维护成本高 | --- ### **二、路由实现方式** #### **1. 代码层手动路由** - **适用场景**:分规则简单、业务逻辑明确。 - **示例**(Java): ```java // 根据 user_id 计算分编号 int tableSuffix = Math.abs(userId.hashCode() % 1024); String tableName = "order_" + tableSuffix; String sql = "SELECT * FROM " + tableName + " WHERE user_id = ?"; ``` #### **2. 中间件自动路由** - **工具**:ShardingSphere、MyCAT、Vitess 等。 - **原理**:解析SQL中的分片键,自动计算目标。 - **示例**(ShardingSphere 配置): ```yaml # 配置分规则:按 order_id 取模分16张 rules: - !SHARDING tables: order: actualDataNodes: ds_0.order_${0..15} tableStrategy: standard: shardingColumn: order_id shardingAlgorithmName: mod_order shardingAlgorithms: mod_order: type: MOD props: sharding-count: 16 ``` - **查询时**:中间件自动从 `WHERE order_id = 1001` 中提取值,计算 `1001 % 16 = 9`,路由到 `order_9` 。 -- ### **三、特殊场景的路由处理** #### **1. 无分片键查询(兜底方案)** - **问题**:查询未携带分片键(如 `SELECT * FROM order WHERE status = 'PAID'`)。 - **解决方案**: - **广播查询**:向所有分发送查询,合并结果(性能差,慎用)。 - **全局索引**:建立 `status` 字段的索引,记录 `status` 对应的 `order_id`,再二次查询。 - **搜索引擎**:将数据同步到 Elasticsearch,通过ES检索。 #### **2. 复合分片键** - **场景**:分键由多个字段组成(如 `user_id + region`)。 - **路由算法**:组合字段哈希或自定义拼接。 ```sql -- 示例:按 user_id 和 region 拼接后哈希 SELECT * FROM order WHERE user_id = 123 AND region = 'us-east'; ``` - **计算**:`hash("123-us-east") % 1024` → 定位 `order_527`。 --- ### **四、路由优化实践** #### **1. 避免热点分片** - **问题**:某些分片键导致数据倾斜(如某商户订单量极大)。 - **优化**: - **增加分片粒度**:分数量从 1024 提升到 2048。 - **二级分片**:先按 `merchant_id` 分库,再按 `order_time` 分。 #### **2. 分片键与查询绑定** - **强制规则**:业务层要求查询必须包含分片键,否则拒绝请求。 - **兜底逻辑**:无分片键时走离线数仓(如Hive、ClickHouse)。 #### **3. 动态扩容** - **一致性哈希**:扩容时仅迁移部分数据,避免全量重新分片。 - **双倍分片法**:提前分配2倍分片数,按需激活(如1024初始仅用512)。 -- ### **五、示例:订单路由** #### **分规则** - **分片键**:`order_id`(雪花算法生成,包含时间戳、分片位)。 - **分数**:1024 张,命名 `order_0` 到 `order_1023`。 #### **路由步骤** 1. **提取分片位**:从 `order_id` 中提取后10位(二进制),计算十进制值。 ```java // 雪花算法生成 order_id 的结构:时间戳(41位) + 分片位(10位) + 序列号(12位) long shardBits = (orderId >> 12) & 0x3FF; // 取中间10位 int tableSuffix = (int) shardBits; // 0~1023 ``` 2. **定位名**:拼接名 `order_${tableSuffix}`。 3. **查询**:直接访问目标,避免扫描其他。 --- ### **六、总结** 分后的路由本质是 **通过分片键和算法映射到物理**,核心方法包括: 1. **哈希/取模**:简单均衡,适合通用场景。 2. **范围分片**:适合按时间或ID区间查询。 3. **一致性哈希**:适合动态扩容。 4. **自定义规则**:适配复杂业务逻辑。 **最佳实践**: - 高频查询必须带分片键。 - 避免分片查询(如JOIN、排序分页)。 - 结合中间件降低开发成本(如ShardingSphere)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值