1 問題背景
線上有一個批處理任務,會批量讀取昨日的資料,經過一系列加工後,插入到今日的表中。表結構如下:
1 CREATE TABLE `detail_yyyyMMdd` (
2 `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
3 `batch_no` varchar(64) NOT NULL COMMENT '批次號',
4 `order_id` varchar(64) NOT NULL COMMENT '訂單ID',
5 `user_id` varchar(64) NOT NULL COMMENT '使用者ID',
6 `status` varchar(4) NOT NULL COMMENT '狀態',
7 `product_id` varchar(32) NOT NULL COMMENT '產品ID',
8 PRIMARY KEY (`id`),
9 KEY `idx_batchno_userid` (`batch_no`,`user_id`)
10 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='明細表';
因資料量較大,批量讀取昨日資料時,使用了分頁查詢limit語句,查詢sql如下:
SELECT id,batch_no,order_id,user_id,status,product_id FROM detail_yyyyMMdd WHERE batch_no=‘batch_type_yyyyMMdd’ LIMIT ?,?;
從某一天開始,客服頻繁收到客訴,反饋資料未更新。
2 問題排查
初步排查,客訴反饋的資料確實沒有插入。線上增加了一些業務日誌後,發現分頁查詢昨日資料時,第二次查詢的結果集與第一次查詢的結果集有重複資料。
與DBA溝通,確認了客訴爆發前一天做了一次資料庫變更,兄弟團隊為了解決一個慢查詢問題,增加了一個索引,變更sql如下:
ALTER TABLE `detail_yyyyMMdd` ADD INDEX `idx_batchno_status_productid` (`batch_no`, `status`, `product_id`);
DBA分析,該變更sql上線前,分頁查詢會走idx_batchno_userid索引,上線後則走idx_batchno_status_productid,而該索引存在大量重複記錄,導致每次分頁查詢的資料都可能和之前的重複。
為什麼索引有重複記錄時,分頁查詢的資料就可能與之前的重複呢?在網上搜了下,這裡貼一篇官方的文件:https://dev.mysql.com/doc/refman/5.6/en/limit-optimization.html
If multiple rows have identical values in the ORDER BY columns, the server is free to return those rows in any order, and may do so differently depending on the overall execution plan. In other words, the sort order of those rows is nondeterministic with respect to the nonordered columns.
One factor that affects the execution plan is LIMIT, so an ORDER BY query with and without LIMIT may return rows in different orders.
If it is important to ensure the same row order with and without LIMIT, include additional columns in the ORDER BY clause to make the order deterministic.
在MySQL 5.6的版本上,優化器在遇到order by limit語句時,做了個查詢優化,即使用了priority queue(優先順序佇列)。採用priority queue能夠根據limit N維護一個大小為N的堆,在排序的過程中,只用保留N條記錄即可。但堆排序是一個不穩定的排序演算法,所以當排序欄位值存在重複時,返回的資料順序可能會不一樣,其中一個影響因素就是limit。
解決方案:查詢語句加上order by id(保證排序欄位的唯一性),上線後,問題得到解決。
但仍然存在兩個疑點:
原來的索引idx_batchno_userid也會有重複記錄,為什麼一直沒有爆出問題?
線上的查詢語句只有limit,沒有order by,可以直接按照索引的有序性(索引聚簇表)進行讀取並分頁,不需要觸發priority queue優化。
3 問題定位
仔細觀察了兩次分頁查詢的結果集後發現,兩次結果集的資料順序分別對應兩個索引,即第一次分頁查詢走了索引idx_batchno_userid,第二次分頁查詢走了索引idx_batchno_status_productid,兩個索引的資料順序是不一樣的,從而導致兩次分頁查詢的資料存在重複。與DBA交流,MySQL的優化器會基於成本選擇最優的索引,而這兩個索引的成本相差不大。
這個結論線上下得到復現,但並不能穩定復現。在總結果集沒有變化的情況下,兩次分頁查詢分別走了不同索引的根因,還有待繼續深挖。
End
MySQL使用limit進行分頁查詢時,可能會出現重複資料,可以通過加上order by子句並保證排序欄位的唯一性來解決。