本文基於微信羣裏的一個問題,感謝這位朋友提供的分享。
題目描述
假設某電商銷售數據有如下幾張表:
Brand(品牌表)
bid | name |
---|---|
1 | 品牌1 |
Category(品類表)
cid | name |
---|---|
1 | 食品 |
Monthlysales(月度銷量統計表)
month | bid | cid | paltform | sales |
---|---|---|---|---|
2019-12-01 | 1 | 1 | 1 | 1000 |
其中,
- month:date 類型,存儲的是每個月的第一天;
- bid:對應 Brand.bid;
- cid:對應 Category.cid;
- paltform:對應不同的電商平臺,有 2 種:1,2;
- sales:銷售額;
- 主鍵爲 (month, paltform, bid, cid),一行數據對應一個月一個平臺一個 bid 的一個 cid 的銷售額。
基於上面的描述,我們可以創建以下示例表和模擬數據(使用 MySQL 數據庫):
create table brand(bid int auto_increment primary key, name varchar(50));
insert into brand(name) values ('品牌1');
insert into brand(name) values ('品牌2');
insert into brand(name) values ('品牌3');
insert into brand(name) values ('品牌4');
insert into brand(name) values ('品牌5');
create table category(cid int auto_increment primary key, name varchar(10));
insert into category(name) values ('食品');
insert into category(name) values ('手機');
insert into category(name) values ('服飾');
insert into category(name) values ('圖書');
insert into category(name) values ('電腦');
create table monthlysales(month date, bid int, cid int, platform int,sales int);
alter table monthlysales add constraint pk_monthlysales primary key(month, platform, bid, cid);
insert into monthlysales
with recursive dt as (
select date '2019-01-01' as v
union all
select v + interval '1' month from dt where v < date '2019-12-01'
),
platform(pid) as (
values row(1), row(2)
)
select v, bid, cid, pid, 1000 + round((rand(1) * 500))
from dt
cross join brand
cross join category
cross join platform;
爲了生成示例數據,我們使用了遞歸形式的通用表表達式(WITH
子句),相關語法可以參考這篇文章。另外,rand(1) 函數確保了結果的可復現。
問題 1
對於指定的 cid 範圍(cid 列表:1,3,5),查詢 2019 年每個平臺上每個 bid 對應每個 cid 的累計銷售額,輸出格式如下:
bid | brand_name | cid | category_name | platform | total_sales |
---|
請寫出 SQL。
解析
這個題目比較簡單,就是按照品牌、品類以及平臺分組再加上 sum 函數統計銷售額;同時關聯其他表獲取品牌和品類名稱。
select b.bid, b.name brand_name, c.cid, c.name category_name, s.platform, sum(s.sales) total_sales
from monthlysales s
join brand b on (b.bid = s.bid)
join category c on (c.cid = s.cid)
where b.bid in (1, 3, 5)
and s.month between date '2019-01-01' and date '2019-12-01'
group by b.bid, b.name, c.cid, c.name, s.platform;
bid|brand_name |cid|category_name |platform|total_sales|
---|------------|---|---------------|--------|-----------|
1|品牌1 | 1|食品 | 1| 15115|
1|品牌1 | 2|手機 | 1| 15441|
1|品牌1 | 3|服飾 | 1| 14869|
1|品牌1 | 4|圖書 | 1| 15516|
1|品牌1 | 5|電腦 | 1| 14971|
...
對於 2019 年的判斷,最好不要使用 year(month) 函數,因爲這樣會導致索引失效。另外,關於各種數據庫中的分組彙總和聚合函數可以參考這篇文章。
問題 2
查詢 2019 年有 5 個以上(包含 5 個)不同 cid 的單月單平臺銷售額大於等於 1480 的品牌列表,以及對應的不同 cid 數量,輸出格式如下:
bid | brand_name | cid_count |
---|
解析
這個問題和上面的問題差不多,主要是按照品牌分組統計;但是在分組統計之後,還需要過濾一下統計的數量。
select b.bid, b.name brand_name, count(distinct s.cid) cid_count
from monthlysales s
join brand b on (b.bid = s.bid)
where s.month between date '2019-01-01' and date '2019-12-01'
and s.sales >= 1480
group by b.bid, b.name
having cid_count >= 5;
bid|brand_name |cid_count|
---|------------|---------|
4|品牌4 | 5|
其中,count() 函數用於統計各種品牌單月在單平臺上不同 cid 的數量;having 最終返回數量大於等於 5 的品牌。
問題 3
查詢 2019 年只在平臺 1 上有銷售額的品牌中(排除平臺爲 2 時銷售累計額大於 0 的品牌),平臺 1 累計銷售額最大的 Top 3 品牌以及對應的銷售額,輸出格式如下:
bid | brand_name | total_sales_p1 |
---|
解析
同樣是先進行分組統計,獲取每個品牌在平臺 1 上的累計銷售額;然後加上排序和數量限定操作返回 Top 3 結果。
select b.bid, b.name brand_name, sum(s.sales) total_sales_p1
from monthlysales s
join brand b on (b.bid = s.bid)
where s.month between date '2019-01-01' and date '2019-12-01'
and s.platform = 1
group by b.bid, b.name
having total_sales_p1 > 0
order by total_sales_p1 desc
limit 3;
bid|brand_name |total_sales_p1|
---|------------|--------------|
3|品牌3 | 76464|
1|品牌1 | 75912|
4|品牌4 | 74931|
其中,ORDER BY
用於按照累計銷售額從高到低進行排序;LIMIT
用於返回前 3 條記錄。
問題 4
查詢 2019 年在兩個平臺中分別同時都能進入銷售額 Top 3 的品牌以及對應的全平臺累計銷售額,輸出格式如下:
bid | brand_name | total_sales_all |
---|
解析
這個問題至少有兩種解決辦法:基於問題 3 中的解決方法分別獲取平臺 1 和平臺 2 上的 Top 3 品牌,然後使用一個連接查詢;或者使用 MySQL 8.0 中的窗口函數。
第一種方法的實現如下:
select top3_p1.bid, top3_p1.brand_name, top3_p1.total_sales_p1 + top3_p2.total_sales_p2 as total_sales_all
from (
select b.bid, b.name brand_name, sum(s.sales) total_sales_p1
from monthlysales s
join brand b on (b.bid = s.bid)
where s.month between date '2019-01-01' and date '2019-12-01'
and s.platform = 1
group by b.bid, b.name
having total_sales_p1 > 0
order by total_sales_p1 desc
limit 3) top3_p1
join (
select b.bid, b.name brand_name, sum(s.sales) total_sales_p2
from monthlysales s
join brand b on (b.bid = s.bid)
where s.month between date '2019-01-01' and date '2019-12-01'
and s.platform = 2
group by b.bid, b.name
having total_sales_p2 > 0
order by total_sales_p2 desc
limit 3) top3_p2
on (top3_p1.bid = top3_p2.bid)
order by total_sales_all desc;
bid|brand_name |total_sales_all|
---|------------|---------------|
1|品牌1 | 153579|
3|品牌3 | 152819|
4|品牌4 | 150860|
其中,top3_p1 代表了平臺 1 上的 Top 3 品牌和累計銷售額;op3_p2 代表了平臺 2 上的 Top 3 品牌和累計銷售額。
如果使用的 MySQL 8.0 版本,我們可以利用窗口函數計算排名:
with sales2019 as (
select bid, platform, sum(sales) sales
from monthlysales
where month between date '2019-01-01' and date '2019-12-01'
group by bid, platform
),
top3 as (
select * from (
select bid, platform, sum(sales) over (partition by bid) total_sales_all,
rank() over (partition by platform order by sales desc) as rk
from sales2019
) t where rk <= 3
)
select p1.bid, b.name brand_name, p1.total_sales_all
from top3 p1
join top3 p2 on (p1.bid = p2.bid)
join brand b on (b.bid = p1.bid)
where p1.platform = 1
and p2.platform = 2;
bid|brand_name |total_sales_all|
---|------------|---------------|
1|品牌1 | 153579|
3|品牌3 | 152819|
4|品牌4 | 150860|
其中,sales2019 代表了 2019 年各種品牌在不同平臺上的銷售額;top3 基於 sales2019 計算不同平臺上的 Top 3 品牌和累計銷售額;這裏使用了兩個窗口函數(OVER
子句是窗口函數的標識),sum() 用於獲取品牌的累計銷售額,rank() 用於計算不同平臺上的銷售額排名。
除此之外,WITH
語句比第一種方法語義上更加清晰;而且從性能角度來說也會更好,因爲它只需要訪問 monthlysales 表一次。
總結
總的來說,這幾道題目都是高頻的 SQL 數據分析問題。依次考察了GROUP BY
分組加聚合函數、HAVING
過濾和DISTINCT
去重、ORDER BY
排序加上LIMIT
子句實現 Top-N 排行榜,以及窗口函數的使用。
定期更新數據庫領域相關文章,歡迎關注❤️、評論📝、點贊👍!