华为云GaussDB(for Influx)揭秘第五期:最佳实践之子查询


摘要: GaussDB(for influx)提供灵活的子查询能力,满足海量数据场景下的高性能查询需求。

本文分享自华为云社区《华为云GaussDB(for Influx)揭秘第五期:最佳实践之子查询》,作者: GaussDB 数据库 。

"告警了!告警了!"。

"什么告警了?"正在睡梦中迷糊的小王突然被运维同事的一个电话叫醒,顿时一脸惊愕。

"慢查询!客户报障了!赶紧起来处理啊!"

小王赶紧打开便携,远程连上环境查找问题所在,最终发现该慢查询是一条子查询。"不对啊,相同语句昨天还没报慢查询啊?"

不过小王很快得出了原因。这条慢查询的问题在于:子查询的内部查询本来可以将数据汇聚后再输出到外部查询,但由于没有做汇聚,因此当数据量大的时候,就会很慢!

找到症结所在,小王立马将优化后的sql语句通过运维同事转给客户,该告警终于得以解决。

"看来得好好整理下子查询了!",趁着思路清晰,小王开始整理了起来......

01 什么是子查询?

子查询是嵌套在另一个查询中的查询,在InfluxQL语法中一般放在from语句中,以增强代码的灵活性。子查询主要有以下几大分类:

  • 标量子查询(scalarsubquery):返回1行1列一个值
  • 行子查询(rowsubquery):返回的结果集是 1 行 N 列
  • 列子查询(columnsubquery):返回的结果集是 N 行 1列
  • 表子查询(tablesubquery):返回的结果集是 N 行 N 列

例如在查询语句:

select first(sum_f1)/first(sum_f2) from (select sum(f1) as sum_f1 from mst), (select sum(f2) as sum_f2 from mst)

使用了两个子查询,分别是从表mst中求得f1和f2两列之和,并将结果sum_f1和sum_f2作为外部查询的源,供外层查询语句使用。

GaussDB(for Influx)子查询的一般语法为SELECT_clause FROM (SELECT_statement ) [...]。在处理子查询时的逻辑如下图所示。

系统会首先处理子查询语句,子查询的结果被缓存起来,作为外查询的数据源,最终由外层查询处理完成后将结果返回给客户。

02 子查询的使用场景

子查询用在一次简单查询无法处理的情况下,或者是基于一个查询的数据做进一步的处理,比如想找出每个分组的最小值中最大的三个:

SELECT top (v,3)
FROM (
SELECT min (value) AS v
FROM mst
GROUP BY tag1
)

子查询为我们带来了很大的灵活性,但是原则上不推荐使用子查询。原因很简单,相比于普通的查询来说,子查询有更深的函数调用和更大的数据量,消耗的资源和时延都会增加。

03 案例剖析

我们在使用GaussDB(for Influx)开发的过程中,常常会面临一些子查询方面的困扰,例如:

1. 什么时候使用子查询?

2. 面对一个复杂场景,如何分解成为子查询来解决?

3. 写好的子查询是最优的么?可不可以再优化?

接下来我们结合一个具体案例来简单分析下如何高效的使用子查询和分析思路。

华为云某用户使用GaussDB(for Influx),每天写入约5.4亿个点,时间线100w+,业务中有时空查询,请求成功率查询,topN查询。

以下面脱敏数据作为sample数据做案例分析和实践:

案例1 什么时候使用子查询?

用户使用了子查询做时空分组并且作为外查询的源,外查询将时空分组的结果做聚合。查询语句为:

SELECT SUM(req_nums)
FROM(
SELECT requestNum AS req_nums 
FROM req_table 
WHERE statement=’SUCCESS’ AND time >= 1629129600000000000 
AND time<=1629129611000000000 )
WHERE time>=1629129600000000000 AND time<=1629129611000000000 
AND req_nums  < 50
GROUP BY time(1s), group
ORDER BY time ASC

产生的问题:

用户的使用场景下,可以发现该查询子查询内部仅仅实现了条件过滤和列名更改,因此内部查询等同于SELECT requestNum AS req_nums + 过滤, 非聚合场景的查询由于需要捞出大量原始数据导致查询速度较慢,因此该查询效率达不到用户的要求。

解决思路:

通过分析查询语句可知,用户的需求是将符合条件(statement=’SUCCESS’ AND requestNum < 50)的数据做一个时空聚合(GROUPBY TAG, time(5m)),明确了查询目标之后能写出更加清晰高效的查询语句:将所有的过滤条件放在一起,直接做时空聚合。

语法改进:

SELECT SUM(requestNum)
FORM req_table
WHERE statement=’SUCCESS’ AND requestNum < 50
AND time>=1629129600000000000 AND time<=1629129611000000000
GROUP BY time(1s), group
ORDER BY time ASC

案例2 使用子查询解决复杂问题

用户的业务场景中需要计算请求成功率,即按照不同的过滤条件对某一列数据进行筛选和计数,最后求出比例。GaussDB(for Influx)不支持case when语句,因此如何根据不同case过滤出同一列的不同数据是一个难点。很多开发者碰到这样的问题时,便没了思路。

解决思路:

第一步:利用子查询+多表特性,根据过滤条件将同一列数据变为两列:

