1.limit offset分页查询结果的不确定性
对于分页查询,类似sql语句select * from table [where ...] limit m,n;一般情况下执行结果是不稳定的,每次输出的n条记录不唯一不固定。例如如下sql:
select
lo_orderkey,lo_linenumber,lo_custkey,lo_partkey,lo_suppkey
from lineorder
where lo_linenumber = 1
limit 10000000,5;
连续查询三次,结果如下:
执行计划如下:
根据执行计划,所有节点下发QueryCountEstimate,根据count值选取一个或者多个节点返回n条记录,结果具有一定的随机性。
关闭limit优化参数(set _t_gcluster_limit_optimize=0)时,结果也是不稳定的。执行计划是所有节点选取m+n条记录汇总,然后选取offset m的limit n条,汇总时也有随机性,顺序也
不固定。多次执行结果如下:
参数_t_gcluster_limit_optimize关闭时的执行计划:
所以,不管优化参数打开还是关闭,分页结果都是不稳定的。
2.limit offset分页查询结果的唯一性优化
最近在产品上做了一个优化功能,使得分页数据稳定唯一。新增如下两个参数:
| gcluster_flip_pages | OFF |
| gcluster_force_node_serial_for_flip_pages | OFF |
两个参数默认都是关闭的。在打开参数情况下,分页结果集稳定。
优化的原理是对每个表按照分片号和行号内部固定了一个查询物化的顺序,以保持每次的查询顺序固定。测试结果如下:
3.order by limit offset分页查询性能不稳定问题
除了使用上面产品上的优化以外,想要保持分页结果集固定还可以增加一些排序字段之后,再取offset limit。
对于排序后再分页的sql语句,例如select * from table [where ...] order by col limit m,n;这类sql上面的分页固定优化参数就无效了。
多次测试执行加了排序列后的分页sql,执行结果固定。
在执行limit 10000000,5时,结果集:
在执行limit 10000,5时,结果集:
我们发现,加了order by之后分页结果固定了,但又发现了一个性能问题。在limit n值固定,offset越大时,执行越慢,也就是在分页每页的记录条数固定时,翻页越往后越慢。
4.order by limit offset分页查询性能不稳定原因分析
我们尝试分析一下上面性能不稳定的原因,并尝试分析看能否加以优化。
对于这类排序分页sql,执行计划如下:
上面这个执行计划是在参数gcluster_order_by_limit_offset_optimize默认关闭的情况下获取的。执行计划显示:
第一步,所有节点将等值查询过滤后的结果进行节点内排序;
第二步,取节点内排序后结果集的limit m+n条汇总到一个节点;
第三步,对汇总后的结果集再次排序取offset m limit n条。
在打开参数gcluster_order_by_limit_offset_optimize后,对于offset大的分页,也并没有变快。
参数gcluster_order_by_limit_offset_optimize打开的执行计划,看起来更复杂了一些,而且参数默认关闭,所以可能参数优化的适用场景有一些限制。
我们还是先从分析参数关闭时的执行计划入手。
如上执行计划显示:
每个节点执行where条件过滤之后进行局部排序,再取limit 10000000+5条做汇总。汇总到一个节点临时表里面排序,取limit 10000000,5。可以看到,执行计划里面涉及到两次排序,一般情况下排序是比较耗时的。
先看第一次排序。在limit值n固定,offset值m变大时,局部排序的数据量始终不变,排序后物化的数据条数m+n变大,排序后的物化dc相对集中,物化列数不是特别多的情况下,这一步性能相对稳定。
再看第二次排序。需要全局排序的数据条数是(m+n)*nodes数。在limit值n固定,offset值m变大时,需要全局排序的数据量增大,节点数越多越明显。
5.order by limit offset分页查询性能不稳定优化
排序分页sql里面涉及局部排序和全局排序两次排序,我们的优化思路就是看能不能减少排序次数,只做一次排序。优化思路:
(1)不做各节点的局部排序,那么各节点汇总的需要是过滤后的全量数据,类似于使用了复制表。
(2)不做汇总后的全局排序,即不做汇总,那就需要结果集只分布在一个节点上,可以走单节点hash优化。
具体优化方案示例如下。
原始sql语句:
select
lo_orderkey,lo_linenumber,lo_custkey,lo_partkey,lo_suppkey
from lineorder
where lo_linenumber = 1
order by lo_orderkey,lo_linenumber
limit 10000000,5;
建表语句:
CREATE TABLE "lineorder" (
"lo_orderkey" bigint(20) DEFAULT NULL,
"lo_linenumber" int(11) DEFAULT NULL,
"lo_custkey" int(11) DEFAULT NULL,
"lo_partkey" int(11) DEFAULT NULL,
"lo_suppkey" int(11) DEFAULT NULL,
"lo_orderdate" int(11) DEFAULT NULL,
"lo_orderpriority" varchar(15) DEFAULT NULL COMMENT 'lookup',
"lo_shippriority" varchar(1) DEFAULT NULL COMMENT 'lookup',
"lo_quantity" int(11) DEFAULT NULL,
"lo_extendedprice" int(11) DEFAULT NULL,
"lo_ordtotalprice" int(11) DEFAULT NULL,
"lo_discount" int(11) DEFAULT NULL,
"lo_revenue" int(11) DEFAULT NULL,
"lo_supplycost" int(11) DEFAULT NULL,
"lo_tax" int(11) DEFAULT NULL,
"lo_commitdate" int(11) DEFAULT NULL,
"lo_shipmode" varchar(10) DEFAULT NULL COMMENT 'lookup'
) ENGINE=EXPRESS DISTRIBUTED BY('lo_orderkey') DEFAULT CHARSET=utf8 TABLESPACE='sys_tablespace';
数据统计:
gbase> select count() from lineorder;
+----------+
| count() |
+----------+
| 59986052 |
+----------+
1 row in set (Elapsed: 00:00:00.08)
gbase> select count(distinct lo_orderkey || lo_linenumber) from lineorder;
+----------------------------------------------+
| count(distinct lo_orderkey || lo_linenumber) |
+----------------------------------------------+
| 59986052 |
+----------------------------------------------+
1 row in set (Elapsed: 00:01:12.29)
gbase> select count() from lineorder where lo_linenumber=1;
+----------+
| count() |
+----------+
| 15000000 |
+----------+
1 row in set (Elapsed: 00:00:00.19)
(1)复制表排序方案。
过滤后的结果集插入到复制表1,将复制表1排序后的数据插入到复制表2,分页查询时直接单线程(多线程物化依然可能导致结果不稳定)查询复制表2的对应offset limit值。sql操作
示例如下:
gccli ssbm -vvv -e"drop table if exists lineorder_rep;create table lineorder_rep replicated as select * from lineorder where lo_linenumber = 1;"
-- Query OK, 15000000 rows affected (Elapsed: 00:00:19.51)
gccli ssbm -vvv -e"drop table if exists lineorder_rep_order;create table lineorder_rep_order replicated as select * from lineorder_rep order by lo_orderkey,lo_linenumber;"
-- Query OK, 15000000 rows affected (Elapsed: 00:01:10.66)
gccli ssbm -vvv -e"set gbase_parallel_degree=1;select lo_orderkey,lo_linenumber,lo_custkey,lo_partkey,lo_suppkey from lineorder_rep_order limit 10000000,5;"
-- 5 rows in set (Elapsed: 00:00:00.49)
(2)hash分布表单节点排序方案。
过滤后的结果集插入到以等值过滤列为hash分布列的hash分布表1,将hash分布表1排序后的数据插入到相同表结构的hash分布表2(此步骤需要使用sql直接下发的hint),分页查询
时直接单线程(多线程物化依然可能导致结果不稳定)查询hash分布表2的对应offset limit值。Sql操作示例如下:
gccli ssbm -vvv -e"drop table if exists lineorder_hash;create table lineorder_hash distributed by ('lo_linenumber') as select * from lineorder where lo_linenumber = 1;"
-- Query OK, 15000000 rows affected (Elapsed: 00:00:18.01)
gccli ssbm -vvv -c -e"drop table if exists lineorder_hash_order;create table lineorder_hash_order distributed by ('lo_linenumber') as select /+distribute_exec/ * from lineorder_hash order by lo_orderkey,lo_linenumber;"
-- Query OK, 15000000 rows affected (Elapsed: 00:01:14.41)
gccli ssbm -vvv -e"set gbase_parallel_degree=1;select lo_orderkey,lo_linenumber,lo_custkey,lo_partkey,lo_suppkey
from lineorder_hash_order
limit 10000000,5;"
-- 5 rows in set (Elapsed: 00:00:00.46)
经过上面优化方案的处理,虽然在生成中间排序临时表时多了一些耗时,但是因为临时表是可以复用的,所以整体上达到了分页数据固定且分页性能稳定的目的。