Elasticsearch之join关联查询及使用场景 | 京东云技术团队

京东云开发者
• 阅读 293

在Elasticsearch这样的分布式系统中执行类似SQL的join连接是代价是比较大的,然而,Elasticsearch却给我们提供了基于水平扩展的两种连接形式 。这句话摘自Elasticsearch官网,从“然而”来看,说明某些场景某些情况下我们还是可以使用的

一、join总述

1、关系类比

在关系型数据库中,以MySQL为例,尤其B端类系统且数据量不是特别大的场景,我们经常用到join关键字对有关系的两张或者多张表进行关联查询。但是当数据量达到一定量级时,查询性能就是经常困扰的问题。由于es可以做到数亿量级的秒查(具体由分片数量决定),这时候把数据同步到es是我们可以使用解决方案之一。

那么不禁有疑问问了,由于业务场景的决定,之前必须关联查询的两张表还能做到进行关联吗?

答案是可以的,es也提供了类似于关系型数据库的关联查询,但是它又与关系型数据的关联查询有明显的区别与限制。

2、使用场景

如果把关系数据库原有关联的两张表,同步到es后,通常情况下,我们业务开发中会有两种查询诉求的场景

场景1

诉求:展示子表维度的明细数据(包含父表和子表中字段的条件)

方案:对于此种查询诉求,我们可以把原来关联的父子表打成父子表字段混合在一起的大宽表,既能满足查询条件,又有查询性能的保障,也是常用存储方案之一

场景2

诉求:展示父表维度的明细数据(包含父表和子表中字段的条件)

方案:然而,对于此种查询诉求,需要通过子表的条件来查询出父表的明细结果,场景1的宽表存储方案是子表明细数据,而最终我们要的是父表明细数据,显然对于场景1的存储方案是不能满足的。如果非要使用场景1的存储方案,我们还要对宽表结果进行一次groupby或者collapse操作来得到父表结果。

这个时候我们就可以使用es提供的join功能来完成场景2的诉求查询,同时它也满足场景1的诉求查询

3、使用限制

由于es属于分布式文档型数据库,数据自然是存在于多个分片之上的。Join字段自然不能像关系型数据库中的join使用。在es中为了保证良好的查询性能,最佳的实践是将数据模型设置为非规范化文档,通过字段冗余构造宽表,即存储在一个索引中。需要满足条件如下:

(1)父子文档(数据)必须存储在同一index中

(2)父子文档(数据)必须存储在同一个分片中,通过关联父文档ID关联

(3)一个index中只能包含一个join字段,但是可以有多个关系

(4)同一个index中,一个父关系可以对应多个子关系,一个子关系只对应一个父关系

4、性能问题

当然执行了join查询固然性能会受到一定程度的影响。对于带has_child/has_parent而言,其查询性能会随着指向唯一父文档的匹配子文档的数量增加而降低。本文开篇第一句摘自es官网描述,从ES官方的描述来看join关联查询对性能的损耗是比较大的。

不过,在笔者使用的过程中,在5个分片的前提下,且父表十万量级,子表数据量在千万量级的情况下,关联查询的耗时还是在100ms内完成的,对于B端许多场景还是可以接受的。

若有类似场景,建议我们在使用前,根据分片的多少和预估未来数据量的大小提前做好性能测试,防止以后数量达到一定程度时,性能有明显下降,那个时候再改存储方案得不偿失。

二、Mapping

1、举例说明

这里以优惠券活动与优惠券明细为例,在一个优惠券活动中可以发放几千万的优惠券,所以券活动与券明细是一对多的关系。

券活动表字段

字段 说明
activity_id 活动ID
activity_name 活动名称

券明细表字段

字段 说明
coupon_id 券ID
coupon_amount 券面额
activity_id 外键-活动ID

2、mapping释义

join类型的字段主要用来在同一个索引中构建父子关联关系。通过relations定义一组父子关系,每个关系都包含一个父级关系名称和一个或多个子级关系名称

activity_coupon_field是一个关联字段,内部定义了一组join关系,该字段为自命名

type指定关联关系是join,固定写法

relations定义父子关系,activity父类型名称,coupon子类型名称,名称均为自命名

{
    "mappings": {
        "properties": {
            "activity_coupon_field": {
                "type": "join",
                "relations": {
                    "activity": "coupon"
                }
            },
            "activity_id": {
                "type": "keyword"
            },
            "activity_name": {
                "type": "keyword"
            },
            "coupon_id": {
                "type": "long"
            },
            "coupon_amount": {
                "type": "long"
            }
        }
    }
}

