不久前,裸考國內知名電商平臺拼多多的大數據崗位在線筆試,問答題(寫SQL)被虐的很慘,完了下來默默學習一波。順便藉此機會複習一下SQL語句的用法。html
本文主要涉及到的SQL知識點包括CREATE
建立數據庫和表、INSERT
插入數據、SUM()
求和、GROUP BY
分組、DATE_FORMAT()
格式化日期、ORDER BY
排序、COUNT()
統計行數、添加排名、MySQL實現統計排名、並列排名等,若是你對這些操做還有點不熟練,那麼相信你讀完本文會有收穫的,若是本身再實現一遍效果更好。mysql
根據筆試時遺留的線索,在本地MySQL建立數據庫和表,爲後續鋪墊。面試
CREATE DATABASE
語句用於建立數據庫,基本語法以下:算法
CREATE DATABASE database_name
sql
在本地建立一個名爲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-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-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()
函數)。這個問題能夠分三個步驟解決:
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月的記錄。排序以後的結果如圖所示:
在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
結果如我所願:
類目 | 商品id | 排名 |
---|---|---|
水果 | 223 | 1 |
花茶 | 444 | 1 |
花茶 | 5555 | 2 |
數碼 | 45454 | 1 |
這個問題是考察分組排名的問題:按照商品類目進行分組,按goods_id
統計行數做爲銷量,找出每一個商品種類銷量前2名的goods_id
,並給出排名。若是已經徹底理解了第2問的使用自定義變量來實現添加排名操做,這一問作起來會輕鬆許多。
銷量怎麼計算?題目中沒有明確說明,我理解的銷量應該是表中的記錄行數。統計記錄行數須要使用COUNT()
函數,基本語法以下:
SELECT COUNT(column_name) FROM table_name
這個問題也能夠分三個步驟解決:
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
執行結果如圖:
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
排序之後相同數據順序隨機究竟和底層索引之間有怎麼的聯繫?因爲水平有限,這些問題我還須要再好好研究一番,也但願各位能夠多指教。