Mysql慢查詢優化小記

1.背景

當數據庫中表的數據達到必定級別後,就須要考慮解決方案。事實上MySQL單表能夠存儲10億級數據,只是這時候性能比較差,業界公認MySQL單表容量在1KW如下是最佳狀態,由於這時它的BTREE索引樹高在3~5之間。既然一張表沒法搞定,那麼就想辦法將數據放到多個地方,目前比較廣泛的方案可能有下列幾種:java

  • 歸檔
  • 分庫分表
  • NoSQL/NewSQL

歸檔適用場景:最新的數據會被常用,舊數據不多被使用。mysql

爲何不用NoSQL/NewSQLsql

首先,爲何不選擇第三種方案NoSQL/NewSQL,RDBMS主要有如下幾個優勢:數據庫

  • RDBMS生態完善;
  • RDBMS絕對穩定;
  • RDBMS的事務特性;
NoSQL/NewSQL做爲新生兒,在咱們把可靠性當作首要考察對象時,它是沒法與RDBMS相提並論的。RDBMS發展幾十年,只要有軟件的地方,它都是核心存儲的首選。
目前絕大部分公司的核心數據都是:**以RDBMS存儲爲主,NoSQL/NewSQL存儲爲輔**!
目前互聯網行業處理海量數據的通用方法:**分庫分表**。

2.關於數據庫中間件及分庫分表

image.png

典型的數據庫中間件設計方案有2種:proxy、smart-client。下圖演示了這兩種方案的架構apache

功能點 代理模式 客戶端模式
代理方式 服務端代理(proxy:代理數據庫)中: 咱們獨立部署一個代理服務,這個代理服務背後管理多個數據庫實例。而在應用中,咱們經過一個普通的數據源(c3p0、druid、dbcp等)與代理服務器創建鏈接,全部的sql操做語句都是發送給這個代理,由這個代理去操做底層數據庫,獲得結果並返回給應用。在這種方案下,分庫分表和讀寫分離的邏輯對開發人員是徹底透明的。 客戶端代理(datasource:代理數據源): 應用程序須要使用一個特定的數據源,其做用是代理,內部管理了多個普通的數據源(c3p0、druid、dbcp等),每一個普通數據源各自與不一樣的庫創建鏈接。應用程序產生的sql交給數據源代理進行處理,數據源內部對sql進行必要的操做,如sql改寫等,而後交給各個普通的數據源去執行,將獲得的結果進行合併,返回給應用。數據源代理一般也實現了JDBC規範定義的API,所以可以直接與orm框架整合。在這種方案下,用戶的代碼須要修改,使用這個代理的數據源,而不是直接使用c3p0、druid、dbcp這樣的鏈接池。
實現區別 複寫MySql協議 給予JDBC擴展,以Jar包形式提供輕量級服務
優勢 一、支持多語言,以mysql爲例,若是proxy實現了mysql通訊協議,能夠將其堪稱一個mysql服務器 二、對業務透明 一、實現簡單 2自然去中心化
缺點 一、實現複雜:須要實現被代理的數據庫server端的通訊協議,也許只能代理某一種數據庫,如mysql 二、proxy自己須要保證高可用:不能掛 三、租戶隔離:多個應用都訪問proxy代理的底層數據庫時 一、一般僅支持某一種語言:例如tddl需使用java語言開發 二、版本升級困難:多個應用都依賴某版本jar包,有bug都要升級;而proxy只要升級代理服務器
業界實現的產品 一、阿里開源cobar 二、阿里雲drds 三、奇虎360在mysql-proxy基礎上開發的atlas 一、 阿里開源tddl 二、大衆點評開源zebra 三、螞蟻金服zal
image.png

3讀寫分離核心要點

2.1.1 sql類型判斷
  • write語句:insert、update、delete、create、alter、truncate…
  • query語句:select、show、desc、explain…
2.1.2 強制走主庫

具體實現上有2種方案:hint 或APIapi

  • hint:好比/master/select * from table_xx
  • Api:數據庫中間件決定,ForceMasterHelper.forceMaster() //…執行多條sqlForceMasterHelper.clear()

2.2 從庫路由策略

一些簡單的選擇策略包括:服務器

  • 隨機選擇(random)
  • 按照權重進行選擇(weight)
  • 或者輪循(round-robin)
  • 等等
  • 就近路由:跨IDC(Internet Data Center)部署的數據庫集羣

分庫分表

數據庫中間件主要對應用屏蔽瞭如下過程:架構

  • sql解析:首先對sql進行解析,獲得抽象語法樹,從語法樹中獲得一些關鍵sql信息
  • sql路由:sql路由包括庫路由和表路由。庫路由用於肯定這條記錄應該操做哪一個分庫,表路由用於肯定這條記錄應該操做哪一個分表。
  • sql改寫:將sql改寫成正確的執行方式。例如,對於一個批量插入sql,同時插入4條記錄。但實際上用戶但願4個記錄分表存儲到一個分表中,那麼就要對sql進行改寫成4條sql,每一個sql都只能插入1條記錄。
  • sql執行:一條sql通過改寫後可能變成了多條sql,爲了提高效率應該併發的去執行,而不是按照順序逐一執行
  • 結果集合並:每一個sql執行以後,都會有一個執行結果,咱們須要對分庫分表的結果集進行合併,從而獲得一個完整的結果。
    3.1 SQL解析

目前較爲流行的sql解析器包括:併發

  • FoundationDB SQL Parser
  • Jsqlparser
  • Druid SQL Parser:解析性能最好,支持數據庫方言最多

3.2 SQL路由
image.png框架

路由分則分爲:

· 庫規則:用於肯定到哪個分庫

· 表規則:用於肯定到哪個分表

在上例中,咱們使用id來做爲計算分表、分表,所以把id字段就稱之爲路由字段,或者分區字段。

須要注意的是,無論執行的是INSERT、UPDATE、DELETE、SELECT語句,SQL中都應該包含這個路由字段。不然,對於插入語句來講,就不知道插入到哪一個分庫或者分表;對於UPDATE、DELETE、SELECT語句而言,則更爲嚴重,由於不知道操做哪一個分庫分表,意味着必需要對全部分表都進行操做。SELECT聚合全部分表的內容,極容易內存溢出,UPDATE、DELETE更新、刪除全部的記錄,很是容易誤更新、刪除數據。所以,一些數據庫中間件,對於SQL可能有一些限制,例如UPDATE、DELETE必需要帶上分區字段,或者指定過濾條件。

路由引擎

https://shardingsphere.apache.org/document/current/cn/features/sharding/principle/route/

3.3 SQL 改寫
前面已經介紹過,如一個批量插入語句,若是記錄要插入到不一樣的分庫分表中,那麼就須要對SQL進行改寫。 例如,將如下SQL

insert into user(id,name) values (1,」tianshouzhi」),(2,」huhuamin」), (3,」wanghanao」),(4,」luyang」)

改寫爲

insert into user_1(id,name) values (1,」tianshouzhi」)insert into user_2(id,name) values (2,」huhuamin」)insert into user_3(id,name) values (3,」wanghanao」)insert into user_0(id,name) values (4,」luyang」)

這裏只是一個簡單的案例,一般對於INSERT、UPDATE、DELETE等,改寫相對簡單。比較複雜的是SELECT語句的改寫,對於一些複雜的SELECT語句,改寫過程當中會進行一些優化,例如將子查詢改爲JOIN,過濾條件下推等。由於SQL改寫很複雜,因此不少數據庫中間件並不支持複雜的SQL(一般有一個支持的SQL),只能支持一些簡單的OLTP場景。

