前言:
MySQL的性能是大家在使用時十分關心的問題,比如在高併發訪問時,並且有慢sql存在的情況下,MySQL的性能會明顯下降,這會導致數據庫響應時間變慢,甚至導致數據庫宕機。那麼爲了避免Mysql性能問題,比較常用的方式創建適當的索引,提升sql語句的執行效率。而本文簡單介紹一下和索引有關的回表,從實際案例出發,講講什麼是回表,如何避免回表,如何減少回表。
實際案例:
前置倉產能動態ETA時效降級需求,提供一個數據同步接口,接送大數據同步過來的前置倉的倉負債和配負債,需要支持批量,最多可支持10個批量。
表結構如下,可以看到唯一索引是 UNIQUE KEY `uniq_shop` (`shop_id`,`shop_type`)
CREATE TABLE `shop_load_degrade_strategy` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
`shop_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '門店ID',
`shop_type` int(11) NOT NULL DEFAULT '0' COMMENT '門店類型',
`current_store_load` int(10) NOT NULL DEFAULT '0' COMMENT '當前倉負載',
`active_store_load` int(10) NOT NULL DEFAULT '0' COMMENT '正在使用的倉負載',
`active_store_degrade_delay` int(11) NOT NULL DEFAULT '0' COMMENT '正在使用的倉降級時間分鐘數',
`current_delivery_load` int(10) NOT NULL DEFAULT '0' COMMENT '當前配送負載',
`active_delivery_load` int(10) NOT NULL DEFAULT '0' COMMENT '正在使用的配送負載',
`active_delivery_degrade_delay` int(11) NOT NULL DEFAULT '0' COMMENT '正在使用的配送降級時間分鐘數',
`current_load_active_auto` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '倉配負載自動生效,1開啓',
`enable` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '是否開啓,1開啓',
`artificial_active_time` datetime DEFAULT NULL COMMENT '運營手動生效時間',
`active_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最後生效時間',
`approver` varchar(100) DEFAULT NULL COMMENT '審批人',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '狀態:1啓用,0失效',
`yn` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '1刪除',
`update_pin` varchar(50) NOT NULL DEFAULT '' COMMENT '最後修改人',
`update_time` datetime NOT NULL COMMENT '修改時間',
`create_pin` varchar(50) NOT NULL DEFAULT '' COMMENT '創建人',
`create_time` datetime NOT NULL COMMENT '創建時間',
`ts` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '時間戳',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_shop` (`shop_id`,`shop_type`),
KEY `idx_active_time` (`active_time`),
KEY `idx_update_pin` (`update_pin`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='門店負載降級策略表';
數據處理流程
最簡單的處理方式就是,逐條數據處理
public void handRecords(List<ShopLoadDegradeStrategy> records){
for(ShopLoadDegradeStrategy shopLoadDegradeStrategy: records){
dbShopLoadDegradeStrategy = findByshopIdAndShopType(shopLoadDegradeStrategy);
if(null != dbShopLoadDegradeStrategy)
shopLoadDegradeStrategy.setId(dbShopLoadDegradeStrategy.getId())
updateById(shopLoadDegradeStrategy);
}else{
insert(shopLoadDegradeStrategy);
}
}
}
最初的優化思想的減少網絡傳輸次數
public void handRecords(List<ShopLoadDegradeStrategy> records){
//批量查詢
List<ShopLoadDegradeStrategy> dbShopLoadDegradeStrategys = findAllByshopIdAndShopTypeList(records);
updateShopLoadDegradeStrategys,insertShopLoadDegradeStrategys = handleShopLoadDegradeStrategys(records,dbShopLoadDegradeStrategys);
updateForEach(updateShopLoadDegradeStrategys);
//批量插入
batchInsert(insertShopLoadDegradeStrategys);
}
具體sql語句:
批量按照門店Id和門店類型查詢數據是否存在
select * from shop_load_degrade_strategy where (shop_id=? and shop_type=?) or (shop_id=? and shop_type=?)
存在更新數據, 因爲shop_id和shop_type是組合的唯一索引,所以按照主鍵更新和按照唯一索引更新的結果是一樣的。但是過程真的一樣嗎?
update shop_load_degrade_strategy set current_store_load=?,current_delivery_load=? where shop_id=? and shop_type=?
看是很簡單的兩個sql語句,explain分析sql執行計劃,可以看出用到了索引,並且sql語句執行也很快,不存在慢sql的問題。還有優化的空間嗎?
優化過程分析
先簡單介紹一下索引按物理存儲分類
mysql的B+樹
1)聚集索引(聚簇索引) InnoDB的聚簇索引就是按照主鍵順序構建 B+Tree結構。葉子節點存儲數據行的數據。
2)輔助索引(二級索引) InnoDB的輔助索引就是按照索引列構建 B+Tree結構。葉子節點存儲的是索引列的值以及對應行的主鍵值
覆蓋索引並不是說是索引結構,覆蓋索引是一種很常用的優化手段。因爲在使用輔助索引的時候,我們只可以拿到主鍵值,相當於獲取數據還需要再根據主鍵查詢主鍵索引再獲取到數據。但是試想下這麼一種情況,在上面shop_load_degrade_strategy表中的組合索引查詢時,如果我只需要id,shop_id,shop_type字段的,那是不是意味着我們查詢到組合索引的葉子節點就可以直接返回了,而不需要回表。這種情況就是覆蓋索引。
查詢sql語句優化爲:
select id,shop_id,shop_type from shop_load_degrade_strategy where (shop_id=? and shop_type=?) or (shop_id=? and shop_type=?)
優化前
1.select * from shop_load_degrade_strategy where (shop_id=? and shop_type=?) or (shop_id=? and shop_type=?)
優化前:
1.先查詢輔助索引找到對應的主鍵Id
2.然後通過id回表查詢數據表的數據。
優化後
1.只需要查詢輔助索引就可以,通過查詢輔助索引就可以查詢到所以需要的列,想要使用覆蓋索引,查詢的結果的列只能包含在輔助索引列和主鍵索引列。通過查看sql計劃可以到Extra的值爲Using index。
總結:優化前要查詢除了索引字段(shop_id,shop_type)和主鍵(id)之外的字段,不能用到覆蓋索引這種方式。所以在編寫sql的時候,查詢的數據列一定是滿足業務的最小列集合,首先減少網絡傳輸的數據量,同時也會減少應用的內存使用,還有看看能不能使用到覆蓋索引這種方式進行優化,有些情況下甚至會在輔助索引中增加聯合索引的列來使用到覆蓋索引減少回表。
更新sql語句的優化
優化後:
update shop_load_degrade_strategy set current_store_load=?,current_delivery_load=? where id=?
優化前:
update shop_load_degrade_strategy set current_store_load=?,current_delivery_load=? where shop_id=? and shop_type=?
優化前的更新語句雖然使用到了輔助索引作爲查詢條件來進行數據更新,但是mysql執行sql的過程爲:
1.先查詢輔助索引找到索引對應的主鍵Id
2.然後通過id回表查詢數據表的數據。
3.最後更新數據。
優化後的sql執行過程爲:
1.通過id回表查詢數據表的數據。
2.最會更新對應id的數據,
從以上描述可以看出,減少回表也是我們優化sql語句的一種很重要的思想。通過減少回表這個思想和處理數據的方式,引出說明一下mysql索引下推這個概念,其核心思想就是減少回表
什麼是索引下推
索引下推(Index Condition Pushdown,索引條件下推,簡稱ICP),是MySQL5.6版本的新特性,它可以在對聯合索引遍歷過程中,對索引中包含的所有字段先做判斷,過濾掉不符合條件的記錄之後再回表,能有效的減少回表次數(目前我們使用的mysql版本較高,一般大家可能感覺這是正常的,但是mysql5.6之前都不是這樣實現的,下面會細細道來)。
適用條件
我們先來了解一下索引下推的使用條件及限制:
原理介紹
具體例子
聚簇索引
輔助索引
假設有一條查詢語句
select * from shop_load_degrade_strategy where shop_id <102 and shop_type=1;
根據索引最左匹配原則,上面這個sql語句在查索引樹的時候,只能用102,查到第一個滿足條件的記錄:id爲7,8兩條記錄。
那接下來我們具體看一下 使用與未使用ICP的情況。
未使用ICP情況下:
1.存儲引擎根據聯合索引先找到shop_id<102 的主鍵id(7、8),再逐一進行回表掃描,去聚簇索引找到完整的行記錄,再根據條件shop_type=1對拿到的數據進行篩,可以看到有兩次回表。
使用ICP情況下:
1.存儲引擎會根據(shop_id,shop_type)聯合索引,找到shop_id <102,由於聯合索引中包含shop_type
列,所以存儲引擎直接再聯合索引裏按照條件shop_type=1進行過濾,就只會找到主鍵id(7),然後根據過濾後的數據再依次進行回表掃描。可以看到只有一次回表。通過sql執行計劃可以看出 Extra的值爲“Using index condition”表示使用了索引下推。
結語:
回表操作:當所要查找的字段不在非主鍵索引樹上時,需要通過葉子節點的主鍵值去主鍵索引上獲取對應的行數據,這個過程稱爲回表操作,可以看出Mysql通過索引下推來減少回表次數,從而提高sql性能。所以在項目開發編寫sql語句的時候,要時刻注意是否可以通過減少回表或者不回表來優化sql性能,減少Mysql數據性能的壓力。
參考文獻:
https://dev.mysql.com/doc/refman/5.6/en/index-condition-pushdown-optimization.html