SQL for Data Analysis - Lesson 3 - SQL 聚合

目录


正文

聚合简介

通常,JOIN语句和WHERE筛选语句, 对于查询结果很有用, 比如说我们想要查看给定销售人员的个人订单, 以评估它们在给定月份的绩效, 但有时候, 行数据太多了, 最终效果不如聚合数据. 比如计算每个月每个地区的总订单数, 这时候就需要使用聚合. SQL聚合函数总体和Excel聚合函数类似, 而且针对的是, 不是行, 比如你可以对纸张的总供应量求和.

NULL简介

NULL 是一种数据类型,表示 SQL 中没有数据, 与零不同. 对于Parch&Posey公司, 零意味着没有卖出任何纸张. 空值意味着根本没有尝试过销售, 空值可能是多种原因, 可能是数据设计的一部分, 也可能仅仅是数据锁定不佳的结果.

为了更深入的了解空值, 选取accounts账户ID在1500与1600之间的记录

image.png

假设你是Parch&Posey的销售经理, 你可能想了解哪些账户的主要联系人为Null

要找出这些账户, 你必须在WHERE子句中使用一些特殊语法, IS NULL, 比如针对primary_poc一列

SELECT *
  FROM accounts
WHERE primary_poc IS NULL

结果显示, 有不少账户现在没有联系人

image.png

如果你想找到和现在的结果集相反的结果, 使用IS NOT NULL

在以下两种常见情况下,你可能会遇到 NULL:

  • 在执行 LEFT JOIN 或 RIGHT JOIN 时,NULL 经常会发生。左侧表格中的某些行在做连接时与右侧表格中的行如果不匹配,这些行在结果集中就会包含一些 NULL 值。

  • NULL 也可能是因为数据库中缺失数据。
     

COUNT

COUNT函数会返回, 所有包含非空值数据行的计数, 整行都是空值的情况很少见, 所有当执行COUNT(*), 一般和表格行数是相等的

计算表格中的行数
以下是计算 accounts 表格中的行数示例:

SELECT COUNT(*)
FROM accounts;

还可以给count添加别名, 这样易于阅读

SELECT COUNT(*) AS count_accounts
FROM accounts;

也可以轻松地选择某一列来放置聚合函数:

SELECT COUNT(accounts.id)
FROM accounts;

用来计算单个列中非空值记录的数量
为了说明这一点, 我们来看一下账户表

image.png

*换为id, 我们可以看到ID列中有多少非空值记录,由于id列没有空值, 所以它返回与COUNT(*)相同的结果.

image.png

如果使用已知的包含一些空值primary_poc列来尝试, 这一次我们得到了的结果比之前小9.

image.png

 

SUM

假设你是Parch&Posey团队的营运经理, 你在制定库存计划, 想知道每种类型的纸张应生产多少?

一种好的开始方式是, 先合计每种纸张类型的总销量, 再进行相互间的对比, 我们将通过SUM来完成该操作, 它的用法和COUNT类似, 只是你需要说明具体列的名称, 而不是使用*.

SELECT SUM(standard_qty) AS standard,
       SUM(gloss_qty) AS gloss,
       SUM(poster_qty) AS poster
   FROM orders

结果显示标准纸张的销量比两种非标准纸类销量之和更多.

image.png

与COUNT不同, SUM只用于包含(numerical)的列, 并且你不需要考虑空值的问题. 因为SUM函数将空值当作0来处理

注意事项:聚合函数只能垂直聚合,即聚合列的值。

练习

1.算出 orders 表格中的 poster_qty 纸张总订单量。

SELECT SUM(poster_qty) AS poster
    FROM orders;

2.算出 orders 表格中 standard_qty 纸张的总订单量。

SELECT SUM(standard_qty) AS standard
    FROM orders;

3.根据 orders 表格中的 total_amt_usd 得出总销售额。

SELECT SUM(total_amt_usd) AS total_usd
    FROM orders;

4.算出 orders 表格中每个订单在 standard 和 gloss 纸张上消费的数额。结果应该是表格中每个订单的金额。

SELECT standard_qty + gloss_qty AS total_standard_gloss
FROM orders;

5.每个订单的 price/standard_qty 纸张各不相同。我想得出 orders 表格中标准纸张的平均单价。

SELECT SUM(standard_amt_usd)/SUM(standard_qty) AS standard_price_per_unit
FROM orders;

 

MIN和MAX

