在MySQL中使用Exotic GROUP BY

时间:2018-08-30 01:18:29

标签: mysql indexing

考虑SQL中的典型GROUP BY语句:您有一个类似

的表
+------+-------+
| Name | Value |
+------+-------+
| A    |     1 |
| B    |     2 |
| A    |     3 |
| B    |     4 |
+------+-------+

你要

SELECT Name, SUM(Value) as Value
FROM table
GROUP BY Name

您会收到

+------+-------+
| Name | Value |
+------+-------+
| A    |     4 |
| B    |     6 |
+------+-------+

在您的脑海中,您可以想象SQL会生成一个中间排序表,例如

+------+-------+
| Name | Value |
+------+-------+
| A    |     1 |
| A    |     3 |
| B    |     2 |
| B    |     4 |
+------+-------+

,然后将连续的行聚合在一起:“值”列已被赋予一个聚合器(在本例中为SUM),因此易于聚合。没有为“名称”列提供任何聚合器,因此使用了您可以称为“琐碎的部分聚合器”的方法:给定两个相同的条件(例如A和A),它将它们聚合到一个输入(在本例中为A)。给出任何其他输入后,它便不知道要做什么,而被迫重新开始聚合(这次,“名称”列等于B)。

我想进行更奇特的聚合。我的桌子看起来像

+------+-------+
| Name | Value |
+------+-------+
| A    |     1 |
| BC   |     2 |
| AY   |     3 |
| AZ   |     4 |
| B    |     5 |
| BCR  |     6 |
+------+-------+

预期的输出是

+------+-------+
| Name | Value |
+------+-------+
| A    |     8 |
| B    |    13 |
+------+-------+

这是哪里来的? A和B是这组名称的“最小前缀”:它们出现在数据集中,每个名称都恰好有一个作为前缀。我想通过在行的名称具有相同的最小前缀时将行分组在一起来聚合数据(当然还要添加值)。

在以前的玩具分组模型中,中间排序表将是

+------+-------+
| Name | Value |
+------+-------+
| A    |     1 |
| AY   |     3 |
| AZ   |     4 |
| B    |     5 |
| BC   |     2 |
| BCR  |     6 |
+------+-------+

我们将使用可以将X和Y聚合在一起的名称,而不是对Names使用“琐碎的部分聚合器”,前提是X是Y的前缀。在这种情况下,它返回X。因此,前三行将一起聚合为(Name,Value)=(A,8)的行,然后聚合器将看到A和B无法聚合,并继续到一个新的“块”行进行汇总。

棘手的是,我们要分组的值是“非本地的”:如果A不是数据集中的名称,则AY和AZ都是最小前缀。事实证明,AY和AZ行在最终输出中汇总到同一行中,但是仅通过单独查看它们是无法知道的。

奇迹般地,在我的用例中,可以确定字符串的最小前缀而无需引用数据集中的任何其他内容。 (想象一下,我的每个名字都是字符串“ hello”,“ world”和“ bar”之一,后跟任意数量的z。我想将所有具有相同“ base”词的名字组合在一起。)

我看到有两个选择:

1)一个简单的选择:直接计算每个行的前缀,并根据该值分组。不幸的是,我在Name上有一个索引,计算最小前缀(其长度取决于Name本身)使我无法使用该索引。这会强制进行全表扫描,这非常慢。

2)复杂的选项:以某种方式说服MySQL使用Name的“部分前缀聚合器”。这遇到了上面的“非本地性”问题,但是只要我们根据我在Name上的索引扫描表,就可以了,因为那样的话,每个最小前缀都将在它作为其前缀的任何其他字符串之前遇到;如果数据集中有A,我们将永远不会尝试将AY和AZ汇总在一起。

在声明式编程语言中,#2会很容易:按字母顺序一次提取一行,并跟踪当前前缀。如果新行的名称以该行作为前缀,它将进入您当前正在使用的存储桶中。否则,以该桶为前缀启动一个新存储桶。在MySQL中,我不知道该怎么做。请注意,最小前缀集是事先未知的。

2 个答案:

答案 0 :(得分:2)

编辑2

我想到如果表是按Name排序的,那会容易得多(而且更快)。由于我不知道您的数据是否已排序,因此我在此查询中包括了排序,但是如果数据已排序,则可以删除(SELECT * FROM table1 ORDER BY Name) t1并仅使用FROM table1

SELECT prefix, SUM(`Value`)
FROM (SELECT Name, Value, @prefix:=IF(Name NOT LIKE CONCAT(@prefix, '_%'), Name, @prefix) AS prefix
      FROM (SELECT * FROM table1 ORDER BY Name) t1
      JOIN (SELECT @prefix := '~') p
      ) t2
GROUP BY prefix

Updated SQLFiddle

修改

在这个问题上睡觉了,我意识到不需要做IN,在JOINed表上有一个WHERE NOT EXISTS子句就足够了:

