OLAP盘算引擎是一架呆板,而操作这架呆板的是编程语言。利用者通过特定语言告诉盘算引擎,需要读取哪些数据、以及需要进行什么样的盘算。编程语言有许多种,任何人都可以计划出一门编程语言,然后计划对应的编译器做解析。编程语言从分类上来说,可以分为下令式,声明式。
下令式编程语言是我们最常见的编程语言,C/C++/Java等都是下令式编程语言,这类语言明确的告诉呆板应该执行什么样的指令,留给编译器优化的空间很小了。
声明式编程描述程序应该获得什么效果,至于如何做到,并不关注细节。SQL就是一种声明式编程语言。例如SQL语句select count(1) from department where kpi =3.25,指明盘算kpi=3.25的人数,但不会具体指定如何完成盘算。这给后续的优化器留下了很大的操作空间,优化器可以根据SQL的需求和现实的数据做各种各样的探索,寻找到最佳的执行方式。
一个优秀的分析语言应该具有以下几个特性:
语言简朴,门槛低
语意明确,无歧义
资料丰富,方便学习
生态丰富,工具多
方便扩展,可编排复杂的逻辑
SQL是一种历史悠久的,利用比力广泛的分析语言。在关系型数据库时代就广泛利用的一种语言。在21世纪初,出现了MapReduce算法,数据分析师需要编写MapReduce程序来分析数据。MapReduce程序是一种下令式语言,编写过程非常麻烦,堪比写程序,这就需要数据分析师不仅具备算法本事,还要具备工程本事,利用体验非常糟糕。这就需要两个团队来互助,BI团队把分析需求传递给开发团队,由开发团队去开发分析程序。为了改善分析体验,出现了SQL on Hadoop的办理方案,范例的如Hive,提供SQL接口,并把用户输入的SQL转写成MapReduce执行计划,因而极大的提升了数据分析的体验,实现了BI团队的自主分析,降低了数据分析的门槛,大大增加了受众范围。因此,SQL的影响力是非常大的。从Hive开始,大数据的主要利用接口就转移到了SQL上。而工程师们可以在SQL这张皮之下,用心的优化性能,无缝的升级盘算引擎,保持利用接口的一致性。
SQL的语法简朴,逻辑清晰,了解了最简朴的查询语句之后,就可以嵌套多层表达很复杂的逻辑。SQL基于关系代数,有理论底子,包管语意明确没有歧义。SQL的发展历史非常久远,因而学习资料也比力多,方便新入门者学习。同时围绕SQL的生态也比力丰富,有许多工具利用SQL做分析。
除了SQL之外,也有一些软件推出自定义的语言,例如Elasticsearch利用Lucene语法,Prometheus推出了自定义的PromQL,而Splunk推出了SPL。每一种新的语法,对于新用户而言,都存在一定的学习门槛。因而都不如SQL利用广泛。可以说SQL是数据分析的事实标准。
数据模型
SQL语法包含了几个种别的功能,分别是
Data Manipulation Language(DML):数据操作语言,用于增删改查数据。
Data Definition Language(DDL):数据定义语言,用于定义表的格式。
Data Control Language(DCL):数据控制语言,用于控制权限等。
虽然DML和DCL是SQL体系的底子功能,本文的关留意点更多是数据处理的技能,以及如何加快数据处理的技能,因此更多关注DDL。 在DDL中,也有增删改查,在这几项中,本文更多关注查的部分内容,即如何加快数据的读取和盘算。而数据的写入、存储部分的优化手段,也是为了满足加速数据盘算的目的。
SQL的处理过程
聚合函数和转换函数的不同点在于:聚合函数无论担当多少行输入数据,输出数据都只有一个值,即一行一列;如果是按照窗口聚合(group by某些列),那么每个窗口内的输入数据只会产生一个输出数据。例如求均值的函数avg,无论输入的数据有多少行,最终都只输出一个均值。另一个不同点在于,转换函数没有内部状态,输入数据后可以立马得到输出效果;而聚合函数,要在内存中保存一个状态,直到全部数据都输入结束后,才能拿到最终的效果。例如avg函数,在内存中保存一个sum和一个count这两个值作为状态,分别表示输入数据的求和值以及输入行数,每输入一个新的数据,都更新状态,最终输出时才把两者相除,获得均值。
聚合函数也是一种UDAF(用户自定义聚合函数)。用户可以开发本身的UDAF,并且注册到执行引擎中供调用。
聚合函数的一个样例,求访问日志的平均延时:
SELECT status,avg(dValue) FROM accesslog group by status
按照status分别窗口,分别有200和500两个窗口,每个窗口内的数据分别盘算avg这个集合函数,产生一个聚合效果。
图2-4 聚合函数
选择性聚合
如果在SQL里边只有一个聚合函数,我们只期望对部分数据做聚合盘算,那么只需要把过滤条件放在where中,先过滤出本身想要的数据即可。但是,如果有多个聚合函数呢,每个聚合函数需要的过滤条件不一样呢?对于count算子,有对应的count_if函数可以附加过滤条件。对于其他的聚合函数,也可以利用case when先过滤出来需要的数据,然后再执行聚合盘算,例如avg(case when status=200 then latency end)。不过case when并不是专门用来做过滤的,语法利用起来也不叫复杂,也不是全部聚合函数都满足这种过滤的语意。除了case when,另有一种专门的选择性聚合算子,可以对每个聚合函数附加一个过滤条件。具体语法如:
SELECT
key,
AGG1(x) FILTER (WHERE condition1),
AGG2(y) FILTER (WHERE condition2),
AGG3(z) FILTER (WHERE condition3),
...
FROM
复制代码
每个聚合算子后都跟着一个filter( where bool表达式),满足bool表达式的内容才会参与对应的聚合。在同一层的的各个聚合函数,可以指定各自的过滤条件,也可以不指定过滤条件,每个聚合函数对应的过滤条件之间没有任何关系,可以相同,也可以不同。这就是选择性聚合,在语法层面给多样化的聚合提供了方便。
Distinct 聚合
在聚合函数的输入参数中,如果参数值是null,那么不参与盘算。例如sum(key),只统计非null值的和。count(key)只统计非null的个数。此处有个例外,就是count(*),因为*并不是具体的列,不存在null或非null的差异,因此全部的行都会统计在内。
如果聚合函数的全部输入,排除掉null值后,只有0行有用数据,那么聚合函数的返回效果是null,因为没有任何有用数据参与盘算。以sum为例,如果全都是null,大概只有0行输入,返回0大概其他特别值是不合适的,因为没有特别值可以唯一代表这种场景,只有返回null才合适。在全部的聚合函数中,除了count之外,都符合这一定义,count 0行输入的效果是0。
GROUP BY分组聚合
只有聚合函数的场景,全部的输入都聚合成一个效果。如果要把输入分到多个分组中,每个分组分别生成聚合效果,则需要用group by指定分组。 Group by后跟一列大概多列、大概有某些列经过转换函数盘算后的效果。Group by子句是配合聚合算子利用的。没有group by的情况下,聚合算子担当全部的输入数据,产生一个盘算效果;有group by的情况,称为分组聚合,各行数据先按照group by中指定的列或列的转换效果,盘算所属分组,每个分组内无论有多少行数据,都会盘算产生一行聚合效果。图2-4是一个group by分组聚合的样例,按照status分组,总共有2个分组,每个分组产生一行聚合效果,即共两行聚合效果。
Group by的一个样例,求访问日志中每个站点的平均延时:
SELECT avg(latency), host from accesslog GROUP BY host
在一个分组内,可以执行多个聚合函数,每个聚合函数产生一列聚合效果。即分组的数目决定效果行数,聚合函数的数目决定效果的列数。
在有group by的场景下,select中指定的表达式,除了聚合函数外,还可以select某些列,大概某些列经过转换函数盘算后的效果,这些列是有限制条件的,只能是group by中出现的列。如果是非group by的列,就会出现一个难以抉择的问题,因为分组是按照group by的列分组的,每个分组只输出一行效果,如果select 非group by的列,那么在分组中,会有多行数据进入同一分组,在输出时到底选择哪一行作为办理呢?这没有明确的答案。有几种可能性,第一种是随机的选择一行;第二种是选择第一行;第三种是选择最后一行;第四种是全部输出。可能性太多,如果用户不明确的告诉SQL选择哪一种选项,就会造成误判,输出效果不一定满足用户预期。每一种选项都会有对应的聚合函数实现。当然在mysql体系中,是按照第一种选项输出的。
对于需要在分组内产生多行聚合效果的利用场景,可以参考窗口函数。
如果要分组的列是null值,则null值会作为一个单独的分组。
一般的场景下,一个原始数据只会在一个分组内参与聚合盘算,不会同时出如今多个分组中。但也有一些高级用法就是grouping set操作,在下文详细介绍。
Grouping sets操作
上文介绍的group by子句,是比力简朴的一种分组聚合操作。全量的数据,会按照分组条件分到不同的组里边,每一行数据,都只会在一个分组中参与聚合。另有一种更加复杂的分组聚合操作是grouping sets操作。相干关键字是grouping sets, cube, rollup。该算子可以允许在一次查询中,按照不同的分组条件,多次分组。每一条数据,都会按照不同的分组条件多次参与聚合。
例如,如果你盼望按照多个分组聚合(grade, class), (grade),(class),如果利用group by,那么要分别执行三次group by操作。利用grouping sets则可以在一次查询中完成,语法是select grade,class,count(1) from log group by grouping sets((grade, class), (grade),(class))。在输出的效果中,grade class两列都会输出,但是在后两个集合中,只group by了一列,另一列以null出如今效果中。
Rollup语法是一种特别的grouping sets语法,roll up后跟的集合,会按照层级聚合的方式,罗列出全部的前缀集合。例如group by rollup(grade, class),相称于group by grouping sets ((grade, class),(grade),())。最后一个分组条件是空分组,也就是不分组,相称于没有group by的场景。
Cube语法也是一种特别的grouping sets语法,cube和roll up不同之处在于,cube会罗列全部可能的集合。例如group by cube(grade,class),相称于group by grouping sets((grade,class),(grade),(class),())。
窗口函数
窗口函数最简朴的场景,例如:avg(key2) over(),表示把全部数据当成一个分组做avg聚合,并且写回每条数据中,虽然效果中的每行数字都相同,但是没有改变效果行数。如下图中的out3的效果所示,全部行的均值为3,3就是每一行对应的效果。
再复杂一点的窗口函数场景,例如:avg(key2) over(partition by key1),表示按照key1作为分组,每个分组内分别执行avg聚合盘算,并且更新到每个分组的每条数据中。如下图的out1所示,a这个窗口的均值是1.5,窗口内全部的效果都填充为1.5。b这个窗口内均值是4,窗口内全部的效果都填充成4。
更加复杂一点的窗口函数样例如:avg(key2) over(partition by key1 order by key2),表示按照key1作为分组,在每个分组内再按照key2排序,盘算窗口内从第一行到当前行为止的数据的avg聚合效果,也就是分组内每一行的效果可能是不一样的。参考下图中的out2这个效果,a这个窗口,第一行数据是1,均值就是1;第二行数据是2,第二行对应的窗口均值就是第一行和第二行的均值,也就是1.5。因此效果中,第一行的效果是1,第二行的效果是1.5。这个和out1的对比比力明显,out1的效果中,每个窗口内的效果都是一样的。
上边的样例还不是最复杂的,前2个样例,都是在分组内的全部数据上执行聚合;加上order by之后,是聚合从第一行到当前行的数据。那有没有一种方法,只聚合当前行附近的几行呢?能否更加灵活的指定窗口内哪些行参与聚合盘算呢?答案是可以的。窗口函数可以指定当前行的前后多少行参与聚合盘算,例如avg(key2) over(partition by key1 order by key2 range between unbounded preceding and current row),表示从第一行到当前行。range between 1 precedingand 2 following,表示包含前一行、当前行、后两行总共4行构成的数据进行聚合,更新到当前行的效果。参与聚合的行称为一个frame,一个frame内的数据聚合生成一个效果。
比力表达式通过比力运算符>,>=,<,<=,=,<>等毗连两个表达式,用于判断两个表达式的巨细关系。左右的表达式不一定是底子范例,也可能是复杂的表达式,例如函数调用表达式。底子范例的数据包括integer、bigint等数值范例,也可能是varchar,char等字符串范例。除了上述比力算法,另有between关键字,key between x to y,等价于key >=x and key <=y,是一个闭区间。
Bool表达式
Lambda表达式又称为是匿名函数,没有函数名称,只有参数列表和盘算表达式。Lambda表达式可以用于让用户自定义处理逻辑,相称于一种UDF。通常在利用中,lambda表达也可以作为函数的参数传入函数,然后在函数内调用该lambda表达式迭代处理数据。
一个简朴的lambda表达式如:x -> x + 1,表示担当一个参数x,返回x+1。
WHERE子句
Where子句后跟一个bool表达式,表示从表中读取数据后,会对每一行数据评估该bool表达式的效果。如果表达式评估效果为true,则该行数据就会传递后给后续的算子做进一步盘算;如果评估效果为false大概位置状态,则抛弃改行数据,不再参与后续盘算。
Bool表达式可以是一个简朴的表达式,例如a=1;也可以是嵌套多层转换函数构成的bool表达式,例如a%10=1;大概由逻辑运算符毗连起来的逻辑表达式,例如 a AND b。Bool表达式中的函数都是转换函数,不能是聚合函数。
Where子句的操作发生在聚合盘算之前。Where 子句非常重要,可以帮助淘汰读取和盘算的数据量,常常用于加速盘算。在优化器中,有一些规则帮助把过滤条件尽可能的下推到叶子结点。filter下推是一种非常常用且有用的加速手段。
Where子句的一个样例,获取弟子中全部的男生信息:
SELECT * FROM student where gender = ‘male’
HAVING子句
Having子句常常跟随group by子句出现。Having子句类似于where,是一个bool表达式。但Having应用于group by聚合盘算之后,每个分组的盘算效果会用来继续评估Having表达式的效果,只有满足having子句为true的分组,才能输出到后续的算子。
Having和where的区别在于:1, where在group by之前完成,having 在group by之后执行;2,where应用于每条原始数据上,having应用于group by分组聚合效果上。
理论上而言,即便没有group by盘算,只有一个全局聚合操作,可以或许利用having,但是全局聚合的效果只有一样,那么这个时间having的作用就是判断这一行效果是否满足条件。例如select avg(latency) as avg_latency from log having avg_latency > 100
即便没有group by没有任何聚合函数,select中只有原始列大概转换函数的效果时,也可以用having,但这时间having就没故意义了,因为having中的条件是可以合并到where中的。例如select * from log where latency > 10000000 having status>200,完全可以写成select * from log where latency > 10000000 and status>200。
总而言之,having子句一般和group by语句联合利用,用于过滤分组聚合后的效果,筛选出分组聚合效果满足特定条件的某些分组。
Having子句的一个样例,求访问日志中平均延时大于10秒的站点及其延时:
SELECT avg(latency), host from accesslog GROUP BY host HAVING avg(latency) > 10
having子句的执行发生在group by之后,order by之前。次序参考图2-2。
Order By子句
Order by子句包含一个或多个表达式,用于排序输出的效果。在order by中可以指定多个表达式,每个表达式指定排序方式,可以升序,也可以降序,默认是升序排列。排序时多个表达式从左到右依次评估。当左侧表达式评估出来的多个行效果一样时,会评估右侧表达式的值用于排序。例如order by key1 asc, key2 desc 表示按照key1升序排列,当key1相同时,按照key2降序排列。
Order by子句的一个样例,弟子按照分数排序:Select * from student order by score asc
Limit 子句
Set操作是一种集合操作,集合的元素是行,用于把多个表前后拼接成一个表。拼接后不改变列的个数,原表中的一行,原样输出到效果中,参与set操作的左右表的列个数和范例必须保持一致。set操作和join操作的差异在于,join是左右表列与列按照毗连条件拼接成一行,set操作是行与行拼接成更多行,不改变原始一行的内容。Set操作包括Union、Intersect、Except。分别代表并集、交集、差集。
集合的理论底子是集合代数,默认场景下,集合是不包含重复元素的。集合运算符后可以添加distinct大概all关键字,分别表示效果去重和不去重。默认是去重的效果。例如table1 union table2,输出两个表去重后的效果。
嵌套查询
在一个最简朴的查询中,from语句指定了要从什么表中读取数据。在from中,最简朴的情况是指定一个表,从这一个表中读取数据出来;稍微复杂的情况是from多张表的join效果;再复杂一点,from的来源,根本不是一张表,而是另一个查询的输出效果。我们都知道,一个SQL查询的效果也能成为一个新的表,这个新的表可以作为另一个查询的输入。这就是关系模型的优秀之处,任何关系经过盘算后,形成第二个关系,再经过第二次盘算,则形成了第三个关系。理论上,表活着关系可以经过无数轮盘算,构成一个单向流动的链表。这就是嵌套查询。嵌套查询的效果,可以像一张普通的表一样,参与卑鄙的盘算、join、union等。
在SQL中,写嵌套查询有两种形式,第一种,最直观的就是from 后写一个子查询,并且把子查询用()包含起来,形成一个完备的整体,例如:
select abc from ( select abc from table)
()内部的即为一个完备的子查询。
第二种是with语法:
出如今select输出效果中,select (select 1) as one from student。
出如今where中,select name from student where id in (select id from applied)。
对于判断外层查询属性和内层子查询效果之间关系的判断方式,有几种方式:
ALL 表示外层表达式要满足子查询的全部效果。
ANY表示外层表达式需要满足子查询的至少一个效果。
IN 等同于ANY。
EXISTS表示至少有一行效果返回。
按照输出效果,子查询包括三种范例:
标量子查询(scalar subquery):只返回一行一列效果。
多行输出子查询:输出多行一列,或多行多列。
exists子查询:输出效果是bool范例。
按是否引用外层查询的属性,分为:
关联子查询:子查询中引用到了外层查询的属性。
无关联子查询:子查询没有引用外层查询的属性。
标量子查询表达式
标量子查询的效果只有一行一列一个值。针对这个特性,可以有许多优化手段。在后续的优化器章节会给出介绍。理论上来说,对于外层查询的每一行数据,都需要去执行一次子查询表达式。但是这里另有些不同点,对于相干子查询和不相干子查询的处理是不同的。对于不相干子查询,子查询没有引用外部的任何列,因此对于外部的每一行数据,子查询的执行效果都是相同的,因此执行一次即可。这种场景下,子查询只会执行一次。
标量子查询可以用于case表达式、select子句、where子句、order by子句、函数的参数等。由于标量子查询只返回一行一列,因此可以当成单个值利用。
scalar 子查询在被利用之处,只能要求出现一个效果,但并未在语法上约束子查询返回一个效果。用户可以写一个聚合子查询只返回一个效果,大概用limit 1限定返回一个效果;也可以写一个可能返回多行数据的SQL,只有在执行时,如果现实返回多行效果则会报错。
例如select count(1) from log where latency >= (select avg(latency) from log),子查询中时聚合函数,一定会返回一行效果,因而可以正常执行。但参加用户写这样一个子查询select count(1) from log where latency >= (select (latency) from log),则存在三种可能,返回0行效果,返回1行效果,返回大于1行效果。如果返回0行效果,则以null作为子查询的输出,如果返回大于1行效果,则运行报错。因为标量子查询的外层需要一行一列输入。大概说,标量子查询之所以称为是标量子查询,是因为外层查询要求子查询输出一行一列,而不是子查询本身通过语法大概现实运行只能得到一行一列效果。
除了where中,还可以在select中,例如select *, (select max(latency) from log )from log,在每一行都输出最大的latency值。如果写成select *, (select latency from log )from log则会报错。
也可以作为函数参数:select *, abs((select max(latency) from log) )from log。基本上,在需要单个值的地方就可以利用标量子查询。
子查询用于判断集合从属关系
in和not in用于判断外层查询的属性是否属于内层子查询效果的集合内。例如:
select * from course where student_id in (select student_id from student where apply_year='2018')
in和not in除了用于子查询,还可以指定一个list常量,例如:
select * from course where student_id in(1,2,3)
Exists子查询用于判断是否是空集合
Exists子查询检查子查询是否有输出效果,如果有至少一行效果,则判断为true,否则判断为false。通常Exists子查询被用于关联子查询,也就是说针对外层查询的每一行数据,判断Exists子查询的效果。 如果是非关联子查询,则对于外层查询的每一行数据,Exists的效果都是一行的效果,这样做没故意义。
例如,SELECT name FROM websites WHERE EXISTS ( select count from access_log WHERE websites.id = access_log.site_id and count > 100) 表示输出访问日志中count > 100的网站的名字。
not exists是相反的语意,表示子查询的效果为空集合。
Exists查询也可以用in语法来表达,in语法表示判断某一列的每一行是否在子查询的输出效果中。例如上述的逻辑,可以用in语法来表达:SELECT name FROM websites WHERE id in ( SELECT site_id from access_log where count > 100)。显然,在in查询中,子查询是不相干查询,因此,子查询只需要执行一次即可,因而查询效率较高。
子查询用于比力级和数值巨细关系
外层查询可以和子查询的效果进行对比,对比的运算符包括<,>, <=, >=, =, <>。子查询的效果可以包含修饰符SOME,ANY,ALL。外层表的每一行会逐个和子查询效果的每一行进行对比,返回true大概false。如果是SOME大概ANY修饰符,那么只需要至少1行对比为true即可。如果是ALL修饰符,那么就需要全部的行对比效果都为true才行。=ANY的语义和IN相同。<>ALL的意义和NOT IN相同。
一个样例:SELECT Num FROM Test2 WHERE Num > ANY (SELECT Num FROM Test1)表示Test2这张表中的Num,如果在Test1表中存在比之小的值,则该行数据满足条件,会输出到卑鄙算子中。
量化子查询会在优化器章节进行深入的介绍其优化方法。
子查询用于判断集合是否包含重复值
和exists类似,另有一个unique关键字,用于判断子查询的全部行是否包含重复值,如果包含重复值,那么返回false;如果不包含重复值,则返回true。
例如:
select * from log where unique (select projectName from log)
子查询的现实运行方式
对于通例的数据处理是很简朴的,但是往往有一些非法的case需要处理。null就是一个范例的场景。一个非法值,大概不知道具体值的值就用null表示。
在聚合函数中,输入null值的处理在上文已经描述过了。在这个章节中,主要考虑转换函数输入null的情况。
对于一个转换函数大概转换表达式,如果返回值是非boolean的情况,例如代数运算,如果输入是null,那么输出也是null。
如果转换函数大概转换表达式返回值是boolean的情况,例如一个比力表达式,正常情况输出只有true、false两种场景,如果输入的一个参数是null,无法明确判断是true照旧false的情况下,则需要第三种状态,即unkonwn状态用于判断。为什么简朴粗暴的输出null呢?这是因为,unknown代表的信息量要大于null。在后续的盘算中,即便存在unkonwn状态,也能过推断出效果。
针对and、or、not逻辑表达式,当出现unkonwn时,乃至可以借助短路求值的思想,获得最终效果,无需关心unknown到底是true照旧false。
AND: 如果是true and unknown,效果取决于unkonwn,那么效果就是unkonwn;如果是false and unkonwn,无论unkonwn是true照旧false,效果都是false。
OR:如果是true or unkonwn,无论unknown是true照旧false,效果都是true;如果是false or unknown,效果取决于unknown,效果仍为unknown。
NOT: not unknown,效果照旧unknown。
Is null语法和is not null语法:is null可以判断一个表达式是否是null,is not null恰好相反。同时在SQL标准中,另有is unknown语法和is not unknown语法,不过这两个关于unknown的语法并不是全部的SQL引擎都支持。
在用于分组操作时,例如group by,distinct,union等, 如果指定的列中包含null,那么全部对应null的行都会作为一个分组。这一点和盘算表达式中的表现是不同的,例如判断null=null,那么输出将是unknown,而不是true。
Unnest语法
在SQL中,生成新的数据依靠于表达式大概函数,在上文中提到,函数分成两种范例,分别是标量转换函数,另一种是聚合盘算函数。标量盘算函数把一行输入数据转换成一行一列输出效果;聚合盘算函数把多行输入数据转换成一行一列输出效果。如果我们要输出一列转换成多列,那么可以通过多个表达式实现。如果我们需要把一行转化成多行,该怎么处理呢?在这种需求场景下,就用到了Unnest语法。
Unnest语法可以把一行数据转换成多行数据。例如输入数据是一个数组范例,那么可以把数组中的每一个元素作为一行效果输出。语法如:
SELECT element FROM (VALUES ( ARRAY[1,2,3]) ) as t(element)。输出效果为3行,分别是1、2、3.。
其他SQL语法