每日SQL一练#20231023

133 阅读6分钟

今天也继续来练SQL了。今天先不练牛客了,去leetcode看看《高频SQL基础版》。leetcode的平均难度会比牛客要高一点,主要是leetcode的测试用例会多一点,牛客只有几个,所以leetcode提交的代码需要多注意一下隐藏的信息,不然很容易掉坑里。但是leetcode非常不好的一点就是它不提供测试用的sql代码,只提供数据库输出信息。。。。要么手动输入数据,要么用提交做测试(因为可以直接反馈出来最终的执行结果),然后你就会发现提交记录满满都是执行失败或者解答错误。。。QAQ 入正题了。

题目

题目链接: 平均售价 题干信息: 表:Prices

+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| product_id    | int     |
| start_date    | date    |
| end_date      | date    |
| price         | int     |
+---------------+---------+
(product_id,start_date,end_date) 是 prices 表的主键(具有唯一值的列的组合)。
prices 表的每一行表示的是某个产品在一段时期内的价格。
每个产品的对应时间段是不会重叠的,这也意味着同一个产品的价格时段不会出现交叉。

表:UnitsSold

+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| product_id    | int     |
| purchase_date | date    |
| units         | int     |
+---------------+---------+
该表可能包含重复数据。
该表的每一行表示的是每种产品的出售日期,单位和产品 id。

编写解决方案以查找每种产品的平均售价。average_price 应该 四舍五入到小数点后两位。 返回结果表 无顺序要求 。 结果格式如下例所示。

输入:
Prices table:
+------------+------------+------------+--------+
| product_id | start_date | end_date   | price  |
+------------+------------+------------+--------+
| 1          | 2019-02-17 | 2019-02-28 | 5      |
| 1          | 2019-03-01 | 2019-03-22 | 20     |
| 2          | 2019-02-01 | 2019-02-20 | 15     |
| 2          | 2019-02-21 | 2019-03-31 | 30     |
+------------+------------+------------+--------+
UnitsSold table:
+------------+---------------+-------+
| product_id | purchase_date | units |
+------------+---------------+-------+
| 1          | 2019-02-25    | 100   |
| 1          | 2019-03-01    | 15    |
| 2          | 2019-02-10    | 200   |
| 2          | 2019-03-22    | 30    |
+------------+---------------+-------+
输出:
+------------+---------------+
| product_id | average_price |
+------------+---------------+
| 1          | 6.96          |
| 2          | 16.96         |
+------------+---------------+
解释:
平均售价 = 产品总价 / 销售的产品数量。
产品 1 的平均售价 = ((100 * 5)+(15 * 20) )/ 115 = 6.96
产品 2 的平均售价 = ((200 * 15)+(30 * 30) )/ 230 = 16.96

解析

UnitsSold表记录的是某个商品在某天被购买的数量,对于同一个product_id会存在多行数据,这点需要注意。 Prices表的所有出现过的product_id都要在最终结果出现,即使该商品从未售出。

这题官方说考察的点是聚合函数,但是实际需要配合窗口函数使用。(难度偏大就体现在这里)

测试用例

这里提供两份测试用例数据。

