当数据很多的时候,对数据的聚合进行观察就是很必要的了(但看一行数据太多了,所以要看更大颗粒度的进行决策)。
这节主要讲的是 NULL 的由来,和怎么使用 WHERE 对 NULL 进行过滤(在执行 LEFT JOIN 或 RIGHT JOIN 时,NULL 经常会发生。你在上节课见到了,左侧表格中的某些行在做连接时与右侧表格中的行如果不匹配,这些行在结果集中就会包含一些 NULL 值):
WHERE primary_poc IS NULL
WHERE primary_poc IS NOT NULL
这样使用:
SELECT COUNT(*)
FROM accounts;
SELECT COUNT(accounts.id)
FROM accounts;
像 SUM() 这种聚合,只能对数值的列使用,在做数值计算时 NULL 将被忽略。如果是对行做聚合的话,使用简单算术表达式就可以了。/简单算术表达式/
MIN 和 MAX 与 COUNT 相似,它们都可以用在非数字列上。MIN 将返回最小的数字、最早的日期或按字母表排序的最之前的非数字值,具体取决于列类型。MAX 则正好相反,返回的是最大的数字、最近的日期,或与“Z”最接近(按字母表顺序排列)的非数字值。同时 MIN 和 MAX 也会忽略 NULL值。
如果你想将 NULL 当做零,则需要使用 SUM 和 COUNT。但是,如果 NULL 值真的只是代表单元格的未知值,那么这么做可能不太合适(可能需要额外的数据清理)。这里要注意 AVG 一样是不计算 NULL 的,所以和 SUM / COUNT 的值可能会不同。
其实使用 MIN 可以等价于输出第一个元素,以下这两个是等价的(不过处理时间不同),后面尝试执行和第一个问题一样的查询,但是不使用聚合函数:
SELECT MIN(occurred_at)
FROM orders;
SELECT occurred_at
FROM orders
ORDER BY occurred_at
LIMIT 1;
SQL没有特殊求解中位数的功能,如果要取中位数,需要手动指定 LIMIT 完成:
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;
通过GROUP BY可以创建在聚合时相互独立的分组(Group By:Allows creatingsegments that will aggregate independent from one another)。可以抓取单个账户的数据之和而不是整个数据集
在这节的练习中的一个判断难点是,何时使用某个聚合函数或其他 SQL 功能最简单:
SELECT a.name, o.occurred_at
FROM accounts a
JOIN orders o
ON a.id = o.account_id
ORDER BY occurred_at
LIMIT 1;
SELECT a.name, SUM(total_amt_usd) total_sales
FROM orders o
JOIN accounts a
ON a.id = o.account_id
GROUP BY a.name;
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;
SELECT w.channel, COUNT(*)
FROM web_events w
JOIN accounts a
ON a.id = w.account_id
GROUP BY w.channel
SELECT a.primary_poc
FROM web_events w
JOIN accounts a
ON a.id = w.account_id
ORDER BY w.occurred_at
LIMIT 1;
SELECT a.name, MIN(total_amt_usd) smallest_order
FROM accounts a
JOIN orders o
ON a.id = o.account_id
GROUP BY a.name
ORDER BY smallest_order;
SELECT r.name, COUNT(*) num_reps
FROM region r
JOIN sales_reps s
ON r.id = s.region_id
GROUP BY r.name
ORDER BY num_reps;
这节对 GROUP BY 做了扩展:
SELECT a.name, AVG(o.standard_qty) avg_stand, AVG(gloss_qty) avg_gloss, AVG(poster_qty) avg_post
FROM accounts a
JOIN orders o
ON a.id = o.account_id
GROUP BY a.name;
SELECT a.name, AVG(o.standard_amt_usd) avg_stand, AVG(gloss_amt_usd) avg_gloss, AVG(poster_amt_usd) avg_post
FROM accounts a
JOIN orders o
ON a.id = o.account_id
GROUP BY a.name;
SELECT s.name, w.channel, COUNT(*) num_events
FROM accounts a
JOIN web_events w
ON a.id = w.account_id
JOIN sales_reps s
ON s.id = a.sales_rep_id
GROUP BY s.name, w.channel
ORDER BY num_events DESC;
SELECT r.name, w.channel, COUNT(*) num_events
FROM accounts a
JOIN web_events w
ON a.id = w.account_id
JOIN sales_reps s
ON s.id = a.sales_rep_id
JOIN region r
ON r.id = s.region_id
GROUP BY r.name, w.channel
ORDER BY num_events DESC;
你可以将 DISTINCT 看做仅返回特定列的唯一值的函数。需要注意的是,在使用 DISTINCT 时,尤其是在聚合函数中使用时,会让查询速度有所减慢。
SELECT DISTINCT a.id, r.id rid, a.name, r.name rname
FROM accounts a
JOIN sales_reps s
ON s.id = a.sales_rep_id
JOIN region r
ON r.id = s.region_id;
SELECT DISTINCT id, name
FROM accounts;
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;
ps:/本地环境 DB.Browser for SQLite//sqlite/
HAVING 是过滤被聚合的查询的 “整洁”方式,但是通常采用子查询的方式来实现。本质上,只要你想对通过聚合创建的查询中的元素执行 WHERE 条件,就需要使用 HAVING。(就是说想对聚合的 SELECT 元素做类似 WHERE 的筛选时,要用 HAVING 实现)。顺序上的区别是:
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
HAVING COUNT(*) > 5
ORDER BY num_accounts;
SELECT COUNT(*) num_reps_above5
FROM(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
HAVING COUNT(*) > 5
ORDER BY num_accounts) AS Table1;
SELECT a.id, a.name, COUNT(*) num_orders
FROM accounts a
JOIN orders o
ON a.id = o.account_id
GROUP BY a.id, a.name
ORDER BY num_orders DESC
LIMIT 1;
SELECT a.id, a.name, SUM(o.total_amt_usd) total_spent
FROM accounts a
JOIN orders o
ON a.id = o.account_id
GROUP BY a.id, a.name
HAVING SUM(o.total_amt_usd) > 30000
ORDER BY total_spent;
SELECT a.id, a.name, w.channel, COUNT(*) use_of_channel
FROM accounts a
JOIN web_events w
ON a.id = w.account_id
WHERE w.channel = 'facebook'
GROUP BY a.id, a.name, w.channel
ORDER BY use_of_channel DESC
LIMIT 1;
在 SQL 中,按照日期列分组通常不太实用,因为这些列可能包含小到一秒的交易数据。按照如此详细的级别保存信息即有好处,又存在不足之处,因为提供了非常准确的信息(好处),但是也让信息分组变得很难(不足之处)。数据库的日期存储是 YYYY-MM-DD 方式,方便进行检索:
附加参考资料
实际上,要想统计一天发生了多少次,实际是将日期的数据都归为 00:00:00
本节主要使用的是 DATA_TRUNC 和 DATA_PART(如果拆除日期,无法分开页)
SELECT DATE_PART('year', occurred_at) ord_year, SUM(total_amt_usd) total_spent
FROM orders
GROUP BY 1
ORDER BY 2 DESC;
结论:对于 2013 年和 2017 年来说,每一年只有一个月的销量(2013 年为 12,2017 年为 1)。 因此,二者都不是均匀分布。销量一年比一年高,2016 年是到目前为止最高的一年。按照这个速度,我们预计 2017 年可能是最高销量的一年。
SELECT DATE_PART('month', occurred_at) ord_month, SUM(total_amt_usd) total_spent
FROM orders
WHERE occurred_at BETWEEN '2014-01-01' AND '2017-01-01'
GROUP BY 1
ORDER BY 2 DESC;
SELECT DATE_TRUNC('month', o.occurred_at) ord_date, SUM(o.gloss_amt_usd) tot_spent
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;
派生列:Derive:Take data from existing columns and modify them。之前使用的是列之间的直接运算,这里主要学习 CASE 语句(处理 IF THEN 逻辑的方式)。
大家还记得之前那个用 +0.01 避免除零的情况发生,这里就可以用 CASE 的方式解决了:
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 和 WHERE 的区别:CASE 可以输出多个条件的结果,而 WHERE 只能输出一种:使用 WHERE 条件获取相同的信息意味着一次只能从 CASE 中获取一组数据。像这样将数据分成几列有一些优势,这取决于你要执行的操作。但通常,这种级别的划分可能使用其他编程语言更简单,而不是使用 SQL。
我们想要根据相关的购买量了解三组不同的客户。最高的一组是终身价值(所有订单的总销售额)大于 200,000 美元的客户。第二组是在 200,000 到 100,000 美元之间的客户。最低的一组是低于 under 100,000 美元的客户。请提供一个表格,其中包含与每个客户相关的级别。你应该提供客户的名称、所有订单的总销售额和级别。消费最高的客户列在最上面。
- 注意第二个 WHEN 并没有闭合范围,在上一节的课程中,老师建议的是每个 WHEN 都是闭合的更加规范
- 可以后面加一个 AND (total_amt_usd) <= 200000
SELECT a.name, SUM(total_amt_usd) total_spent,
CASE WHEN SUM(total_amt_usd) > 200000 THEN 'top'
WHEN SUM(total_amt_usd) > 100000 THEN 'middle'
ELSE 'low' END AS customer_level
FROM orders o
JOIN accounts a
ON o.account_id = a.id
GROUP BY a.name
ORDER BY 2 DESC;
SELECT a.name, SUM(total_amt_usd) total_spent,
CASE WHEN SUM(total_amt_usd) > 200000 THEN 'top'
WHEN SUM(total_amt_usd) > 100000 THEN 'middle'
ELSE 'low' END AS customer_level
FROM orders o
JOIN accounts a
ON o.account_id = a.id
WHERE occurred_at > '2015-12-31'
GROUP BY 1
ORDER BY 2 DESC;
SELECT s.name, COUNT(*), SUM(o.total_amt_usd) total_spent,
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 s.id = a.sales_rep_id
GROUP BY s.name
ORDER BY 3 DESC;