考虑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中,我不知道该怎么做。请注意,最小前缀集是事先未知的。
答案 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
修改
在这个问题上睡觉了,我意识到不需要做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
更新后的说明(Name
从UNIQUE
更改为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
模式,使其也与前缀匹配,否则在您的示例中,我们将不计算A
和B
的值:
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
答案 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)