MIN和MAX的语法与SUM和COUNT相似

SELECT MIN(standard_qty) AS standard_min,
       MIN(gloss_qty) AS gloss_min,
       MIN(poster_qty) AS poster_min,
       MAX(standard_qty) AS standard_max,
       MAX(gloss_qty) AS gloss_max,
       MAX(poster_qty) AS poster_max
FROM orders;
image.png

注意,MIN 和 MAX 聚合函数也会忽略 NULL 值。

从功能上来说,MIN 和 MAX 与 COUNT 相似,它们都可以用在非数字列上。MIN 将返回最小的数字最早的日期按字母表排序的最之前的非数字值,具体取决于列类型。MAX 则正好相反,返回的是最大的数字最近的日期,或与“Z”最接近(按字母表顺序排列)的非数字值。
 

AVG

通过MIN和MAX, 了解到最受欢迎的纸张类型, 以及在某些时间满足的最大订单量, 如果想知道平均订单量, 我们使用AVG函数(average), 它与其他聚合函数的语法相似.

SELECT AVG(standard_qty) AS standard_avg,
       AVG(gloss_qty) AS gloss_avg,
       AVG(poster_qty) AS poster_avg
FROM orders;
image.png

AVG 返回的是数据的平均值,即列中所有的值之和除以列中值的数量。该聚合函数同样会忽略分子和分母中的 NULL 值

如果你想将 NULL 当做零,则需要使用 SUM 和 COUNT, 先求和,然后用和除以计数。但是,如果 NULL 值真的只是代表单元格的未知值,那么这么做可能不太合适。

练习

1.最早的订单下于何时?

SELECT MIN(occurred_at) AS time
FROM orders;

2.尝试执行和第一个问题一样的查询,但是不使用聚合函数。

SELECT occurred_at AS time
FROM orders
ORDER BY occurred_at
LIMIT 1;

3.最近的 web_event 发生在什么时候?

SELECT MAX(occurred_at) AS time
FROM web_events;

4.尝试以另一种方式执行上个问题的查询,不使用聚合函数。

SELECT occurred_at AS time
FROM web_events
ORDER BY occurred_at DESC
LIMIT 1;

5.算出每个订单在每种纸张上消费的平均 (AVERAGE) 金额,以及每个订单针对每种纸张购买的平均数量。最终答案应该有 6 个值,每个纸张类型平均销量对应一个值,以及平均数量对应一个值。

SELECT AVG(standard_amt_usd) AS standard_usd_avg,
       AVG(gloss_amt_usd) AS gloss_usd_avg,
       AVG(poster_amt_usd) AS poster_usd_avg,
       AVG(standard_qty) AS standard_qty_avg,
       AVG(gloss_qty) AS gloss_qty_avg,
       AVG(poster_qty) AS poster_qty_avg
FROM orders;       

6.看过视频后,你可能对如何计算中位数感兴趣。虽然这已经超出了目前我们所学的范围,但请尝试探索这个问题:对于所有订单(orders)数据,其total_usd字段的中位数是多少?请注意,构建一个此问题的通用解决方案已经超出了目前所学的课程范围,但我们可以硬写出以下这段代码:

SELECT *
FROM (SELECT total_amt_usd
      FROM orders
      ORDER BY total_amt_usd
      LIMIT 3457) AS Table1
ORDER BY total_amt_usd DESC
LIMIT 2;

因为订单一共有6912个,因此我们需要第3456和第3457个订单(按total_amt_usd排序)的total_amt_usd字段的平均值。这样就能得出中位数结果,为2482.855。这显然不是一个好办法。如果我们有了新订单,再次计算时就必须修改LIMIT。SQL实际上并不会为我们计算中位数。以上代码使用了一个子查询(SUBQUERY),但你可以使用任何方法找到需要的两个值,然后再求平均即可得到中位数。
 

GROUP BY - 单列

作为一名销售经理, 你可能需要计算每个账户每种纸张的总销量, 即依据账户ID创建各自总和的集合.

通过GROUP BY你可以创建分组, 在聚合时相互独立, 也就是说, GROUP BY 可以抓取单个账户的数据之和, 而不是整个数据集.

SELECT account_id,
       SUM(standard_qty) AS standard_sum,
       SUM(gloss_qty) AS gloss_sum,
       SUM(poster_qty) AS poster_sum
FROM orders
GROUP BY account_id
ORDER BY account_id

