2021-11-07大数据学习日志——MySQL进阶——报表项目

01_数据表介绍

学习目标

  • 了解项目使用的数据表结构及表关系

课程使用微软的 Northwind 数据集, 零售业务,包含了客户,供应商和订单数据。原始数据集可以在 微软GitHub 仓库下载。为了满足课程需求,数据库数据在原始数据基础上做了微调。

基于此份数据,我们将通过 SQL 来创建数据报表,满足业务需求。

1.1 数据表整体概览

2021-11-07大数据学习日志——MySQL进阶——报表项目_第1张图片

1.2 员工表(employees)

保存员工基本信息,包含如下字段:

  • employee_id:员工唯一ID
  • first_name:名
  • last_name:姓
  • title:职务
  • ...
  • reports_to:员工直属领导的员工ID (也在这张表中保存)

1.3 客户表(customers)

保存客户的信息,包含如下字段:

  • customer_id:客户唯一ID, 客户的ID是公司全名的缩写,用5个字母表示
  • company_name:公司名称
  • contact_name:客户公司的联系人
  • contact_title:客户公司联系人的职务
  • 除此之外还保存了顾客的地址信息和联系方式cityregionpostal_codecountryfax

1.4 商品表(products)和商品类别表(categories)

1)商品表中保存了在 Northwind 商店中出售的商品信息,包含如下字段:

  • product_id:商品唯一ID
  • product_name:商品名称
  • supplier_id:供应商ID
  • category_id:商品类别ID
  • uite_price:商品单价
  • discontinued:商品是否缺货:false (有货)、true (缺货)

2)商品类别表 保存了所有商品的类别信息,包含如下字段:

  • id:商品类别ID
  • category_name:商品类别名称
  • description:商品类别简单描述信息

1.5 供应商表(suppliers)

供应商表保存了商品供应商的信息,包含字段如下:

  • supplier_id:供应商唯一ID
  • company_name:供应商公司名称

  • 表中还记录了供应商的地址信息 addresscityregionpostal_codecountry

1.6 订单表(orders)和订单明细表(order_items)

1)订单表中保存一个订单的基本信息,包含以下字段:

  • order_id:订单ID
  • customer_id:客户ID
  • employee_id:销售员工ID
  • order_date:下单日期
  • shipped_date:配送日期
  • ship_via:运输方式
  • freight :运费
  • ship_address:收货地址
  • ship_city :收货城市
  • ship_region:收货地区
  • ship_postal_code:收货地址邮编
  • ship_country:收货国家

2)订单明细表保存了订单中的具体商品信息,包含如下字段:

  • order_id:订单ID
  • product_id:商品ID
  • unit_price:商品单价
  • quantity:购买数量
  • discount:折扣

02_SQL 数据汇总操作

学习目标

  • 掌握 GROUP BY 分组和 COUNT、SUM 等聚合函数的使用

上一小节中,我们介绍了数据库中的表关系,了解了基本数据情况,在这一小节中,我们将通过SQL来创建简单数据报表

将一个或者多个业务对象的详细信息汇总到一张表中是一种比较常见的报表形式,我们需要的信息可能分散在多张表中,在写 SQL 时可以通过一个或者多个 JOIN 子句将信息进行汇总

2.1 详细报告

练习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 时,我们可以为每一张表都起了一个别名,可以减少输入的字符数

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第2张图片

练习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;

查询结果

2.2 带时间限制的报表

另一种常见的报表需求是查询某段时间内的业务指标

练习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';

查询结果

2.3 计算多个对象

在业务报表中,我们通常希望同时计算多个业务对象的某些指标

练习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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第3张图片

2.4 订单金额计算

在销售报表中,我们经常需要计算订单的总付款额

练习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 后出现

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第4张图片

2.5 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字段后,重名的问题就可以解决了

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第5张图片

注意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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第6张图片

2.6 COUNT()计数统计注意点

注意点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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第7张图片

注意点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';

查询结果

03_CASE WHEN 语法

学习目标

  • 熟练掌握CASE WHEN 的使用方法

为了计算更复杂的业务指标,下面将练习如何根据自己的标准对业务对象进行“分类和计数”

3.1 CASE WHEN自定义分组

练习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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第8张图片

3.2 CASE WHEN中ELSE的使用

练习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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第9张图片

练习3

需求:创建报表统计来自不同大洲的供应商

查询结果字段:

  • supplier_id(供应商ID)、supplier_continent(大洲)

供应商来自哪个大洲的取值规则:

  • USACanada两个国家的大洲取值为:'North America'
  • JapanSingapore两个国家的大洲取值为:'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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第10张图片

3.3 在GROUP BY中使用CASE WHEN

练习4

需求:创建报表统计来自不同大洲的供应商的供应的产品数量,包括没有提供产品的供应商

查询结果字段:

  • supplier_continent(大洲)、products_count(供应产品数量)

