MySQL中的奇异分组
考虑SQL中一个典型的GROUPBY语句:您有一个类似MySQL中的奇异分组,mysql,indexing,Mysql,Indexing,考虑SQL中一个典型的GROUPBY语句:您有一个类似 +------+-------+ | Name | Value | +------+-------+ | A | 1 | | B | 2 | | A | 3 | | B | 4 | +------+-------+ +------+-------+ | Name | Value | +------+-------+ | A | 1 | | A | 3 |
+------+-------+
| Name | Value |
+------+-------+
| A | 1 |
| B | 2 |
| A | 3 |
| B | 4 |
+------+-------+
+------+-------+
| Name | Value |
+------+-------+
| A | 1 |
| A | 3 |
| B | 2 |
| B | 4 |
+------+-------+
而你要求
SELECT Name, SUM(Value) as Value
FROM table
GROUP BY Name
你会收到
+------+-------+
| Name | Value |
+------+-------+
| A | 4 |
| B | 6 |
+------+-------+
在您的头脑中,您可以想象SQL生成一个中间排序表,如
+------+-------+
| Name | Value |
+------+-------+
| A | 1 |
| B | 2 |
| A | 3 |
| B | 4 |
+------+-------+
+------+-------+
| Name | Value |
+------+-------+
| A | 1 |
| A | 3 |
| B | 2 |
| B | 4 |
+------+-------+
然后将连续的行聚合在一起:“Value”列被赋予了聚合器(在本例中为SUM),因此很容易聚合。“名称”列没有提供聚合器,因此使用了您可能称之为“普通部分聚合器”的内容:给定两个相同的东西(例如A和A),它将它们聚合到一个输入(在本例中为A)的单个副本中。给定任何其他输入,它不知道该做什么,被迫重新开始聚合(这次“Name”列等于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的前缀,我们将使用一个可以将X和Y聚合在一起的聚合器,而不是使用名称的“普通部分聚合器”;在这种情况下,它返回X。因此,前三行将聚合到一行(名称,值)=(a,8),然后聚合器将看到a和B无法聚合,并将移动到一个新的行“块”进行聚合
棘手的是,我们分组所依据的值是“非本地的”:如果A不是数据集中的名称,那么AY和AZ将分别是最小前缀。结果表明,AY和AZ行在最终输出中聚合到同一行中,但您无法通过单独查看它们来了解这一点
不可思议的是,在我的用例中,字符串的最小前缀可以在不参考数据集中任何其他内容的情况下确定。(假设我的每个名字都是字符串“hello”、“world”和“bar”中的一个,后跟任意数量的z。我想将所有名字与相同的“base”字组合在一起。)
在我看来,我有两个选择:
1) 简单的选项是:直接根据该值计算每行和每个组的前缀。不幸的是,我在名称上有一个索引,而计算最小前缀(其长度取决于名称本身)会阻止我使用该索引。这将强制进行全表扫描,速度非常慢
2) 复杂的选择:以某种方式说服MySQL使用“部分前缀聚合器”作为名称。这会遇到上面的“非局部性”问题,但只要我们根据我的Name索引扫描表就可以了,因为这样每个最小前缀都会在它作为前缀的任何其他字符串之前遇到;如果数据集中有A,我们将永远不会尝试将AY和AZ聚合在一起
在声明式编程语言中#2相当简单:按字母顺序一次提取一行,跟踪当前前缀。如果新行的名称以该名称作为前缀,则它将放入当前使用的存储桶中。否则,启动一个新的bucket,并将其作为前缀。在MySQL中,我不知道怎么做。请注意,最小前缀集事先不知道。编辑2 我突然想到,如果表格是按
名称
排序的,这将更容易(更快)。因为我不知道您的数据是否已排序,所以我在这个查询中包含了一个排序,但是如果数据已排序,您可以去掉(按名称从表1中选择*ORDER)t1
,只需使用表1中的
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
编辑
在这个问题上睡了一觉之后,我意识到没有必要在中执行,只要在联接表中不存在的地方有一个子句就足够了:
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
从PRIMARY
更改为UNIQUE
键)
更新
原始答案
这里有一种方法可以做到这一点。首先,您需要在表中找到所有唯一的前缀。您可以通过查找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
以下是一些关于如何完成任务的提示。这将定位任何有用的前缀。这不是您所要求的,但是查询流程和@变量的使用
,再加上需要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)
mysql>选择与我们不同的城市,如“Al%”按城市排序;
+-------------------+
|城市|
+-------------------+
|雪花石膏|
|阿拉米达|
|Alamo |不要使用regexp,而是像使用
模式一样使用
模式,因为MySQL可以更好地优化它们RLIKE CONCAT(t1.Name,'.'.'.'')
与CONCAT(t1.Name,'%')相似
,RLIKE CONCAT(t4.Name,'.+')
与CONCAT(t4.Name,'.%')相似@Barmar谢谢-这是一个很好的观点。我从一个RLIKE开始,因为我有更复杂的东西,但应该简化。我会更新答案的。注意点,你的正文