如果SELECT语句中有字段没有进行聚合, 则应该将其放入GROUP BY子句, 如果有一列内容没有进行聚合且不在GROUP BY语句中, 则会返回错误结果, 这一点很重要, 时正确使用GROUP BY语句的关键

主要知识点概括如下:

  • GROUP BY 可以用来在数据子集中聚合数据。例如,不同客户、不同区域或不同销售代表分组。

  • SELECT 语句中的任何一列如果不在聚合函数中,则必须在 GROUP BY 条件中。

  • GROUP BY 始终在 WHERE 和 ORDER BY 之间。

  • ORDER BY 有点像电子表格软件中的 SORT。

练习

1.哪个客户(按照名称)下的订单最早?你的答案应该包含订单的客户名称和日期。

SELECT a.name, o.occurred_at
FROM accounts a
JOIN orders o
ON a.id = o.account_id
ORDER BY occurred_at
LIMIT 1;

2.算出每个客户的总销售额(单位是美元)。答案应该包括两列:每个公司的订单总销售额(单位是美元)以及公司名称。

SELECT a.name,
   SUM(o.total_amt_usd) AS total_amt_usd
  FROM accounts a
  JOIN orders o
    ON o.account_id = a.id
 GROUP BY  a.name;

3.最近的 web_event 是通过哪个渠道发生的,与此 web_event 相关的客户是哪个?你的查询应该仅返回三个值:日期、渠道和客户名称。

SELECT w.occurred_at, w.channel, a.name
FROM web_events w
JOIN accounts a
ON w.account_id = a.id 
ORDER BY w.occurred_at DESC
LIMIT 1;

4.算出 web_events 中每种渠道的次数。最终表格应该有两列:渠道和渠道的使用次数。

SELECT w.channel,
   COUNT(w.channel) 
  FROM web_events w
 GROUP BY  w.channel

5.与最早的 web_event 相关的主要联系人是谁?

SELECT a.primary_poc
FROM web_events w
JOIN accounts a
ON a.id = w.account_id
ORDER BY w.occurred_at
LIMIT 1;

6.每个客户所下的最小订单是什么(以总金额(美元)为准)。答案只需两列:客户名称和总金额(美元)。从最小金额到最大金额排序。

SELECT a.name,
   MIN(o.total_amt_usd) AS total_amt_usd
  FROM aoccounts a
  JOIN orders 
    ON o.account_id = a.id
 GROUP BY  a.name
ORDER BY total_amt_usd;

7.算出每个区域的销售代表人数。最早表格应该包含两列:区域和 sales_reps 数量。从最少到最多的代表人数排序。

SELECT r.name,
   COUNT(*) AS sales_reps
  FROM region r
  JOIN sales_reps s
    ON s.region_id = r.id
 GROUP BY  r.name
ORDER BY sales_reps;

 

GROUP BY - 多列

假设你在Parch&Posey担任营销经理, 想要了解每个账户如何与各种广告渠道互动, 哪些渠道在吸引流量并促成交易, 我们是否投资了不值得的渠道上, 每个渠道获得了多少流量?

实现上述问题的方法就是统计每个账户ID的每个渠道事件, 然后突出显示每个账户事件量最高的渠道

SELECT account_id,
       channel,
       COUNT(id) AS events 
  FROM web_events
GROUP BY account_id, channel
ORDER BY account_id, events DESC

 

主要知识点:
  • 你可以同时按照多列分组,正如此处所显示的那样。这样经常可以在大量不同的细分中更好地获得聚合结果。
  • ORDER BY 条件中列出的列顺序有区别。你是从左到右让列排序。

GROUP BY - 提示
GROUP BY 条件中的列名称顺序并不重要,结果还是一样的。如果运行相同的查询并颠倒 GROUP BY 条件中列名称的顺序,可以看到结果是一样的。

和 ORDER BY 一样,你可以在 GROUP BY 条件中用数字替换列名称。仅当你对大量的列分组时,或者其他原因导致 GROUP BY 条件中的文字过长时,才建议这么做。

提醒下,任何不在聚合函数中的列必须显示 GROUP BY 语句。如果忘记了,可能会遇到错误。但是,即使查询可行,你也可能不会喜欢最后的结果!

问题:GROUP BY(第二部分)
根据以下 SQL 表格信息回答以下问题。如果你遇到问题或想要对比检查你的答案,可以在下一页面的顶部找到我的答案。

