MySQL_分頁優化
阿新 • • 發佈:2022-05-10
MySQL_分頁優化
起因:
offset+limit方式的分頁查詢,當資料表超過100w條記錄,效能會很差。 主要原因是offset limit的分頁方式是從頭開始查詢,然後捨棄前offset個記錄,所以offset偏移量越大,查詢速度越慢。
舉例:
《高效能 MySQL》第六章:查詢效能優化,對這個問題有過說明:分頁操作通常會使用 limit 加上偏移量的辦法實現,同時再加上合適的 order by 子句。
1、資料模擬
①建立兩個表:員工表和部門表
/*部門表,存在則進行刪除 */ drop table if EXISTS dep; create table dep( id int unsigned primary key auto_increment, depno mediumint unsigned not null default 0, depname varchar(20) not null default "", memo varchar(200) not null default "" ); /*員工表,存在則進行刪除*/ drop table if EXISTS emp;create table emp( id int unsigned primary key auto_increment, empno mediumint unsigned not null default 0, empname varchar(20) not null default "", job varchar(9) not null default "", mgr mediumint unsigned not null default 0, hiredate datetime not null, sal decimal(7,2) notnull, comn decimal(7,2) not null, depno mediumint unsigned not null default 0 );
②建立兩個函式:生成隨機字串和隨機編號(方便填充資料)
/* 產生隨機字串的函式*/ DELIMITER $ drop FUNCTION if EXISTS rand_string; CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255) BEGIN DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmlopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; DECLARE return_str VARCHAR(255) DEFAULT ''; DECLARE i INT DEFAULT 0; WHILE i < n DO SET return_str = CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1)); SET i = i+1; END WHILE; RETURN return_str; END $ DELIMITER; /*產生隨機部門編號的函式*/ DELIMITER $ drop FUNCTION if EXISTS rand_num; CREATE FUNCTION rand_num() RETURNS INT(5) BEGIN DECLARE i INT DEFAULT 0; SET i = FLOOR(100+RAND()*10); RETURN i; END $ DELIMITER;
③編寫儲存過程,模擬 500W 的員工資料(過程有點慢)
/*建立儲存過程:往emp表中插入資料*/ DELIMITER $ drop PROCEDURE if EXISTS insert_emp; CREATE PROCEDURE insert_emp(IN START INT(10),IN max_num INT(10)) BEGIN DECLARE i INT DEFAULT 0; /*set autocommit =0 把autocommit設定成0,把預設提交關閉*/ SET autocommit = 0; REPEAT SET i = i + 1; INSERT INTO emp(empno,empname,job,mgr,hiredate,sal,comn,depno) VALUES ((START+i),rand_string(6),'SALEMAN',0001,now(),2000,400,rand_num()); UNTIL i = max_num END REPEAT; COMMIT; END $ DELIMITER; /*插入500W條資料*/ call insert_emp(0,5000000);
④編寫儲存過程,模擬 120 的部門資料
/*建立儲存過程:往dep表中插入資料*/ DELIMITER $ drop PROCEDURE if EXISTS insert_dept; CREATE PROCEDURE insert_dept(IN START INT(10),IN max_num INT(10)) BEGIN DECLARE i INT DEFAULT 0; SET autocommit = 0; REPEAT SET i = i+1; INSERT INTO dep( depno,depname,memo) VALUES((START+i),rand_string(10),rand_string(8)); UNTIL i = max_num END REPEAT; COMMIT; END $ DELIMITER; /*插入120條資料*/ call insert_dept(1,120);
⑤建立關鍵欄位的索引,這邊是跑完資料之後再建索引,會導致建索引耗時長,但是跑資料就會快一些。
/*建立關鍵欄位的索引:排序、條件*/ CREATE INDEX idx_emp_id ON emp(id); CREATE INDEX idx_emp_depno ON emp(depno); CREATE INDEX idx_dep_depno ON dep(depno);
2、測試
測試一(直接分頁):
/*偏移量為100,取25*/ SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depname from emp a left join dep b on a.depno = b.depno order by a.id desc limit 100,25; /*偏移量為4800000,取25*/ SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depname from emp a left join dep b on a.depno = b.depno order by a.id desc limit 4800000,25;
執行結果:
[SQL]/*偏移量為100,取25*/ SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depname from emp a left join dep b on a.depno = b.depno order by a.id desc limit 100,25; 受影響的行: 0 時間: 0.001s [SQL] /*偏移量為4800000,取25*/ SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depname from emp a left join dep b on a.depno = b.depno order by a.id desc limit 4800000,25; 受影響的行: 0 時間: 9.325s
因為掃描的資料多,所以這個明顯不是一個量級上的耗時。
測試二(使用索引覆蓋+子查詢優化):
因為我們有主鍵 id,並且在上面建了索引,所以可以先在索引樹中找到開始位置的 id 值,再根據找到的 id 值查詢行資料。
/*子查詢獲取偏移100條的位置的id,在這個位置上往後取25*/ SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depname from emp a left join dep b on a.depno = b.depno where a.id >= (select id from emp order by id limit 100,1) order by a.id limit 25; /*子查詢獲取偏移4800000條的位置的id,在這個位置上往後取25*/ SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depname from emp a left join dep b on a.depno = b.depno where a.id >= (select id from emp order by id limit 4800000,1) order by a.id limit 25;
執行結果:
[SQL] /*子查詢獲取偏移100條的位置的id,在這個位置上往後取25*/ SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depname from emp a left join dep b on a.depno = b.depno where a.id >= (select id from emp order by id limit 100,1) order by a.id limit 25; 受影響的行: 0 時間: 0.014s [SQL] /*子查詢獲取偏移4800000條的位置的id,在這個位置上往後取25*/ SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depname from emp a left join dep b on a.depno = b.depno where a.id >= (select id from emp order by id limit 4800000,1) order by a.id limit 25; 受影響的行: 0 時間: 1.464s
測試三(起始位置重定義):
記住上次查詢結果的主鍵位置,避免使用偏移量 offset
/*記住了上次的分頁的最後一條資料的id是100,這邊就直接跳過100,從101開始掃描表*/ SELECT a.id,a.empno,a.empname,a.job,a.sal,b.depno,b.depname from emp a left join dep b on a.depno = b.depno where a.id > 100 order by a.id limit 25; /*記住了上次的分頁的最後一條資料的id是4800000,這邊就直接跳過4800000,從4800001開始掃描表*/ SELECT a.id,a.empno,a.empname,a.job,a.sal,b.depno,b.depname from emp a left join dep b on a.depno = b.depno where a.id > 4800000 order by a.id limit 25;
執行結果:
[SQL] /*記住了上次的分頁的最後一條資料的id是100,這邊就直接跳過100,從101開始掃描表*/ SELECT a.id,a.empno,a.empname,a.job,a.sal,b.depno,b.depname from emp a left join dep b on a.depno = b.depno where a.id > 100 order by a.id limit 25; 受影響的行: 0 時間: 0.013s [SQL] /*記住了上次的分頁的最後一條資料的id是4800000,這邊就直接跳過4800000,從4800001開始掃描表*/ SELECT a.id,a.empno,a.empname,a.job,a.sal,b.depno,b.depname from emp a left join dep b on a.depno = b.depno where a.id > 4800000 order by a.id limit 25; 受影響的行: 0 時間: 0.000s
這個效率是最好的,無論怎麼分頁,耗時基本都是一致的,因為他執行完條件之後,都只掃描了 25 條資料。
!>配置 limit 的偏移量和獲取數一個最大值,超過這個最大值,就返回空資料。 因為他覺得超過這個值你已經不是在分頁了,而是在刷資料了,如果確認要找資料,應該輸入合適條件來縮小範圍,而不是一頁一頁分頁。( request 的時候如果 offset 大於某個數值就先返回一個 4xx 的錯誤。 )