SELECT t1.Name, SUM(t2.Value) AS `Value`
FROM table1 t1
JOIN table1 t2 ON t2.Name LIKE CONCAT(t1.Name, '%')
WHERE NOT EXISTS (SELECT * 
                  FROM table1 t3
                  WHERE t1.Name LIKE CONCAT(t3.Name, '_%')
                  )
GROUP BY t1.Name

更新后的说明(NameUNIQUE更改为PRIMARY键)

id  select_type         table   type    possible_keys   key         key_len     ref             rows    Extra   
1   PRIMARY             t1      index   Name            Name        11          NULL            6       Using where; Using index; Using temporary; Using filesort
1   PRIMARY             t2      ALL     NULL            NULL        NULL        NULL            6       Using where; Using join buffer (Block Nested Loop)
3   DEPENDENT SUBQUERY  t3      index   NULL            Name        11          NULL            6       Using where; Using index

更新了SQLFiddle

原始答案

这是一种方法。首先,您需要在表中找到所有唯一的前缀。您可以通过查找Name的所有值来实现这一点,其中所有值看起来都不像Name的另一个值,并且末尾有其他字符。这可以通过以下查询完成:

SELECT Name
FROM table1 t1
WHERE NOT EXISTS (SELECT * 
                  FROM table1 t2
                  WHERE t1.Name LIKE CONCAT(t2.Name, '_%')
                  )

对于您的示例数据,这将给

Name
A
B

现在,您可以对“名称”以这些前缀之一开头的所有值求和。请注意,我们在此查询中更改了LIKE模式,使其也与前缀匹配,否则在您的示例中,我们将不计算AB的值:

SELECT t1.Name, SUM(t2.Value) AS `Value`
FROM table1 t1
JOIN table1 t2 ON t2.Name LIKE CONCAT(t1.Name, '%')
WHERE t1.Name IN (SELECT Name
                  FROM table1 t3
                  WHERE NOT EXISTS (SELECT * 
                                    FROM table1 t4
                                    WHERE t3.Name LIKE CONCAT(t4.Name, '_%')
                                    )
                  )
GROUP BY t1.Name

输出:

Name    Value
A       8
B       13

EXPLAIN表示这两个查询都使用Name上的索引,因此应该合理有效。这是我的MySQL 5.6服务器上的解释结果:

id  select_type         table   type    possible_keys   key         key_len     ref             rows    Extra   
1   PRIMARY             t1      index   PRIMARY         PRIMARY     11          NULL            6       Using index; Using temporary; Using filesort
1   PRIMARY             t3      eq_ref  PRIMARY         PRIMARY     11          test.t1.Name    1       Using where; Using index
1   PRIMARY             t2      ALL     NULL            NULL        NULL        NULL            6       Using where; Using join buffer (Block Nested Loop)
3   DEPENDENT SUBQUERY  t4      index   NULL            PRIMARY     11          NULL            6       Using where; Using index

SQLFiddle Demo

答案 1 :(得分:0)

以下是有关如何完成任务的一些提示。这将找到所有有用的前缀。这不是您要的,但是查询流程和@variables的用法,以及需要2个(实际上是3个)嵌套级别都可以为您提供帮助。

SELECT  DISTINCT `Prev`
    FROM  
        (
        SELECT  @prev := @next AS 'Prev',
                @next := IF(LEFT(city, LENGTH(@prev)) = @prev, @next, city) AS 'Next'
            FROM ( SELECT  @next := ' ' ) AS init
            JOIN ( SELECT  DISTINCT city FROM  us ) AS dedup
            ORDER BY  city
        ) x
    WHERE  `Prev` = `Next` ;

部分输出:

+----------------+
| Prev           |
+----------------+
| Alamo          |
| Allen          |
| Altamont       |
| Ames           |
| Amherst        |
| Anderson       |
| Arlington      |
| Arroyo         |
| Auburn         |
| Austin         |
| Avon           |
| Baker          |

检查Al%个城市:

mysql> SELECT DISTINCT city FROM us WHERE city LIKE 'Al%' ORDER BY city;
+-------------------+
| city              |
+-------------------+
| Alabaster         |
| Alameda           |
| Alamo             | <--
| Alamogordo        | <--
| Alamosa           |
| Albany            |
| Albemarle         |
...
| Alhambra          |
| Alice             |
| Aliquippa         |
| Aliso Viejo       |
| Allen             | <--
| Allen Park        | <--
| Allentown         | <--
| Alliance          |
| Allouez           |
| Alma              |
| Aloha             |
| Alondra Park      |
| Alpena            |
| Alpharetta        |
| Alpine            |
| Alsip             |
| Altadena          |
| Altamont          | <--
| Altamonte Springs | <--
| Alton             |
| Altoona           |
| Altus             |
| Alvin             |
+-------------------+
40 rows in set (0.01 sec)