三、插入数据

1、插入父文档

在put父文档数据的时候,我们通常按照某种规则指定文档ID,方便子文档数据变更时易于得到父文档ID。比如这里我们用activity_id的值:activity_100来作为父id

PUT /coupon/_doc/activity_100

{
    "activity_id": 100,
    "activity_name": "年货节5元促销优惠券",
    "activity_coupon_field": {
        "name": "activity"
    }
}

2、插入子文档

上边已经指定了父文档ID,而子表中已经包含有activity_id,所以很容易得到父文档ID

put子文档数据时候,必须指定父文档ID,就是父文档中的_id,这样父子数据才建立了关联关系。与此同时还要指定routing字段为父文档ID,这样保证了父子数据在同一分片上。

PUT /coupon/_doc/coupon_12345678?routing=activity_id_100

{
    "coupon_id": 12345678,
    "coupon_amount": "5",
    "activity_id": 100,
    "activity_coupon_field": {
        "name": "coupon",
        "parent": "activity_id_100" //父ID
    }
}

四、关联查询

1、has_parent查询(父查子)

根据父文档条件字段查询符合条件的子文档数据

例如:查询包含“年货节”活动字样,且已经被领取过的券

{
    "query": {
        "bool": {
            "must": [{
                "parent_type": "activity",
                "has_parent": {
                    "query": {
                        "bool": {
                            "must": [{
                                "term": {
                                    "status": {
                                        "value": 1
                                    }
                                }
                            }, {
                                "wildcard": {
                                    "activity_name": {
                                        "wildcard": "*年货节*"
                                    }
                                }
                            }]
                        }
                    }
                }
            }]
        }
    }
}

2、has_child查询(子查父)

根据子文档条件字段符合条件的父文档数据

例如:查询coupon_id=12345678在那个存在于哪个券活动中

{
    "query": {
        "bool": {
            "must": [{
                "has_child": {
                    "type": "coupon",
                    "query": {
                        "bool": {
                            "must": [{
                                "term": {
                                    "coupon_id": {
                                        "value": 12345678
                                    }
                                }
                            }]
                        }
                    }
                }
            }]
        }
    }
}

参考:Joining queries | Elasticsearch Guide [7.9] | Elastic

以上文中如有不正之处欢迎留言指正

作者:京东零售 李振乾

内容来源:京东云开发者社区

点赞
收藏
评论区
推荐文章
Easter79 Easter79
2年前
sql注入
反引号是个比较特别的字符,下面记录下怎么利用0x00SQL注入反引号可利用在分隔符及注释作用,不过使用范围只于表名、数据库名、字段名、起别名这些场景,下面具体说下1)表名payload:select\from\users\whereuser\_id1limit0,1;!(https://o
Peter20 Peter20
3年前
mysql中like用法
like的通配符有两种%(百分号):代表零个、一个或者多个字符。\(下划线):代表一个数字或者字符。1\.name以"李"开头wherenamelike'李%'2\.name中包含"云",“云”可以在任何位置wherenamelike'%云%'3\.第二个和第三个字符是0的值wheresalarylike'\00%'4\
Stella981 Stella981
2年前
Python将字符串转换成ObjectId类型
MongoDB自动生成的_id是ObjectId类型的。我需要将MongoDB的_id存到ElasticSearch中,而ElasticSearch又只能存String类型的_id,所以就涉及到两种类型的转换。ObjectId类型—→String类型这个非常简单
Wesley13 Wesley13
2年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Stella981 Stella981
2年前
Linux应急响应(二):捕捉短连接
0x00前言​短连接(shortconnnection)是相对于长连接而言的概念,指的是在数据传送过程中,只在需要发送数据时,才去建立一个连接,数据发送完成后,则断开此连接,即每次连接只完成一项业务的发送。在系统维护中,一般很难去察觉,需要借助网络安全设备或者抓包分析,才能够去发现。0x01应急场景​
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Stella981 Stella981
2年前
ELK学习笔记之ElasticSearch的索引详解
0x00ElasticSearch的索引和MySQL的索引方式对比Elasticsearch是通过Lucene的倒排索引技术实现比关系型数据库更快的过滤。特别是它对多条件的过滤支持非常好,比如年龄在18和30之间,性别为女性这样的组合查询。倒排索引很多地方都有介绍,但是其比关系型