系列文章目录(兼容Oracle与MySQL)
针对数据库的查询结果进行分组是很常见的情况,所以就存在了针对分组的结果进行字符串结果拼接的函数,比如Oracle中的LISTAGG和MySQL中的GROUP_CONCAT。本文针对这两个函数的使用以及差异进行分析,以达到在兼容Oracle与MySQL时的结果一致。
LISTAGG是从Oracle 11.2开始才有的,官方文档参考:https://docs.oracle.com/cd/E11882_01/server.112/e41084/functions089.htm#SQLRF30030
按照官方的说明,这个函数的功能就是:根据组将指定的字段按照某种排序进行拼接。比如:
在这个查询结果当中,部分编号为20的员工有5个,如果我们想将这些员工的名字都拼接为一个字段,那么就会使用到LISTAGG这个函数了。
这是这个函数最常见的功能。
SELECT T.DEPTNO,listagg (T.ENAME, ',') WITHIN GROUP (ORDER BY T.ENAME) names
FROM SCOTT.EMP T WHERE T.DEPTNO = '20' GROUP BY T.DEPTNO;
从结果来看,返回结果为1条,而且通过LISTAGG函数聚合结果包含了整个组的员工名称,分割符为,
,拼接的结果为ADAMS,FORD,JONES,SCOTT,SMITH
。
这个语句当中包含了三个比较重要的元素,首先就是measure_expr
,也就是上面listagg
函数体中的T.ENAME
,这个参数可以是任意的语句,但是空值会被忽略。比如以下的案例将measure_expr
修改为T.SAL
。
SELECT T.DEPTNO,listagg (T.SAL, ',') WITHIN GROUP (ORDER BY T.ENAME) names
FROM SCOTT.EMP T WHERE T .DEPTNO = '20' GROUP BY T .DEPTNO;
此时拼接的结果变为了1100,3000,2975,3000,800
。
分隔符表达式用于指定分组数据拼接使用的分割符,在上面的案例当中我们使用的是,
,在下面我们修改为:
;
SELECT T .DEPTNO,listagg (T .ENAME, ':') WITHIN GROUP (ORDER BY T.ENAME) names
FROM SCOTT.EMP T WHERE T .DEPTNO = '20' GROUP BY T .DEPTNO;
此时拼接的结果变为了ADAMS:FORD:JONES:SCOTT:SMITH
。
ADAMS,FORD,JONES,SCOTT,SMITH
这个拼接结果首先是通过员工的名称排序之后再拼接的结果,在上面的LISTAGG中是由ORDER BY T.ENAME
这个order_by_clause 语句来指定的,当然也可以指定其他字段,比如按照工资来进行排序。
SELECT T .DEPTNO,listagg (T .ENAME, ',') WITHIN GROUP (ORDER BY T.SAL) names
FROM SCOTT.EMP T WHERE T .DEPTNO = '20' GROUP BY T .DEPTNO;
此时聚合的结果就变为了SMITH,ADAMS,JONES,FORD,SCOTT
。如果指定的字段是一个常量,结果是什么样的呢?
如果从第一个结果看好像是排序好的,但是从第二个结果来看,其实结果未进行排序,其实返回的结果应该是按照数据库中的行号来的,也就是硬盘中本来存储的顺序。
另外从上面的结果,我们也不难得出另一个结论:这个函数没法针对返回的结果进行重复值过滤。1100,2975,3000,3000,800
这个结果当中包含了3000
这个重复值。
上面的结果返回的都是一条数据,因为我们通过WHERE T.DEPTNO = '20' GROUP BY T .DEPTNO
限定后的组只有一条,如果去掉这个where条件呢?此时结果如下
SELECT T.DEPTNO,listagg (T .ENAME, ',') WITHIN GROUP (ORDER BY T.ENAME) names
FROM SCOTT.EMP T GROUP BY T.DEPTNO ORDER BY T.DEPTNO;
其实这个功能只是上面单集合聚合功能在不同组上的叠加结果而已,没有啥特别的。只是要注意返回的结果为多条。从这里也可以得出一个结论:LISTAGG函数并不能保证返回结果的数据条数,关键还在于过滤之后的组的数量,如果通过where过滤之后进行分组只有一组,那么返回结果就是一条,否则就是多条。
比如查询某个部分的员工信息如下所示
我们希望某一列可以包含有字符串类型的统计信息,比如当前部门的员工名称的拼接符。通过关键字PARTITION
来实现。
SELECT
T.EMPNO,T.ENAME,T.JOB,T.HIREDATE,T.SAL,T.DEPTNO
,listagg (T.ENAME, ',') WITHIN GROUP (ORDER BY T.ENAME) over(PARTITION BY T.DEPTNO) deptEmps
FROM
SCOTT.EMP T WHERE T .DEPTNO = '20' ;
同样可以包含多个部门(去掉where或者修改where子句)
SELECT
T.EMPNO,T.ENAME,T.JOB,T.HIREDATE,T.SAL,T.DEPTNO
,listagg (T.ENAME, ',') WITHIN GROUP (ORDER BY T.ENAME) over(PARTITION BY T.DEPTNO) deptEmps
FROM
SCOTT.EMP T
查询所有员工以及针对员工信息做统计
查询所有员工以及针对员工工资做统计
在MySQL当中,并不存在LISTAGG函数,可以用于替代的是另一个函数GROUP_CONCAT,GROUP_CONCAT在MySQL当中是作为一个聚合函数的。对应的官方文档参考:
https://dev.mysql.com/doc/refman/5.6/en/aggregate-functions.html。
对应的语法如下(这个函数从5.5就开始有了)
GROUP_CONCAT([DISTINCT] expr [,expr ...]
[ORDER BY {unsigned_integer | col_name | expr}
[ASC | DESC] [,col_name ...]]
[SEPARATOR str_val])
在这里同样涉及到measure_expr
、delimiter_expr
、order_by_clause
,分别对应上面的expr
、SEPARATOR
、ORDER BY
.但是与Oracle不同的是,MySQL中除了expr之外其他都是可选的,Oracle当中,只有delimiter_expr
是可选的。比如在MySQL当中
只指定measure_expr
SELECT t.DEPTNO,GROUP_CONCAT(t.ENAME) FROM EMP t WHERE t.DEPTNO = '20' GROUP BY t.DEPTNO;
此处没有指定分割符,默认为,
,没有进行排序,所以按照数据在硬盘中正常存储拼接,不进行任何排序。在Oracle当中,如果不指定分隔符,其实默认的为空字符串。如下所示
接下来在MySQL当中指定分隔符和排序规则.
SELECT t.DEPTNO,GROUP_CONCAT(t.ENAME ORDER BY t.ENAME SEPARATOR ':') FROM EMP t WHERE t.DEPTNO = '20' GROUP BY t.DEPTNO;
返回的聚合结果为:ADAMS:FORD:JONES:SCOTT:SMITH
,可以看出,这里和Oracle中的单集合聚合功能是一模一样的。
修改为按照工资进行聚合
SELECT t.DEPTNO,GROUP_CONCAT(t.SAL ORDER BY t.ENAME SEPARATOR ':') FROM EMP t WHERE t.DEPTNO = '20' GROUP BY t.DEPTNO;
由于这里SAL字段是数字类型的,Oracle与MySQL中针对数字类型的处理方式不同(前者不保留小数中的0,而MySQL保留小数中的0)。所以拼接的字符串多了一堆的0.与Oracle是不同的。另外MySQL当中还可以通过加入DISTINCT
关键字来去重。
这个去重的效果在Oracle当中是不提供的。
由于组集合聚合功能与单集合聚合功能的本质是一样的,所以MySQL也是可以的.
由于MySQL中这个函数中没有PARTITION类似的关键字,数据分析功能需要通过子表关联来做
SELECT E.EMPNO,E.ENAME,E.JOB,E.HIREDATE,E.SAL,E.DEPTNO,TE.GC AS deptEmps FROM EMP E INNER JOIN
( SELECT t.DEPTNO,GROUP_CONCAT(DISTINCT t.ENAME ORDER BY t.ENAME SEPARATOR ':') AS GC FROM EMP t GROUP BY t.DEPTNO) TE
ON E.DEPTNO = TE.DEPTNO ORDER BY DEPTNO;
通过以上的分析我们不难得出如下的结论:LISTAGG与GROUP_CONCAT要实现的目标大致都是相同的,针对查询的结果分组、排序然后按照分隔符拼接成结果字符串,但是要做到Oracle与MySQL的绝对兼容必须把握几点:
1. MySQL的GROUP_CONCAT
中不要使用DISTINCT关键字来去重,因为Oracle的LISTAGG
不支持去重功能
2. 仅仅针对字符串(VARCHAR)类型字段拼接,因为MySQL中拼接数字会产生意想不到的结果(小数位中无用的0也会拼接)
3. 必须明确指定分割符,GROUP_CONCAT
默认分割符为,
,而LISTAGG
默认分隔符为空字符串。