關于各種 DBMS 的介紹
答疑 1
文章中有句話不太理解,“列式數據庫是將數據按照列存儲到數據庫中,這樣做的好處是可以大量降低系統的 I/O”,可以解釋一些“降低系統 I/O”是什么意思嗎?
解答
行式存儲是把一行的數據都串起來進行存儲,然后再存儲下一行。同樣,列式存儲是把一列的數據都串起來進行存儲,然后再存儲下一列。這樣做的話,相鄰數據的數據類型都是一樣的,更容易壓縮,壓縮之后就自然降低了 I/O。
我們還需要從數據處理的需求出發,去理解行式存儲和列式存儲。數據處理可以分為 OLTP(聯機事務處理)和 OLAP(聯機分析處理)兩大類。
OLTP 一般用于處理客戶的事務和進行查詢,需要隨時對數據表中的記錄進行增刪改查,對實時性要求高。
OLAP 一般用于市場的數據分析,通常數據量大,需要進行復雜的分析操作,可以對大量歷史數據進行匯總和分析,對實時性要求不高。
那么對于 OLTP 來說,由于隨時需要對數據記錄進行增刪改查,更適合采用行式存儲,因為一行數據的寫入會同時修改多個列。傳統的 RDBMS 都屬于行式存儲,比如 Oracle、SQL Server 和 MySQL 等。
對于 OLAP 來說,由于需要對大量歷史數據進行匯總和分析,則適合采用列式存儲,這樣的話匯總數據會非常快,但是對于插入(INSERT)和更新(UPDATE)會比較麻煩,相比于行式存儲性能會差不少。
所以說列式存儲適合大批量數據查詢,可以降低 I/O,但如果對實時性要求高,則更適合行式存儲。
關于查詢優化
答疑 1
在 MySQL 中統計數據表的行數,可以使用三種方式:SELECT COUNT()、SELECT COUNT(1)和SELECT COUNT(具體字段),使用這三者之間的查詢效率是怎樣的?之前看到說是:SELECT COUNT()> SELECT COUNT(1)> SELECT COUNT(具體字段)。
解答
在 MySQL InnoDB 存儲引擎中,COUNT(*)和COUNT(1)都是對所有結果進行COUNT。如果有 WHERE 子句,則是對所有符合篩選條件的數據行進行統計;如果沒有 WHERE 子句,則是對數據表的數據行數進行統計。
因此COUNT(*)和COUNT(1)本質上并沒有區別,執行的復雜度都是O(N),也就是采用全表掃描,進行循環 + 計數的方式進行統計。
如果是 MySQL MyISAM 存儲引擎,統計數據表的行數只需要O(1)的復雜度,這是因為每張 MyISAM 的數據表都有一個 meta 信息存儲了row_count值,而一致性則由表級鎖來保證。因為 InnoDB 支持事務,采用行級鎖和 MVCC 機制,所以無法像 MyISAM 一樣,只維護一個row_count變量,因此需要采用掃描全表,進行循環 + 計數的方式來完成統計。
需要注意的是,在實際執行中,COUNT(*)和COUNT(1)的執行時間可能略有差別,不過你還是可以把它倆的執行效率看成是相等的。
另外在 InnoDB 引擎中,如果采用COUNT()和COUNT(1)來統計數據行數,要盡量采用二級索引。因為主鍵采用的索引是聚簇索引,聚簇索引包含的信息多,明顯會大于二級索引(非聚簇索引)。對于COUNT()和COUNT(1)來說,它們不需要查找具體的行,只是統計行數,系統會自動采用占用空間更小的二級索引來進行統計。
然而如果想要查找具體的行,那么采用主鍵索引的效率更高。如果有多個二級索引,會使用 key_len 小的二級索引進行掃描。當沒有二級索引的時候,才會采用主鍵索引來進行統計。
這里我總結一下:
一般情況下,三者執行的效率為 COUNT()= COUNT(1)> COUNT(字段)。我們盡量使用COUNT(),當然如果你要統計的是某個字段的非空數據行數,則另當別論,畢竟比較執行效率的前提是結果一樣才可以。
如果要統計COUNT(),盡量在數據表上建立二級索引,系統會自動采用key_len小的二級索引進行掃描,這樣當我們使用SELECT COUNT()的時候效率就會提升,有時候可以提升幾倍甚至更高。
答疑 2
在 MySQL 中,LIMIT關鍵詞是最后執行的,如果可以確定只有一條結果,那么就起不到查詢優化的效果了吧,因為LIMIT是對最后的結果集過濾,如果結果集本來就只有一條,那就沒有什么用了。
解答
如果你可以確定結果集只有一條,那么加上LIMIT 1的時候,當找到一條結果的時候就不會繼續掃描了,這樣會加快查詢速度。這里指的查詢優化針對的是會掃描全表的 SQL 語句,如果數據表已經對字段建立了唯一索引,那么可以通過索引進行查詢,不會全表掃描的話,就不需要加上LIMIT 1了。
關于通配符的解釋
關于查詢語句中通配符的使用理解,我舉了一個查詢英雄名除了第一個字以外,包含“太”字的英雄都有誰的例子,使用的 SQL 語句是:
SQL> SELECT name FROM heros WHERE name LIKE '% 太 %'
()匹配任意一個字符,(%) 匹配大于等于 0 個任意字符。
所以通配符'_%太%'說明在第一個字符之后需要有“太”字,這里就不能匹配上“太乙真人”,但是可以匹配上“東皇太一”。如果數據表中有“太乙真人太太”,那么結果集中也可以匹配到。
另外,單獨的LIKE '%'無法查出 NULL 值,比如:SELECT * FROM heros WHERE role_assist LIKE '%'。
答疑 4
可以理解在 WHERE 條件字段上加索引,但是為什么在 ORDER BY 字段上還要加索引呢?這個時候已經通過 WHERE 條件過濾得到了數據,已經不需要再篩選過濾數據了,只需要根據字段排序就好了。
解答
在 MySQL 中,支持兩種排序方式,分別是 FileSort 和 Index 排序。在 Index 排序中,索引可以保證數據的有序性,不需要再進行排序,效率更高。而 FileSort 排序則一般在內存中進行排序,占用 CPU 較多。如果待排結果較大,會產生臨時文件 I/O 到磁盤進行排序的情況,效率較低。
所以使用 ORDER BY 子句時,應該盡量使用 Index 排序,避免使用 FileSort 排序。當然你可以使用 explain 來查看執行計劃,看下優化器是否采用索引進行排序。
優化建議:
SQL 中,可以在 WHERE 子句和 ORDER BY 子句中使用索引,目的是在 WHERE 子句中避免全表掃描,在 ORDER BY 子句避免使用 FileSort 排序。當然,某些情況下全表掃描,或者 FileSort 排序不一定比索引慢。但總的來說,我們還是要避免,以提高查詢效率。一般情況下,優化器會幫我們進行更好的選擇,當然我們也需要建立合理的索引。
盡量使用 Index 完成 ORDER BY 排序。如果 WHERE 和 ORDER BY 后面是相同的列就使用單索引列;如果不同就使用聯合索引。
無法使用 Index 時,需要對 FileSort 方式進行調優。
答疑 5
ORDER BY 是對分的組排序還是對分組中的記錄排序呢?
解答
ORDER BY 就是對記錄進行排序。如果你在 ORDER BY 前面用到了 GROUP BY,實際上這是一種分組的聚合方式,已經把一組的數據聚合成為了一條記錄,再進行排序的時候,相當于對分的組進行了排序。
答疑 6
請問下關于 SELECT 語句內部的執行步驟。
解答
一條完整的 SELECT 語句內部的執行順序是這樣的:
FROM 子句組裝數據(包括通過 ON 進行連接);
WHERE 子句進行條件篩選;
GROUP BY 分組 ;
使用聚集函數進行計算;
HAVING 篩選分組;
計算所有的表達式;
SELECT 的字段;
ORDER BY 排序;
LIMIT 篩選。
答疑 7
不太理解哪種情況下應該使用 EXISTS,哪種情況應該用 IN。選擇的標準是看能否使用表的索引嗎?
解答
索引是個前提,其實選擇與否還是要看表的大小。你可以將選擇的標準理解為小表驅動大表。在這種方式下效率是最高的。
比如下面這樣:
SELECT * FROM A WHERE cc IN (SELECT cc FROM B)
SELECT * FROM A WHERE EXISTS (SELECT cc FROM B WHERE B.cc=A.cc)
當 A 小于 B 時,用 EXISTS。因為 EXISTS 的實現,相當于外表循環,實現的邏輯類似于:
for i in A
for j in B
if j.cc == i.cc then ...
當 B 小于 A 時用 IN,因為實現的邏輯類似于:
for i in B
for j in A
if j.cc == i.cc then ...
哪個表小就用哪個表來驅動,A 表小就用 EXISTS,B 表小就用 IN。
關于存儲過程
答疑 1
在使用存儲過程聲明變量時,都支持哪些數據類型呢?
解答
不同的 DBMS 對數據類型的定義不同,你需要查詢相關的 DBMS 文檔。以 MySQL 為例,常見的數據類型可以分成三類,分別是數值類型、字符串類型和日期/時間類型。
答疑 2
“IN 參數必須在調用存儲過程時指定”的含義是什么?我查詢了 MySQL 的存儲過程定義,可以不包含 IN 參數。當存儲過程的定義語句里有 IN 參數時,存儲過程的語句中必須用到這個參數嗎?
解答
如果存儲過程定義了 IN 參數,就需要在調用的時候傳入。當然在定義存儲過程的時候,如果不指定參數類型,就默認是 IN 類型的參數。因為 IN 參數在存儲過程中是默認值,可以省略不寫。比如下面兩種定義方式都是一樣的:
CREATE PROCEDURE add_num
(IN n INT)
CREATE PROCEDURE add_num
(n INT)
在存儲過程中的語句里,不一定要用到 IN 參數,只是在調用的時候需要傳入這個。另外 IN 參數在存儲過程中進行了修改,也不會進行返回的。如果想要返回參數,需要使用 OUT,或者 INOUT 參數類型。
關于事務處理
答疑 1
如果INSERT INTO test SELECT '關羽';之后沒有執行 COMMIT,結果應該是空。但是我執行出來的結果是'關羽',為什么 ROLLBACK 沒有全部回滾?
代碼如下:
CREATE TABLE test(name varchar(255), PRIMARY KEY (name)) ENGINE=InnoDB;
BEGIN;
INSERT INTO test SELECT '關羽';
BEGIN;
INSERT INTO test SELECT '張飛';
INSERT INTO test SELECT '張飛';
ROLLBACK;
SELECT * FROM test;
解答
先解釋下連續 BEGIN 的情況。
在 MySQL 中 BEGIN 用于開啟事務,如果是連續 BEGIN,當開啟了第一個事務,還沒有進行 COMMIT 提交時,會直接進行第二個事務的 BEGIN,這時數據庫會隱式地 COMMIT 第一個事務,然后再進入到第二個事務。
為什么 ROLLBACK 沒有全部回滾呢?
因為 ROLLBACK 是針對當前事務的,在 BEGIN 之后已經開啟了第二個事務,當遇到 ROLLBACK 的時候,第二個事務都進行了回滾,也就得到了第一個事務執行之后的結果即“關羽”。
關于事務的 ACID,以及我們使用 COMMIT 和 ROLLBACK 來控制事務的時候,有一個容易出錯的地方。
在一個事務的執行過程中可能會失敗。遇到失敗的時候是進行回滾,還是將事務執行過程中已經成功操作的來進行提交,這個邏輯是需要開發者自己來控制的。
這里開發者可以決定,如果遇到了小錯誤是直接忽略,提交事務,還是遇到任何錯誤都進行回滾。如果我們強行進行 COMMIT,數據庫會將這個事務中成功的操作進行提交,它會認為你覺得已經是 ACID 了(就是你認為可以做 COMMIT 了,即使遇到了一些小問題也是可以忽略的)。