1.对于每个客户,确定他们在订单中购买的每种纸张的平均数额。结果应该有四列:客户名称一列,每种纸张类型的平均数额一列。

SELECT a.name,
       AVG(standard_qty) AS mean_of_standard,
       AVG(gloss_qty) AS mean_of_gloss,
       AVG(poster_qty) AS mean_of_poster
  FROM orders o
  JOIN accounts a
    ON o.account_id = a.id
GROUP BY a.name

2.对于每个客户,确定在每个订单中针对每个纸张类型的平均消费数额。结果应该有四列:客户名称一列,每种纸张类型的平均消费数额一列。

SELECT a.name,
       AVG(standard_amt_usd) AS mean_of_standard,
       AVG(gloss_amt_usd) AS mean_of_gloss,
       AVG(poster_amt_usd) AS mean_of_poster
  FROM orders o
  JOIN accounts a
    ON o.account_id = a.id
GROUP BY a.name

3.确定在 web_events 表格中每个销售代表使用特定渠道的次数。最终表格应该有三列:销售代表的名称、渠道和发生次数。按照最高的发生次数在最上面对表格排序。

SELECT s.name,
       w.channel,
       COUNT(w.id) AS events 
  FROM web_events w
  JOIN accounts a
    ON w.account_id = a.id
  JOIN sales_reps s
    ON a.sales_rep_id = s.id
GROUP BY s.name, w.channel
ORDER BY events DESC

4.确定在 web_events 表格中针对每个地区特定渠道的使用次数。最终表格应该有三列:区域名称、渠道和发生次数。按照最高的发生次数在最上面对表格排序。

SELECT r.name,
       w.channel,
       COUNT(w.id) AS events 
  FROM web_events w
  JOIN accounts a
    ON w.account_id = a.id
  JOIN sales_reps s
    ON a.sales_rep_id = s.id
  JOIN region r
    ON s.region_id = r.id
GROUP BY r.name, w.channel
ORDER BY events DESC;

 

DISTINCT

如果你要对几列内容进行分组, 但不包含任何聚合函数, 你可以使用DISTINCT代替, 可以将 DISTINCT 看做仅返回特定列的唯一值的函数。例如对每个账户每个渠道的时间(events)计数

SELECT account_id,
       channel,
       COUNT(id) AS events 
  FROM web_events
GROUP BY account_id, channel
ORDER BY account_id, events DESC
image.png

如果我们去除events列, 行数1509没有变化

image.png

如果使用DISTINCT, 和上图的结果时一样的.

image.png

练习

1.使用 DISTINCT 检查是否有任何客户与多个区域相关联?

下面的两个查询产生了相同的行数(351 行),因此我们知道每个客户仅与一个区域相关联。
如果每个客户与多个区域相关联,则第一个查询返回的行数应该比第二个查询的多。

SELECT DISTINCT a.id, r.id, a.name, r.name
FROM accounts a
JOIN sales_reps s
ON s.id = a.sales_rep_id
JOIN region r
ON r.id = s.region_id;

and

SELECT DISTINCT id, name
FROM accounts;

2.有没有销售代表要处理多个客户?

实际上,所有销售代表都要处理多个客户。
销售代表处理的最少客户数量是 3 个。
有 50 个销售代表,他们都有多个客户。
在第二个查询中使用 DISTINCT 确保包含了第一个查询中的所有销售代表。

SELECT s.id, s.name, COUNT(*) num_accounts
FROM accounts a
JOIN sales_reps s
ON s.id = a.sales_rep_id
GROUP BY s.id, s.name
ORDER BY num_accounts;

and

SELECT DISTINCT id, name
FROM sales_reps;

 

HAVING

假设你是Parch&Posey的账户经理, 负责的是公司最大的账户, 你可能需要确定销售额超过25万的账户的总销售额, 以便更好地了解, 来自这些大额账户的收入的比例.

要得到这个清单, 首先要知道每个账户的销售总额, 并采用降序排序

SELECT account_id, SUM(total_amt_usd) AS total_amt_usd
FROM orders
GROUP BY 1
ORDER BY 2 DESC;

然后从这些账户中过滤处销售额超过25万的账户

image.png

但是当使用WHERE子句进行过滤时, 会出现报错, 因为它不允许过滤聚合后的列, 在这个例子中 就是SUM(total_amt_usd), 而聚合列要用到HAVING子句, HAVING子句的顺序总是出现在GROUP BY语句后面.