供应商来自哪个大洲的取值规则:

  • USACanada两个国家的大洲取值为:'North America'
  • JapanSingapore两个国家的大洲取值为:'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 写了两次
  • MySQL允许 在GROUP BY中使用列别名,在本案例中两种写法都可以
  • 注意: CASE WHEN 语句在 GROUP BY 和 SELECT 子句中,写法必须相同

查询结果

3.4 CASE WHEN 和 COUNT

可以将 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;

查询结果

3.5 GROUP BY 和 CASE WHEN组合使用

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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第11张图片

3.6 SUM 中使用 CASE WHEN

上面通过我们通过 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;

查询结果

3.7 SUM中使用CASE WHEN进行复杂计算

练习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计数替代

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第12张图片

练习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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第13张图片

04_CTE 公用表表达式

学习目标

  • 熟练使用 WITH (Common Table Expressions) 公用表表达式进行复杂查询

4.1 CTE 公用表表达式简介

在创建报表时,我们经常会在一张报表中先计算一个指标,然后计算这个指标的平均值,此时可以使用WITH CTE

比如,有如下需求,创建报表计算:

  • 平均订单价值是多少?
  • 运输到不同国家/地区的单均订购商品数量是多少?

公用表表达式 CTE( common table expression)与子查询十分类似

CTE 的基础语法如下:

WITH some_name AS (
  --- your CTE ---
)
SELECT
  ... 
FROM some_name
  • 需要给CTE起一个名字(上面的例子中使用了some_name),具体的查询语句写在括号中
  • 在括号后面,就可以通过SELECT 将CTE的结果当作一张表来使用
  • 将CTE称为“内部查询”,其后的部分称为“外部查询”
  • 需要先定义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

需求: 统计每个类别平均产品数量,通过 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;

查询结果

4.2 CTE 另外一种写法

使用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;
  • 这种写法的特点是在AS前将CTE创建的临时表字段定义好,在内部查询中,不需要使用 AS 起别名
  • 具体使用哪种方式,根据个人喜好选择

查询结果

4.3 多层聚合

前面的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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第14张图片

 4.4 多级聚合 与 CASE WHEN 组合使用

我们还可以将多级聚合与自定义分类结合在一起

练习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;

查询结果

4.5 三层聚合

有时,我们需要多层聚合完成指标计算。 例如,我们可以计算每个客户的平均订单价值,然后找到最大平均值。

练习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

  • 第二个CTE之前没有WITH关键字; 只需要用逗号隔开即可。
  • 在第一个CTE中计算每个订单的总金额
  • 在第二个CTE中,根据第一个CTE计算每个客户的平均订单价值
  • 最后,在外部查询中从第二个CTE中找到最大平均值

查询结果

练习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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第15张图片

05_计算多个指标

学习目标

  • 掌握通过SQL在报表中计算一个业务对象的多个指标的方法
  • 掌握如何通过SQL计算比率、百分比

5.1 计算一个业务对象的多个指标

我们在制作报表时,经常需要对同一业务对象计算多个指标

练习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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第16张图片

练习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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第17张图片

5.2 自定义指标

在制作报表的时候,经常需要在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;
  • 上面的 SQL 中,我们按customer_id将订单数据分组,分组后将每个客户的订单划分成已发货和未发货两类,并统计已发货和未发货的订单数量
  • 注意:COUNT()CASE WHEN一起使用了两次,以计算两个不同组中的数据条目数量

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第18张图片

练习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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第19张图片

5.3 计算百分比

在制作报表是,经常需要计算百分比,比率(退货率,好评率...) 这样的指标,接下来看一下如何通过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;
  • ROUND(...,2) 将比率四舍五入到小数点后两位

查询结果

步骤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;

查询结果

5.4 分组计算百分比

我们还可以计算分组中的比率/百分比

练习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 BYSELECT子句中添加了ship_country列,可以统计每个国家/地区的发货订单百分比

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第20张图片

练习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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第21张图片

5.5 统计总量并计算占比

接下来我们要在报表中统计某个指标的总量并分组计算占比

练习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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第22张图片

练习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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第23张图片

06_分组对比分析

学习目标

  • 掌握创建对比分析报表的方法

6.1 按行比较

第一种方法:我们将要对比的两组数据放到不同行中

练习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;

查询结果

6.2 按列对比

练习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';

查询结果

6.3 多列对比

将对比分类按列显示的一个优点是我们可以在报表中添加其它信息

练习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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第24张图片

练习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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第25张图片

6.4 占比对比

在创建对比分析的报表时,我们经常将对比的结果转换成百分比

练习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来分别处理目的地为德国和美国的订单

  • 在每个CTE中都按员工ID分组,分别统计运输到德国和美国的已发货订单和总订单数量
  • 在外部查询中,进行百分比计算

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第26张图片

练习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;

查询结果

2021-11-07大数据学习日志——MySQL进阶——报表项目_第27张图片

你可能感兴趣的:(mysql,数据库,database)