遇到的問題
我們大家都知道,mysql查詢使用select命令,配合limit,offset參數可以讀取指定範圍的記錄,但是offset過大影響查詢性能的原因及優化方法,這次工作中因爲要導出40W的數據遇到這個offset過大的問題,遍歷寫入excel的時間花了2個多小時。
準備測試數據表及數據
1、創建表
CREATE TABLE `member` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(10) NOT NULL COMMENT '姓名',
`gender` tinyint(3) unsigned NOT NULL COMMENT '性別',
PRIMARY KEY (`id`),
KEY `gender` (`gender`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2、插入1000000條記錄
<?php
$pdo = new PDO("mysql:host=localhost;dbname=user","root",'');
for($i=0; $i<1000000; $i++){
$name = substr(md5(time().mt_rand(000,999)),0,10);
$gender = mt_rand(1,2);
$sqlstr = "insert into member(name,gender) values('".$name."','".$gender."')";
$stmt = $pdo->prepare($sqlstr);
$stmt->execute();
}
?>
mysql> select count(*) from member;
+----------+
| count(*) |
+----------+
| 1000000 |
+----------+
1 row in set (0.23 sec)
分析offset過大影響性能的原因
1 、offset較小的情況
mysql> select * from member where gender=1 limit 10,1;
+----+------------+--------+
| id | name | gender |
+----+------------+--------+
| 26 | 509e279687 | 1 |
+----+------------+--------+
1 row in set (0.00 sec)
mysql> select * from member where gender=1 limit 100,1;
+-----+------------+--------+
| id | name | gender |
+-----+------------+--------+
| 211 | 07c4cbca3a | 1 |
+-----+------------+--------+
1 row in set (0.00 sec)
mysql> select * from member where gender=1 limit 1000,1;
+------+------------+--------+
| id | name | gender |
+------+------------+--------+
| 1975 | e95b8b6ca1 | 1 |
+------+------------+--------+
1 row in set (0.00 sec)
當offset較小時,查詢速度很快,效率較高。
2、offset較大的情況
mysql> select * from member where gender=1 limit 100000,1;
+--------+------------+--------+
| id | name | gender |
+--------+------------+--------+
| 199798 | 540db8c5bc | 1 |
+--------+------------+--------+
1 row in set (0.12 sec)
mysql> select * from member where gender=1 limit 200000,1;
+--------+------------+--------+
| id | name | gender |
+--------+------------+--------+
| 399649 | 0b21fec4c6 | 1 |
+--------+------------+--------+
1 row in set (0.23 sec)
mysql> select * from member where gender=1 limit 300000,1;
+--------+------------+--------+
| id | name | gender |
+--------+------------+--------+
| 599465 | f48375bdb8 | 1 |
+--------+------------+--------+
1 row in set (0.31 sec)
當offset很大時,會出現效率問題,隨着offset的增大,執行效率下降。
分析影響性能原因
select * from member where gender=1 limit 300000,1;
因爲數據表是InnoDB,根據InnoDB索引的結構,查詢過程爲:
- 通過二級索引查到主鍵值(找出所有gender=1的id)。
- 再根據查到的主鍵值通過主鍵索引找到相應的數據塊(根據id找出對應的數據塊內容)。
-
根據offset的值,查詢300001次主鍵索引的數據,最後將之前的300000條丟棄,取出最後1條。
所以,mysql查詢時,offset過大影響性能的原因是多次通過主鍵索引訪問數據塊的I/O操作,如下圖。
InnoDB與MyISAM引擎索引結構對比圖
InnoDB有這個問題,而MYISAM索引結構與InnoDB不同,二級索引都是直接指向數據塊的,因此沒有此問題。
優化方法
根據上面的分析,我們知道查詢所有字段會導致主鍵索引多次訪問數據塊造成的I/O操作。
因此我們先查出偏移後的主鍵,再根據主鍵索引查詢數據塊的所有內容即可優化。
mysql> select a.* from member as a inner join (select id from member where gender=1 limit 300000,1) as b on a.id=b.id;
+--------+------------+--------+
| id | name | gender |
+--------+------------+--------+
| 599465 | f48375bdb8 | 1 |
+--------+------------+--------+
1 row in set (0.08 sec)
所以最後我通過這個方式查詢數據,百萬級的數據基本都是毫秒級別的查詢出結果,40W數據寫入excel時間從2個多小時優化成20分鐘,滿滿的成就感哈。
參考文章
: