1. 程式人生 > >日常工作中的資料特徵引發的慢查詢

日常工作中的資料特徵引發的慢查詢

在進入主題之前,首先來了解一個概念“過濾因子”。
過濾因子:
影響 SQL 查詢的除了查詢本身還與資料庫表中的資料特徵有關。一個 SQL 查詢掃描的索引片大小其實是由過濾因子決定的,也就是滿足查詢條件的記錄行數所佔的比例。
users 表,有主鍵(id)、姓名(name)、性別(sex)、年齡(age)欄位。
在這裡插入圖片描述
對於 users 表來說,sex=”male” 就不是一個好的過濾因子,它會選擇整張表中一半的資料,所以在一般情況下我們最好不要使用 sex 列作為整個索引的第一列;而 name=”draven” 的使用就可以得到一個比較好的過濾因子了,它的使用能過濾整個資料表中 99.9% 的資料。
當然我們也可以將這三個過濾進行組合,建立一個新的索引 (name, age, sex) 並同時使用這三列作為過濾條件:
在這裡插入圖片描述


組合條件的過濾因子就可以達到十萬分之 6 了,如果整張表中有 10w 行資料,也只需要在掃描薄索引片後進行 6 次隨機讀取,這種直接使用乘積來計算組合條件的過濾因子其實有一個比較重要的問題:列與列之間不應該有太強的相關性,如果不同的列之間有相關性,那麼得到的結果就會比直接乘積得出的結果大一些,比如:所在的城市和郵政編碼就有非常強的相關性,兩者的過濾因子直接相乘其實與實際的過濾因子會有很大的偏差,不過這在多數情況下都不是太大的問題。
對於一張表中的同一個列,不同的值也會有不同的過濾因子,這也就造成了同一列的不同值最終的查詢效能也會有很大差別:
在這裡插入圖片描述
當我們評估一個索引是否合適時,需要考慮極端情況下查詢語句的效能,比如 0% 或者 50% 等;最差的輸入往往意味著最差的效能,在平均情況下表現良好的 SQL 語句在極端的輸入下可能就完全無法正常工作,這也是在設計索引時需要注意的問題。

以上內容來自http://blog.jobbole.com/112487/

在日常工作中,我們需要建立索引,來提升查詢效率。有些索引,在平均情況下表現良好,在極端的輸入下可能就完全無法正常工作。

場景一:

表結構如下

CREATE TABLE `job_queue` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `status` varchar(1) NOT NULL DEFAULT '0' COMMENT '狀態,0:未處理,1:成功,2:失敗',
  PRIMARY KEY (`id`),
  KEY `idx_jq_status` (`status`)
) ;

生產者往job_queue表插入status=0記錄。
消費者執行SQL:select * from job_queue where status=0 order by id asc limit 500;掃描status=0的記錄,處理完之後更新記錄status=1。正常情況下,
status=0的記錄總數在1000以內,job_queue表數量級在100W以上,status=0過濾性很強,可以過濾掉99.9%的記錄
原則上對於值比較單一的列(如status的值只有0,1,2三個)是不允許建立索引的。
但針對這種場景是有必要建立索引的。索引建立之後效率確實提升不少,一切相安無事。
在一次版本釋出後,因為疏忽了某些場景,status沒有由0改成1。隨著時間的推移,status=0記錄增長到10W以上,status=0過濾性下降。一切就變得不那麼美好,程式修復之後,焦急地等待status=0數量趕緊下降,儘快恢復到之前美好的時光。
思考:
將job_queue表拆分成兩個表job_queue_to_do(待處理表)和job_queue_result(結果表)。job_queue_to_do表只記錄status=0的資料,處理完之後資料遷移到job_queue_result表。規避掉status=0數量不可控,導致出現慢查詢的風險。

場景二:

索引列絕大部分(99%以上)值分佈均勻,且記錄不多,索引過濾效果明顯;在某些值上聚集了大量資料,查詢這些值對應的資料時,出現了慢查詢,這些聚集了大量資料的值很容易被忽視。例如:

CREATE TABLE `retail_price` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `store_id` varchar(50) NOT NULL DEFAULT '' COMMENT '商品店鋪ID',
  `product_id` varchar(50) NOT NULL DEFAULT '' COMMENT '商品ID',
  `retail_price` decimal(12,2) NOT NULL DEFAULT '-100000000.00' COMMENT '零售價',
  PRIMARY KEY (`id`),
  KEY `idx_rp_store_id` (`store_id`)
) ;

retail_price表記錄零售價,store_id為空字串表示商品預設價格,store_id不為空字串表示店鋪價格。每個商品都有預設價格和店鋪價格。店鋪價格大致分佈如下,每個店鋪的價格記錄數都不多,分佈很均勻,store_id不為空字串時過濾性很好。如果需要支援按店鋪ID查詢價格,對store_id列建立了索引,查詢效率也很快。
在這裡插入圖片描述
但是如果需要獲取預設價格資料,select * from retail_price wherestore_id='',此時sql就會出現慢查詢。
針對這種情況,我們也可以像場景一 一樣把retail_price拆分成default_retail_price(預設價格)和store_retail_price(店鋪價格)兩個表。
以為這樣做就萬事大吉了嗎?
過了一段時間,有些店鋪悄悄上架了幾萬個商品,每個商品又改過好幾次價,最終這些店鋪下的價格記錄數達到十幾萬,甚至百萬。這時候慢查詢又來了,繼續拆分表肯定不現實。該怎麼辦?
假設
每個店鋪下的價格主鍵ID是連續的,我們可以先獲取該店鋪價格maxId和minId。select max(id),min(id) from store_retail_price where store_id='ST00001'。然後按ID範圍條件,從minId開始按固定步長,迴圈從表裡查詢資料,直到id範圍大於maxId。

long startId = 0;
long endId = minId;
int size=1000;
do{
    startId = endId;
    endId+=size;
    `select * from store_retail_price where store_id='ST00001' and id>=startId and id<endId;
}while(endId<=maxId)

這個假設是一種比較理想的情況,大多數場景下主鍵id是不連續的。這時候需要降低粒度,從store_id級別細分到store_id+product_id級別,建立組合索引(store_id,product_id)。當然如果你有store_id資訊,如何將store_id級別細分到store_id+product_id級別也是一個難題。有一個辦法是統計store_id下去重的product_id,select distinct product_id from store_retail_price where store_id='ST00001',這個sql語句可能也是一個慢SQL。感覺又進入了死角,懇請大神指點迷津

思考
建立表的時候,應該儘量一個表對應一種業務,避免多種業務資料糅合到一個表(雖然可以減少表的數量,提高開發效率,但是一旦某種業務的資料量超出預期的時候,這個表上的所有業務都會受影響)。寫程式碼的時候應當考慮某個維度下資料量暴漲的情況,儘量將業務處理邏輯降到最細粒度,不僅可以避免因資料量過大導致程式OOM,也可以提高資料庫查詢效能。

by:Dani.he