不久前,裸考國內知名電商平台拼多多的大數據崗位在線筆試,問答題(寫SQL)被虐的很慘,完了下來默默學習一波。順便借此機會復習一下SQL語句的用法。
本文主要涉及到的SQL知識點包括CREATE
創建數據庫和表、INSERT
插入數據、SUM()
求和、GROUP BY
分組、DATE_FORMAT()
格式化日期、ORDER BY
排序、COUNT()
統計行數、添加排名、MySQL實現統計排名、並列排名等,如果你對這些操作還有點不熟練,那么相信你讀完本文會有收獲的,如果自己再實現一遍效果更好。
准備工作
根據筆試時遺留的線索,在本地MySQL創建數據庫和表,為后續鋪墊。
-
創建數據庫和表
CREATE DATABASE
語句用於創建數據庫,基本語法如下:
CREATE DATABASE database_name
在本地創建一個名為test的測試數據庫:
CREATE TABLE test;
CREATE TABLE
語句用於創建表,基本語法如下:
CREATE TABLE table_name(
column_name1 type,
column_name2 type,
column_name3 type,
...
)
在test
數據庫下面創建一張名為orders
的表:
USE test;
CREATE TABLE orders(
id INT PRIMARY KEY AUTO_INCREMENT,
order_time TIMESTAMP,
cate VARCHAR(255),
goods_id int,
order_amount int
)
-
插入數據
INSERT INTO
語句用於向表格中插入新的行,基本語法如下:
INSERT INTO table_name VALUES (value1, value2,....)
向orders
表中插入一些測試數據:
INSERT INTO orders(order_time,cate,goods_id,order_amount)
VALUES ('2018-02-28 00:00:01', '水果',223,100),
('2018-02-28 01:01:01', '花茶',444,111),
('2018-02-28 06:06:06', '花茶',444,666),
('2018-03-01 07:01:10', '花茶',5555,170),
('2018-03-01 08:00:00', '花茶',5555,180),
('2018-03-01 00:00:01', '花茶',333,100),
('2018-03-01 00:00:01', '花茶',444,188),
('2018-03-01 00:00:01', '數碼',45454,5399)
結果如圖所示:
題目解析
-
請統計2018年全年每月銷售金額,按下表格式返回。
日期 | 銷售金額 |
---|---|
2018-01 | **** |
2018-02 | **** |
... | ... |
分析:統計每月的銷售金額,需要用到求和函數SUM()
。SUM()
函數用於返回數值列的總和。基本語法如下:
SELECT SUM(column_name) FROM table_name
求和通常需要用到GROUP BY
,GROUP BY
可以根據一個或多個列對結果集進行分組,本題也是這個套路,需要根據月份進行分組統計。GROUP BY
的基本語法如下:
SELECT column_name, aggregate_function(column_name)
FROM table_name
WHERE column_name operator value
GROUP BY column_name
當然本題還有其他附加要求,按照規定形式返回,需要對日期進行進行格式化處理。DATE_FORMAT() 函數用於以不同的格式顯示日期/時間數據,基本語法如下:
DATE_FORMAT(date,format)
date 參數是合法的日期。format 規定日期/時間的輸出格式。可以使用的格式有:
格式 | 描述 |
---|---|
%a | 縮寫星期名 |
%b | 縮寫月名 |
%c | 月,數值 |
%D | 帶有英文前綴的月中的天 |
%d | 月的天,數值(00-31) |
%e | 月的天,數值(0-31) |
%f | 微秒 |
%H | 小時 (00-23) |
%h | 小時 (01-12) |
%I | 小時 (01-12) |
%i | 分鍾,數值(00-59) |
%j | 年的天 (001-366) |
%k | 小時 (0-23) |
%l | 小時 (1-12) |
%M | 月名 |
%m | 月,數值(00-12) |
%p | AM 或 PM |
%r | 時間,12-小時(hh:mm:ss AM 或 PM) |
%S | 秒(00-59) |
%s | 秒(00-59) |
%T | 時間, 24-小時 (hh:mm:ss) |
%U | 周 (00-53) 星期日是一周的第一天 |
%u | 周 (00-53) 星期一是一周的第一天 |
%V | 周 (01-53) 星期日是一周的第一天,與 %X 使用 |
%v | 周 (01-53) 星期一是一周的第一天,與 %x 使用 |
%W | 星期名 |
%w | 周的天 (0=星期日, 6=星期六) |
%X | 年,其中的星期日是周的第一天,4 位,與 %V 使用 |
%x | 年,其中的星期一是周的第一天,4 位,與 %v 使用 |
%Y | 年,4 位 |
%y | 年,2 位 |
本題中的形式可以用DATE_FORMAT(t.order_time,'%Y-%m')
把時間格式化成表格中的形式(年份-月份),然后按照題目要求的別名返回即可。
這題比較簡單,分析了這么多,可以直接寫SQL語句了:
SELECT DATE_FORMAT(t.order_time,'%Y-%m') AS '日期', SUM(t.order_amount) AS '銷售金額'
FROM orders t
WHERE YEAR(t.order_time) = 2018
GROUP BY MONTH(t.order_time)
執行結果正確,如圖:
-
請統計2018年每月銷售金額,以及金額排名。
日期 | 銷售金額 | 金額排名 |
---|---|---|
2018-01 | **** | 2 |
2018-02 | **** | 3 |
... | ... | ... |
2018-12 | **** | 9 |
這個題是要求銷售金額的排名情況,求這個月的銷售額在這一年的12月中排第幾,需要得到具體排第幾名。比如說2018年1月的銷售金額在12個月中排第2名。不是用ORDER BY
粗暴的進行排序完事!不是用ORDER BY
粗暴的進行排序完事!不是用ORDER BY
粗暴的進行排序完事!這個是我理解的題意。
對於這個問題,我剛開始也是比較懵逼的,沒有思路。感覺這道題還有點東西哈。網上搜索了一下,沒有找到和我這個需求一模一樣的,看了一些相似的博客,然后從這些博客中找到了解答本題的思路。
在這過程中我也嘗試着在某個技術交流群里面請教了一下各位技術大佬,有說用ORDER BY
就好了的,有說用LIMIT
的,還有的說問這么傻的問題。。。如果一個ORDER BY
就可以輕易解答這個問題,我特么用得着來群里問你們?只好留下一句”我們的ORDER BY
好像不是太一樣,打擾了“,然后默默離開,沒有失望,也沒有憤怒。
因為我多年前早也經習慣了,習慣了大多數時候在群里面請教問題,不僅得不到滿意的解答,反而會遭到各種冷嘲熱諷。我也常常在反思這個問題,別人的問題難倒真的沒有一絲價值嗎?難倒我們真的是別人口中所說的“技術大佬”,別人的難題對於自己來說都不算是問題嗎?有些時候,看到一些交流群里的問題,貌似很簡單,但是有時候做起來還真的不好做;就像面試的時候手撕個很簡單的算法(比如快速排序、堆排序),很難保證“一次編寫,到處正確運行”。所以,面對別人的問題,我都告訴自己要認真對待。因為大多數人是在自己解決不了的時候才會把問題拋出來,沒有誰天生喜歡厚着臉皮去求人解答,這往往是更有價值的問題,是有助於提高自己的問題。哎,好像扯得有點遠了。下面繼續說這個問題。
看了看網上相似的問題,結合自己的分析,我覺得這道題完全可以解答出來,即使我使用的是MySQL數據庫(MySQL數據庫不能使用rank()
函數)。這個問題可以分三個步驟解決:
- 在第(1)問的基礎上按照銷售金額進行排序;要求排名,當然先要按銷售金額排序。
ORDER BY
用於對結果集按照一個列或者多個列進行排序。基本語法如下:
SELECT column_name,column_name
FROM table_name
ORDER BY column_name,column_name ASC|DESC;
對金額進行排序(降序需要加上DESC
關鍵字):
SELECT DATE_FORMAT(t.order_time,'%Y-%m') AS mon, SUM(t.order_amount) AS sum
FROM orders t
WHERE YEAR(t.order_time) = 2018
GROUP BY MONTH(t.order_time)
ORDER BY SUM(t.order_amount) DESC
為了排序和之后的效果顯示,我又在表格中插入了2018年4月的記錄。排序之后的結果如圖所示:
- 對排序的結果添加一個排名列;其實就是在上圖結果后面添加一個排名字段。這里自定義一個排名變量rank,初始化為0,由於數據已經是排好序的,所以每次加1就是排名,從而實現一個取得排序后名次的效果。
在MySQL中聲明一個變量,需要在變量名之前使用@
符號。FROM子句中的(@rank:= 0)
部分可以進行變量初始化,而不需要單獨的SET
命令。更多關於MySQL自定義變量可以參考Mysql自定義變量的使用和MySQL官網文檔用戶自定義變量。
例子:
SELECT (@rank := @rank+1) AS rank FROM (
SELECT * FROM table_name
) a,(SELECT @rank :=0) b
對本題中的銷售金額進行排序后添加排名列的SQL語句:
SELECT a.mon AS r,a.sum AS x,@rank :=@rank + 1 AS j
FROM
(SELECT DATE_FORMAT(t.order_time,'%Y-%m') AS mon, SUM(t.order_amount) AS sum
FROM orders t
WHERE YEAR(t.order_time) = 2018
GROUP BY MONTH(t.order_time)
ORDER BY SUM(t.order_amount) DESC) a,(SELECT @rank := 0) b
執行結果如圖:
這樣就實現了簡單的rank排名函數,也基本滿足了題意。但是這樣寫還有一個問題需要注意,遇到銷售金額相等的情況,名次也會加1。如果向表中再插入一條記錄2018年5月的記錄,使得5月份的銷售金額和2月份相等:INSERT INTO orders(order_time,cate,goods_id,order_amount) VALUES ('2018-05-22 13:23:39', '果粒橙',111,877)
,再去執行剛才的查詢操作,結果如圖:
可以看見圖中2018年2月和2018年5月的銷售額都是877,2月排第2,5月排第3。這樣排名貌似不合理吧?
還有更神奇的呢!再次執行相同的操作,結果卻不相同。what?這次5月排第2,2月排第3了?什么情況?關於ORDER BY
排序以后順序為什么隨機,我需要再好好研究一下MySQL底層原理。所以這個問題先留着。
如果是面試的話,在上面排名情況這個細節問題上就需要和面試官進行交流了,銷售金額會不會有相等的情況?如果有相等的情況,遇到名次並列情況怎么辦?如果說第1名有1個,第2名有兩個並列,那么接下來的排名是第3名還是第4名呢?
接下來實現並列排名。如果題目要求相同數據並列排名,求排名的時候,需要拿前一個排名的數據來對比從而判斷排名是否進行加1操作。SQL層面則需要自定義兩個變量,一個記錄之前排名的數據,一個記錄現在的排名。如果之前排名的數據等於需要排名的數據,那么就是並列,排名不變。如果不相等,排名加1。也許我描述的不夠清楚,看看SQL語句估計就明白了:
SELECT a.mon AS r,a.sum AS x,
CASE
WHEN @prevRank = a.sum THEN @curRank
WHEN @prevRank := a.sum THEN @curRank := @curRank + 1
END AS j
FROM
(SELECT DATE_FORMAT(t.order_time,'%Y-%m') AS mon, SUM(t.order_amount) AS sum
FROM orders t
WHERE YEAR(t.order_time) = 2018
GROUP BY MONTH(t.order_time)
ORDER BY SUM(t.order_amount) DESC) a,(SELECT @curRank :=0, @prevRank := NULL) b
執行上述語句,2月和5月排名實現了並列,如圖:
上面實現了普通並列排名,如果想實現高級並列排名(使上圖中2018年4月數據排第4),需要定義3個變量,寫起來有點復雜,這里先不寫了。關於高級並列排名可以參考:在MySQL中實現Rank高級排名函數。
- 在第二步的基礎上按照月份排序,完成。
經過了上面的步驟,離目標僅有一步之遙:按月份排序,還有替換別名。第二步的結果當成一張表,新建一個查詢,對其進行月份排列,並把列名替換成為最終題目需要的列名即可。
SELECT tt.r AS '日期',tt.x AS '銷售金額',tt.j AS '金額排名'
FROM
(SELECT a.mon AS r,a.sum AS x,
CASE
WHEN @prevRank = a.sum THEN @curRank
WHEN @prevRank := a.sum THEN @curRank := @curRank + 1
END AS j
FROM
(SELECT DATE_FORMAT(t.order_time,'%Y-%m') AS mon, SUM(t.order_amount) AS sum
FROM orders t
WHERE YEAR(t.order_time) = 2018
GROUP BY MONTH(t.order_time)
ORDER BY SUM(t.order_amount) DESC) a,(SELECT @curRank :=0, @prevRank := NULL) b) tt
ORDER BY tt.r
結果如我所願:
-
請用SQL選出2018年2月每個類目銷量最高的2個爆款商品以及排名先后。
類目 | 商品id | 排名 |
---|---|---|
水果 | 223 | 1 |
花茶 | 444 | 1 |
花茶 | 5555 | 2 |
數碼 | 45454 | 1 |
這個問題是考察分組排名的問題:按照商品類目進行分組,按goods_id
統計行數作為銷量,找出每個商品種類銷量前2名的goods_id
,並給出排名。如果已經完全理解了第2問的使用自定義變量來實現添加排名操作,這一問做起來會輕松許多。
銷量怎么計算?題目中沒有明確說明,我理解的銷量應該是表中的記錄行數。統計記錄行數需要使用COUNT()
函數,基本語法如下:
SELECT COUNT(column_name) FROM table_name
這個問題也可以分三個步驟解決:
- 統計出來每種商品的銷量,並按照類目、銷量進行排序;這里由於表中的數據庫記錄較少,所以我直接統計的是2018年全年的數據,其實道理是一樣的。SQL語句如下:
SELECT
a.cate,a.goods_id,a.count
FROM
(
SELECT t.cate,t.goods_id,count(goods_id) AS count
FROM orders t
WHERE date_format(t.order_time, '%Y%m%d%H%i%s')LIKE "2018%"
GROUP BY t.goods_id
ORDER BY t.cate,count(t.goods_id) DESC
) AS a
執行結果如圖:
- 使用自定義變量為排序結果添加排名。原理和用法與上一個問題是一樣的,這里不贅述了。SQL語句如下:
SELECT
a.cate,a.goods_id,a.count,
@rank:= CASE WHEN @prevCate=a.cate THEN @rank+1 ELSE 1 END AS rankNO,
@prevCate:=a.cate AS type
FROM
(
SELECT t.cate,t.goods_id,count(goods_id) AS count
FROM orders t
WHERE date_format(t.order_time, '%Y%m%d%H%i%s')LIKE "2018%"
GROUP BY t.goods_id
ORDER BY t.cate,count(t.goods_id) DESC
) AS a,(SELECT @rank:=0 ,@prevCate:='') b
執行結果如圖:
- 根據
rankNO
篩選前2名並按照題目要求格式返回;由於前面的鋪墊,只需要用WHERE
對rankNO
進行篩選。SQL語句如下:
SELECT t.cate AS '類目',t.goods_id AS '商品id',t.rankNO AS '排名'
FROM
(SELECT
a.cate,a.goods_id,a.count,
@rank:= CASE WHEN @prevCate=a.cate THEN @rank+1 ELSE 1 END AS rankNO,
@prevCate:=a.cate AS type
FROM
(
SELECT t.cate,t.goods_id,count(goods_id) AS count
FROM orders t
WHERE date_format(t.order_time, '%Y%m%d%H%i%s')LIKE "2018%"
GROUP BY t.goods_id
ORDER BY t.cate,count(t.goods_id) DESC
) AS a,(SELECT @rank:=0 ,@prevCate:='') b) t
WHERE t.rankNO <= 2
執行結果和要求一模一樣:
總結
筆試已涼,但是學習之路沒有終點。經過幾天的學習和調試,終於解決了這個SQL語句的問題,也算是了卻了一樁心事。
本文僅根據題目要求實現了基本功能,關於性能方面的問題還沒有考慮。在大數據量的情況下這么寫是否還可以接受呢?應該怎么優化?ORDEY BY
排序以后相同數據順序隨機究竟和底層索引之間有怎么的聯系?由於水平有限,這些問題我還需要再好好研究一番,也希望各位可以多指教。