SELECT account_id, SUM(total_amt_usd) AS total_amt_usd
FROM orders
GROUP BY 1
HAVING SUM(total_amt_usd) > 250000
ORDER BY 2 DESC;

注意
这只有在按一个或多个列分组时才有用, 也就是说没有分组时, 没有必要使用, 本质上,只要你想对通过聚合创建的查询中的元素执行 WHERE 条件,就需要使用 HAVING。

练习

image.png

1.有多少位销售代表需要管理超过 5 个客户?

SELECT s.id, s.name, COUNT(a.id) num_accounts
FROM accounts a
JOIN sales_reps s
ON s.id = a.sales_rep_id
GROUP BY s.id, s.name
HAVING COUNT(a.id) > 5

2.有多少个客户具有超过 20 个订单?

SELECT a.name,
       COUNT(*) num_orders
  FROM orders o
  JOIN accounts a
  ON o.account_id = a.id
  GROUP BY a.name
  HAVING COUNT(*) > 20

3.哪个客户的订单最多?

SELECT a.name,
       COUNT(*) num_orders
  FROM orders o
  JOIN accounts a
  ON o.account_id = a.id
  GROUP BY a.name
  ORDER BY 2 DESC
  LIMIT 1

4.有多少个客户在所有订单上消费的总额超过了 30,000 美元?

SELECT a.name,
       SUM(o.total_amt_usd) AS total_amt
  FROM orders o
  JOIN accounts a
  ON o.account_id = a.id
  GROUP BY a.name
  HAVING SUM(o.total_amt_usd) > 30000
  ORDER BY 2 DESC

5.有多少个客户在所有订单上消费的总额不到 1,000 美元?

SELECT a.name,
       SUM(o.total_amt_usd) AS total_amt
  FROM orders o
  JOIN accounts a
  ON o.account_id = a.id
  GROUP BY a.name
  HAVING SUM(o.total_amt_usd) < 1000
  ORDER BY 2 DESC

6.哪个客户消费的最多?

SELECT a.name,
       SUM(o.total_amt_usd) AS total_amt
  FROM orders o
  JOIN accounts a
  ON o.account_id = a.id
  GROUP BY a.name
  ORDER BY 2 DESC
  LIMIT 1

7.哪个客户消费的最少?

SELECT a.name,
       SUM(o.total_amt_usd) AS total_amt
  FROM orders o
  JOIN accounts a
  ON o.account_id = a.id
  GROUP BY a.name
  ORDER BY 2 
  LIMIT 1

8.哪个客户使用 facebook 作为与消费者沟通的渠道超过 6 次?

SELECT a.name, w.channel,
       COUNT(w.channel) AS num_channel
  FROM web_events w
  JOIN accounts a
  ON w.account_id = a.id
  WHERE w.channel = 'facebook'
  GROUP BY a.name, w.channel
  HAVING COUNT(w.channel) > 6
  ORDER BY num_channel DESC
或者
SELECT a.id, a.name, w.channel, COUNT(*) use_of_channel
FROM accounts a
JOIN web_events w
ON a.id = w.account_id
GROUP BY a.id, a.name, w.channel
HAVING COUNT(*) > 6 AND w.channel = 'facebook'
ORDER BY use_of_channel;

9.哪个客户使用 facebook 作为沟通渠道的次数最多?

SELECT a.name, w.channel,
       COUNT(w.channel) AS num_channel
  FROM web_events w
  JOIN accounts a
  ON w.account_id = a.id
  WHERE w.channel = 'facebook'
  GROUP BY a.name, w.channel
  ORDER BY 3 DESC
  LIMIT 1

10.哪个渠道是客户最常用的渠道?

SELECT w.channel,
       COUNT(w.channel) AS num_channel
  FROM web_events w
  JOIN accounts a
  ON w.account_id = a.id
  GROUP BY w.channel
  ORDER BY 2 DESC
  LIMIT 1

 

DATE_TRUNC

在 SQL 中,按照日期列分组通常不太实用, 每个时间戳都是独一无二的,因为这些列可能包含小到一秒的交易数据。所以最现实的方法还是将其处理为最近一天, 一个星期或一个月, 并对此期间进行聚合.

首先了解一下数据库中时间的存储方式, 日期排序和文本排序都是一样的, 看到日期存储为年、月、日、小时、分钟、秒,可以帮助我们截取信息。

