1. 程式人生 > >《MySQL實戰45講》16~30講 —丁奇大大,學習筆記

《MySQL實戰45講》16~30講 —丁奇大大,學習筆記

圖片來自極客時間,如有版權問題,請聯絡我刪除。
掃碼加入學習!
在這裡插入圖片描述

16 | “order by”是怎麼工作的?

假設部分表定義:

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `city` varchar(16) NOT NULL,
  `name` varchar(16) NOT NULL,
  `age` int(11) NOT NULL,
  `addr` varchar(128) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `city` (`city`)
) ENGINE=InnoDB
;

假設按照下面的sql查詢並排序。

select city,name,age from t where city='杭州' order by name limit 1000  ;

全欄位排序

explain
在這裡插入圖片描述
Extra中"Using filesort"表示排序,mysql會給每個執行緒分配一個塊記憶體(sort_buffer)用來排序。
city索引示意圖:
在這裡插入圖片描述
sql執行過程:

  1. 初始化sort_buffer,確定放入name、city、age 這三個欄位;
  2. 從city索引找到第一個city='杭州’的主鍵id,圖中的ID_X;
  3. 根據id去聚集索引取這三個欄位,放到sort_buffer;
  4. 在從city索引取下一個;
  5. 重複3、4查詢所有的值;
  6. 在sort_buffer按name快速排序;
  7. 按照排序結果取前1000行返回給客戶端。

如果sort_buffer太小,記憶體放不下排序的資料,則需要使用外部排序,利用磁碟臨時檔案輔助排序。這取決於排序所需記憶體和引數 sort_buffer_size。
下面方法可以確定排序是否使用臨時檔案:

/* 開啟 optimizer_trace,只對本執行緒有效 */
SET optimizer_trace='enabled=on'; 
/* @a 儲存 Innodb_rows_read 的初始值 */
select VARIABLE_VALUE into
@a from performance_schema.session_status where variable_name = 'Innodb_rows_read'; /* 執行語句 */ select city, name,age from t where city='杭州' order by name limit 1000; /* 檢視 OPTIMIZER_TRACE 輸出 */ SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G /* @b 儲存 Innodb_rows_read 的當前值 */ select VARIABLE_VALUE into @b from performance_schema.session_status where variable_name = 'Innodb_rows_read'; /* 計算 Innodb_rows_read 差值 */ select @b-@a;

在這裡插入圖片描述
通過檢視 OPTIMIZER_TRACE,number_of_tmp_files表示排序使用的臨時檔案數,外部排序一般使用歸併排序演算法
rows表示滿足city='杭州’有4000條,examined_rows=4000表示4000行參與排序。
sort_mode packed_additional_fields表示排序過程字串做了“緊湊”處理。name欄位定義varchar(16),排序過程中按照實際長度分配空間。
最後一個查詢語句 select @[email protected]返回結果是 4000,表示只掃描了4000行。

這邊老師把internal_tmp_disk_storage_engine 設定成MyISAM,否則,select @[email protected]結果為 4001。因為innodb把資料從臨時表取出來時,會讓Innodb_rows_read 的值加 1。

rowid 排序

如果排序的單行長度太大mysql會使用另一種演算法。

SET max_length_for_sort_data = 16;

city、name、age 這三個欄位的定義總長度是 36 > max_length_for_sort_data,所以會使用別的演算法。
該演算法和全欄位排序的差別:

  1. sort_buffer只會確定放入name 和 id欄位,所以只會取這兩個欄位。
  2. 最後根據name排完序,會根據id欄位去原表取city、name 和 age 三個欄位返回給客戶端。

需要注意,不做合併操作,而是直接將原表查到的欄位返回給客戶端。
和上述過程對比:
在這裡插入圖片描述
examined_rows和rows沒有變化,但select @[email protected]會變成5000。因為排完序需要去原表再取1000行。

全欄位排序 VS rowid 排序

對於 InnoDB 表來說,rowid 排序會要求回表多造成磁碟讀,因此不會被優先選擇。
假設從city索引上取出來的行天然按照name遞增排序,就不需要再進行排序了
所以可以建一個city和name的聯合索引

alter table t add index city_user(city, name);

整個查詢流程就變成了:

  1. 從索引(city, name)找到第一個city='杭州’的主鍵id;
  2. 到聚集索引取name、city、age三個欄位,作為結果集一部分直接返回;
  3. 從索引(city, name)取下一個。
  4. 重複2、3,直到查到1000條記錄,或不滿足city='杭州’時結束。

