我們開發的大部分軟件,其基本業務流程都是:采集數據→將數據存儲到數據庫中→根據業務需求查詢相應數據→對數據進行處理→傳給前台展示。對整個流程進行分析,可以發現軟件大部分的操作時間消耗都花在了數據庫相關的IO操作上。所以對我們的SQL語句進行優化,可以提高軟件的響應性能,帶來更好的用戶體驗。
在開始介紹SQL優化技巧之前,先推介一款數據庫管理神器Navicat,官網:https://www.navicat.com.cn/whatisnavicat
Navicat是一套快速、可靠和全面的數據庫管理工具,專門用於簡化數據庫管理和降低管理成本。Navicat 的直觀圖形用戶界面,提供簡單的方法管理,設計和操作MySQL、MariaDB、SQL Server、Oracle、PostgreSQL和 SQLite的數據。
在遇到Navicat之前,開發軟件常用的數據庫管理工具有:
(1)MySQL
phpMyAdmin,官網:https://www.phpmyadmin.net/
MySQL Workbench,官網:http://dev.mysql.com/downloads/workbench/
(2)Orace
PL/SQL Developer,官網:https://www.plsqldev.com/
PL/SQL Developer是一個集成開發環境,由Allround Automations公司開發,專門面向Oracle數據庫存儲的程序單元的開發。
(3)SQL Server
SQL Server Management Studio 是一個集成環境,用於訪問、配置、管理和開發 SQL Server 的所有組件。SQL Server Management Studio 組合了大量圖形工具和豐富的腳本編輯器,使各種技術水平的開發人員和管理員都能訪問 SQL Server。
前面侃了很多廢話,言歸正傳,正式進入正題:SQL優化技巧。
1.查詢索引優化:
①-⑤條測試中使用的SQL基於Oracle數據庫。
① 查詢出年是2015的所有行:表字段放到函數里執行查詢時,索引將不起作用。
CREATE INDEX tb1_idx ON tb1 (date_column); SELECT text_column1, date_column FROM tb1 WHERE date_column >= TO_DATE ('2015-01-01', 'YYYY-MM-DD') AND date_column < TO_DATE ('2016-01-01', 'YYYY-MM-DD');
② 查詢出最近日期的一行數據:
CREATE INDEX tb1_idx ON tb1 (a, date_column); SELECT * FROM ( SELECT id, text_column1, date_column FROM tb1 WHERE a =: a ORDER BY date_column DESC ) WHERE rownum < = 1;
這條SQL語句將會按照經過索引的 Top-N 查詢方式執行,它的效率跟INDEX UNIQUE SCAN是等效的。
③ 兩個查詢語句,通過一個普通列查詢:
CREATE INDEX tb1_idx ON tb1 (a, b); SELECT id, a, b FROM tb1 WHERE a =: a AND b =: b; SELECT id, a, b FROM tb1 WHERE b =: b;
建立的索引只能用於第一個查詢,第二個SQL無法利用索引提高效率。
④ 查詢一個字符串:
CREATE INDEX tb1_idx ON tb1 (text_column1); SELECT id, text_column1 FROM tb1 WHERE text_column1 LIKE '%TermStr%';
LIKE對應的查詢字符如果是以通配符開頭的,索引將無法發揮效能。也沒有一個簡單的方法來優化這種SQL。
⑤ 查詢某條件下的記錄數:
CREATE INDEX tb1_idx ON tb1 (a, date_column); SELECT date_column, count(*) FROM tb1 WHERE a= :a GROUP BY date_column; SELECT date_column, count(*) FROM tb1 WHERE a = :a AND b = :b GROUP BY date_column;
上面兩條查詢語句,第一條可能會查出幾千或者幾萬條記錄,而第二條語句因為多了一個條件可能只查出幾條或幾十條記錄,也許大家會認為第二條語句的效率更快。其實剛好相反,第一條語句的執行效率更快。因為第一條語句中,索引覆蓋了所有查詢字段,而第二個SQL中的b條件沒有索引。
2.分頁性能優化:
以下測試中使用的SQL基於MySQL數據庫。
① 高效的計算行數:
如果采用的引擎是MyISAM,可以直接執行COUNT(*)去獲取行數即可。相似的,在堆表中也會將行數存儲到表的元信息中。但如果引擎是InnoDB情況就會復雜一些,因為InnoDB不保存表的具體行數。可以將行數緩存起來,然后可以通過一個守護進程定期更新或者用戶的某些操作導致緩存失效時,執行下面的語句:
SELECT COUNT(*) FROM test USE INDEX(PRIMARY);
我的一個測試實例:
offset(分頁偏移量)很大時,像下面這樣:
SELECT vendorcode, vendorname FROM dm_vendor_performance_mx_v LIMIT 10000000,20
大的分頁偏移量會增加使用的數據,MySQL會將大量最終不會使用的數據加載到內存中。就算我們假設大部分網站的用戶只訪問前幾頁數據,但少量的大的分頁偏移量的請求也會對整個系統造成危害。Facebook意識到了這一點,但Facebook並沒有為了每秒可以處理更多的請求而去優化數據庫,而是將重心放在將請求響應時間的方差變小。
② 獲取記錄:
按照實時性排序(最新發布的在最前面,即Id最大的在最前面),實現一個高性能的分頁。
一個比較高效的方式是基於要查詢的最大Id。查詢下一頁的語句如下,需要傳入當前頁面展示的最后一個Id。
SELECT id, vendorcode, perioddate, materialcode FROM dm_vendor_performance_mx_v WHERE id < 1000000 ORDER BY id DESC LIMIT 20
查詢上一頁的語句類似,只不過需要傳入當前頁的第一個Id,並且要逆序。
SELECT id, vendorcode, perioddate, materialcode FROM dm_vendor_performance_mx_v WHERE id > 1500000 ORDER BY id DESC LIMIT 20
上面的查詢方式適合實現簡易的分頁,即不顯示具體的頁數導航,只顯示“上一頁”和“下一頁”,例如博客中頁腳顯示“上一頁”,“下一頁”的按鈕。但如果要實現真正的頁面導航還是很難的,下面看看另一種方式。
如果表中的記錄很少被刪除、修改,還可以將記錄對應的頁碼存儲到表中,並在該列上創建合適的索引。采用這種方式,當新增一個記錄的時候,需要執行下面的查詢重新生成對應的頁號。
SET p:= 0; UPDATE test SET page=CEIL((p:= p + 1) / $perpage) ORDER BY id DESC;
當然,也可以新增一個專用於分頁的表,可以用個后台程序來維護。
UPDATE pagination T JOIN ( SELECT id, CEIL((p:= p + 1) / $perpage) page FROM test ORDER BY id )C ON C.id = T.id SET T.page = C.page;
現在想獲取任意一頁的元素就很簡單了:
SELECT * FROM test A JOIN pagination B ON A.id=B.ID WHERE page=$offset;
SQL優化還有很多技巧,我在這里也只是班門弄斧,和資深的DBA比起來還差十萬八千里。
以下是我推薦的一些SQL優化的文章:
(1)MySQL知識分享網站:http://ourmysql.com/archives/category/optimize
(2)Sql養成一個好習慣是一筆財富:http://www.cnblogs.com/MR_ke/archive/2011/05/29/2062085.html
(3)MySQL查詢語句執行過程:http://shanks.leanote.com/post/MySQL%E6%9F%A5%E8%AF%A2%E8%BF%87%E7%A8%8B
(4)MySQL分頁性能優化指南:http://www.codeceo.com/article/mysql-page-performance.html
(5)21條最佳MySQL性能優化:http://www.phpxs.com/post/5092/
(6)100+個MySQL調試和優化技巧:http://mp.weixin.qq.com/s?__biz=MzAwMDM2NzUxMg==&mid=2247484514&idx=1&sn=2cb4246bbf991186eb08aeacd71b2893&scene=21#wechat_redirect