App*_*ema 4 mysql pivot group-by group-concatenation
我被困在一个连接表查询上,每月显示涉及 GROUP BY 和 GROUP_CONCAT 的数据。
这是一个简单的客户端表(本文底部的 DDL 和 DML):
id | Name
1 | Sony
2 | Toshiba
3 | Apple
4 | LG
5 | Uco
Run Code Online (Sandbox Code Playgroud)
然后是事件表
id | client_id | date_start
1 | 1 | 2017-01-12 18:44:42
2 | 1 | 2017-01-13 18:44:42
3 | 1 | 2017-01-14 18:44:42
4 | 1 | 2017-02-12 18:44:42
5 | 1 | 2017-03-12 18:44:42
6 | 1 | 2017-07-12 18:44:42
7 | 2 | 2017-02-12 18:44:42
8 | 2 | 2017-03-12 18:44:42
9 | 2 | 2017-04-12 18:44:42
10 | 3 | 2017-01-12 18:44:42
11 | 3 | 2017-01-14 18:44:42
12 | 3 | 2017-01-20 18:44:42
13 | 3 | 2017-03-12 18:44:42
14 | 3 | 2017-05-12 18:44:42
15 | 3 | 2017-06-12 18:44:42
16 | 4 | 2017-07-12 18:44:42
17 | 4 | 2017-07-20 18:44:42
18 | 5 | 2017-09-12 18:44:42
19 | 5 | 2017-10-12 18:44:42
20 | 5 | 2017-03-12 18:44:42
Run Code Online (Sandbox Code Playgroud)
想要的结果如下。字符串编号,例如 Jan/Apple 上的 (10-01-12) 格式为 id-month-day。
到目前为止,我所做的是使用 case 何时拆分结果:
select * from (
select e.id, c.name as client,
(CASE WHEN MONTH(e.date_start) = 1 then GROUP_CONCAT(CONCAT(e.id,'-',LPAD(month(date_start),2,'0'), '-', LPAD(day(date_start),2,'0')) SEPARATOR ',')END) as jan,
(CASE WHEN MONTH(e.date_start) = 2 then GROUP_CONCAT(CONCAT(e.id,'-',LPAD(month(date_start),2,'0'), '-', LPAD(day(date_start),2,'0')) SEPARATOR ',')END) as feb,
(CASE WHEN MONTH(e.date_start) = 3 then GROUP_CONCAT(CONCAT(e.id,'-',LPAD(month(date_start),2,'0'), '-', LPAD(day(date_start),2,'0')) SEPARATOR ',')END) as mar,
(CASE WHEN MONTH(e.date_start) = 4 then GROUP_CONCAT(CONCAT(e.id,'-',LPAD(month(date_start),2,'0'), '-', LPAD(day(date_start),2,'0')) SEPARATOR ',')END) as apr,
(CASE WHEN MONTH(e.date_start) = 5 then GROUP_CONCAT(CONCAT(e.id,'-',LPAD(month(date_start),2,'0'), '-', LPAD(day(date_start),2,'0')) SEPARATOR ',')END) as may,
(CASE WHEN MONTH(e.date_start) = 6 then GROUP_CONCAT(CONCAT(e.id,'-',LPAD(month(date_start),2,'0'), '-', LPAD(day(date_start),2,'0')) SEPARATOR ',')END) as jun,
(CASE WHEN MONTH(e.date_start) = 7 then GROUP_CONCAT(CONCAT(e.id,'-',LPAD(month(date_start),2,'0'), '-', LPAD(day(date_start),2,'0')) SEPARATOR ',')END) as jul,
(CASE WHEN MONTH(e.date_start) = 8 then GROUP_CONCAT(CONCAT(e.id,'-',LPAD(month(date_start),2,'0'), '-', LPAD(day(date_start),2,'0')) SEPARATOR ',')END) as aug,
(CASE WHEN MONTH(e.date_start) = 9 then GROUP_CONCAT(CONCAT(e.id,'-',LPAD(month(date_start),2,'0'), '-', LPAD(day(date_start),2,'0')) SEPARATOR ',')END) as sep,
(CASE WHEN MONTH(e.date_start) = 10 then GROUP_CONCAT(CONCAT(e.id,'-',LPAD(month(date_start),2,'0'), '-', LPAD(day(date_start),2,'0')) SEPARATOR ',')END) as oct,
(CASE WHEN MONTH(e.date_start) = 11 then GROUP_CONCAT(CONCAT(e.id,'-',LPAD(month(date_start),2,'0'), '-', LPAD(day(date_start),2,'0')) SEPARATOR ',')END) as nov,
(CASE WHEN MONTH(e.date_start) = 12 then GROUP_CONCAT(CONCAT(e.id,'-',LPAD(month(date_start),2,'0'), '-', LPAD(day(date_start),2,'0')) SEPARATOR ',')END) as `dec`
from
event as e
left join client as c on c.id=e.client_id
group by month(date_start),client
order by client
) t
Run Code Online (Sandbox Code Playgroud)
但是上面的查询需要最后按客户分组。如何按客户端结果进行分组,如上面所需表格中显示的那样,以逗号作为分隔符?
第二部分是每月统计每个数据的总和。没那么重要,我真的需要让第一部分工作。
这是数据和表的 SQL。
CREATE TABLE `client` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
LOCK TABLES `client` WRITE;
INSERT INTO `client` (`id`, `name`)
VALUES
(1,'Sony'),
(2,'Toshiba'),
(3,'Apple'),
(4,'LG'),
(5,'Uco');
UNLOCK TABLES;
CREATE TABLE `event` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`client_id` int(11) unsigned DEFAULT NULL,
`date_start` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`),
KEY `client_id` (`client_id`),
KEY `date_start` (`date_start`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
LOCK TABLES `event` WRITE;
INSERT INTO `event` (`id`, `client_id`, `date_start`)
VALUES
(1,1,'2017-01-12 18:44:42'),
(2,1,'2017-01-13 18:44:42'),
(3,1,'2017-01-14 18:44:42'),
(4,1,'2017-02-12 18:44:42'),
(5,1,'2017-03-12 18:44:42'),
(6,1,'2017-07-12 18:44:42'),
(7,2,'2017-02-12 18:44:42'),
(8,2,'2017-03-12 18:44:42'),
(9,2,'2017-04-12 18:44:42'),
(10,3,'2017-01-12 18:44:42'),
(11,3,'2017-01-14 18:44:42'),
(12,3,'2017-01-20 18:44:42'),
(13,3,'2017-03-12 18:44:42'),
(14,3,'2017-05-12 18:44:42'),
(15,3,'2017-06-12 18:44:42'),
(16,4,'2017-07-12 18:44:42'),
(17,4,'2017-07-20 18:44:42'),
(18,5,'2017-09-12 18:44:42'),
(19,5,'2017-10-12 18:44:42'),
(20,5,'2017-03-12 18:44:42');
UNLOCK TABLES;
Run Code Online (Sandbox Code Playgroud)
And*_*y M 10
这种从行到列的转换称为pivoting。通常将数据与聚合同时进行透视,这似乎也是您的情况的要求。在 SQL 中,您可以将这两个操作作为单个逻辑步骤执行。其他 SQL 产品甚至为数据透视提供特殊的语法扩展,但有一种方法可以使用更通用的语法来实现这一点,至少每个主要 RDBMS 都支持这种语法,包括 MySQL。
该方法称为条件聚合,您几乎掌握了它。有条件的,如在查询一个CASE表达式实现的,应该去里面聚合函数,而条件是在检查(标准MONTH(e.date_start)
必须在你的情况)排除在GROUP BY。
所以,而不是
SELECT
CASE WHEN MONTH(e.date_start) = 1 THEN GROUP_CONCAT(...),
...
FROM
...
GROUP BY
MONTH(e.date_start),
client
Run Code Online (Sandbox Code Playgroud)
它应该是
SELECT
GROUP_CONCAT(CASE WHEN MONTH(e.date_start) = 1 THEN ...),
...
FROM
...
GROUP BY
MONTH(e.date_start),
client
Run Code Online (Sandbox Code Playgroud)
排除部分似乎违反直觉-毕竟,你是打算拿到月度数据。但是,您应该记住,在 SQL 中您将行分组。在您的情况下,一行是一个客户端 - 因此,分组应该仅按客户端进行。您可以说每月分组是隐式的,因为它仅通过条件聚合实现。
无论如何,最后一行呢?最后一行是特殊的,不仅仅是因为它是一个汇总行,因此代表整个集合的聚合数据。在我看来,它更特别,因为它包含完全不同的数据:计数而不是连接的字符串。
基于这一事实,对我来说,考虑一个不同的逻辑步骤——一个单独的 SELECT——来获取最后一行的结果似乎很自然。然后将在 UNION ALL 运算符的帮助下将两个结果集合并为一个。在我看来,这种方法将使逻辑清晰:输出中的不同类型的数据将由查询的不同部分来解释。清晰的逻辑最终意味着易于维护。
因此,考虑到以上所有因素,完整的查询可能如下所示:
SELECT
c.name AS client,
GROUP_CONCAT(CASE MONTH(e.date_start) WHEN 1 THEN CONCAT(e.id, '-', RIGHT(DATE(e.date_start), 5)) END SEPARATOR ',') AS jan,
GROUP_CONCAT(CASE MONTH(e.date_start) WHEN 2 THEN CONCAT(e.id, '-', RIGHT(DATE(e.date_start), 5)) END SEPARATOR ',') AS feb,
GROUP_CONCAT(CASE MONTH(e.date_start) WHEN 3 THEN CONCAT(e.id, '-', RIGHT(DATE(e.date_start), 5)) END SEPARATOR ',') AS mar,
GROUP_CONCAT(CASE MONTH(e.date_start) WHEN 4 THEN CONCAT(e.id, '-', RIGHT(DATE(e.date_start), 5)) END SEPARATOR ',') AS apr,
GROUP_CONCAT(CASE MONTH(e.date_start) WHEN 5 THEN CONCAT(e.id, '-', RIGHT(DATE(e.date_start), 5)) END SEPARATOR ',') AS may,
GROUP_CONCAT(CASE MONTH(e.date_start) WHEN 6 THEN CONCAT(e.id, '-', RIGHT(DATE(e.date_start), 5)) END SEPARATOR ',') AS jun,
GROUP_CONCAT(CASE MONTH(e.date_start) WHEN 7 THEN CONCAT(e.id, '-', RIGHT(DATE(e.date_start), 5)) END SEPARATOR ',') AS jul,
GROUP_CONCAT(CASE MONTH(e.date_start) WHEN 8 THEN CONCAT(e.id, '-', RIGHT(DATE(e.date_start), 5)) END SEPARATOR ',') AS aug,
GROUP_CONCAT(CASE MONTH(e.date_start) WHEN 9 THEN CONCAT(e.id, '-', RIGHT(DATE(e.date_start), 5)) END SEPARATOR ',') AS sep,
GROUP_CONCAT(CASE MONTH(e.date_start) WHEN 10 THEN CONCAT(e.id, '-', RIGHT(DATE(e.date_start), 5)) END SEPARATOR ',') AS oct,
GROUP_CONCAT(CASE MONTH(e.date_start) WHEN 11 THEN CONCAT(e.id, '-', RIGHT(DATE(e.date_start), 5)) END SEPARATOR ',') AS nov,
GROUP_CONCAT(CASE MONTH(e.date_start) WHEN 12 THEN CONCAT(e.id, '-', RIGHT(DATE(e.date_start), 5)) END SEPARATOR ',') AS `dec`
FROM
event AS e
INNER JOIN client AS c ON e.client_id = c.id
GROUP BY
c.name
UNION ALL
SELECT
NULL,
COUNT(MONTH(e.date_start) = 1 OR NULL),
COUNT(MONTH(e.date_start) = 2 OR NULL),
COUNT(MONTH(e.date_start) = 3 OR NULL),
COUNT(MONTH(e.date_start) = 4 OR NULL),
COUNT(MONTH(e.date_start) = 5 OR NULL),
COUNT(MONTH(e.date_start) = 6 OR NULL),
COUNT(MONTH(e.date_start) = 7 OR NULL),
COUNT(MONTH(e.date_start) = 8 OR NULL),
COUNT(MONTH(e.date_start) = 9 OR NULL),
COUNT(MONTH(e.date_start) = 10 OR NULL),
COUNT(MONTH(e.date_start) = 11 OR NULL),
COUNT(MONTH(e.date_start) = 12 OR NULL)
FROM
event AS e
;
Run Code Online (Sandbox Code Playgroud)
或者,也许,像这样,如果我们想通过消除某些代码的重复来使它看起来不那么麻烦:
SELECT
client,
GROUP_CONCAT(CASE month WHEN 1 THEN item END SEPARATOR ',') AS jan,
GROUP_CONCAT(CASE month WHEN 2 THEN item END SEPARATOR ',') AS feb,
GROUP_CONCAT(CASE month WHEN 3 THEN item END SEPARATOR ',') AS mar,
GROUP_CONCAT(CASE month WHEN 4 THEN item END SEPARATOR ',') AS apr,
GROUP_CONCAT(CASE month WHEN 5 THEN item END SEPARATOR ',') AS may,
GROUP_CONCAT(CASE month WHEN 6 THEN item END SEPARATOR ',') AS jun,
GROUP_CONCAT(CASE month WHEN 7 THEN item END SEPARATOR ',') AS jul,
GROUP_CONCAT(CASE month WHEN 8 THEN item END SEPARATOR ',') AS aug,
GROUP_CONCAT(CASE month WHEN 9 THEN item END SEPARATOR ',') AS sep,
GROUP_CONCAT(CASE month WHEN 10 THEN item END SEPARATOR ',') AS oct,
GROUP_CONCAT(CASE month WHEN 11 THEN item END SEPARATOR ',') AS nov,
GROUP_CONCAT(CASE month WHEN 12 THEN item END SEPARATOR ',') AS `dec`
FROM
(
SELECT
c.name AS client,
MONTH(e.date_start) AS month,
CONCAT(e.id, '-', RIGHT(DATE(e.date_start), 5)) AS item
FROM
event AS e
INNER JOIN client AS c ON e.client_id = c.id
) AS derived
GROUP BY
client
UNION ALL
SELECT
NULL,
COUNT(month = 1 OR NULL),
COUNT(month = 2 OR NULL),
COUNT(month = 3 OR NULL),
COUNT(month = 4 OR NULL),
COUNT(month = 5 OR NULL),
COUNT(month = 6 OR NULL),
COUNT(month = 7 OR NULL),
COUNT(month = 8 OR NULL),
COUNT(month = 9 OR NULL),
COUNT(month = 10 OR NULL),
COUNT(month = 11 OR NULL),
COUNT(month = 12 OR NULL)
FROM
(
SELECT
MONTH(e.date_start) AS month
FROM
event AS e
) AS derived
;
Run Code Online (Sandbox Code Playgroud)
如果您对A = B OR NULL
公式不是很熟悉,可以将其视为CASE WHEN A = B THEN 1 ELSE NULL END
. 有关它如何真正工作的详细信息,我向您推荐这个 Stack Overflow 问题:
尽管所有关于清晰逻辑和可维护性的讨论都很好,但您可能仍然希望能够将查询实现为单个 SELECT。即使我们通过减少代码重复设法简化了初始版本,MONTH(date_start)
表达式仍然必须在整个查询中指定两次,因为每个 SELECT 分支都需要它,那么为什么不尝试消除重复呢?如果这还不是一个足够的理由,替代解决方案可能会更快,甚至可能显着。也许由此产生的查询看起来不会太难看。最后,有一个选择就好了,简单明了。
那么,如何使用WITH ROLLUP重写查询,以便客户端详细信息和汇总行都由同一个 SELECT 语句生成(没有任何 UNION ALL 类型的作弊)?
好吧,您可以使用前一个解决方案作为原型。该查询的一部分在客户端上执行组串联。另一部分计算整个集合的行数。现在,如果您想进行单部分查询,则该单部分必须在两个级别执行这两个操作。
在哪个级别显示哪种信息应该由另一组条件决定。
考虑到上述几点,这里是我对单步查询的尝试:
SELECT
client,
IF(client IS NULL, COUNT(month = 1 OR NULL), GROUP_CONCAT(CASE month WHEN 1 THEN item END SEPARATOR ',')) AS jan,
IF(client IS NULL, COUNT(month = 2 OR NULL), GROUP_CONCAT(CASE month WHEN 2 THEN item END SEPARATOR ',')) AS feb,
IF(client IS NULL, COUNT(month = 3 OR NULL), GROUP_CONCAT(CASE month WHEN 3 THEN item END SEPARATOR ',')) AS mar,
IF(client IS NULL, COUNT(month = 4 OR NULL), GROUP_CONCAT(CASE month WHEN 4 THEN item END SEPARATOR ',')) AS apr,
IF(client IS NULL, COUNT(month = 5 OR NULL), GROUP_CONCAT(CASE month WHEN 5 THEN item END SEPARATOR ',')) AS may,
IF(client IS NULL, COUNT(month = 6 OR NULL), GROUP_CONCAT(CASE month WHEN 6 THEN item END SEPARATOR ',')) AS jun,
IF(client IS NULL, COUNT(month = 7 OR NULL), GROUP_CONCAT(CASE month WHEN 7 THEN item END SEPARATOR ',')) AS jul,
IF(client IS NULL, COUNT(month = 8 OR NULL), GROUP_CONCAT(CASE month WHEN 8 THEN item END SEPARATOR ',')) AS aug,
IF(client IS NULL, COUNT(month = 9 OR NULL), GROUP_CONCAT(CASE month WHEN 9 THEN item END SEPARATOR ',')) AS sep,
IF(client IS NULL, COUNT(month = 10 OR NULL), GROUP_CONCAT(CASE month WHEN 10 THEN item END SEPARATOR ',')) AS oct,
IF(client IS NULL, COUNT(month = 11 OR NULL), GROUP_CONCAT(CASE month WHEN 11 THEN item END SEPARATOR ',')) AS nov,
IF(client IS NULL, COUNT(month = 12 OR NULL), GROUP_CONCAT(CASE month WHEN 12 THEN item END SEPARATOR ',')) AS `dec`
FROM
(
SELECT
c.name AS client,
MONTH(e.date_start) AS month,
CONCAT(e.id, '-', RIGHT(DATE(e.date_start), 5)) AS item
FROM
event AS e
INNER JOIN client AS c ON e.client_id = c.id
) AS derived
GROUP BY
client
WITH ROLLUP
;
Run Code Online (Sandbox Code Playgroud)
如您所见,查询同时在客户端级别和整个集级别计算 COUNT 和 GROUP_CONCAT。但是每对结果都放在一个 IF 函数中,因此最终每列中只返回一个或另一个结果。
检查的条件是client IS NULL
。如果client
恰好为空,则意味着当前组代表整个集合,在这种情况下,每个 IF 函数都选择 COUNT 结果。当client
value 不为 null 时,这意味着我们处于客户端级别,每组行代表一个特定的客户端。在这种情况下,根据要求返回 GROUP_CONCAT 结果,因为对于客户端,我们必须显示连接的字符串。
这两种解决方案都可以在dbfiddle.uk上找到。
在我上面的解释中,我试图将重点放在解决方案及其工作原理上。为了避免分心,我在代码中允许使用某些值得一提的反模式。
隐式转换数据时依赖优先规则。
函数 COUNT() 和 GROUP_CONCAT() 的结果是不同的。一个返回一个整数,另一个返回一个字符串。当您尝试将这些不同类型的值放入单个列中时,服务器必须决定将哪种类型转换为哪种其他类型。了解这些规则很好,但您永远不应该在生产代码中依赖它们。那只是不好的做法。
在上面的查询中,一个 COUNT 和一个 GROUP_CONCAT 要么在同一查询的不同部分的同一列中,要么在两者之间选择的相同条件中。在每种情况下,MySQL 都需要应用其类型优先级规则。为避免这种情况,您可以将每个 COUNT 显式转换为字符串:
CAST(COUNT(...) AS char)
Run Code Online (Sandbox Code Playgroud)GROUP_CONCAT 中缺少 ORDER BY。
如果您省略 ORDER BY,您只是在说您不在乎查询是否一次返回字符串 asA,B,C
和另一个 asB,A,C
以及稍后返回 as C,B,A
。如果您希望您的结果是可预测的,请始终指定 ORDER BY 并始终使用足够的条件来避免平局。
在上面的查询中,行已经很长了,为了便于展示,我故意省略了 ORDER BY。这个问题可以通过这样的 ORDER BY 轻松解决:
ORDER BY item ASC
Run Code Online (Sandbox Code Playgroud)
更具体地说,在 GROUP_CONCAT 中,它将像这样使用:
GROUP_CONCAT(CASE month WHEN 12 THEN item END ORDER BY item ASC SEPARATOR ',')
Run Code Online (Sandbox Code Playgroud) 归档时间: |
|
查看次数: |
3590 次 |
最近记录: |