explian:
在這裡插入圖片描述
沒有"Using filesort"。
使用覆蓋索引

alter table t add index city_user_age(city, name, age);

但維護索引是有代價的,所以需要權衡。

小結

mysql> select * from t where city in ('杭州'," 蘇州 ") order by name limit 100;

上述sql需要排序,因為name不是遞增的。
可以將sql拆分成兩條,最後通過程式記憶體取前100條。
進一步,如果需要分頁,“limit 10000,100”,則可以使用下面的思想:

select * from t where city=" 杭州 " order by name limit 10100; 
select * from t where city=" 蘇州 " order by name limit 10100

根據,name排序,然後取10001~10100,但這樣返回的資料量較大,所以可以改成:

select id,name from t where city=" 杭州 " order by name limit 10100; 
select id,name from t where city=" 蘇州 " order by name limit 10100

根據,name排序,然後取10001~10100,然後在通過id查詢100條資料。

另外

評論區大神多,特別是@某、人,看到好多次了。下面是他的回答:
問題一 :這種無條件查列表頁除了全表掃還有其他建立索引的辦法麼
1)無條件查詢如果只有order by create_time,即便create_time上有索引,也不會使用到。
因為優化器認為走二級索引再去回表成本比全表掃描排序更高。
所以選擇走全表掃描,然後根據老師講的兩種方式選擇一種來排序
2)無條件查詢但是是order by create_time limit m.如果m值較小,是可以走索引的.
因為優化器認為根據索引有序性去回表查資料,然後得到m條資料,就可以終止迴圈,那麼成本比全表掃描小,則選擇走二級索引。
即便沒有二級索引,mysql針對order by limit也做了優化,採用堆排序。這部分老師明天會講
問題二 : 如果加入 group by , 資料該如何走
如果是group by a,a上不能使用索引的情況,是走rowid排序。
如果是group by limit,不能使用索引的情況,是走堆排序
如果是隻有group by a,a上有索引的情況,又根據選取值不同,索引的掃描方式又有不同
select * from t group by a --走的是索引全掃描,至於這裡為什麼選擇走索引全掃描,還需要老師解惑下
select a from t group by a --走的是索引鬆散掃描,也就說只需要掃描每組的第一行資料即可,不用掃描每一行的值
問題三 :老師之後的文章會有講解 bigInt(20) 、 tinyint(2) 、varchar(32) 這種後面帶數字與不帶數字有何區別的文章麼 。 每次建欄位都會考慮長度 ,但實際卻不知道他有何作用
bigint和int加數字都不影響能儲存的值。
bigint(1)和bigint(19)都能儲存2^64-1範圍內的值,int是 2^32-1。只是有些前端會根據括號裡來擷取顯示而已。建議不加varchar()就必須帶,因為varchar()括號裡的數字代表能存多少字元。假設varchar(2),就只能存兩個字元,不管是中文還是英文。目前來看varchar()這個值可以設得稍稍大點,因為記憶體是按照實際的大小來分配記憶體空間的,不是按照值來預分配的。

17 | 如何正確地顯示隨機訊息?

mysql> CREATE TABLE `words` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `word` varchar(64) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

delimiter ;;
create procedure idata()
begin
  declare i int;
  set i=0;
  while i<10000 do
    insert into words(word) values(concat(char(97+(i div 1000)), char(97+(i % 1000 div 100)), char(97+(i % 100 div 10)), char(97+(i % 10))));
    set i=i+1;
  end while;
end;;
delimiter ;

call idata();

需求:每次隨機獲取三個word;

記憶體臨時表

mysql> select word from words order by rand() limit 3;

explain:
在這裡插入圖片描述
這個 Extra 的意思就是,需要臨時表,並且需要在臨時表上排序。
上一篇文章的一個結論:對於 InnoDB 表來說,執行全欄位排序會減少磁碟訪問,因此會被優先選擇。
**對於記憶體表,回表過程只是簡單地根據資料行的位置,直接訪問記憶體得到資料,根本不會導致多訪問磁碟。**所以,MySQL 這時就會選擇 rowid 排序。
上述sql的執行流程:

  1. 建立一個memory引擎的臨時表,第一個欄位double型別,假設欄位為R,第二個欄位varchar(64),記為欄位W。並且這個表沒有索引。
  2. 從 words 表中,按主鍵順序取出所有的 word 值。對於每一個 word 值,呼叫 rand() 函式生成一個大於 0 小於 1 的隨機小數,並把這個隨機小數和 word分別存入臨時表的 R 和 W 欄位中,到此,掃描行數是 10000。
  3. 接著在沒有索引的記憶體臨時表上,按欄位R排序。
  4. 初始化sort_buffer。sort_buffer和臨時表一直兩個欄位。
  5. 臨時表全表掃描去取R值和位置資訊(稍後解釋),放入sort_buffer兩個欄位,此時掃描行數增加10000,變成20000。
  6. 在sort_buffer對R值排序。
  7. 排序完成取前三行,總掃描行數變成20003行。