下表都是按照order_id 分表 改以前 改以後
標識符改寫 SELECT order_id FROM t_order WHERE order_id=1; SELECT order_id FROM t_order_1 WHERE order_id=1;
排序補列 SELECT order_id FROM t_order ORDER BY user_id; SELECT order_id, user_id FROM t_order ORDER BY user_id;
聚合補列 SELECT AVG(price) FROM t_order WHERE user_id=1; SELECT COUNT(price) AS AVG_DERIVED_COUNT_0, SUM(price) AS AVG_DERIVED_ SUM _0 FROM t_order WHERE user_id=1;
自增主鍵補列 INSERT INTO t_order (field1, field2) VALUES (10, 1); INSERT INTO t_order (field1, field2, order_id) VALUES (10, 1, xxxxx);
批量插入查分 INSERT INTO t_order (order_id, xxx) VALUES (1, 'xxx'), (2, 'xxx'), (3, 'xxx'); INSERT INTO t_order_0 (order_id, xxx) VALUES (2, 'xxx'); INSERT INTO t_order_1 (order_id, xxx) VALUES (1, 'xxx'), (3, 'xxx');
in查詢拆分 SELECT * FROM t_order WHERE order_id IN (1, 2, 3); vSELECT FROM t_order_0 WHERE order_id IN (2); SELECT FROM t_order_1 WHERE order_id IN (1, 3);

image.png

3.4 SQL 執行
當通過SQL改寫階段後,會產生多個SQL,須要到不一樣的分片上去執行,一般咱們會使用一個線程池,將每一個SQL包裝成一個任務,提交到線程池裏面併發的去執行,以提高效率。

這些執行的SQL中,若是有一個失敗,則總體失敗,返回異常給業務代碼。

3.5 結果集合並
結果集合並,是數據庫中間件的一大難點,須要case by case的分析,主要是考慮實現的複雜度,以及執行的效率問題,對於一些複雜的SQL,可能並不支持。例如:

對於查詢條件:大部分中間件都支持=、IN做爲查詢條件,且能夠做爲分區字段。可是對於NOT IN、BETWEEN…AND、LIKE,NOT LIKE等,只能做爲普通的查詢條件,由於根據這些條件,沒法記錄究竟是在哪一個分庫或者分表,只能全表掃描。

聚合函數:大部分中間件都支持MAX、MIN、COUNT、SUM,可是對於AVG可能只是部分支持。另外,若是是函數嵌套、分組(GROUP BY)聚合,可能也有一些數據庫中間件不支持。

 子查詢:分爲FROM部分的子查詢和WHERE部分的子查詢。大部分中對於子查詢的支持都是很是有限,例如語法上兼容,可是沒法識別子查詢中的分區字段,或者要求子查詢的表名必須與外部查詢表名相同,又或者只能支持一級嵌套子查詢。

 JOIN:對於JOIN的支持一般很複雜,若是作不到過濾條件下推和流式讀取,在中間件層面,基本沒法對JOIN進行支持,由於不可能把兩個表的全部分表,所有拿到內存中來進行JOIN,內存早就崩了。固然也有一些取巧的辦法,一個是Binding Table,另一個是小表廣播(見後文)。

  分頁排序:一般中間件都是支持ORDER BY和LIMIT的。可是在分庫分表的狀況下,分頁的效率較低。例如對於limit 100,10 ORDER BY id。表示按照id排序,從第100個位置開始取10條記錄。那麼,大部分數據庫中間件其實是要從每一個分表都查詢110(100+10)條記錄,拿到內存中進行從新排序,而後取出10條。假設有10個分表,那麼實際上要查詢1100條記錄,而最終只過濾出了10記錄。所以,在分頁的狀況下,一般建議使用"where id > ? limit 10」的方式來進行查詢,應用記住每次查詢的最大的記錄id。以後查詢時,每一個分表只須要從這個id以後,取10條記錄便可,而不是取offset + rows條記錄。 

關於JOIN的特屬說明

  1. Binding Table: 強關聯的表的路由規則設置爲徹底同樣,在同一個分庫中join
  2. 小表廣播: 每一個分庫內都實時同步一份完整的數據,在同一個分庫中join
相關文章
相關標籤/搜索