学习目标
课程使用微软的 Northwind 数据集, 零售业务,包含了客户,供应商和订单数据。原始数据集可以在 微软GitHub 仓库下载。为了满足课程需求,数据库数据在原始数据基础上做了微调。
基于此份数据,我们将通过 SQL 来创建数据报表,满足业务需求。
保存员工基本信息,包含如下字段:
employee_id
:员工唯一IDfirst_name
:名last_name
:姓title
:职务reports_to
:员工直属领导的员工ID (也在这张表中保存)保存客户的信息,包含如下字段:
customer_id
:客户唯一ID, 客户的ID是公司全名的缩写,用5个字母表示company_name
:公司名称contact_name
:客户公司的联系人contact_title
:客户公司联系人的职务city
、region
、postal_code
、country
、fax
1)商品表中保存了在 Northwind 商店中出售的商品信息,包含如下字段:
product_id
:商品唯一IDproduct_name
:商品名称supplier_id
:供应商IDcategory_id
:商品类别IDuite_price
:商品单价discontinued
:商品是否缺货:false
(有货)、true
(缺货)2)商品类别表 保存了所有商品的类别信息,包含如下字段:
id
:商品类别IDcategory_name
:商品类别名称description
:商品类别简单描述信息供应商表保存了商品供应商的信息,包含字段如下:
supplier_id
:供应商唯一IDcompany_name
:供应商公司名称
表中还记录了供应商的地址信息 address
、city
、region
、postal_code
、country
1)订单表中保存一个订单的基本信息,包含以下字段:
order_id
:订单IDcustomer_id
:客户IDemployee_id
:销售员工IDorder_date
:下单日期shipped_date
:配送日期ship_via
:运输方式freight
:运费ship_address
:收货地址ship_city
:收货城市ship_region
:收货地区ship_postal_code
:收货地址邮编ship_country
:收货国家2)订单明细表保存了订单中的具体商品信息,包含如下字段:
order_id
:订单IDproduct_id
:商品IDunit_price
:商品单价quantity
:购买数量discount
:折扣学习目标
上一小节中,我们介绍了数据库中的表关系,了解了基本数据情况,在这一小节中,我们将通过SQL来创建简单数据报表
将一个或者多个业务对象的详细信息汇总到一张表中是一种比较常见的报表形式,我们需要的信息可能分散在多张表中,在写 SQL 时可以通过一个或者多个 JOIN 子句将信息进行汇总
练习1
需求:查询运输到法国的订单信息,返回如下结果
查询结果字段:
- customer_company_name(客户公司名称)、employee_first_name和employee_last_name(销售员工姓名)、order_date(下单日期)、shipped_date(发货日期)、ship_country(收货国家)
SELECT
c.company_name AS `customer_company_name`,
e.first_name AS `employee_first_name`,
e.last_name AS `employee_last_name`,
o.order_date,
o.shipped_date,
o.ship_country
FROM orders o
JOIN employees e
ON o.employee_id = e.employee_id
JOIN customers c
ON o.customer_id = c.customer_id
WHERE o.ship_country = 'France';
注意在写 SQL 时,我们可以为每一张表都起了一个别名,可以减少输入的字符数
查询结果
练习2
需求:查询订单编号为10250的订单详情,按商品名称排序,返回如下结果
查询结果字段:
- product_name(商品名称)、quantity(购买数量)、unit_price(购买单价)、discount(折扣)、order_date(下单日期)
SELECT
product_name,
quantity,
oi.unit_price,
discount,
order_date
FROM orders o
JOIN order_items oi
ON o.order_id = oi.order_id
JOIN products p
ON oi.product_id = p.product_id
WHERE o.order_id = 10250
ORDER BY product_name;
查询结果
另一种常见的报表需求是查询某段时间内的业务指标
练习3
需求:统计2016年7月的订单数量
查询结果字段:
- order_count(2016年7月的订单数量)
SELECT
COUNT(*) As `order_count`
FROM orders
WHERE order_date >= '2016-07-01' AND order_date <= '2016-07-31';
查询结果
在业务报表中,我们通常希望同时计算多个业务对象的某些指标
练习4
需求:统计订单号在10200-10260之间的订单中的总商品件数
查询结果字段:
- order_id(订单ID)、order_items_count(订单中的总商品件数)
SELECT
order_id,
COUNT(*) AS `order_items_count`
FROM order_items
WHERE order_id BETWEEN 10200 AND 10260
GROUP BY order_id;
查询结果
在销售报表中,我们经常需要计算订单的总付款额
练习5
需求:统计ID为10250的订单的总价(折扣前)
查询结果字段:
- order_id(订单ID)、total_price(订单总价-折扣前)
SELECT
order_id,
SUM(unit_price * quantity) AS `total_price`
FROM order_items
WHERE order_id = 10250;
查询结果
练习6
需求:统计运输到法国的每个订单的总金额
查询结果字段:
- order_id(订单ID)、company_name(客户公司名称)、total_price(每个订单的总金额)
SELECT
o.order_id,
c.company_name AS `customer_company_name`,
SUM(unit_price * quantity) AS `total_price`
FROM orders o
JOIN order_items oi
ON o.order_id = oi.order_id
JOIN customers c
ON o.customer_id = c.customer_id
WHERE o.ship_country = 'France'
GROUP BY o.order_id, c.company_name;
GROUP BY
我们只需要对 order_id
进行分组就可以了,但MySQL 5.7之后要求,在使用GROUP BY
分组时,SELECT 后的字段,如果没有在聚合函数中使用,就必须在GROUP BY 后出现查询结果
在业务报表中,我们经常也需要分组进行数据的汇总
注意1:使用GROUP BY分组聚合统计时,需要考虑分组字段中的相同值的业务含义是否相同
练习7
需求:统计每个员工销售的订单数量
查询结果字段:
- first_name和last_name(员工姓和名)、orders_count(员工销售订单数)
1)首先思考下面的 SQL 语句是否正确
SELECT
e.first_name,
e.last_name,
COUNT(*) AS `orders_count`
FROM orders o
JOIN employees e
ON o.employee_id = e.employee_id
GROUP BY e.first_name, e.last_name;
上面的SQL貌似正确,但是没有考虑到员工重名的问题
2)正确的写法
SELECT
e.employee_id,
e.first_name,
e.last_name,
COUNT(*) AS `orders_count`
FROM orders o
JOIN employees e
ON o.employee_id = e.employee_id
GROUP BY e.employee_id, e.first_name, e.last_name;
SELECT
和 GROUP BY
中添加了 员工ID employee_id
字段后,重名的问题就可以解决了查询结果:
注意2:GROUP BY之后的分组字段不是必须在 SELECT 中出现
练习8
需求:统计2016年6月到2016年7月每个客户的总下单金额,并按金额从高到低排序
提示:
- 计算实际总付款金额: SUM(unit_price * quantity * (1 - discount))
查询结果字段:
- company_name(客户公司名称)、total_paid(客户总下单金额-折扣后)
SELECT
c.company_name,
SUM(unit_price * quantity * (1 - discount)) AS `total_paid`
FROM orders o
JOIN order_items oi
ON o.order_id = oi.order_id
JOIN customers c
ON o.customer_id = c.customer_id
WHERE order_date >= '2016-06-01' AND order_date <= '2016-07-31'
GROUP BY c.customer_id, c.company_name
ORDER BY total_paid DESC;
查询结果
注意点1:COUNT(*)
和 COUNT(列名)
之间的区别
- COUNT(*):进行计数,包括NULL
- COUNT(列名):对指定列的非NULL数据进行计数
练习9
需求:统计要发货到不同国家的订单数量以及已经发货的订单数量
提示:
- shipped_date为NULL,表示还未发货
查询结果字段:
- ship_country(国家)、all_orders(总订单数)、shipped_orders(已发货订单数)
SELECT
ship_country,
COUNT(*) AS `all_orders`,
COUNT(shipped_date) AS `shipped_orders`
FROM orders
GROUP BY ship_country;
查询结果
注意点2:COUNT()
和 LEFT JOIN
配合使用
使用SQL出报表时,必须记住关联某些对象可能不存在
练习10
需求:统计客户ID为 ALFKI、FISSA、PARIS 这三客户各自的订单总数,没有订单的客户也计算在内
查询结果字段:
- customer_id(客户ID)、company_name(客户公司名称)、orders_count(客户订单总数)
1)首先思考下面的 SQL 语句是否有问题?
SELECT
c.customer_id,
c.company_name,
COUNT(*) AS `orders_count`
FROM customers c
LEFT JOIN orders o
ON c.customer_id = o.customer_id
WHERE c.customer_id IN ('ALFKI', 'FISSA', 'PARIS')
GROUP BY c.customer_id, c.company_name;
查询结果
注意:结果可能有问题,
LEFT JOIN
会确保报表中看到全部三个客户ID,如果客户没下过订单,则关联之后的数据中order_id为NULL,而此处COUNT(*)
会对NULL计数,结果可能会有问题
2)将上面的 SQL 修改如下,再次执行查看统计结果
SELECT
c.customer_id,
c.company_name,
COUNT(order_id) AS `orders_count`
FROM customers c
LEFT JOIN orders o
ON c.customer_id = o.customer_id
WHERE c.customer_id IN ('ALFKI', 'FISSA', 'PARIS')
GROUP BY c.customer_id, c.company_name;
查询结果:
由查询结果可知,FISSA 和 PARIS 两个客户没有下过订单
注意点3:COUNT()统计时考虑是否需要去重
练习11
需求:查询订单运送到西班牙的客户数量
提示:
- 一个客户可能下了多个订单
查询结果字段:
- number_of_companies(客户数)
SELECT
COUNT(DISTINCT customer_id) AS `number_of_companies`
FROM orders
WHERE ship_country = 'Spain';
查询结果
学习目标
为了计算更复杂的业务指标,下面将练习如何根据自己的标准对业务对象进行“分类和计数”
练习1
需求:我们要在报表中显示每种产品的库存量,但我们不想简单地将"units_in_stock"列放在报表中,还需要按照如下规则显示一个库存级别列:
- 库存>100,显示 "high"
- 50 < 库存 <= 100,显示 "moderate"
- 0 < 库存 <= 50,显示 "low"
- 库存=0,显示 "none"
查询结果字段:
- product_id(商品ID)、product_name(商品名称)、units_in_stock(商品库存量)、stock_level(库存级别)
SELECT
product_id,
product_name,
units_in_stock,
CASE
WHEN units_in_stock > 100 THEN 'high'
WHEN units_in_stock > 50 THEN 'moderate'
WHEN units_in_stock > 0 THEN 'low'
WHEN units_in_stock = 0 THEN 'none'
END AS `availability`
FROM products;
查询结果
练习2
需求:查询客户基本信息报表,返回结果如下:
查询结果字段:
- customer_id(客户ID)、company_name(公司名称)、country(所在国家)、language(使用语言)
使用语言的取值规则如下:
- Germany、Switzerland、and Austria 语言为德语
'German'
- UK、Canada、the USA、and Ireland 语言为英语
'English'
- 其他所有国家
'Other'
SELECT
customer_id,
company_name,
country,
CASE
WHEN country IN ('Germany', 'Switzerland', 'Austria') THEN 'German'
WHEN country IN ('UK', 'Canada', 'USA', 'Ireland') THEN 'English'
ELSE 'Other'
END AS `language`
FROM customers;
查询结果
练习3
需求:创建报表统计来自不同大洲的供应商
查询结果字段:
- supplier_id(供应商ID)、supplier_continent(大洲)
供应商来自哪个大洲的取值规则:
USA
和Canada
两个国家的大洲取值为:'North America'Japan
和Singapore
两个国家的大洲取值为:'Asia'- 其他国家的大洲取值为 'Other'
SELECT
supplier_id,
CASE
WHEN country IN ('USA', 'Canada') THEN 'North America'
WHEN country IN ('Japan', 'Singapore') THEN 'Asia'
ELSE 'Other'
END AS `supplier_continent`
FROM suppliers;
查询结果
练习4
需求:创建报表统计来自不同大洲的供应商的供应的产品数量,包括没有提供产品的供应商
查询结果字段:
- supplier_continent(大洲)、products_count(供应产品数量)
供应商来自哪个大洲的取值规则:
USA
和Canada
两个国家的大洲取值为:'North America'Japan
和Singapore
两个国家的大洲取值为:'Asia'- 其他国家的大洲取值为 'Other'
SELECT
CASE
WHEN country IN ('USA', 'Canada') THEN 'North America'
WHEN country IN ('Japan', 'Singapore') THEN 'Asia'
ELSE 'Other'
END AS `supplier_continent`,
COUNT(product_id) AS `product_count`
FROM suppliers s
LEFT JOIN products p
ON s.supplier_id = p.supplier_id
GROUP BY CASE
WHEN country IN ('USA', 'Canada') THEN 'North America'
WHEN country IN ('Japan', 'Singapore') THEN 'Asia'
ELSE 'Other'
END;
SELECT
子句和GROUP BY
子句中,有相同的CASE WHEN
出现在GROUP BY
子句中supplier_continent
,标准SQL不允许在GROUP BY子句中引用别名,所以这里CASE WHEN
写了两次GROUP BY
中使用列别名,在本案例中两种写法都可以CASE WHEN
语句在 GROUP BY
和 SELECT
子句中,写法必须相同查询结果
可以将
CASE WHEN
和COUNT
结合使用,自定义分组并统计每组数据数量
练习5
需求:Washington (WA) 是 Northwind 的主要运营地区,统计有多少订单是由华盛顿地区的员工处理的,多少订单是有其他地区的员工处理的
查询结果字段:
- orders_wa_employees(华盛顿地区员工处理订单数)、orders_not_wa_employees(其他地区员工处理订单数)
SELECT
COUNT(CASE
WHEN region = 'WA' THEN order_id
END) AS `orders_wa_employees`,
COUNT(CASE
WHEN region != 'WA' THEN order_id
END) AS `orders_not_wa_employees`
FROM employees e
JOIN orders o
ON e.employee_id = o.employee_id;
查询结果
将
COUNT(CASE WHEN...)
和GROUP BY
组合使用,可以创建更复杂的报表
练习6
需求:统计运往不同国家的订单中,低运费订单、一般运费订单、高运费订单的数量
查询结果字段:
- ship_country(订单运往国家)、low_freight(低运费订单数量)、moderate_freight(一般运费订单数量)、high_freight(高运费订单数量)
SELECT
ship_country,
COUNT(CASE
WHEN freight < 40.0 THEN order_id
END) AS `low_freight`,
COUNT(CASE
WHEN freight >= 40.0 AND freight < 80.0 THEN order_id
END) AS `moderate_freight`,
COUNT(CASE
WHEN freight >= 80.0 THEN order_id
END) AS `high_freight`
FROM orders
GROUP BY ship_country;
查询结果
上面通过我们通过
COUNT()
函数 和CASE WHEN
子句联合使用来创建的报表,也可以通过SUM()
来替代COUNT()
练习7
需求:Washington (WA) 是 Northwind 的主要运营地区,统计有多少订单是由华盛顿地区的员工处理的,多少订单是有其他地区的员工处理的
查询结果字段:
- orders_wa_employees(华盛顿地区员工处理订单数)、orders_not_wa_employees(其他地区员工处理订单数)
SELECT
COUNT(CASE
WHEN region = 'WA' THEN order_id
END) AS `orders_wa_employees`,
COUNT(CASE
WHEN region != 'WA' THEN order_id
END) AS `orders_not_wa_employees`
FROM employees e
JOIN orders o
ON e.employee_id = o.employee_id;
将上述 SQL 使用 SUM 实现,修改如下:
SELECT
SUM(CASE
WHEN region = 'WA' THEN 1
END) AS `orders_wa_employees`,
SUM(CASE
WHEN region != 'WA' THEN 1
END) AS `orders_not_wa_employees`
FROM employees e
JOIN orders o
ON e.employee_id = o.employee_id;
查询结果
练习8
需求:统计每个订单的总金额(折扣后)以及该订单中非素食产品的总金额(折扣后)
查询结果字段:
- order_id(订单ID)、total_price(订单总金额-折扣后)、non_vegetarian_price(订单非素食产品的总金额-折扣后)
提示:非素食产品的类别ID (
category_id
) 是 6 和 8
SELECT
oi.order_id,
SUM(oi.quantity * oi.unit_price * (1 - oi.discount)) AS `total_price`,
SUM(CASE
WHEN p.category_id in (6, 8) THEN oi.quantity * oi.unit_price * (1 - oi.discount)
ELSE 0
END) AS `non_vegetarian_price`
FROM order_items oi
JOIN products p
ON oi.product_id = p.product_id
GROUP BY oi.order_id;
注:之前的场景中,我们可以通过
SUM(CASE WHEN...)
来替换COUNT(CASE WHEN...)
,但在上面的例子中,我们只能使用SUM(CASE WHEN...)
,因为涉及到不同值的累加,不能通过COUNT计数替代
查询结果:
练习9
需求:制作报表统计所有订单的总价(折扣前),并将订单按总价分成3类:high、average、low
查询结果字段:
- order_id(订单ID)、total_price(订单总金额)、price_group(订单总金额分类)
订单总金额分类规则:
- 总价超过2000美元:'high'
- 总价在600到2000美元之间:'average'
- 总价低于600美元:'low'
SELECT
order_id,
SUM(unit_price * quantity) AS `total_price`,
CASE
WHEN SUM(unit_price * quantity) > 2000 THEN 'high'
WHEN SUM(unit_price * quantity) > 600 THEN 'average'
ELSE 'low'
END AS `price_group`
FROM order_items
GROUP BY order_id;
查询结果
学习目标
在创建报表时,我们经常会在一张报表中先计算一个指标,然后计算这个指标的平均值,此时可以使用WITH CTE
比如,有如下需求,创建报表计算:
- 平均订单价值是多少?
- 运输到不同国家/地区的单均订购商品数量是多少?
公用表表达式 CTE( common table expression)与子查询十分类似
CTE 的基础语法如下:
WITH some_name AS (
--- your CTE ---
)
SELECT
...
FROM some_name
some_name
),具体的查询语句写在括号中SELECT
将CTE的结果当作一张表来使用SELECT
之前定义CTE练习1
需求:计算每个订单支付的平均总价(折扣前)
查询结果字段:
- avg_total_price(订单支付平均总价)
提示:
- 可以使用
SUM(unit_price * quantity)
来计算单个订单的总价格,需要对这个结果再计算平均值
WITH order_total_prices AS (
SELECT
order_id,
SUM(unit_price * quantity) AS `total_price`
FROM order_items
GROUP BY order_id
)
SELECT
AVG(total_price) AS `avg_total_price`
FROM order_total_prices;
查询结果
注意:平均值计算使用
AVG()
函数,但不能嵌套调用AVG(SUM(unit_price * quantity))
练习2
需求: 统计每个类别平均产品数量,通过 CTE 实现
查询结果字段:
- avg_products_count(每个类别平均产品数量)
WITH products_category_count AS (
SELECT
category_id,
COUNT(product_id) AS `count_products`
FROM products
GROUP BY category_id
)
SELECT
AVG(count_products) AS `avg_products_count`
FROM products_category_count;
查询结果
使用CTE时,也可以选择使用另外一种语法:
WITH products_category_count(category_id, count_products) AS (
SELECT
category_id,
COUNT(product_id)
FROM products
GROUP BY category_id
)
SELECT
AVG(count_products) AS `avg_products_count`
FROM products_category_count;
查询结果:
前面的CTE练习中,我们的结果都只返回了一个值,接下来我们看一下稍微复杂的情况
练习3
需求:统计来自加拿大的每个客户的平均订单价值(折扣前)
查询结果字段:
- customer_id(客户ID)、company_name(客户公司名称)、avg_total_price(该客户平均订单价值)
提示:
- 内层查询统计客户的每个订单的金额,外层查询统计来自加拿大的每个客户的平均订单价值
WITH order_total_prices AS (
SELECT
o.order_id,
o.customer_id,
SUM(unit_price * quantity) AS `total_price`
FROM orders o
JOIN order_items oi
ON o.order_id = oi.order_id
GROUP BY o.order_id, o.customer_id
)
SELECT
c.customer_id,
c.company_name,
AVG(total_price) AS `avg_total_price`
FROM order_total_prices otp
JOIN customers c
ON otp.customer_id = c.customer_id
WHERE c.country = 'Canada'
GROUP BY c.customer_id, c.company_name;
练习4
需求:创建报表,统计华盛顿地区(WA)每位员工所处理订单的订单平均价值
查询结果字段:
- employee_id(员工ID)、first_name和last_name(员工姓和名)、avg_total_price(该员工所处理每个订单的平均价值)
提示:
- 内层查询统计员工处理的每个订单的总价值,外层查询计算华盛顿地区员工所处理每个订单的平均价值
WITH order_total_prices AS (
SELECT
o.order_id,
o.employee_id,
SUM(unit_price * quantity) AS `total_price`
FROM orders o
JOIN order_items oi
ON o.order_id = oi.order_id
GROUP BY o.order_id, o.employee_id
)
SELECT
e.employee_id,
e.first_name,
e.last_name,
AVG(total_price) AS `avg_total_price`
FROM order_total_prices otp
JOIN employees e
ON otp.employee_id = e.employee_id
WHERE e.region = 'WA'
GROUP BY e.employee_id, e.first_name, e.last_name;
查询结果
我们还可以将多级聚合与自定义分类结合在一起
练习5
需求:通过客户下单数量对用户分三组,并统计每组的客户数量
查询结果字段:
- order_count_cat(客户分组)、customer_count(该组客户数量)
客户分组规则:
- 订单数超过20的客户:'more than 20'
- 订单数为10–20的客户:'between 10 and 20'
- 订单数少于10的客户:'less than 10'
提示:
- 内层查询统计客户订单数并对客户进行分组,外层查询统计每组用户的数量
WITH customer_order_counts AS (
SELECT
customer_id,
CASE
WHEN COUNT(o.order_id) > 20
THEN 'more than 20'
WHEN COUNT(o.order_id) >= 10
THEN 'between 10 and 20'
ELSE 'less than 10'
END AS `order_count_cat`
FROM orders o
GROUP BY customer_id
)
SELECT
order_count_cat,
COUNT(customer_id) AS `customer_count`
FROM customer_order_counts
GROUP BY order_count_cat;
查询结果
练习6
需求:根据客户的订单总价将客户分为高价值和低价值,并创建报表统计高价值和低价值客户的数量
查询结果字段:
- category(客户价值)、customer_count(客户数量)
客户价格分组规则:
- 客户在折扣前支付的所有订单的总价大于 20000 美元,则将该客户视为“高价值”,否则视为“低价值”
提示:
- 内层查询统计客户订单总价值并对客户进行分组,外层查询统计每组用户的数量
WITH customer_order_values AS (
SELECT
customer_id,
CASE
WHEN SUM(quantity * unit_price) > 20000 THEN 'high-value'
ELSE 'low-value'
END AS `category`
FROM orders o
JOIN order_items oi
ON o.order_id = oi.order_id
GROUP BY customer_id
)
SELECT
category,
COUNT(customer_id) AS `customer_count`
FROM customer_order_values
GROUP BY category;
查询结果
有时,我们需要多层聚合完成指标计算。 例如,我们可以计算每个客户的平均订单价值,然后找到最大平均值。
练习7
需求:统计每个客户的平均订单价值,然后找到最大平均值
查询结果字段:
- maximal_average(最大平均订单价值)
WITH order_values AS (
SELECT
customer_id,
SUM(unit_price * quantity) AS `total_price`
FROM orders o
JOIN order_items oi
ON o.order_id = oi.order_id
GROUP BY o.order_id, customer_id
),
customer_averages AS (
SELECT
customer_id,
AVG(total_price) AS `avg_total_price`
FROM order_values
GROUP BY customer_id
)
SELECT
MAX(avg_total_price) AS `maximal_average`
FROM customer_averages;
注意:这里引入了两个CTE
WITH
关键字; 只需要用逗号隔开即可。查询结果
练习8
需求:创建报表查找所有处理的订单数量大于平均水平的员工
查询结果字段:
- employee_id(员工ID)、order_count(该员工处理的所有订单数),avg_order_count(所有员工处理的平均订单数)
提示:
- 在第一个CTE中,我们计算每个员工处理的订单数量
- 在第二个CTE中,我们根据第一个CTE找到平均订单数
- 在外部查询中使用前两个CTE查找处理订单数大于平均值的员工
WITH order_count_employees AS (
SELECT
employee_id,
COUNT(order_id) AS `order_count`
FROM orders
GROUP BY employee_id
),
avg_order_count AS (
SELECT
AVG(order_count) AS `avg_order_count`
FROM order_count_employees
)
SELECT
employee_id,
order_count,
avg_order_count
FROM order_count_employees, avg_order_count
WHERE order_count > avg_order_count;
查询结果
学习目标
我们在制作报表时,经常需要对同一业务对象计算多个指标
练习1
需求:统计每个订单的商品数量和总价
查询结果字段:
- order_id(订单ID)、products_count(商品数量)、total_price(订单总价)
SELECT
order_id,
COUNT(product_id) AS `products_count`,
SUM(unit_price * quantity) AS `total_price`
FROM order_items
GROUP BY order_id;
查询结果
练习2
需求:创建报表计算2016年的绩效,计算每位员工处理的订单的总数和总收入
查询结果字段:
- first_name和last_name(员工的姓和名)、orders_count(员工处理的订单数)、orders_revenue(员工处理订单的总收入-折扣后)
SELECT
first_name,
last_name,
COUNT(DISTINCT o.order_id) AS `orders_count`,
SUM(unit_price * quantity * (1 - discount)) AS `orders_revenue`
FROM employees e
LEFT JOIN orders o
ON e.employee_id = o.employee_id
LEFT JOIN order_items oi
ON o.order_id = oi.order_id
WHERE order_date BETWEEN '2016-01-01' AND '2016-12-31'
GROUP BY e.employee_id, first_name, last_name;
查询结果
在制作报表的时候,经常需要在GROUP BY 分组的基础上进一步对数据进行自定义分组,并统计自定义分组中其数量
练习3
需求:统计每个客户的订单中,已发货订单和未发货订单的数量
提示:
- shipped_date 为 NULL 的是未发货订单
查询结果字段:
- customer_id(客户ID)、orders_shipped(已发货订单数量)、orders_pending(未发货订单数量)
SELECT
customer_id,
COUNT(CASE
WHEN shipped_date IS NOT NULL THEN order_id
END) AS `orders_shipped`,
COUNT(CASE
WHEN shipped_date IS NULL THEN order_id
END) AS `orders_pending`
FROM orders
GROUP BY customer_id;
customer_id
将订单数据分组,分组后将每个客户的订单划分成已发货和未发货两类,并统计已发货和未发货的订单数量COUNT()
与CASE WHEN
一起使用了两次,以计算两个不同组中的数据条目数量查询结果
练习4
需求:创建报表统计每个类别的商品中,有库存的商品数量和没有库存的商品数量
提示:
- units_in_stock > 0:表示有库存
查询结果字段:
- category_name(分类名称)、products_in_stock(有库存的商品数量)、products_not_in_stock(没有库存的商品数量)
SELECT
category_name,
SUM(CASE
WHEN units_in_stock > 0 THEN 1
ELSE 0
END) AS `products_in_stock`,
SUM(CASE
WHEN units_in_stock = 0 THEN 1
ELSE 0
END) AS `products_not_in_stock`
FROM products p
JOIN categories c
ON p.category_id = c.category_id
GROUP BY c.category_id, category_name;
查询结果
在制作报表是,经常需要计算百分比,比率(退货率,好评率...) 这样的指标,接下来看一下如何通过SQL实现
练习5
需求:统计所有订单中已经出货的百分比
查询结果字段:
- count_shipped(已发货订单数量)、count_all(总订单数量)、shipped_ratio(已发货订单百分比)
步骤1:计算出货百分比的分子和分母
SELECT
COUNT(CASE
WHEN shipped_date IS NOT NULL THEN order_id
END) AS `count_shipped`,
COUNT(order_id) AS `count_all`
FROM orders;
查询结果
步骤2:统计所有订单中已经出货的百分比
SELECT
COUNT(CASE
WHEN shipped_date IS NOT NULL THEN order_id
END) AS `count_shipped`,
COUNT(order_id) AS `count_all`,
COUNT(CASE
WHEN shipped_date IS NOT NULL
THEN order_id
END) / COUNT(order_id) AS `shipped_ratio`
FROM orders;
查询结果
步骤3:将比率结果保留指定位数的有效数字
可以使用
ROUND(value,decimal_places)
函数
SELECT
COUNT(CASE
WHEN shipped_date IS NOT NULL THEN order_id
END) AS `count_shipped`,
COUNT(order_id) AS `count_all`,
ROUND(COUNT(CASE
WHEN shipped_date IS NOT NULL THEN order_id
END) / COUNT(order_id), 2) AS `shipped_ratio`
FROM orders;
查询结果
步骤4:计算出一个百分比,比率乘100即可
SELECT
COUNT(CASE
WHEN shipped_date IS NOT NULL THEN order_id
END) AS `count_shipped`,
COUNT(order_id) AS `count_all`,
ROUND(COUNT(CASE
WHEN shipped_date IS NOT NULL THEN order_id
END) / COUNT(order_id) * 100, 2) AS `shipped_ratio`
FROM orders;
查询结果
我们还可以计算分组中的比率/百分比
练习6
需求:统计每个国家的订单中已经出货的百分比
查询结果字段:
- ship_country(订单运往国家)、count_shipped(已发货订单数量)、count_all(总订单数量)、shipped_ratio(已发货订单百分比)
SELECT
ship_country,
COUNT(CASE
WHEN shipped_date IS NOT NULL THEN order_id
END) AS `count_shipped`,
COUNT(order_id) AS `count_all`,
ROUND(COUNT(CASE
WHEN shipped_date IS NOT NULL THEN order_id
END) / COUNT(order_id) * 100, 2) AS `shipped_ratio`
FROM orders
GROUP BY ship_country;
GROUP BY
和SELECT
子句中添加了ship_country
列,可以统计每个国家/地区的发货订单百分比查询结果
练习7
需求: 统计每位员工处理的订单中, 法国客户下单的百分比
查询结果字段:
- first_name和last_name(员工姓和名)、count_france(该员工处理的法国客户订单数量)、count_all(该员工处理的订单总数量)、percentage_france(法国客户下单百分比-保留一位有效数字)
提示:
- 要查找客户所在的国家/地区,请使用
customers
表中的country
列
SELECT
first_name,
last_name,
COUNT(CASE
WHEN c.country = 'France' THEN order_id
END) AS `count_france`,
COUNT(order_id) AS `count_all`,
ROUND(COUNT(CASE
WHEN c.country = 'France' THEN order_id
END) / COUNT(order_id) * 100, 1) AS `percentage_france`
FROM employees e
JOIN orders o
ON e.employee_id = o.employee_id
JOIN customers c
ON o.customer_id = c.customer_id
GROUP BY e.employee_id, first_name, last_name;
查询结果
接下来我们要在报表中统计某个指标的总量并分组计算占比
练习8
需求:创建报表,统计2016年7月下订单的客户以及每个客户的消费金额占2016年7月总销售金额的占比
查询结果字段:
- customer_id(客户ID)、revenue(客户2016-07的消费金额)、revenue_percentage(客户2016-07的消费金额占总销售金额的占比)
-- 窗口函数实现
SELECT
o.customer_id,
SUM(oi.unit_price * oi.quantity) AS `revenue`,
SUM(SUM(oi.unit_price * oi.quantity)) OVER() AS `july_sales`,
ROUND(SUM(oi.unit_price * oi.quantity) /
SUM(SUM(oi.unit_price * oi.quantity)) OVER() * 100, 2) AS `revenue_percentage`
FROM orders o
JOIN order_items oi
ON o.order_id = oi.order_id
WHERE o.order_date BETWEEN '2016-07-01' AND '2016-07-31'
GROUP BY o.customer_id;
-- 多级CTE实现
-- 第一步:统计每个客户在2016-07的消费金额
-- 第二步:统计2016-07的总销售额
-- 第三步:计算每个客户在2016-07的消费金额占2016-07的总销售额的占比
WITH order_total_prices AS (
SELECT
o.customer_id,
SUM(oi.unit_price * oi.quantity) AS `revenue`
FROM orders o
JOIN order_items oi
ON o.order_id = oi.order_id
WHERE o.order_date BETWEEN '2016-07-01' AND '2016-07-31'
GROUP BY o.customer_id
), order_july_sales AS (
SELECT
SUM(oi.unit_price * oi.quantity) AS `july_sales`
FROM orders o
JOIN order_items oi
ON o.order_id = oi.order_id
WHERE o.order_date BETWEEN '2016-07-01' AND '2016-07-31'
)
SELECT
customer_id,
revenue,
july_sales,
ROUND(revenue / july_sales * 100, 2) AS `revenue_percentage`
FROm order_total_prices, order_july_sales;
查询结果
练习9
需求:创建报表,统计每个员工2017年处理的订单数量,及其在2017年所有订单中的占比
查询结果字段:
- employee_id(员工ID)、first_name和last_name(员工姓和名)、order_count(该员工在2017年处理的订单数)、order_count_percentage(该员工2017年处理的订单占2017年全部订单的百分比-保留2为小数)
-- 窗口函数实现
SELECT
e.employee_id,
e.first_name,
e.last_name,
COUNT(order_id) AS `order_count`,
SUM(COUNT(order_id)) OVER() AS `total_count`,
ROUND(COUNT(order_id) / SUM(COUNT(order_id)) OVER() * 100, 2) AS `order_count_percentage`
FROM employees e
JOIN orders o
ON e.employee_id = o.employee_id
WHERE o.order_date BETWEEN '2017-01-01' AND '2017-12-31'
GROUP BY e.employee_id, e.first_name, e.last_name;
-- 多级CTE实现
-- 第一步:统计每个员工在2017年处理的订单数量
-- 第二步:统计2017年所有的订单数量
-- 第三步:统计每个员工在2017年处理的订单数量占统计2017年所有的订单数量的占比
WITH employee_order_counts AS (
SELECT
e.employee_id,
e.first_name,
e.last_name,
COUNT(order_id) AS `order_count`
FROM employees e
JOIN orders o
ON e.employee_id = o.employee_id
WHERE o.order_date BETWEEN '2017-01-01' AND '2017-12-31'
GROUP BY e.employee_id, e.first_name, e.last_name
), order_total_counts AS (
SELECT
COUNT(order_id) AS `total_count`
FROM employees e
JOIN orders o
ON e.employee_id = o.employee_id
WHERE o.order_date BETWEEN '2017-01-01' AND '2017-12-31'
)
SELECT
first_name,
last_name,
order_count,
total_count,
ROUND(order_count / total_count * 100, 2) AS `order_count_percentage`
FROM employee_order_counts, order_total_counts;
查询结果
学习目标
第一种方法:我们将要对比的两组数据放到不同行中
练习1
需求:统计运往北美地区和除北美地区外的订单数量
提示:
- ship_country为('USA', 'Canada', 'Mexico),为北美地区,否则为非北美地区
查询结果字段:
- shipping_continent(运往地址-北美地区和除北美地区)、orders_count(运往该地区的订单数量)
WITH orders_by_group AS (
SELECT
order_id,
CASE
WHEN ship_country IN ('USA', 'Canada', 'Mexico') THEN 'North America'
ELSE 'Other'
END AS `shipping_continent`
FROM orders
)
SELECT
shipping_continent,
COUNT(order_id) AS `order_count`
FROM orders_by_group
GROUP BY shipping_continent;
CASE WHEN
对订单按ship_country
列进行分类(North America 或 Other),分类结果保存在名为shipping_continent
的列中shipping_continent
列进行分组,并使用 COUNT(order_id)
对匹配的订单进行计数查询结果
练习2
需求:统计法国客户和其他国家客户消费订单的总金额(折扣后保留2位小数)
查询结果字段:
- customer_country(客户国家:France或Other)、discount_revenue(该地区客户消费订单总金额)
WITH orders_by_group AS (
SELECT
o.order_id,
CASE
WHEN country = 'France' THEN 'France'
ELSE 'Other'
END AS `customer_country`
FROM orders o
JOIN customers c
ON o.customer_id = c.customer_id
)
SELECT
customer_country,
ROUND(SUM(quantity * unit_price * (1 - discount)), 2) AS `discount_revenue`
FROM orders_by_group obg
JOIN order_items oi
ON obg.order_id = oi.order_id
GROUP BY customer_country;
查询结果
练习3
需求:统计已经发货的订单数量和尚未发货的订单数量
查询结果字段:
- orders_shipped(已发货订单数量)、orders_pending(未发货订单数量)
SELECT
COUNT(CASE
WHEN shipped_date IS NOT NULL THEN order_id
END) AS `orders_shipped`,
COUNT(CASE
WHEN shipped_date IS NULL THEN order_id
END) AS `orders_pending`
FROM orders;
COUNT(CASE WHEN ...)
或SUM(CASE WHEN ...)
语句,每个语句用于在新列中显示不同组的数据CASE WHEN
语句。 在这里,每组数据用不同列表示,当要对比的分组不多时,可以使用这种方式查询结果
练习4
需求:统计运往加拿大的订单中的全价出售的产品数量和打折出售的产品数量
查询结果字段:
- full_price_count(全价出售产品数量)、discounted_price_count(打折出售的产品数量 )
SELECT
COUNT(DISTINCT CASE
WHEN discount=0 THEN oi.product_id
END) AS `full_price_count`,
COUNT(DISTINCT CASE
WHEN discount!=0 THEN oi.product_id
END) AS `discounted_price_count`
FROM orders o
JOIN order_items oi
ON o.order_id=oi.order_id
WHERE ship_country='Canada';
查询结果
将对比分类按列显示的一个优点是我们可以在报表中添加其它信息
练习5
需求:统计每个客户的已发货订单和待处理订单数量
查询结果字段:
- customer_id(客户ID)、order_shipped(该客户已发货订单数量)、order_pending(该客户待处理订单数量)
SELECT
customer_id,
COUNT(CASE
WHEN shipped_date IS NOT NULL THEN order_id
END) AS `orders_shipped`,
COUNT(CASE
WHEN shipped_date IS NULL THEN order_id
END) AS `orders_pending`
FROM orders
GROUP BY customer_id;
查询结果
练习6
需求:统计每位员工处理的运往德语地区的订单总金额和运往其它国家的订单总金额(折扣前)
提示:
- 'dach_orders':运往德国
Germany
,奥地利Austria
或瑞士Switzerland
的订单- 'other_orders':运往所有其他国家的订单
查询结果字段:
- employee_id(员工ID)、first_name和last_name(员工姓和名)、dach_orders(该员工处理的运往德语地区国家的订单总金额)、other_orders(该员工处理的运往其他地区国家的订单总金额)
SELECT
e.employee_id,
e.first_name,
e.last_name,
SUM(CASE
WHEN ship_country IN ('Switzerland', 'Germany', 'Austria') THEN quantity * unit_price
ELSE 0
END) AS `dach_orders`,
SUM(CASE
WHEN ship_country NOT IN ('Switzerland', 'Germany', 'Austria') THEN quantity * unit_price
ELSE 0
END) AS `other_orders`
FROM employees e
LEFT JOIN orders o
ON e.employee_id = o.employee_id
LEFT JOIN order_items oi
ON o.order_id = oi.order_id
GROUP BY e.employee_id, e.first_name, e.last_name;
查询结果
在创建对比分析的报表时,我们经常将对比的结果转换成百分比
练习7
需求:统计每一个员工处理的,收货地为德国
Germany
和美国USA
的订单中,已发货的订单占比查询结果字段:
- first_name和last_name(员工姓和名)、germany_perc(该员工处理的运往德国订单中已发货的占比)、usa_perc(该员工处理的运往美国订单中已发货的占比)
WITH germany_orders AS (
SELECT
employee_id,
COUNT(CASE
WHEN shipped_date IS NOT NULL THEN order_id
END) AS `count_shipped`,
COUNT(order_id) AS `count_all`
FROM orders o
WHERE o.ship_country = 'Germany'
GROUP BY employee_id
),
usa_orders AS (
SELECT
employee_id,
COUNT(CASE
WHEN shipped_date IS NOT NULL THEN order_id
END) AS `count_shipped`,
COUNT(order_id) AS `count_all`
FROM orders o
WHERE o.ship_country = 'USA'
GROUP BY employee_id
)
SELECT
e.first_name,
e.last_name,
ROUND(ge_or.count_shipped / ge_or.count_all * 100, 2) AS `germany_perc`,
ROUND(us_or.count_shipped / us_or.count_all * 100, 2) AS `usa_perc`
FROM germany_orders ge_or
JOIN usa_orders us_or
ON ge_or.employee_id = us_or.employee_id
JOIN employees e
ON ge_or.employee_id = e.employee_id;
上面的SQL中,我们通过两个CTE来分别处理目的地为德国和美国的订单
查询结果
练习8
需求:统计不同收货国家的订单金额中,ID为1和2的员工处理的订单金额占发往该国的总订单金额百分比(折扣前)保留两位有效数字
查询结果字段:
- ship_country(订单运往国家)、percentage_employee_1(ID为1的员工所处理的订单收入占比)、percentage_employee_2(ID为2的员工所处理的订单收入占比)
WITH employee_order_revenue AS (
SELECT
o.ship_country,
SUM(CASE
WHEN o.employee_id = 1 THEN oi.unit_price * oi.quantity
ELSE 0
END) AS `employee1_revenue`,
SUM(CASE
WHEN o.employee_id = 2 THEN oi.unit_price * oi.quantity
ELSE 0
END) AS `employee2_revenue`,
SUM(oi.unit_price * oi.quantity) AS `total_revenue`
FROM orders o
JOIN order_items oi
ON o.order_id = oi.order_id
GROUP BY o.ship_country
)
SELECT
ship_country,
ROUND(employee1_revenue / total_revenue * 100, 2) AS `percentage_employee_1`,
ROUND(employee2_revenue / total_revenue * 100, 2) AS `percentage_employee_2`
FROM employee_order_revenue;
查询结果