image.png

要按日期分组的话, 我们需要把2017年4月1日这一天所有的时间, 调整为00:00:00, 这样4月1日所有小时,分钟和秒内发生的每一个事件, 它们会被归为一组, 要实现这一点, 可以使用DATE_TRUNC函数来完成.

image.png
注意:要按包含在SELECT语句中的相同度量进行分组, 这一点非常重要, 这样可以保证结果的一致.

SELECT DATE_TRUNC('day', occurred_at) AS day,
       SUM(standard_qty) AS standard_qty_sum
  FROM orders
  GROUP BY DATE_TRUNC('day', occurred_at)
  ORDER BY DATE_TRUNC('day', occurred_at)

执行结果

image.png

大部分时候, 你会使用事件间隔进行聚合, 因为它从业务角度来看很有意义, 比如'day', 'week', 'month', 'quarter', 'year'. 在某些情况下, 你可能只需要提取一个日期的某一部分, 例如如果你想知道, 本周哪一天中Parch&Poise网站的流量最大, 但你不想使用DATE_TRUNC, 要想知道是那一天, 你必须使用DATE_PART
 

DATE_PART

DATE_PART 可以提取处感兴趣的日期中某个部分

image.png

在Parch&Posey公司中, 一周中的哪一天的销售额最多?

首先确认每周每天的订单, dow 代表星期几, 会返回一个从0到6的值, 其中0代表星期日, 6代表星期六.
然后, 对这一天的数据进行汇总总销量
最后对每一周中每一天进行分组, 再排序.

SELECT DATE_PART('dow', occurred_at) AS day_of_week,
       SUM(total) AS total_qty
  FROM orders
GROUP BY 1
ORDER BY 2 DESC;
image.png

从执行结果可知, 周日的订单量最大.

提示: DATE_PART 可以用来获取日期的特定部分,但是注意获取 month 或 dow 意味着无法让年份按顺序排列。而是按照特定的部分分组,无论它们属于哪个年份。

练习

  1. Parch & Posey 在哪一年的总销售额最高?数据集中的所有年份保持均匀分布吗?
SELECT DATE_PART('year', occurred_at) AS year,
       SUM(total_amt_usd) AS total_usd
  FROM orders
  GROUP BY 1
  ORDER BY 2 DESC
image.png

结论:
对于 2013 年和 2017 年来说,每一年只有一个月的销量(2013 年为 12,2017 年为 1)。 因此,二者都不是均匀分布。销量一年比一年高,2016 年是到目前为止最高的一年。按照这个速度,我们预计 2017 年可能是最高销量的一年。
 

  1. Parch & Posey 在哪一个月的总销售额最高?数据集中的所有月份保持均匀分布吗?
SELECT DATE_PART('month', occurred_at) AS Month,
       SUM(total_amt_usd) AS total_usd
  FROM orders
  WHERE occurred_at BETWEEN '2014-01-01' AND '2017-01-01'
  GROUP BY DATE_PART('month', occurred_at)
  ORDER BY 2 DESC
image.png

结论:
为了保持公平,我们应该删掉 2013 年和 2017 年的销量。原因如上。12 月的销量最高。
 

  1. Parch & Posey 在哪一年的总订单量最多?数据集中的所有年份保持均匀分布吗?
SELECT DATE_PART('year', occurred_at) AS year,
       COUNT(*) total_sales
  FROM orders
  GROUP BY DATE_PART('year', occurred_at)
  ORDER BY 2 DESC
image.png

结论:
同样,到目前为止,2016 年的订单量最多,但是与数据集中的其他年份相比,2013 年和 2017 年的分布不均匀。
 

  1. Parch & Posey 在哪一个月的总订单量最多?数据集中的所有月份保均匀分布吗?
SELECT DATE_PART('month', occurred_at) AS Month,
       COUNT(*) total_sales
  FROM orders
WHERE occurred_at BETWEEN '2014-01-01' AND '2017-01-01'
  GROUP BY DATE_PART('month', occurred_at)
  ORDER BY 2 DESC
image.png

结论:
12 月依然是销量最多的月份,但是有趣的是,11 月是销量第二多的月份。为了保持公平,删掉了 2017 年和 2013 年的数据。
 

  1. Walmart 在哪一年的哪一个月在铜版纸上的消费最多?