通過慢查詢日誌(slow log)可以看到

# Query_time: 0.900376  Lock_time: 0.000347 Rows_sent: 3 Rows_examined: 20003
SET timestamp=1541402277;
select word from words order by rand() limit 3;

流程圖如下,圖中的pos就是位置資訊,類似主鍵id:
在這裡插入圖片描述

磁碟臨時表

tmp_table_size限制了記憶體臨時表的大小,預設16M。如果記憶體大於tmp_table_size,則會轉成磁碟臨時表。
磁碟臨時表使用的引擎預設是 InnoDB,由引數 internal_tmp_disk_storage_engine 控制。
復現:

set tmp_table_size=1024;
set sort_buffer_size=32768;
set max_length_for_sort_data=16;
/* 開啟 optimizer_trace,只對本執行緒有效 */
SET optimizer_trace='enabled=on'; 
/* 執行語句 */
select word from words order by rand() limit 3;
/* 檢視 OPTIMIZER_TRACE 輸出 */
SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G

部分PTIMIZER_TRACE 的結果如下:
在這裡插入圖片描述
由於max_length_for_sort_data 設定成 16,所以參與排序的是R欄位和row_id欄位組成的行。
R欄位8個位元組,rowid是6個位元組,總行數10000,這樣總共140000位元組,超過sort_buffer_size,但沒有使用臨時檔案。
是因為MySQL 5.6 版本引入的一個新的排序演算法,即:優先佇列排序演算法。
因為sql只需要去R值最小的3個rowid,所以不需要將所有的資料排序,所以沒有使用臨時檔案(歸併排序演算法)。

優先順序佇列演算法執行流程如下:

  1. 先取前三行,構造成一個堆。
  2. 取下一行(R’,rowid’),跟當前堆最大的R比較,如果 R’小於 R,把這個 (R,rowid)從堆中去掉,換成 (R’,rowid’);
  3. 重複第 2 步,直到第 10000 個 (R’,rowid’) 完成比較。

上圖OPTIMIZER_TRACE 結果中,filesort_priority_queue_optimization 這個部分的chosen=true,就表示使用了優先佇列排序演算法。

select city,name,age from t where city='杭州' order by name limit 1000;

這句sql沒有使用優先佇列排序演算法,因為limit 1000堆大小超過了sort_buffer_size 大小。

隨機排序方法

隨機選取一個word值。

mysql> select max(id),min(id) into @M,@N from t ;
set @X= floor((@M-@N+1)*rand() + @N);
select * from t where id >= @X limit 1;

取 max(id) 和 min(id) 都是不需要掃描索引,而第三步的 select 也可以用索引快速定位,可以認為就只掃描了3行。
但id中間可能有空洞,所以不同行概率不一樣。
所以,為了得到嚴格隨機的結果,你可以用下面這個流程:

mysql> select count(*) into @C from t;
set @Y = floor(@C * rand());
set @sql = concat("select * from t limit ", @Y, ",1");
prepare stmt from @sql;
execute stmt;
DEALLOCATE prepare stmt;

MySQL 處理 limit Y,1 的做法就是按順序一個一個地讀出來,丟掉前 Y 個,然後把下一個記錄作為返回結果,此這一步需要掃描 Y+1 行。
再加上,第一步掃描的 C 行,總共需要掃描 C+Y+1 行,執行代價比第一個隨機演算法的代價要高。
另外一個思路:

mysql> select count(*) into @C from t;
set @Y1 = floor(@C * rand());
set @Y2 = floor(@C * rand());
set @Y3 = floor(@C * rand());
select * from t limit @Y11// 在應用程式碼裡面取 Y1、Y2、Y3 值,拼出 SQL 後執行
select * from t limit @Y21select * from t limit @Y31