mysql 介紹
MySQL 是一種關係型數據庫,在Java企業級開發中非常常用,因爲 MySQL 是開源免費的,並且方便擴展。阿里巴巴數據庫系統也大量用到了 MySQL,因此它的穩定性是有保障的。MySQL是開放源代碼的,因此任何人都可以在 GPL(General Public License) 的許可下下載並根據個性化的需要對其進行修改。MySQL的默認端口號是3306
事務
-
原子性: 事務是最小的執行單位,不允許分割。事務的原子性確保動作要麼全部完成,要麼完全不起作用;
-
一致性: 執行事務前後,數據保持一致,多個事務對同一個數據讀取的結果是相同的;
-
隔離性: 併發訪問數據庫時,一個用戶的事務不被其他事務所幹擾,各併發事務之間數據庫是獨立的;
-
持久性: 一個事務被提交之後。它對數據庫中數據的改變是持久的,即使數據庫發生故障也不應該對其有任何影響。
併發事務帶來哪些問題?
在典型的應用程序中,多個事務併發運行,經常會操作相同的數據來完成各自的任務(多個用戶對統一數據進行操作)。併發雖然是必須的,但可能會導致以下的問題:
-
髒讀(Dirty read): 當一個事務正在訪問數據並且對數據進行了修改,而這種修改還沒有提交到數據庫中,這時另外一個事務也訪問了這個數據,然後使用了這個數據。因爲這個數據是還沒有提交的數據,那麼另外一個事務讀到的這個數據是“髒數據”,依據“髒數據”所做的操作可能是不正確的。
-
丟失修改(Lost to modify): 指在一個事務讀取一個數據時,另外一個事務也訪問了該數據,那麼在第一個事務中修改了這個數據後,第二個事務也修改了這個數據。這樣第一個事務內的修改結果就被丟失,因此稱爲丟失修改。例如:事務1讀取某表中的數據A=20,事務2也讀取A=20,事務1修改A=A-1,事務2也修改A=A-1,最終結果A=19,事務1的修改被丟失。
-
不可重複讀(Unrepeatableread): 指在一個事務內多次讀同一數據。在這個事務還沒有結束時,另一個事務也訪問該數據。那麼,在第一個事務中的兩次讀數據之間,由於第二個事務的修改導致第一個事務兩次讀取的數據可能不太一樣。這就發生了在一個事務內兩次讀到的數據是不一樣的情況,因此稱爲不可重複讀。
-
幻讀(Phantom read): 幻讀與不可重複讀類似。它發生在一個事務(T1)讀取了幾行數據,接着另一個併發事務(T2)插入了一些數據時。在隨後的查詢中,第一個事務(T1)就會發現多了一些原本不存在的記錄,就好像發生了幻覺一樣,所以稱爲幻讀。
例1(同樣的條件, 你讀取過的數據, 再次讀取出來發現值不一樣了 ):事務1中的A先生讀取自己的工資爲 1000的操作還沒完成,事務2中的B先生就修改了A的工資爲2000,導 致A再讀自己的工資時工資變爲 2000;這就是不可重複讀。
例2(同樣的條件, 第1次和第2次讀出來的記錄數不一樣 ):假某工資單表中工資大於3000的有4人,事務1讀取了所有工資大於3000的人,共查到4條記錄,這時事務2 又插入了一條工資大於3000的記錄,事務1再次讀取時查到的記錄就變爲了5條,這樣就導致了幻讀。
事務隔離級別有哪些?MySQL的默認隔離級別是?
SQL 標準定義了四個隔離級別:
READ-UNCOMMITTED(讀取未提交): 最低的隔離級別,允許讀取尚未提交的數據變更,可能會導致髒讀、幻讀或不可重複讀。
READ-COMMITTED(讀取已提交): 允許讀取併發事務已經提交的數據,可以阻止髒讀,但是幻讀或不可重複讀仍有可能發生。
REPEATABLE-READ(可重複讀): 對同一字段的多次讀取結果都是一致的,除非數據是被本身事務自己所修改,可以阻止髒讀和不可重複讀,但幻讀仍有可能發生。
SERIALIZABLE(可串行化): 最高的隔離級別,完全服從ACID的隔離級別。所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止髒讀、不可重複讀以及幻讀。
隔離級別 | 髒讀 | 不可重複讀 | 幻影讀 |
---|---|---|---|
READ-UNCOMMITTED | √ | √ | √ |
READ-COMMITTED | × | √ | √ |
REPEATABLE-READ | × | × | √ |
SERIALIZABLE | × | × | × |
MySQL InnoDB 存儲引擎的默認支持的隔離級別是 REPEATABLE-READ(可重讀)。我們可以通過SELECT @@tx_isolation;命令來查看。
MyISAM和InnoDB區別
MyISAM是MySQL的默認數據庫引擎(5.5版之前)。雖然性能極佳,而且提供了大量的特性,包括全文索引、壓縮、空間函數等,但MyISAM不支持事務和行級鎖,而且最大的缺陷就是崩潰後無法安全恢復。不過,5.5版本之後,MySQL引入了InnoDB(事務性數據庫引擎),MySQL 5.5版本後默認的存儲引擎爲InnoDB。
大多數時候我們使用的都是 InnoDB 存儲引擎,但是在某些情況下使用 MyISAM 也是合適的比如讀密集的情況下。(如果你不介意 MyISAM 崩潰回覆問題的話)。
兩者的對比:
-
是否支持行級鎖 : MyISAM 只有表級鎖(table-level locking),而InnoDB 支持行級鎖(row-level locking)和表級鎖,默認爲行級鎖。
-
是否支持事務和崩潰後的安全恢復:MyISAM 強調的是性能,每次查詢具有原子性,其執行比InnoDB類型更快,但是不提供事務支持。但是InnoDB 提供事務支持事務,外部鍵等高級數據庫功能。具有事務(commit)、回滾(rollback)和崩潰修復能力(crash recovery capabilities)的事務安全(transaction-safe (ACID compliant))型表。
-
是否支持外鍵: MyISAM不支持,而InnoDB支持。
-
是否支持MVCC :僅 InnoDB 支持。應對高併發事務, MVCC比單純的加鎖更高效;MVCC只在 READ COMMITTED 和 REPEATABLE READ 兩個隔離級別下工作;MVCC可以使用 樂觀(optimistic)鎖 和 悲觀(pessimistic)鎖來實現;各數據庫中MVCC實現並不統一。
-
InnoDB不保存表的具體行數,執行select count(*) from table時需要全表掃描。而MyISAM用一個變量保存了整個表的行數,執行上述語句時只需要讀出該變量即可,速度很快;
-
Innodb不支持全文索引,而MyISAM支持全文索引,查詢效率上MyISAM要高;
大表優化
當MySQL單表記錄數過大時,數據庫的CRUD性能會明顯下降,一些常見的優化措施如下:
-
限定數據的範圍
務必禁止不帶任何限制數據範圍條件的查詢語句。比如:我們當用戶在查詢訂單歷史的時候,我們可以控制在一個月的範圍內; -
讀/寫分離
經典的數據庫拆分方案,主庫負責寫,從庫負責讀; -
垂直分區
根據數據庫裏面數據表的相關性進行拆分。 例如,用戶表中既有用戶的登錄信息又有用戶的基本信息,可以將用戶表拆分成兩個單獨的表,甚至放到單獨的庫做分庫。
簡單來說垂直拆分是指數據表列的拆分,把一張列比較多的表拆分爲多張表。 如下圖所示,這樣來說大家應該就更容易理解了。
垂直拆分的優點: 可以使得列數據變小,在查詢時減少讀取的Block數,減少I/O次數。此外,垂直分區可以簡化表的結構,易於維護。
垂直拆分的缺點: 主鍵會出現冗餘,需要管理冗餘列,並會引起Join操作,可以通過在應用層進行Join來解決。此外,垂直分區會讓事務變得更加複雜;
- 水平分區
保持數據表結構不變,通過某種策略存儲數據分片。這樣每一片數據分散到不同的表或者庫中,達到了分佈式的目的。水平拆分可以支撐非常大的數據量。
水平拆分是指數據錶行的拆分,表的行數超過200萬行時,就會變慢,這時可以把一張的表的數據拆成多張表來存放。舉個例子:我們可以將用戶信息表拆分成多個用戶信息表,這樣就可以避免單一表數據量過大對性能造成影響。
數據庫基本設計規範
- 所有表必須使用Innodb存儲引擎
沒有特殊要求(即Innodb無法滿足的功能如:列存儲,存儲空間數據等)的情況下,所有表必須使用Innodb存儲引擎(mysql5.5之前默認使用Myisam,5.6以後默認的爲Innodb)。
Innodb 支持事務,支持行級鎖,更好的恢復性,高併發下性能更好。
-
數據庫和表的字符集統一使用UTF8
兼容性更好,統一字符集可以避免由於字符集轉換產生的亂碼,不同的字符集進行比較前需要進行轉換會造成索引失效,如果數據庫中有存儲emoji表情的需要,字符集需要採用utf8mb4字符集。 -
所有表和字段都需要添加註釋
使用comment從句添加表和列的備註,從一開始就進行數據字典的維護 -
儘量控制單表數據量的大小,建議控制在500萬以內。
500萬並不是Mysql數據庫的限制,過大會造成修改表結構,備份,恢復都會有很大的問題。
可以用歷史數據歸檔(應用於日誌數據),分庫分表(應用於業務數據)等手段來控制數據量大小
- 謹慎使用Mysql分區表
分區表在物理上表現爲多個文件,在邏輯上表現爲一個表;
謹慎選擇分區鍵,跨分區查詢效率可能更低;
建議採用物理分表的方式管理大數據。
- 儘量做到冷熱數據分離,減小表的寬度
Mysql限制每個表最多存儲4096列,並且每一行數據的大小不能超過65535字節。
減少磁盤IO,保證熱數據的內存緩存命中率(表越寬,把表裝載進內存緩衝池時所佔用的內存也就越大,也會消耗更多的IO);
更有效的利用緩存,避免讀入無用的冷數據;
經常一起使用的列放到一個表中(避免更多的關聯操作)。
- 禁止在表中建立預留字段
預留字段的命名很難做到見名識義。
預留字段無法確認存儲的數據類型,所以無法選擇合適的類型。
對預留字段類型的修改,會對錶進行鎖定。
- 禁止在數據庫中存儲圖片,文件等大的二進制數據
通常文件很大,會短時間內造成數據量快速增長,數據庫進行數據庫讀取時,通常會進行大量的隨機IO操作,文件很大時,IO操作很耗時。
通常存儲於文件服務器,數據庫只存儲文件地址信息
- 禁止在線上做數據庫壓力測試
- 禁止從開發環境,測試環境直接連接生產環境數據庫
數據庫字段設計規範
- 優先選擇符合存儲需要的最小的數據類型
原因:
列的字段越大,建立索引時所需要的空間也就越大,這樣一頁中所能存儲的索引節點的數量也就越少也越少,在遍歷時所需要的IO次數也就越多,索引的性能也就越差。
方法:
1、將字符串轉換成數字類型存儲,如:將IP地址轉換成整形數據
mysql提供了兩個方法來處理ip地址
inet_aton 把ip轉爲無符號整型(4-8位)
inet_ntoa 把整型的ip轉爲地址
插入數據前,先用inet_aton把ip地址轉爲整型,可以節省空間,顯示數據時,使用inet_ntoa把整型的ip地址轉爲地址顯示即可。
2、對於非負型的數據(如自增ID、整型IP)來說,要優先使用無符號整型來存儲
原因:
無符號相對於有符號可以多出一倍的存儲空間
SIGNED INT -2147483648~2147483647
UNSIGNED INT 0~4294967295
VARCHAR(N)中的N代表的是字符數,而不是字節數,使用UTF8存儲255個漢字 Varchar(255)=765個字節。過大的長度會消耗更多的內存。
- 避免使用TEXT、BLOB數據類型,最常見的TEXT類型可以存儲64k的數據
1、建議把BLOB或是TEXT列分離到單獨的擴展表中
Mysql內存臨時表不支持TEXT、BLOB這樣的大數據類型,如果查詢中包含這樣的數據,在排序等操作時,就不能使用內存臨時表,必須使用磁盤臨時表進行。而且對於這種數據,Mysql還是要進行二次查詢,會使sql性能變得很差,但是不是說一定不能使用這樣的數據類型。
如果一定要使用,建議把BLOB或是TEXT列分離到單獨的擴展表中,查詢時一定不要使用select * 而只需要取出必要的列,不需要TEXT列的數據時不要對該列進行查詢。
2、TEXT或BLOB類型只能使用前綴索引
因爲MySQL對索引字段長度是有限制的,所以TEXT類型只能使用前綴索引,並且TEXT列上是不能有默認值的
- 避免使用ENUM類型
修改ENUM值需要使用ALTER語句
ENUM類型的ORDER BY操作效率低,需要額外操作
禁止使用數值作爲ENUM的枚舉值
- 儘可能把所有列定義爲NOT NULL
原因:
索引NULL列需要額外的空間來保存,所以要佔用更多的空間
進行比較和計算時要對NULL值做特別的處理
- 使用TIMESTAMP(4個字節)或DATETIME類型(8個字節)存儲時間
TIMESTAMP 存儲的時間範圍 1970-01-01 00:00:01 ~ 2038-01-19-03:14:07
TIMESTAMP 佔用4字節和INT相同,但比INT可讀性高
超出TIMESTAMP取值範圍的使用DATETIME類型存儲
經常會有人用字符串存儲日期型的數據(不正確的做法)
缺點1:無法用日期函數進行計算和比較
缺點2:用字符串存儲日期要佔用更多的空間
- 同財務相關的金額類數據必須使用decimal類型
非精準浮點:float,double
精準浮點:decimal
Decimal類型爲精準浮點數,在計算時不會丟失精度
佔用空間由定義的寬度決定,每4個字節可以存儲9位數字,並且小數點要佔用一個字節
索引設計規範
- 限制每張表上的索引數量,建議單張表索引不超過5個
索引並不是越多越好!索引可以提高效率同樣可以降低效率。
索引可以增加查詢效率,但同樣也會降低插入和更新的效率,甚至有些情況下會降低查詢效率。
因爲mysql優化器在選擇如何優化查詢時,會根據統一信息,對每一個可以用到的索引來進行評估,以生成出一個最好的執行計劃,如果同時有很多個索引都可以用於查詢,就會增加mysql優化器生成執行計劃的時間,同樣會降低查詢性能。
-
禁止給表中的每一列都建立單獨的索引
5.6版本之前,一個sql只能使用到一個表中的一個索引,5.6以後,雖然有了合併索引的優化方式,但是還是遠遠沒有使用一個聯合索引的查詢方式好。 -
每個Innodb表必須有個主鍵
Innodb是一種索引組織表:數據的存儲的邏輯順序和索引的順序是相同的。每個表都可以有多個索引,但是表的存儲順序只能有一種。
Innodb是按照主鍵索引的順序來組織表的
不要使用更新頻繁的列作爲主鍵,不適用多列主鍵(相當於聯合索引)
不要使用UUID,MD5,HASH,字符串列作爲主鍵(無法保證數據的順序增長)
主鍵建議使用自增ID值
常見索引列建議
-
出現在SELECT、UPDATE、DELETE語句的WHERE從句中的列
-
包含在ORDER BY、GROUP BY、DISTINCT中的字段
-
並不要將符合1和2中的字段的列都建立一個索引, 通常將1、2中的字段建立聯合索引效果更好
-
多表join的關聯列
如何選擇索引列的順序
建立索引的目的是:希望通過索引進行數據查找,減少隨機IO,增加查詢性能 ,索引能過濾出越少的數據,則從磁盤中讀入的數據也就越少。
-
區分度最高的放在聯合索引的最左側(區分度=列中不同值的數量/列的總行數)
-
儘量把字段長度小的列放在聯合索引的最左側(因爲字段長度越小,一頁能存儲的數據量越大,IO性能也就越好)
-
使用最頻繁的列放到聯合索引的左側(這樣可以比較少的建立一些索引)
對於頻繁的查詢優先考慮使用覆蓋索引
覆蓋索引:就是包含了所有查詢字段(where,select,ordery by,group by包含的字段)的索引
索引SET規範
儘量避免使用外鍵約束
不建議使用外鍵約束(foreign key),但一定要在表與表之間的關聯鍵上建立索引
外鍵可用於保證數據的參照完整性,但建議在業務端實現
外鍵會影響父表和子表的寫操作從而降低性能
數據庫SQL開發規範
- 建議使用預編譯語句進行數據庫操作
預編譯語句可以重複使用這些計劃,減少SQL編譯所需要的時間,還可以解決動態SQL所帶來的SQL注入的問題。
只傳參數,比傳遞SQL語句更高效。
相同語句可以一次解析,多次使用,提高處理效率。
- 避免數據類型的隱式轉換
隱式轉換會導致索引失效如:
select name,phone from customer where id = ‘111’;
3. 充分利用表上已經存在的索引
避免使用雙%號的查詢條件。如:a like ‘%123%’,(如果無前置%,只有後置%,是可以用到列上的索引的)
一個SQL只能利用到複合索引中的一列進行範圍查詢。如:有 a,b,c列的聯合索引,在查詢條件中有a列的範圍查詢,則在b,c列上的索引將不會被用到。
在定義聯合索引時,如果a列要用到範圍查找的話,就要把a列放到聯合索引的右側,使用left join 或 not exists 來優化not in 操作,因爲not in 也通常會使用索引失效。
- 數據庫設計時,應該要對以後擴展進行考慮
- 程序連接不同的數據庫使用不同的賬號,進制跨庫查詢
爲數據庫遷移和分庫分表留出餘地
降低業務耦合度
避免權限過大而產生的安全風險
- 禁止使用SELECT * 必須使用SELECT <字段列表> 查詢
原因:
消耗更多的CPU和IO以網絡帶寬資源
無法使用覆蓋索引
可減少表結構變更帶來的影響
- 禁止使用不含字段列表的INSERT語句
如:
insert into values (‘a’,‘b’,‘c’);
應使用:
insert into t(c1,c2,c3) values (‘a’,‘b’,‘c’);
8. 避免使用子查詢,可以把子查詢優化爲join操作
通常子查詢在in子句中,且子查詢中爲簡單SQL(不包含union、group by、order by、limit從句)時,纔可以把子查詢轉化爲關聯查詢進行優化。
子查詢性能差的原因:
子查詢的結果集無法使用索引,通常子查詢的結果集會被存儲到臨時表中,不論是內存臨時表還是磁盤臨時表都不會存在索引,所以查詢性能會受到一定的影響。特別是對於返回結果集比較大的子查詢,其對查詢性能的影響也就越大。
由於子查詢會產生大量的臨時表也沒有索引,所以會消耗過多的CPU和IO資源,產生大量的慢查詢。
- 避免使用JOIN關聯太多的表
對於Mysql來說,是存在關聯緩存的,緩存的大小可以由join_buffer_size參數進行設置。
在Mysql中,對於同一個SQL多關聯(join)一個表,就會多分配一個關聯緩存,如果在一個SQL中關聯的表越多,所佔用的內存也就越大。
如果程序中大量的使用了多表關聯的操作,同時join_buffer_size設置的也不合理的情況下,就容易造成服務器內存溢出的情況,就會影響到服務器數據庫性能的穩定性。
同時對於關聯操作來說,會產生臨時表操作,影響查詢效率,Mysql最多允許關聯61個表,建議不超過5個。
-
減少同數據庫的交互次數
數據庫更適合處理批量操作,合併多個相同的操作到一起,可以提高處理效率。 -
對應同一列進行or判斷時,使用in代替or
in 的值不要超過500個,in 操作可以更有效的利用索引,or大多數情況下很少能利用到索引。 -
禁止使用order by rand() 進行隨機排序
order by rand()會把表中所有符合條件的數據裝載到內存中,然後在內存中對所有數據根據隨機生成的值進行排序,並且可能會對每一行都生成一個隨機值,如果滿足條件的數據集非常大,就會消耗大量的CPU和IO及內存資源。
推薦在程序中獲取一個隨機值,然後從數據庫中獲取數據的方式。
- WHERE從句中禁止對列進行函數轉換和計算
對列進行函數轉換或計算時會導致無法使用索引
不推薦:
where date(create_time)=‘20190101’
推薦:
where create_time >= ‘20190101’ and create_time < ‘20190102’
14. 在明顯不會有重複值時使用UNION ALL 而不是UNION
UNION 會把兩個結果集的所有數據放到臨時表中後再進行去重操作
UNION ALL 不會再對結果集進行去重操作
- 拆分複雜的大SQL爲多個小SQL
大SQL邏輯上比較複雜,需要佔用大量CPU進行計算的SQL
MySQL中,一個SQL只能使用一個CPU進行計算
SQL拆分後可以通過並行執行來提高處理效率
1、LIMIT 語句
分頁查詢是最常用的場景之一,但也通常也是最容易出問題的地方。比如對於下面簡單的語句,一般 DBA 想到的辦法是在 type, name, create_time 字段上加組合索引。這樣條件排序都能有效的利用到索引,性能迅速提升。
SELECT *
FROM operation
WHERE type = 'SQLStats'
AND name = 'SlowLog'
ORDER BY create_time
LIMIT 1000, 10;
好吧,可能90%以上的 DBA 解決該問題就到此爲止。但當 LIMIT 子句變成 “LIMIT 1000000,10” 時,程序員仍然會抱怨:我只取10條記錄爲什麼還是慢?
要知道數據庫也並不知道第1000000條記錄從什麼地方開始,即使有索引也需要從頭計算一次。出現這種性能問題,多數情形下是程序員偷懶了。
在前端數據瀏覽翻頁,或者大數據分批導出等場景下,是可以將上一頁的最大值當成參數作爲查詢條件的。SQL 重新設計如下:
SELECT *
FROM operation
WHERE type = 'SQLStats'
AND name = 'SlowLog'
AND create_time > '2017-03-16 14:00:00'
ORDER BY create_time limit 10;
在新設計下查詢時間基本固定,不會隨着數據量的增長而發生變化。
2.隱式轉換
SQL語句中查詢變量和字段定義類型不匹配是另一個常見的錯誤。比如下面的語句:
ql> explain extended SELECT *
> FROM my_balance b
> WHERE b.bpn = 14000000123
> AND b.isverified IS NULL ;
mysql> show warnings;
| Warning | 1739 | Cannot use ref access on index 'bpn' due to type or collation conversion on field 'bpn'
其中字段 bpn 的定義爲 varchar(20),MySQL 的策略是將字符串轉換爲數字之後再比較。函數作用於表字段,索引失效。
上述情況可能是應用程序框架自動填入的參數,而不是程序員的原意。現在應用框架很多很繁雜,使用方便的同時也小心它可能給自己挖坑。
關聯更新、刪除
雖然 MySQL5.6 引入了物化特性,但需要特別注意它目前僅僅針對查詢語句的優化。對於更新或刪除需要手工重寫成 JOIN。
比如下面 UPDATE 語句,MySQL 實際執行的是循環/嵌套子查詢(DEPENDENT SUBQUERY),其執行時間可想而知。
UPDATE operation o
SET status = 'applying'
WHERE o.id IN (SELECT id
FROM (SELECT o.id,
o.status
FROM operation o
WHERE o.group = 123
AND o.status NOT IN ( 'done' )
ORDER BY o.parent,
o.id
LIMIT 1) t);
重寫爲 JOIN 之後,子查詢的選擇模式從 DEPENDENT SUBQUERY 變成 DERIVED,執行速度大大加快,從7秒降低到2毫秒。
UPDATE operation o
JOIN (SELECT o.id,
o.status
FROM operation o
WHERE o.group = 123
AND o.status NOT IN ( 'done' )
ORDER BY o.parent,
o.id
LIMIT 1) t
ON o.id = t.id
SET status = 'applying'
混合排序
SELECT *
FROM my_order o
INNER JOIN my_appraise a ON a.orderid = o.id
ORDER BY a.is_reply ASC,
a.appraise_time DESC
LIMIT 0, 20
由於 is_reply 只有0和1兩種狀態,我們按照下面的方法重寫後,執行時間從1.58秒降低到2毫秒。
SELECT *
FROM ((SELECT *
FROM my_order o
INNER JOIN my_appraise a
ON a.orderid = o.id
AND is_reply = 0
ORDER BY appraise_time DESC
LIMIT 0, 20)
UNION ALL
(SELECT *
FROM my_order o
INNER JOIN my_appraise a
ON a.orderid = o.id
AND is_reply = 1
ORDER BY appraise_time DESC
LIMIT 0, 20)) t
ORDER BY is_reply ASC,
appraisetime DESC
LIMIT 20;
EXISTS語句
SELECT *
FROM my_neighbor n
LEFT JOIN my_neighbor_apply sra
ON n.id = sra.neighbor_id
AND sra.user_id = 'xxx'
WHERE n.topic_status < 4
AND EXISTS(SELECT 1
FROM message_info m
WHERE n.id = m.neighbor_id
AND m.inuser = 'xxx')
AND n.topic_type <> 5
去掉 exists 更改爲 join,能夠避免嵌套子查詢,將執行時間從1.93秒降低爲1毫秒。
SELECT *
FROM my_neighbor n
INNER JOIN message_info m
ON n.id = m.neighbor_id
AND m.inuser = 'xxx'
LEFT JOIN my_neighbor_apply sra
ON n.id = sra.neighbor_id
AND sra.user_id = 'xxx'
WHERE n.topic_status < 4
AND n.topic_type <> 5
提前縮小範圍
SELECT *
FROM my_order o
LEFT JOIN my_userinfo u
ON o.uid = u.uid
LEFT JOIN my_productinfo p
ON o.pid = p.pid
WHERE ( o.display = 0 )
AND ( o.ostaus = 1 )
ORDER BY o.selltime DESC
LIMIT 0, 15
該SQL語句原意是:先做一系列的左連接,然後排序取前15條記錄。從執行計劃也可以看出,最後一步估算排序記錄數爲90萬,時間消耗爲12秒。
由於最後 WHERE 條件以及排序均針對最左主表,因此可以先對 my_order 排序提前縮小數據量再做左連接。SQL 重寫後如下,執行時間縮小爲1毫秒左右。
SELECT *
FROM (
SELECT *
FROM my_order o
WHERE ( o.display = 0 )
AND ( o.ostaus = 1 )
ORDER BY o.selltime DESC
LIMIT 0, 15
) o
LEFT JOIN my_userinfo u
ON o.uid = u.uid
LEFT JOIN my_productinfo p
ON o.pid = p.pid
ORDER BY o.selltime DESC
limit 0, 15
再檢查執行計劃:子查詢物化後(select_type=DERIVED)參與 JOIN。雖然估算行掃描仍然爲90萬,但是利用了索引以及 LIMIT 子句後,實際執行時間變得很小。
中間結果集下推
SELECT a.*,
c.allocated
FROM (
SELECT resourceid
FROM my_distribute d
WHERE isdelete = 0
AND cusmanagercode = '1234567'
ORDER BY salecode limit 20) a
LEFT JOIN
(
SELECT resourcesid, sum(ifnull(allocation, 0) * 12345) allocated
FROM my_resources
GROUP BY resourcesid) c
ON a.resourceid = c.resourcesid
那麼該語句還存在其它問題嗎?不難看出子查詢 c 是全表聚合查詢,在表數量特別大的情況下會導致整個語句的性能下降。
其實對於子查詢 c,左連接最後結果集只關心能和主表 resourceid 能匹配的數據。因此我們可以重寫語句如下,執行時間從原來的2秒下降到2毫秒。
SELECT a.*,
c.allocated
FROM (
SELECT resourceid
FROM my_distribute d
WHERE isdelete = 0
AND cusmanagercode = '1234567'
ORDER BY salecode limit 20) a
LEFT JOIN
(
SELECT resourcesid, sum(ifnull(allocation, 0) * 12345) allocated
FROM my_resources r,
(
SELECT resourceid
FROM my_distribute d
WHERE isdelete = 0
AND cusmanagercode = '1234567'
ORDER BY salecode limit 20) a
WHERE r.resourcesid = a.resourcesid
GROUP BY resourcesid) c
ON a.resourceid = c.resourcesid
但是子查詢 a 在我們的SQL語句中出現了多次。這種寫法不僅存在額外的開銷,還使得整個語句顯的繁雜。使用 WITH 語句再次重寫:
WITH a AS
(
SELECT resourceid
FROM my_distribute d
WHERE isdelete = 0
AND cusmanagercode = '1234567'
ORDER BY salecode limit 20)
SELECT a.*,
c.allocated
FROM a
LEFT JOIN
(
SELECT resourcesid, sum(ifnull(allocation, 0) * 12345) allocated
FROM my_resources r,
a
WHERE r.resourcesid = a.resourcesid
GROUP BY resourcesid) c
ON a.resourceid = c.resourcesid
爲了對下面列舉的一些優化進行測試,下面針對已有的一張表進行說明。
表名:order_history
描述:某個業務的訂單歷史表
主要字段:unsigned int id,tinyint(4) int type
字段情況:該表一共37個字段,不包含text等大型數據,最大爲varchar(500),id字段爲索引,且爲遞增。
數據量:5709294
MySQL版本:5.7.16 線下找一張百萬級的測試表可不容易,如果需要自己測試的話,可以寫shell腳本什麼的插入數據進行測試。 以下的 sql 所有語句執行的環境沒有發生改變,下面是基本測試結果:
select
count(*)
from
orders_history;
返回結果:5709294
三次查詢時間分別爲:
8903 ms
8323 ms
8401 ms
一般分頁查詢
一般的分頁查詢使用簡單的 limit 子句就可以實現。limit 子句聲明如下:
SELECT * FROM table LIMIT [offset,] rows | rows OFFSET offset
LIMIT 子句可以被用於指定 SELECT 語句返回的記錄數。需注意以下幾點:
第一個參數指定第一個返回記錄行的偏移量,注意從 0開始
第二個參數指定返回記錄行的最大數目
如果只給定一個參數:它表示返回最大的記錄行數目
第二個參數爲 -1 表示檢索從某一個偏移量到記錄集的結束所有的記錄行
初始記錄行的偏移量是 0(而不是 1)
下面是一個應用實例:
select
*
from
orders_history
where
type=8
limit 1000,10;
該條語句將會從表 orders_history 中查詢 offset:1000開始之後的10條數據,也就是第1001條到第1010條數據( 1001<=id<=1010)。
數據表中的記錄默認使用主鍵(一般爲id)排序,上面的結果相當於:
select
*
from
orders_history
where
type=8
order
by
id limit 10000,10;
三次查詢時間分別爲:
3040 ms
3063 ms
3018 ms
針對這種查詢方式,下面測試查詢記錄量對時間的影響:
select * from orders_history where type=8 limit 10000,1;
select * from orders_history where type=8 limit 10000,10;
select * from orders_history where type=8 limit 10000,100;
select * from orders_history where type=8 limit 10000,1000;
select * from orders_history where type=8 limit 10000,10000;
三次查詢時間如下:
查詢1條記錄:3072ms 3092ms 3002ms
查詢10條記錄:3081ms 3077ms 3032ms
查詢100條記錄:3118ms 3200ms 3128ms
查詢1000條記錄:3412ms 3468ms 3394ms
查詢10000條記錄:3749ms 3802ms 3696ms
另外我還做了十來次查詢,從查詢時間來看,基本可以確定,在查詢記錄量低於100時,查詢時間基本沒有差距,隨着查詢記錄量越來越大,所花費的時間也會越來越多。
針對查詢偏移量的測試:
select * from orders_history where type=8 limit 100,100;
select * from orders_history where type=8 limit 1000,100;
select * from orders_history where type=8 limit 10000,100;
select * from orders_history where type=8 limit 100000,100;
select * from orders_history where type=8 limit 1000000,100;
三次查詢時間如下:
查詢100偏移:25ms 24ms 24ms
查詢1000偏移:78ms 76ms 77ms
查詢10000偏移:3092ms 3212ms 3128ms
查詢100000偏移:3878ms 3812ms 3798ms
查詢1000000偏移:14608ms 14062ms 14700ms
隨着查詢偏移的增大,尤其查詢偏移大於10萬以後,查詢時間急劇增加。
這種分頁查詢方式會從數據庫第一條記錄開始掃描,所以越往後,查詢速度越慢,而且查詢的數據越多,也會拖慢總查詢速度。
使用子查詢優化
這種方式先定位偏移位置的 id,然後往後查詢,這種方式適用於 id 遞增的情況。
select * from orders_history where type=8 limit 100000,1;
select id from orders_history where type=8 limit 100000,1;
select * from orders_history where type=8 and id>=(select id from orders_history where type=8 limit 100000,1) limit 100;
select * from orders_history where type=8 limit 100000,100;
4條語句的查詢時間如下:
第1條語句:3674ms
第2條語句:1315ms
第3條語句:1327ms
第4條語句:3710ms
針對上面的查詢需要注意:
比較第1條語句和第2條語句:使用 select id 代替 select * 速度增加了3倍
比較第2條語句和第3條語句:速度相差幾十毫秒
比較第3條語句和第4條語句:得益於 select id 速度增加,第3條語句查詢速度增加了3倍
這種方式相較於原始一般的查詢方法,將會增快數倍。
使用 id 限定優化
這種方式假設數據表的id是連續遞增的,則我們根據查詢的頁數和查詢的記錄數可以算出查詢的id的範圍,可以使用 id between and 來查詢:
select * from orders_history where type=2 and id between 1000000 and 1000100 limit 100;
查詢時間:15ms 12ms 9ms
這種查詢方式能夠極大地優化查詢速度,基本能夠在幾十毫秒之內完成。限制是只能使用於明確知道id的情況,不過一般建立表的時候,都會添加基本的id字段,這爲分頁查詢帶來很多便利。
還可以有另外一種寫法:
select * from orders_history where id >= 1000001 limit 100;
當然還可以使用 in 的方式來進行查詢,這種方式經常用在多表關聯的時候進行查詢,使用其他表查詢的id集合,來進行查詢:
select * from orders_history where id in(select order_id from trade_2 where goods = 'pen')limit 100;
這種 in 查詢的方式要注意:某些 mysql 版本不支持在 in 子句中使用 limit。
使用臨時表優化
這種方式已經不屬於查詢優化,這兒附帶提一下。
對於使用 id 限定優化中的問題,需要 id 是連續遞增的,但是在一些場景下,比如使用歷史表的時候,或者出現過數據缺失問題時,可以考慮使用臨時存儲的表來記錄分頁的id,使用分頁的id來進行 in 查詢。這樣能夠極大的提高傳統的分頁查詢速度,尤其是數據量上千萬的時候。
關於數據表的id說明
一般情況下,在數據庫中建立表的時候,強制爲每一張表添加 id 遞增字段,這樣方便查詢。
如果像是訂單庫等數據量非常龐大,一般會進行分庫分表。這個時候不建議使用數據庫的 id 作爲唯一標識,而應該使用分佈式的高併發唯一 id 生成器來生成,並在數據表中使用另外的字段來存儲這個唯一標識。
使用先使用範圍查詢定位 id (或者索引),然後再使用索引進行定位數據,能夠提高好幾倍查詢速度。即先 select id,然後再 select *;
MySQL聯合索引
1、聯合索引是兩個或更多個列上的索引。對於聯合索引:Mysql從左到右的使用索引中的字段,一個查詢可以只使用索引中的一部份,但只能是最左側部分。例如索引是key index (a,b,c). 可以支持a 、 a,b 、 a,b,c 3種組合進行查找,但不支持 b,c進行查找 .當最左側字段是常量引用時,索引就十分有效。
2、利用索引中的附加列,您可以縮小搜索的範圍,但使用一個具有兩列的索引 不同於使用兩個單獨的索引。複合索引的結構與電話簿類似,人名由姓和名構成,電話簿首先按姓氏對進行排序,然後按名字對有相同姓氏的人進行排序。如果您知 道姓,電話簿將非常有用;如果您知道姓和名,電話簿則更爲有用,但如果您只知道名不姓,電話簿將沒有用處。