1. 程式人生 > >MySQL中的非確定性函式(即rand)可能會讓您感到驚訝

MySQL中的非確定性函式(即rand)可能會讓您感到驚訝

使用sysbench處理測試用例,我遇到了這個問題:

的MySQL> 選擇 *  sbtest1 其中 ID = ROUND(RAND()* 100000);
+ ------ + -------- + --------------------------------- -------------------------------------------------- -------------------------------------- + ----------- -------------------------------------------------- +
| id | k | c | 墊|
+ ------ + -------- + --------------------------------- -------------------------------------------------- -------------------------------------- + ----------- -------------------------------------------------- +
|  179 | 499871 | 09833083632 - 34593445843 - 98203182724 - 77632394229 - 31240034691 - 22855093589
- 98577647071 - 95962909368 - 34814236148 - 76937610370 | 62233363025 - 41327474153 - 95482195752 - 11204169522 - 13131828192 |
| 1606 | 502031 | 81212399253 - 12831141664 - 41940957498 - 63947990218 - 16408477860 - 15124776228 - 42269003436 - 07293216458 - 45216889819 - 75452278174 | 25423822623 - 32136209218 - 60113604068
- 17409951653 - 00581045257 |
+ ------ + -------- + --------------------------------- -------------------------------------------------- -------------------------------------- + ----------- -------------------------------------------------- +
2 0.30秒)

我真的很驚訝。首先,最重要的是,id是主鍵,rand()函式應該只生成一個值。怎麼回來兩行?其次,為什麼響應時間為0.30秒?對於主鍵訪問而言,這似乎非常高。

進一步觀察:

CREATE  TABLE  `sbtest1`
  `id`  int11NOT  NULL  AUTO_INCREMENT
  `k`  INT11NOT  NULL  DEFAULT  '0' 
  `c`  char120NOT  NULL  DEFAULT  ''
  `pad`  char60NOT  NULL  DEFAULT  ''
  PRIMARY  KEY`id`),
  KEY  `k_1``k`
ENGINE = InnoDB  AUTO_INCREMENT = 1000001  DEFAULT  CHARSET = latin1
的MySQL> 解釋 選擇 *  sbtest1 其中 ID = ROUND(RAND()* 100000);
+ ---- + ------------- + --------- + ------------ + ------ + --------------- + ------ + --------- + ------ + -------- +  - --------- ------------- + +
| id | select_type |     | 分割槽 | 型別| possible_keys | 關鍵   | key_len | ref | 行| 過濾| 額外的|
+ ---- + ------------- + --------- + ------------ + ------ + --------------- + ------ + --------- + ------ + -------- +  - --------- ------------- + +
|  1 | 簡單| sbtest1 | NULL        | 所有   | NULL           | NULL | NULL     | NULL | 986400 |    10.00 | 使用 何處 |
+ ---- + ------------- + --------- + ------------ + ------ + --------------- + ------ + --------- + ------ + -------- +  - --------- ------------- + +

所以它是一個主鍵,但MySQL不使用索引,它返回兩行。這是一個錯誤嗎?

確定性與非確定性功能

原來它根本不是一個bug。這是MySQL非常合乎邏輯的行為,但它不是我們所期望的。首先,為什麼全表掃描?好吧,rand()是非確定性函式。這意味著我們不知道它將提前返回什麼,實際上,這正是rand()的目的 - 返回一個隨機值。在這種情況下,每次評估每行的函式並比較結果是合乎邏輯。即在我們的情況下:

  1. 讀取第1行,獲取id的值,評估RAND()的值,比較
  2. 繼續使用與剩餘行相同的演算法。

換句話說,由於rand()的值事先未知(未評估),因此我們不能使用索引。

在這種情況下 - rand()函式 - 我們有另一個有趣的結果。對於具有auto_increment主鍵的較大表,匹配rand()值和auto_increment值的概率較高,因此我們可以返回多行。事實上,如果我們從頭開始閱讀整個表並繼續將auto_inc序列與“擲骰子”進行比較,我們可以獲得許多行。

這種行為完全違反直覺。然而,對我而言,這也是唯一正確的行為。

我們希望執行查詢之前評估rand()函式。這實際上可以通過將rand()賦給變數來實現:

的MySQL> 設定 @id = ROUND(RAND()* 100000); 選擇 @id ;  sbtest1 選擇 * 其中 id = @id ;
查詢正常,0行受影響(0.00秒)
+ ------ +
| @id   |
+ ------ +
| 6068 |
+ ------ +
1    集合0.00秒)
+ ------ + -------- + --------------------------------- -------------------------------------------------- -------------------------------------- + ----------- -------------------------------------------------- +
| id | k | c | 墊|
+ ------ + -------- + --------------------------------- -------------------------------------------------- -------------------------------------- + ----------- -------------------------------------------------- +
| 6068 | 502782 | 84971025350 - 12845068791 - 61736600622 - 38249796467 - 85136778555 - 74134284808 - 24438972515 - 17848828748 - 86869270666 - 01547789681 | 17507194006 - 70651503059 - 23792945260 - 94159543806 - 65683812344 |
+ ------ + -------- + --------------------------------- -------------------------------------------------- -------------------------------------- + ----------- -------------------------------------------------- +
1    集合0.00秒)
mysql> 解釋 select * from sbtest1 ,其中 id = @id ;
+ ---- + ------------- + --------- + ------------ + ------- + --------------- + --------- + --------- + ------- + ----- -  + ---------- + ------- +
| id | select_type |     | 分割槽 | 型別| possible_keys | 關鍵      | key_len | ref | 行| 過濾| 額外的|
+ ---- + ------------- + --------- + ------------ + ------- + --------------- + --------- + --------- + ------- + ----- -  + ---------- + ------- +
|  1 | 簡單| sbtest1 | NULL        | const | 主要        | 主要 | 4        | const |    1 |   100.00 | NULL   |
+ ---- + ------------- + --------- + ------------ + ------- + --------------- + --------- + --------- + ------- + ----- -  + ---------- + ------- +
1    集合1個警告(0.01秒)

這符合我們的期望。

提交了(至少)兩個錯誤報告,討論非常有趣:

  1. 標量函式中使用的rand()返回多行
  2. 使用ROUND(RAND())在PK上選擇錯誤

其他資料庫

我想看看它在其他SQL資料庫中是如何工作的。在PostgreSQL中,行為與MySQL完全相同:

postgres = #select * from t2 where id = cast(random()* 10000 as int);
  id | C
------ + ---------
 4093 | asdasda
 9378 | asdasda
2排)
postgres = #select * from t2 where id = cast(random()* 10000 as int);
  id | C
------ + ---------
 5988 | asdasda
 6674 | asdasda
2排)
postgres = #explain select * from t2 where id = cast(random()* 10000 as int);
                             查詢計劃
-------------------------------------------------- ------------------
SEQ掃描 T2(成本= 0.00。0.159837 0.60行數= 1米寬度= 12
   過濾器:(id =((random()* '10000' :: double precision)):: integer)
2排)

而SQLite似乎有所不同,事先評估random()函式:

sqlite> select * from t2 where id = cast(abs(CAST(random()AS  REAL))/ 92233720368547 as  int);
16239 | asdsadasdsa
sqlite> select * from t2 where id = cast(abs(CAST(random()AS  REAL))/ 92233720368547 as  int);
32910 | asdsadasdsa
sqlite> select * from t2 where id = cast(abs(CAST(random()AS  REAL))/ 92233720368547 as  int);
58658 | asdsadasdsa
sqlite> 解釋 select * from t2 where id = cast(abs(CAST(random()AS  REAL))/ 92233720368547 as  int);
addr操作碼p1 p2 p3 p4 p5   評論
---- ------------- ---- ---- ---- -------------   -  ------ -------
0      初始化            0      12     0                     00   開始  12
1      OpenRead        0      30182   0      2               00   root = 30182 iDb = 0 ; T2
2      功能       0 0      0      3      隨機(000   r [ 3 ] = func(r [ 0 ])
3      施放            3      69     0                     00   親和力(r [ 3 ])
4      功能       0 0      3      2      abs(101   r [ 2 ] = func(r [ 3 ])
5      除以          4      2      1                     00   r [ 1 ] = r [ 2 ] / r [ 4 ]
6      施放            1      68     0                     00   親和力(r [ 1 ])
7      SeekRowid       0      11     1                     00   intkey = r [ 1 ]; PK
8      複製            1      5      0                     00   r [ 5 ] = r [ 1 ]
9               0      1      6                     00   r [ 6 ] = t2 .c
10     ResultRow       5      2      0                     00   輸出= R [ 5. 0.6 ]
11     Halt            0      0      0                     00
12     交易    0      0      2      0               01使用   StmtJournal = 0
13     Int64           0      4      0      92233720368547   00 r [ 4 ] = 92233720368547
14     轉到            0      1      0                     00

結論

在“where”條件下使用MySQL非確定性函式時要小心 - rand()是最有趣的例子 - 因為它們的行為可能讓你感到驚訝。許多人認為這是一個應該修復的錯誤。請在評論中告訴我:您認為這是一個錯誤(以及為什麼)?我也有興趣知道它在其他非開源資料庫(Microsoft SQL Server,Oracle等)中的工作原理

PS:  最後,我有一個“聰明”的想法 - 如果我通過使用deterministic關鍵字“欺騙”MySQL會怎麼樣?

MySQL儲存函式:確定性與非確定性

所以,我想知道如果它們被分配了“確定性”和“不確定性”關鍵字,它如何與MySQL儲存函式一起工作。首先,我想“欺騙”mysql並將確定性傳遞給儲存的函式,但在裡面使用rand()。好吧,這不是你真正想要做的!

DELIMITER $
CREATE FUNCTION myrand()RETURNS INT
    確定性
開始
RETURN輪(RAND()* 10000,0);
結束$
DELIMITER;

從MySQL關於MySQL儲存例程的手冊我們可以讀到:

對例程性質的評估基於建立者的“誠實”:MySQL不檢查宣告的例程DETERMINISTIC是否沒有產生非確定性結果的語句。但是,錯誤地說明例程可能會影響結果或影響效能。宣告非確定性例程DETERMINISTIC可能會導致優化程式選擇錯誤的執行計劃,從而導致意外結果。聲明確定性例程NONDETERMINISTIC可能會導致不使用可用的優化,從而降低效能。

結果很有趣:

mysql> select myrand();
+ ---------- +
| myrand()|
+ ---------- +
|     4202 |
+ ---------- +
1    集合0.00秒)
mysql> select myrand();
+ ---------- +
| myrand()|
+ ---------- +
|     7548 |
+ ---------- +
1    集合0.00秒)
mysql> 解釋 select * from t2 ,其中 id = myrand()\ G.
*************************** 1。  ******************** *******
           id:1
  select_type:SIMPLE
        tableNULL
   分割槽NULL
         type:NULL
possible_keys:NULL
          keyNULL
      key_len:NULL
          ref:NULL
         行:NULL
     已過濾:NULL
        附加:不可能WHERE發現閱讀常數
1    集合1個警告(0.00秒)
mysql> show  warnings ;
+ ------- + ------ + ---------------------------------- ---------------------------------------------- +
| 等級 | 程式碼 | 訊息|
+ ------- + ------ + ---------------------------------- ---------------------------------------------- +
| 注意| 1003 | / *選擇#1 * /  選擇 '2745'  AS  'id` 'asasdas'  AS  `C`   `test``t2`  其中 0 |
+ ------- + ------ + ---------------------------------- ---------------------------------------------- +
1    集合0.00秒)
mysql> select * from t2 where id = 4202 ;
+ ------ + --------- +
| id | c |
+ ------ + --------- +
| 4202 | asasdas |
+ ------ + --------- +
1    集合0.00秒)
mysql> select * from t2 ,其中 id = 2745 ;
+ ------ + --------- +
| id | c |
+ ------ + --------- +
| 2745 | asasdas |
+ ------ + --------- +
1    集合0.00秒)

所以MySQL優化器檢測到了這個問題(不知何故)。

如果我使用NOT DETERMINISTIC關鍵字,那麼MySQL的工作方式與使用rand()函式時相同:

DELIMITER $
CREATE  FUNCTION myrand2()RETURNS  INT
   不是 決定性的
開始
 RETURN輪(RAND()* 100000);
結束$
DELIMITER ;
mysql> 解釋 select * from t2 ,其中 id = myrand2()\ G.
*************************** 1。  ******************** *******
           id:1
  select_type:SIMPLE
        :t2
   分割槽NULL
         型別:全部
possible_keys:NULL
          keyNULL
      key_len:NULL
          ref:NULL
         行:262208
     過濾:10.00
        額外:使用 在哪裡
1    集合1個警告(0.00秒)