drop table if exists Prices;
drop table if exists UnitsSold;
CREATE TABLE `Prices` (
  `product_id` int DEFAULT NULL,
  `start_date` date DEFAULT NULL,
  `end_date` date DEFAULT NULL,
  `price` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

CREATE TABLE `UnitsSold` (
  `product_id` int DEFAULT NULL,
  `purchase_date` date DEFAULT NULL,
  `units` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

INSERT INTO `UnitsSold` (`product_id`, `purchase_date`, `units`) VALUES (1, '2019-01-17', 5);
INSERT INTO `UnitsSold` (`product_id`, `purchase_date`, `units`) VALUES (1, '2019-01-31', 4);
INSERT INTO `UnitsSold` (`product_id`, `purchase_date`, `units`) VALUES (2, '2019-01-29', 1);
INSERT INTO `UnitsSold` (`product_id`, `purchase_date`, `units`) VALUES (2, '2019-01-29', 2);
INSERT INTO `Prices` (`product_id`, `start_date`, `end_date`, `price`) VALUES (1, '2019-01-17', '2019-01-25', 18);
INSERT INTO `Prices` (`product_id`, `start_date`, `end_date`, `price`) VALUES (1, '2019-01-26', '2019-02-12', 5);
INSERT INTO `Prices` (`product_id`, `start_date`, `end_date`, `price`) VALUES (2, '2019-01-23', '2019-01-30', 3);
INSERT INTO `Prices` (`product_id`, `start_date`, `end_date`, `price`) VALUES (2, '2019-01-01', '2019-01-02', 18);


另一份测试数据

product_id	purchase_date	units
1	2019/1/15	1
1	2019/1/20	3
1	2019/1/19	3
2	2019/2/26	5
2	2019/2/4	4
2	2019/2/9	4
3	2019/1/5	5
4	2019/2/3	4
4	2019/1/10	4
4	2019/1/31	4

product_id	start_date	end_date	price
1	2019/1/3	2019/1/7	14
1	2019/1/8	2019/1/13	10
1	2019/1/14	2019/1/27	8
2	2019/1/24	2019/2/11	16
2	2019/2/12	2019/2/22	19
2	2019/2/23	2019/3/9	14
3	2019/1/2	2019/1/8	9
4	2019/1/6	2019/1/16	13
4	2019/1/17	2019/2/1	16
4	2019/2/2	2019/2/6	10

作答

这里分步作答,先以UnitsSold表为准,将对应的售价都匹配上,查询出所有售出的商品的售价

select
    tbb.product_id,
    if(tba.units is null, 0, tba.units) as units,
    if(tba.units * tbb.price is null, 0, tba.units * tbb.price) as value
from UnitsSold as tba right outer join Prices as tbb
on tba.product_id=tbb.product_id
and tba.purchase_date between tbb.start_date and tbb.end_date
;

结果如下:

image.png

发现部分商品售价未售出也会出现在这里,这部分数据不用。

可能跟前面说的话有点矛盾。这里解释一下,最终的结果需要prices表中的所有商品都出现,但是只要求解它的平均售价,那么该价格期间未售出的数据不需要统计,但是如果一个商品的所有定价均未售出,最终计算该商品平均售价为0。

还需要调整的一点就是,因为两份数据不平衡,prices表的一行数据可能对应UnitsSold表的多行数据,这个在后续的join后会导致笛卡尔积,这个是必须的,不然会对后续聚合结果产生影响,所以这个笛卡尔积的结果需要去重,这里如果是单纯按照unitsvalue两个字段去重,会发现将一部分数据也丢失了,所以这里引入UnitsSold.purchase_date用于辅助去重。 调整后的代码如下:

select
    tbb.product_id,
    tba.purchase_date,
    if(tba.units is null, 0, tba.units) as units,
    if(tba.units * tbb.price is null, 0, tba.units * tbb.price) as value
from UnitsSold as tba right outer join Prices as tbb
on tba.product_id=tbb.product_id
and tba.purchase_date between tbb.start_date and tbb.end_date HAVING units!=0
;

image.png

然后将上述结果与prices再次join,取得未售出的product_id。这时候未售出的商品的unitsvalue两个字段的值都是null,后续要根据这个来处理。(因为测试数据没有未售出的商品id,所以后面展示的结果都不会有这个数据,测试用例中会存在这个情况,或者可以生成个测试数据进行测试)

with tmp as (select
        tbb.product_id,
        tba.purchase_date,
        if(tba.units is null, 0, tba.units) as units,
        if(tba.units * tbb.price is null, 0, tba.units * tbb.price) as value
    from UnitsSold as tba right outer join Prices as tbb
    on tba.product_id=tbb.product_id
    and tba.purchase_date between tbb.start_date and tbb.end_date HAVING units!=0)
select
    distinct tb1.product_id,
    tmp.purchase_date,
    tmp.units,
    tmp.value
from Prices as tb1 left outer join tmp
on tb1.product_id=tmp.product_id
;

image.png

然后就是聚合求解平均售价了

with tmp as (select
        tbb.product_id,
        tba.purchase_date,
        if(tba.units is null, 0, tba.units) as units,
        if(tba.units * tbb.price is null, 0, tba.units * tbb.price) as value
    from UnitsSold as tba right outer join Prices as tbb
    on tba.product_id=tbb.product_id
    and tba.purchase_date between tbb.start_date and tbb.end_date HAVING units!=0)
SELECT
    distinct t.product_id,
    if(units is null, 0, round(sum(value) over(partition by product_id)/sum(units) over(partition by product_id), 2)) as average_price
from 
    (select
        distinct tb1.product_id,
        tmp.purchase_date,
        tmp.units,
        tmp.value
    from Prices as tb1 left outer join tmp
    on tb1.product_id=tmp.product_id) as t
;

image.png