用例:
有三张表,一张顾客表 customer ,一张 挖掘记录表 dataming 还有一张关联关系表 customer_dataming。现在需要根据costomer表中的username查询每个用户最后一次挖掘的挖掘记录,并且返回。
预期流程:
- 查询customer表中符合条件的数据
- 关联查询customer_dataming与dataming,向结果集中加入挖掘记录数据
- 对结果按照用户名进行分组,并且在组内获取DMTime最大的值,以及挖掘记录名称DMName
- 返回结果
MYSQL示例
1.先排序,再分组
该操作主要是针对预期流程中的第3点,先分组,然后组内排序(分治)等价于 先排序 然后分组。
需要注意的是,先分组,再排序,分组间的顺序是原始数据的顺序。先排序,再分组,分组间的顺序是排序后数据的顺序
因为MYSQL支持GROUP BY ... ORDER BY...(先分组,然后组间排序)并不支持ORDER BY ... GROUP BY ...(先排序,然后分组),所以,需要使用子查询来实现:
SELECT * FROM (
SELECT c.*, d.DMName, d.DMTime FROM customer c
LEFT JOIN customer_datamining cd ON c.username = cd.username
LEFT JOIN datamining d ON cd.mid = d.mid
WHERE
c.classId = 0
AND ( '' = '' OR ( c.username = '' OR c.mobile = '' OR c.customerName = '' ))
ORDER BY
d.DMTime DESC
) g
GROUP BY
g.username
LIMIT 10
这里首先创建了一个已经排序后的结果表g然后,再对g进行group,使用到了临时表。在子查询查询数据量大的情况下,临时表由于没有索引,所以会发生filesort,g数据量为1W的情况下的执行计划如下:
id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
---|---|---|---|---|---|---|---|---|---|
1 | PRIMARY | ALL | 11029 | Using | temporary; | Using filesort | |||
2 | DERIVED | c | ref | idx_classId | idx_classId | 5 | 5996 | Using where; Using temporary; Using filesort | |
2 | DERIVED | cd | ref | idx_username | idx_username | 62 | lubantelesales.c.username | 1 | |
2 | DERIVED | d | eq_ref | PRIMARY | PRIMARY | 4 | lubantelesales.cd.mid | 1 |
这里引入了Using where; Using temporary; Using filesort,并且==limit语句没有作用于子查询==,所以执行速度受到了相当大的影响,执行时间为0.194s
2.先分组,再排序
先分组再排序所需要解决的核心难点在于,如何对组内的数据进行操作,即如何选择聚合函数来实现需求。
单纯的进行DMTime最大值的获取,直接使用max(DMTime)即可,但是,对于dataming表中DMTime所在行的其它字段值获取就无从下手了。这时候如果没有合适的聚合函数,那么,多一个查询再所难免。
所幸的是,虽然==Mysql没有提供直接访问组内元素的聚合函数==,但其提供了GROUP_CONCAT函数,该函数可以间接的访问组内数据。
MYSQL官方给出的语法如下:
GROUP_CONCAT([DISTINCT] expr [,expr ...]
[ORDER BY {unsigned_integer | col_name | expr}
[ASC | DESC] [,col_name ...]]
[SEPARATOR str_val])
可以看到,GROUP_CONCAT聚合函数,可以传入一个支持 ORDER BY 字句的表达式,那么,通过该函数,就可以实现组内数据的排序操作:
SELECT c.*, GROUP_CONCAT(d.DMName order by d.DMTime desc),d.DMName,max(d.DMTime)
通过GROUP_CONCAT字句,成功实现了组内排序,下面需要解决的问题就是TOP 1,即取组内排序后结果集的第一个元素。通过官方文档,可以知道GROUP_CONCAT返回的结果集为一个","(可以通过GROUP_CONCAT的参数指定)拼接的字符串,即'r[0],r[1],r[2]...',接下来,我们只需要对这个字符串按照','进行分割,然后取第一个元素即可,MYSQL中对应的字符串操作函数为SUBSTRING_INDEX:
SUBSTRING_INDEX(str,delim,count)
加入该函数之后的SELECT字句为:
SELECT c.*, SUBSTRING_INDEX(GROUP_CONCAT(d.DMName order by d.DMTime desc),',',1),d.DMName,max(d.DMTime)
下面,再加入后续的JOIN WHERE 以及GROUP BY字句即可:
SELECT c.*, SUBSTRING_INDEX(GROUP_CONCAT(d.DMName order by d.DMTime desc),',',1),d.DMName,max(d.DMTime)
FROM
customer c
LEFT JOIN customer_datamining cd ON c.username = cd.username
LEFT JOIN datamining d ON cd.mid = d.mid
WHERE
c.classId = 0
AND (
'' = ''
OR (
c.username = ''
OR c.mobile = ''
OR c.customerName = ''
)
)
group by c.username
limit 10
id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | c | ref | idx_classId | idx_classId | 5 | const | 5996 | Using where |
1 | SIMPLE | cd | ref | idx_username | idx_username | 62 | lubantelesales.c.username | 1 | |
1 | SIMPLE | d | eq_ref | PRIMARY | PRIMARY | 4 | lubantelesales.cd.mid | 1 |
此时,执行计划变为:
id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | c | ref | idx_classId | idx_classId | 5 | const | 5996 | Using where |
1 | SIMPLE | cd | ref | idx_username | idx_username | 62 | lubantelesales.c.username | 1 | |
1 | SIMPLE | d | eq_ref | PRIMARY | PRIMARY | 4 | lubantelesales.cd.mid | 1 |
这时候,执行计划就变得相当完美了,三个ref类型子查询(对应一个SELECT两个JOIN),GROUP BY以及GROUP_CONCAT并未产生额外的查询操作,排序走的也是索引排序(DMTime)。因为结果集直接是原始表,没有用到临时表,所以limit可以直接作用于原始表的查询操作中。最终,执行时间被成功缩短为0.031s。
总结:
以上问题的核心点在于,如何对数据库的分组内结果集作操作。在了解GROUP_CONCAT之前,由于缺少这个途径,所以必须通过额外的查询来解决该核心点。现在,可以通过GROUP_CONCAT聚合函数进行简单的组内结果集的访问。
再次回顾下GROUP_CONCAT的语法定义:
GROUP_CONCAT([DISTINCT] expr [,expr ...]
[ORDER BY {unsigned_integer | col_name | expr}
[ASC | DESC] [,col_name ...]]
[SEPARATOR str_val])
可以看到GROUP_CONCAT具有如下特点:
- 可以传入多个expr(例子中,expr为字段访问,目前尚未尝试其它的表达式)
- 可以使用DISTINCT关键字以及ORDER BY字句
- 不能使用WHERE字句,也就是说,无法对组内数据进行筛选操作
- 返回结果集为SEPARATOR分隔的字符串格式
- 通过4中返回的结果集,配合MYSQL的字符串操作函数,可以间接的对组内数据进行访问。