SELECT * FROM 
(SELECT requestNum AS success_requestNum FROM req_table WHERE statement=’SUCCESS’ AND time>=1629129600000000000 AND time<=1629129611000000000),
(SELECT requestNum AS total_requestNum FROM req_table WHERE time>=1629129600000000000 AND time<=1629129611000000000)

第二步:对查询出来的数据进行计数:

SELECT SUM(success_requestNum) AS total_success_reqNum, SUM(total_requestNum) AS total_requestNum 
FROM
(SELECT requestNum AS success_requestNum FROM req_table WHERE statement=’SUCCESS’ AND time>=1629129600000000000 AND time<=1629129611000000000),
(SELECT requestNum AS total_requestNum FROM req_table WHERE time>=1629129600000000000 AND time<=1629129611000000000)
    GROUP BY time ASC

第三步:写出最终求成功率的查询语句:

SELECT SUM(success_requestNum)/SUM(total_requestNum) AS success_ratio
FROM
(SELECT requestNum AS success_requestNum FROM req_table WHERE statement=’SUCCESS’ AND time>=1629129600000000000 AND time<=1629129611000000000),
(SELECT requestNum AS total_requestNum FROM req_table WHERE time>=1629129600000000000 AND time<=1629129611000000000)
    GROUP BY time ASC

案例3 如何优化子查询语句?

基于案例2,我们得到了求成功率的方法,查询语句如下:

SELECT SUM(success_requestNum)/SUM(total_requestNum) AS success_ratio
FROM
(SELECT requestNum AS success_requestNum FROM req_table WHERE statement=’SUCCESS’ AND time>=1629129600000000000 AND time<=1629129611000000000),
(SELECT requestNum AS total_requestNum FROM req_table WHERE time>=1629129600000000000 AND time<=1629129611000000000)
    GROUP BY time ASC

产生的问题:

用户所写查询语句查询时长高于120s不满足业务需求,需要进一步优化。

语法改进:

根据前面所述的子查询原则和解决方法,应当把聚合查询放到子查询内部来减少数据量加快查询速度,优化后的查询语句如下:

SELECT SUM(success_requestNum)/SUM(total_requestNum) AS success_ratio
FROM
(SELECT SUM(requestNum) AS success_requestNum FROM req_table WHERE statement=’SUCCESS’ AND time>=1629129600000000000 AND time<=1629129611000000000),
(SELECT SUM(requestNum) AS total_requestNum FROM req_table WHERE time>=1629129600000000000 AND time<=1629129611000000000)
    GROUP BY time ASC

查询结果一致:

优化效果:

未优化查询耗时126s,优化后查询耗时2.7s,性能提升47倍。

注意

使用SUM(success_requestNum),SUM(total_requestNum)的目的是为了让数据对齐。直接使用SELECTsuccess_requestNum / total_requestNum,会因为相同时间数据无法对齐而出现结果不正确的情况:

SELECT * 
FROM
(SELECT SUM(requestNum) AS success_requestNum FROM req_table WHERE statement=’SUCCESS’ AND time>=1629129600000000000 AND time<=1629129611000000000),
(SELECT SUM(requestNum) AS total_requestNum FROM req_table WHERE time>=1629129600000000000 AND time<=1629129611000000000)
    GROUP BY time ASC

查询的总数据量与查询速度正相关,越大的数据查询量意味着越慢的查询速度,因此无论是书写子查询还是非子查询的查询语句,第一原则是尽量在查询中减少数据量,也就意味着聚合查询(典型减少数据量的查询)应当尽可能放到子查询内部。

04 灵活的子查询和高性能

GaussDB(for Influx)不但提供灵活的子查询能力,同时还使用了向量化、内存复用等技术不断提升查询的效率,满足了用户海量数据场景下的查询性能需求。

向量化查询:GaussDB(for Influx) 使用了SIMD指令集,提高数据处理的并行化程度。与此同时,采用向量化数据模型,一次迭代可以处理一批次的点,极大减少了计算迭代次数,加快了计算速度。

内存复用:在查询过程中尽可能减少GC对内存的回收和分配,申请的内存单独管理,解决了查询过程中内存膨胀导致GC频繁降低查询速度的问题。

05 总结

GaussDB(for Influx)支持子查询功能给我们处理问题带来了很大的灵活性,同时对使用者也有很高的要求,不合理的子查询往往会导致查询时延高,资源消耗大等问题,因此在使用GaussDB(for Influx)子查询时应该注意以下几点:

1. 理解子查询适用的业务逻辑,子查询适用于对查询出来的数据做二次(多次)处理的场景;
2. 能不使用子查询的场景下尽量避免使用子查询;
3. 必须使用子查询的场景尽量将减少数据量的查询放到子查询内部以减少整体的查询数据量从而加快查询速度。

06 结束

本文作者:华为云数据库创新Lab & 华为云时空数据库团队
欢迎加入我们!
云数据库创新Lab(成都、北京)简历投递邮箱:xiangyu9@huawei.com
华为云时空数据库团队(西安、深圳)简历投递邮箱:yujiandong@huawei.com

 

点击关注,第一时间了解华为云新鲜技术~