通过TOP 1 WITHIN GROUP 了解 GROUP_CONCAT —— 访问分组内结果集的利器

用例:

有三张表,一张顾客表 customer ,一张 挖掘记录表 dataming 还有一张关联关系表 customer_dataming。现在需要根据costomer表中的username查询每个用户最后一次挖掘的挖掘记录,并且返回。

预期流程:

  1. 查询customer表中符合条件的数据
  2. 关联查询customer_dataming与dataming,向结果集中加入挖掘记录数据
  3. 对结果按照用户名进行分组,并且在组内获取DMTime最大的值,以及挖掘记录名称DMName
  4. 返回结果

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具有如下特点:

  1. 可以传入多个expr(例子中,expr为字段访问,目前尚未尝试其它的表达式)
  2. 可以使用DISTINCT关键字以及ORDER BY字句
  3. 不能使用WHERE字句,也就是说,无法对组内数据进行筛选操作
  4. 返回结果集为SEPARATOR分隔的字符串格式
  5. 通过4中返回的结果集,配合MYSQL的字符串操作函数,可以间接的对组内数据进行访问。

你可能感兴趣的:(通过TOP 1 WITHIN GROUP 了解 GROUP_CONCAT —— 访问分组内结果集的利器)