SELECT DATE_TRUNC('month', o.occurred_at) AS month_of_year,
       SUM(o.gloss_amt_usd) AS gloss_usd
  FROM orders o
  JOIN accounts a
  ON a.id = o.account_id
  WHERE a.name = 'Walmart'
  GROUP BY 1
  ORDER BY 2 DESC
  LIMIT 1;
image.png

结论:
在 2016 年 5 月,Walmart 在铜版纸上的消费做多。

 

CASE

假设你是Parch&Posey的市场经理, 你想把Facebook与其他市场渠道进行对比, 你知道Facebook是你业务的最重要渠道, 但是, 他是否大过所有其他渠道的总和?

代码如下:

SELECT id,
       account_id,
       occurred_at,
       channel,
       CASE WHEN channel = 'facebook' THEN 'yes' END AS is_facebook
  FROM web_events
ORDER BY occurred_at

首先要创建一个派生列, 也就说从当前列中提取数据,然后进行修改, 在前面的文中, 使用算术来完成, 在这个例子中用CASE函数, 这是SQL处理IF-THEN逻辑的方式, CASE语句会附有至少一对WHEN-THEN语句, 在SQL中, 这两者等同于IF-THEN, 完成时必须有END一词, CASE语句会检查条件语句channel = 'facebook'是否为真, 如果为真, 其结果yes就出现在is_facebook列中

image.png

如果想让channel = 'facebook'为假时, 也用相应的词来填充派生列, 那么可以使用ELSE语句, 代码如下:

SELECT id,
       account_id,
       occurred_at,
       channel,
       CASE WHEN channel = 'facebook' THEN 'yes' ELSE 'no' END AS is_facebook
  FROM web_events
ORDER BY occurred_at

如果想让新列不仅持续追踪Facebook渠道, 还追踪direct渠道, 可以修改CASE 语句, 用OR运算符, 代码如下:

SELECT id,
       account_id,
       occurred_at,
       channel,
       CASE WHEN channel = 'facebook' OR channel = 'direct' THEN 'yes' ELSE 'no' END AS is_facebook
  FROM web_events
ORDER BY occurred_at

提示: WHEN语句类似于WHERE语句中的逻辑条件, 因此可以使用AND, LIKE, IN或者其他逻辑运算符

 
使用 CASE语句定义多个输出

假设你负责Parch&Posey的运营, 你想根据订单大小将订单进行分组, 以得到更精细的库存规划

实现上述过程, 可以通过多个WHEN-THEN语句, 代码示例如下:

SELECT account_id,
       occurred_at,
       total,
       CASE WHEN total= '500' THEN 'over 500' 
            WHEN total= '300' THEN '301 - 500' 
            WHEN total= '100' THEN '100 - 300' 
            ELSE '100 or under' END AS total_group
  FROM orders

image.png

在以上的代码中, 条件语句total= '500'total= '300'范围重叠, 对每个大于500的值就会有些混淆, 更好的方法时, 定义不会于其他组重叠的特殊组, 为此, 可以将多种条件语句串联起来, 和WHERE子句中方法相同, 使用ANDOR. 修改后的代码如下:

SELECT account_id,
       occurred_at,
       total,
       CASE WHEN total > 500 THEN 'over 500'
                  WHEN total= '300' AND total <= 500 THEN '301 - 500' 
                  WHEN total= '100' AND total <= 300 THEN '100 - 300' 
                  ELSE '100 or under' END AS total_group
  FROM orders

示例
在第一节课的练习中,你看到了以下问题:

创建一列用于将 standard_amt_usd 除以 standard_qty,以便计算每个订单的标准纸张的单价,将结果限制到前 10 个订单,并包含 id 和 account_id 字段。注意 - 如果你的答案正确,系统将显示一个错误,这是因为你除以了 0。当你在下个部分学习 CASE 语句时,你将了解如何让此查询不会报错。

我们来看看如何使用 CASE 语句来避免这一错误。

SELECT id, account_id, standard_amt_usd/standard_qty AS unit_price
FROM orders
LIMIT 10;

现在我们使用一个 CASE 语句,这样的话,一旦 standard_qty 为 0,我们将返回 0,否则返回 unit_price。

SELECT account_id, 
       CASE WHEN standard_qty = 0 OR standard_qty IS NULL THEN 0
            ELSE standard_amt_usd/standard_qty END AS unit_price
FROM orders
LIMIT 10;

 
 

CASE与聚合

假如你作为Parch&Posey的销售经理, 将订单分进行分组, 并且计算出每一组的所有订单数.

