1. 程式人生 > >MYSQL 使用基礎 - 這麼用就對了

MYSQL 使用基礎 - 這麼用就對了

這篇文章主要梳理了 SQL 的基礎用法,會涉及到以下方面內容: * SQL大小寫的規範 * 資料庫的型別以及適用場景 * SELECT 的執行過程 * WHERE 使用規範 * MySQL 中常見函式 * 子查詢分類 * 如何選擇合適的 EXISTS 和 IN 子查詢 ## 瞭解 SQL SQL 是我們用來最長和資料打交道的方式之一,如果按照功能劃分可分為如下 4 個部分: * DDL,資料定義語言。定義資料庫物件,資料表,資料列。也就是,對資料庫和表結構進行增刪改操作。 * DML,資料操作語言。對資料表的增刪改。 * DCL,資料控制語言。定義訪問許可權和安全級別。 * DQL,資料查詢語言。用來查詢資料。 平時在編寫 SQL 時,可能發現許多 SQL 大小寫不統一,雖然不會影響 SQL 的執行結果,但保持統一的書寫規範,是提高效率的關鍵,通常遵循如下的原則: * 表名,表別名,欄位名,欄位別名等用小寫。 * SQL 保留字,函式名,繫結變數等用大寫。 * 資料表,欄位名採用下劃線命名。 目前排名較前的 DBMS: ![img](https://static001.geekbang.org/resource/image/a7/91/a7237ddbe4ca69353bd21a6eff35d391.png) * 關係型資料庫:建立在關係模型上的資料庫,在建表時,通常先設計 ER 圖表示之間的關係。 * 鍵值型資料庫:以 key-value 的形式儲存資料,優點是查詢速度快,缺點是無法向關係型資料庫一樣使用如 WHERE 等的過濾條件。常見場景是作為內容快取。 * 文件型資料庫,在儲存時以文件作為處理資訊的基本單位。 * 搜尋引擎:針對全文檢索而設計。核心原理是 “倒排索引”。 * 列式資料庫:相對於如 MySQL 等行式儲存的資料庫,是以列將資料存在資料庫中,由於列具有相同的資料型別,所以可以更好的壓縮,從而減低系統的 I/O,適用於分散式檔案系統,但功能相對有限。 * 圖形資料庫,利用圖的資料結構儲存實體之間的關係。比如社交網路中人與人的關係,資料模型為節點和邊來實現。 ## 認識 SELECT SELECT 一般是在學習 SQL 接觸的第一個關鍵字,基礎的內容就是不提了,這裡整理常用的規範: ### 起別名 ``` SELECT name AS n FROM student ``` ### 查詢常數, 增加一列固定的常數列: ``` SELECT '學生資訊' as student_info, name FROM student ``` ### 去重重複行 ``` SELECT DISTINCT age FROM student ``` 需要注意的是 `DISTINCT` 是對後面的所有列進行去重, 下面這種情況就會對 age 和 name 的組合進行去重。 ``` SELECT DISTINCT age,name FROM student ``` ### 排序資料,ASC 代表升序,DESC 代表降序 如先按照 name 排序,name 相等的情況下按照 age 排序。 ``` SELECT DISTINCT age FROM student ORDERY BY name,age DESC ``` ### 限制返回的數量 ``` SELECT DISTINCT age FROM student ORDERY BY name DESC LIMIT 5 ``` ## SELECT 的執行順序 瞭解了 SELECT 的執行順序,才能更好地寫出更有效率的 SQL。 對於 SELECT 順序有兩個原則: 1. 關鍵字的順序不能顛倒: ``` SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... ``` 2. SELECT 會按照如下順序執行: ``` FROM > WHERE > GROUP BY > HAVING > SELECT的欄位 > DISTINCT > ORDER BY > LIMIT ``` ``` SELECT DISTINCT student_id, name, count(*) as num #順序5 FROM student JOIN class ON student.class_id = class.class_id #順序1 WHERE age > 18 #順序2 GROUP BY student.class_id #順序3 HAVING num > 2 #順序4 ORDER BY num DESC #順序6 LIMIT 2 #順序7 ``` 在逐一分析下這個過程前,我們需要知道在上面的每一個步驟中都會產生一個虛擬表,然後將這個虛擬表作為下一個步驟中作為輸入,但這一過程對我們來說是不可見的: 1. 從 FROM 語句開始,對 student 和 class 表進行 CROSS JOIN 笛卡爾積運算,得到虛擬表 vt 1-1; 2. 通過 ON 篩選,在 vt1-1 的基礎上進行過濾然後得到表 vt 1-2; 3. 新增外部行。如使用左連線,右連線和全連線時,就會涉及到外部行,會在 vt1-2 的基礎上增加外部行,得到 vt1-3。 4. 如果超過兩張表,就會重複上面的步驟。 5. 在拿到最終的 vt1 的表資料後,會執行 WHERE 後面的過濾階段,得到表 vt2. 6. 接著到 GROUP 階段,進行分組得到 vt3. 7. 接著到 HAVING 階段,對分組的資料進行過濾,得到 vt4. 8. 後面進入 SELECT 階段,提取需要的欄位,得到 vt5-1,接著通過 DISTINCT 階段,過濾到重複的行,得到 vt5-2. 9. 然後對指定的欄位進行排序,進入 ORDER BY 階段,得到 vt6. 10. 最後在 LIMIT 階段,取出指定的行,對應 vt7,也就是最後的結果。 > 如果涉及到函式的計算比如 sum() 等,會在 GROUP BY分組後,HAVING 分組前,進行聚集函式的計算。 > > 涉及到表示式計算,如 age * 10 等,會在 HAVING 階段後,SELECT 階段前進行計算。 通過這裡,就可以總結出提高 SQL 效率的第一個方法: * **使用 SELECT 時指定明確的列來代替 SELECT * .** 從而減少網路的傳輸量。 ## 使用 WHERE 進行過濾 使用 WHERE 篩選時,常有通過比較運算子,邏輯運算子,萬用字元三種方式。 對於比較運算子,常用的運算子如下表。 ![img](https://static001.geekbang.org/resource/image/3a/e0/3a2667784b4887ef15becc7056f3d3e0.png) 對於邏輯運算子來說,可以將多個比較執行符連線起來,進行多條件的篩選,常用的運算子如下: ![img](https://static001.geekbang.org/resource/image/ae/c1/aeed170c57ae1e5378fbee9f8fb6a8c1.png) 需要注意的是,當 AND 和 OR 同時出現時,AND 的優先順序更高會先被執行。當如果存在 () 的話,則括號的優先順序最高。 使用萬用字元過濾: like:(%)代表零個或多個字元,(_)只代表一個字元 ## 函式 和程式語言中的定義的函式一樣,SQL 同樣定義了一些函式方便使用,比如求和,平均值,長度等。 常見的函式主要分為如下四類,分類的原則是根據定義列時的資料型別: * 算術函式: ![img](https://static001.geekbang.org/resource/image/19/e1/193b171970c90394576d3812a46dd8e1.png) * 字串函式 ![img](https://static001.geekbang.org/resource/image/c1/4d/c161033ebeeaa8eb2436742f0f818a4d.png) 需要注意的是,在使用字串比較日期時,要使用 DATE 函式比較。 * 日期函式 ![img](https://static001.geekbang.org/resource/image/3d/45/3dec8d799b1363d38df34ed3fdd29045.png) * 轉換函式: ![img](https://static001.geekbang.org/resource/image/5d/59/5d977d747ed1fddca3acaab33d29f459.png) CAST 函式在轉換資料型別時,不會四捨五入,如果原數值是小數,在轉換到整數時會報錯。 在轉換時可以使用 DECIMAL(a,b) 函式來規定小數的精度,比如 DECIMAL(8,2) 表示精度為 8 位 - 小數加整數最多 8 位。小數後面最多為 2 位。 然後通過 `SELECT CAST(123.123 AS DECIMAL(8,2))` 來轉換。 ## 聚集函式 通常情況下,我們會使用聚集函式來彙總表的資料,輸入為一組資料,輸出為單個值。 常用的聚集函式有 5 個: ![img](https://static001.geekbang.org/resource/image/d1/15/d101026459ffa96504ba3ebb85054415.png) 其中 COUNT 函式需要額外注意,具體的內容可以[參考這篇](https://www.cnblogs.com/michael9/p/12513327.html)。 ### 如何進行分組 在統計結果時,往往需要對資料按照一定條件進行分組,對應就是 `GROUP BY` 語句。 比如統計每個班級的學生人數: ``` SELECT class_id, COUNT(*) as student_count FROM student \ GROUP BY class_id; ``` `GROUP BY` 後也可接多個列名,進行分組,比如按照班級和性別分組: ``` SELECT class_id, sex, COUNT(*) as student_count FROM \ student GROUP BY class_id, sex; ``` ### HAVING 過濾和 WHERE 的區別 和 WHERE 一樣,可以對分組後的資料進行篩選。區別在於 WHERE 適用於資料行,HAVING 用於分組。 而且 WHERE 支援的操作,HAVING 也同樣支援。 比如可以篩選大於2人的班級: ``` SELECT class_id, COUNT(*) as student_count FROM student \ GROUP BY class_id \ HAVING student_count > 20; ``` ## 子查詢 在一些更為複雜的情況中,往往會進行巢狀的查詢,比如在獲取結果後,該結果作為輸入,去獲取另外一組結果。 在 SQL 中,查詢可以分為關聯子查詢和非關聯子查詢。 假設有如下的表結構: ``` -- ---------------------------- DROP TABLE IF EXISTS `student`; CREATE TABLE `student` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(20) NOT NULL DEFAULT '', `age` int(3) NOT NULL, `sex` varchar(10) NOT NULL DEFAULT '', `class_id` int(11) NOT NULL COMMENT '班級ID', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of Student -- ---------------------------- INSERT INTO `student` VALUES ('1', '胡一', 13, '男', '1'); INSERT INTO `student` VALUES ('3', '王阿', 11, '女', '1'); INSERT INTO `student` VALUES ('5', '王琦', 12, '男', '1'); INSERT INTO `student` VALUES ('7', '劉偉', 11, '女', '1'); INSERT INTO `student` VALUES ('7', '王意識', 11, '女', '2'); -- ---------------------------- DROP TABLE IF EXISTS `student_activities`; CREATE TABLE `student_activities` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(20) NOT NULL DEFAULT '', `stu_id` int(11) NOT NULL COMMENT '班級ID', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8; INSERT INTO `student_activities` VALUES ('1', '博物館', 1); INSERT INTO `student_activities` VALUES ('3, '春遊', 3); ``` ### 非關聯子查詢 子查詢從資料表中查詢了資料結果,如果這個資料結果只執行一次,然後這個資料結果作為主查詢的條件接著執行。 這裡想要查詢和胡一相同班級的同學名稱: ``` SELECT name FROM student WHERE class_id = \ (SELECT class_id FROM student WHERE name='胡一') ``` 這裡先查到胡一的班級,只有一次查詢,再根據該班級查詢學生就是非關聯子查詢。 ### 關聯子查詢 如果子查詢需要執行多次,即採用迴圈的方式,先從外部查詢開始,每次都傳入子查詢進行查詢,然後再將結果反饋給外部 再舉個例子, 比如查詢比每個班級中比平均年齡大的學生姓名資訊: ``` SELECT name FROM student as s1 WHERE age > (SELECT AVG(age) FROM student as s2 where s1.class_id = s2.class_id) ``` 這裡根據每名同學的班級資訊,查找出對應班級的平均年齡,然後做判斷。子查詢每次執行時,都需要根據外部的查詢然後進行計算。這樣的子查詢就是關聯子查詢。 ### EXISTS 子查詢 在關聯子查詢中,常會和 `EXISTS` 一起使用。用來判斷條件是否滿足,滿足的話為 True,不滿足為 False。 比如查詢參加過學校活動的學生名稱: ``` SELECT NAME FROM student as s where \ EXISTS(SELECT stu_id FROM student_activities as sa where sa.stu_id=s.id) ``` 同樣 `NOT EXISTS` 就是不存在的意思,滿足為 FALSE , 不滿足為 True. 比如查詢沒有參加過學校活動的學生名稱: ``` SELECT NAME FROM student as s where \ NOT EXISTS(SELECT stu_id FROM student_activities as sa where sa.stu_id=s.id) ``` ### 集合比較子查詢 可以在子查詢中,使用集合操作符,來比較結果。 ![img](https://static001.geekbang.org/resource/image/d3/2c/d3867c22616cbdf88ed83865604e8e2c.png) 還是上面查詢參加學校活動的學生名字的子查詢, 同樣可以使用 IN: ``` SELECT name FROM student WHERE id IN (SELECT stu_id FROM student_activities) ``` ### EXISTS 和 IN 的區別 既然 EXISTS 和 IN 都能實現相同的功能,那麼他們之間的區別是什麼? 現在假設我們有表 A 和 表 B,其中 A,B 都有欄位 cc,並對 cc 建立了 b+ 索引,其中 A 表 n 條記錄,B 表 m 條索引。 將其模式抽象為: ``` SELECT * FROM A WHERE cc IN (SELECT cc FROM B) SELECT * FROM A WHERE EXIST (SELECT cc FROM B WHERE B.cc=A.cc) ``` 對於 EXISTS 來說,會先對外表進行逐條迴圈,每次拿到外表的結果後,帶入子查詢的內表中,去判斷該值是否存在。 虛擬碼類似於下面: ``` for i in A for j in B if j.cc == i.cc: return result ``` 首先先看外表 A,每一條都需要遍歷到,所以需要 n 次。內表 B,在查詢時由於使用索引進而查詢效率變成 log(m) B+ 的樹高,而不是 m。 進而總效率:n * log(m) **所以對於 A 表的數量明顯小於 B 時,推薦使用 EXISTS 查詢。** 再看 IN ,會先對內表 B 進行查詢,然後用外表 A 進行判斷,虛擬碼如下: ``` for i in B for j in A if j.cc == i.cc: return result ``` 由於需要首先將內表所有資料查出,所以需要的次數就是 m. 再看外表 A ,由於使用了 cc 索引,可將 n 簡化至 log(n), 也就是 m * log(n). **所以對於 A 表的資料明顯大於 B 表時,推薦使用 IN 查詢。** 總結一下對於 IN 和 EXISTS時,採用小表驅動大表的原則。 這裡再擴充套件下 `NOT EXISTS` 和 `NOT IN` 的區別: ``` SELECT * FROM A WHERE cc NOT IN (SELECT cc FROM B) SELECT * FROM A WHERE NOT EXIST (SELECT cc FROM B WHERE B.cc=A.cc) ``` 對於 NOT EXITS 來說,和 EXISTS 一樣,對於內表可以使用 cc 的索引。適用於 A 表小於 B 表的情況。 但對於 `NOT IN` 來說,和 `IN` 就有區別了,由於 cc 設定了索引 `cc IN (1, 2, 3)` 可以轉換成 `WHERE cc=1 OR cc=2 OR cc=3` , 是可以正常走 cc 索引的。但對於 `NOT IN` 也就是轉化為 `cc!=1 OR cc!=2 OR cc!=3` 這時由於是不等號查詢,是無法走索引的,進而全表掃描。 也就是說,在設定索引的情況下 `NOT EXISTS` 比 `NOT IN` 的效率高。 但對於沒有索引的情況,`IN` 和 `OR` 是不同的: ``` 一、操作不同 1、in:in是把父查詢表和子查詢表作hash連線。 2、or:or是對父查詢表作loop迴圈,每次loop迴圈再對子查詢表進行查詢。 二、適用場景不同 1、in:in適合用於子查詢表資料比父查詢表資料多的情況。 2、or:or適合用於子查詢表資料比父查詢表資料少的情況。 三、效率不同 1、in:在沒有索引的情況下,隨著in後面的資料量越多,in的執行效率不會有太大的下降。 2、or:在沒有索引的情況下,隨著or後面的資料量越多,or的執行效率會有明顯的下降。 ``` ## 總結 這篇文章中主要歸納了一些 SQL 的基礎知識: 在使用 SELECT 查詢時,通過顯式指定列名,來減少 IO 的傳輸,從而提高效率。 並且需要注意 SELECT 的查詢過程會從 FROM 後開始到 LIMIT 結束,理解了整體的流程,可以讓我們更好的組織 SQL. 之後詳細介紹了 WHERE 進行過濾的操作符和常用的函式,這裡要注意在比較時間時要使用 DATE 函式,以及如何對資料進行分組和過濾。 最後著重介紹了子查詢,IN 和 EXISTS 的適用