进行组内计数的最简单的办法, 是创建一个按照你期望方式分类的列, 接下来, 创建另外一个列, 进行组的计数, 在此, 我们使用CASE将订单分为 总销售额 超过500和500及以下的分组, 然后对此派生列进行分组.

SELECT CASE WHEN total > 500 THEN 'Over 500'
            ELSE '500 or under ' END AS total_group
       COUNT(*) AS order_count
  FROM orders
GROUP BY 1

思考: 为什么不用WHERE子句过滤掉那些我不想计数的行?
因为WHERE子句只允许一次计算一个条件, 如果有大量的不同案例, 内容会特别冗长.

 
练习

1.我们想要根据相关的消费量了解三组不同的客户。最高的一组是终身价值(所有订单的总销售额)大于 200,000 美元的客户。第二组是在 200,000 到 100,000 美元之间的客户。最低的一组是低于 under 100,000 美元的客户。请提供一个表格,其中包含与每个客户相关的级别。你应该提供客户的名称所有订单的总销售额级别。消费最高的客户列在最上面。

SELECT a.name,
       SUM(o.total_amt_usd) AS total_amt,  
  CASE WHEN SUM(o.total_amt_usd) > 200000 THEN 'top'
       WHEN SUM(o.total_amt_usd) > 100000 AND SUM(o.total_amt_usd) <= 200000 THEN 'middle'
       ELSE 'low' END AS Class
  FROM orders o
  JOIN accounts a
  ON a.id = o.account_id
  GROUP BY 1
ORDER BY 2 DESC

 

2.现在我们想要执行和第一个问题相似的计算过程,但是我们想要获取在 2016 年和 2017 年客户的总消费数额。级别和上一个问题保持一样。消费最高的客户列在最上面。

SELECT a.name, 
       SUM(o.total_amt_usd) AS total_amt,  
  CASE WHEN SUM(o.total_amt_usd) > 200000 THEN 'top'
       WHEN SUM(o.total_amt_usd) > 100000 AND SUM(o.total_amt_usd) <= 200000 THEN 'middle'
       ELSE 'low' END AS Class
  FROM orders o
  JOIN accounts a
  ON a.id = o.account_id
  WHERE o.occurred_at > '2015-12-31' 
  GROUP BY 1
ORDER BY 2 DESC

 

3.我们想要找出绩效最高的销售代表,也就是有超过 200 个订单的销售代表。创建一个包含以下列的表格:销售代表名称订单总量和*标为 top 或 not 的列(取决于是否拥有超过 200 个订单)。销售量最高的销售代表列在最上面。

SELECT s.name, 
       COUNT(*) total_sales,
       CASE WHEN COUNT(*) > 200 THEN 'top'
            ELSE 'not' END AS sales_rep_level
FROM orders o
JOIN accounts a
ON o.account_id = a.id
JOIN sales_reps s
ON a.sales_rep_id = s.id
GROUP BY 1
ORDER BY 2 DESC

 

4.之前的问题没有考虑中间水平的销售代表或销售额。管理层决定也要看看这些数据。我们想要找出绩效很高的销售代表,也就是有超过 200 个订单或总销售额超过 750000 美元的销售代表。中间级别是指有超过 150 个订单或销售额超过 500000 美元的销售代表。创建一个包含以下列的表格:销售代表名称、总订单量、所有订单的总销售额,以及标为 top、middle 或 low 的列(取决于上述条件)。在最终表格中将销售额最高的销售代表列在最上面。根据上述标准,你可能会见到几个表现很差的销售代表!

SELECT s.name, 
       COUNT(*) total_sales,
       SUM(o.total_amt_usd) total_amt,
       CASE WHEN COUNT(*) > 200 OR SUM(o.total_amt_usd) > 750000 THEN 'top'
            WHEN COUNT(*) > 150 OR SUM(o.total_amt_usd) > 500000 THEN 'middle'
            ELSE 'low' END AS sales_rep_level
FROM orders o
JOIN accounts a
ON o.account_id = a.id
JOIN sales_reps s
ON a.sales_rep_id = s.id
GROUP BY 1
ORDER BY 3 DESC;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,527评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,314评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,535评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,006评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,961评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,220评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,664评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,351评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,481评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,397评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,443评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,123评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,713评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,801评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,010评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,494评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,075评论 2 341

推荐阅读更多精彩内容