Mysql自定義變量的使用

用戶自定義變量是一個容易被遺忘的MySQL特性,可是若是能用的好,發揮其潛力,在某些場景能夠寫出很是高效的查詢語句。在查詢中混合使用過程化和關係化邏輯的時候,自定義變量可能會很是有用。單純的關係查詢將全部的東西都當成無序的數據集合,而且一次性操做它們。MySQL則採用了更加程序化的處理方式。MySQL的這種方式有它的弱點,但若是可以熟練地掌握,則會發現其強大之處,而用戶自定義變量也能夠給這種方式帶來很大的幫助。mysql

用戶自定義變量是一個用來存儲內容的臨時容器,在鏈接MySQL的整個過程當中都存在,可使用下面的SET和SELECT語句來定義它們:sql

mysql> SET @one := 1;
mysql> SET @min_actor := (SELECT MIN(actor_id) FROM sakila.actor);
mysql> SET @last_week := CURRENT_DATE - INTERVAL 1 WEEK;

而後能夠在任何可使用表達式的地方使用這些自定義變量:緩存

SELECT ... WHERE col <= @last_week;網絡

在瞭解自定義變量的強大以前,咱們先來看看它自身的一些屬性和限制,看看在哪些場景下咱們不能使用用戶自定義變量:函數

  • 使用自定義變量的查詢,沒法使用查詢緩存
  • 不能再使用常量或者標識符的地方使用自定義變量,例如表名、列名和LIMIT子句中。
  • 用戶自定義變量的生命週期是在一個鏈接中有效,因此不能用它們來作鏈接間的通訊。
  • 若是使用鏈接池或者持久化鏈接,自定義變量可能讓看起來毫無關係的代碼發生交互。
  • 自定義變量的類型是一個動態類型。
  • MySQL優化器在某些場景下可能會將這些變量優化掉,這可能致使代碼不按預想的方式運行。
  • 賦值的順序和賦值的時間點並不老是固定的,這依賴於優化器的決定。
  • 賦值符號 :=的優先級很是低,因此須要注意,賦值表達式應該使用明確的括號。
  • 使用未定義變量不會產生任何語法錯誤,若是沒有意識到這一點,很是容易犯錯。

優化排名語句

使用自定義變量的一個特性是你能夠在給一個變量賦值的同時使用這個變量,即「左值」特性。例如:性能

mysql> SET @rownum := 0;
mysql> SELECT actor_id, @rownum := @rownum + 1 AS rownum
FROM actor order by actor_id LIMIT 3;
    +----------+--------+
    | actor_id | rownum |
    +----------+--------+
    |        1 |      1 |
    |        2 |      2 |
    |        3 |      3 |
    +----------+--------+

這個例子的實際意義並不大,它只是實現了一個和該表主鍵同樣的列。不過,咱們能夠把這看成一個排名。如今咱們來看一個更復雜的用法。咱們先編寫一個查詢獲取演過最多電影的前10位演員,而後根據他們的出演電影次數作一個排名,若是出演的電影數量同樣,則排名相同。咱們先編寫一個查詢,返回每一個演員參演電影的數量。優化

mysql> SET @curr_cnt := 0, @prev_cnt := 0, @rank := 0;
mysql> SELECT actor_id, COUNT(*) as cnt
    -> FROM film_actor
    -> GROUP BY actor_id
    -> ORDER BY cnt DESC
    -> LIMIT 10;
    +----------+-----+
    | actor_id | cnt |
    +----------+-----+
    |      107 |  42 |
    |      102 |  41 |
    |      198 |  40 |
    |      181 |  39 |
    |       23 |  37 |
    |       81 |  36 |
    |       37 |  35 |
    |      106 |  35 |
    |       60 |  35 |
    |       13 |  35 |
    +----------+-----+

如今咱們再把排名加上去,這裏看到有四個演員都參演了35部電影,因此他們的排名應該是相同的。咱們使用三個變量來實現:一個用來記錄當前的排名,一個用來記錄前一個演員的排名,還有一個用來記錄當前演員參演的電影數量。只有當前演員參演的電影的數量和前一個演員不一樣時,排名才變化。咱們試試下面的寫法:code

mysql> SELECT actor_id,
    -> @curr_cnt := COUNT(*) AS cnt,
    -> @rank     := IF(@prev_cnt <> @curr_cnt, @rank + 1, @rank) AS rank,
    -> @prev_cnt := @curr_cnt AS dummy
    -> FROM film_actor
    -> GROUP BY actor_id
    -> ORDER BY cnt DESC
    -> LIMIT 10;
    +----------+-----+------+-------+
    | actor_id | cnt | rank | dummy |
    +----------+-----+------+-------+
    |      107 |  42 |    0 |     0 |
    |      102 |  41 |    0 |     0 |
    |      198 |  40 |    0 |     0 |
    |      181 |  39 |    0 |     0 |
    |       23 |  37 |    0 |     0 |
    |       81 |  36 |    0 |     0 |
    |      106 |  35 |    0 |     0 |
    |       60 |  35 |    0 |     0 |
    |       13 |  35 |    0 |     0 |
    |       37 |  35 |    0 |     0 |
    +----------+-----+------+-------+

咱們發現跟咱們設想的不太同樣。這裏,經過EXPLAIN咱們看到將會使用臨時表和文件排序,因此多是因爲變量賦值的時間和咱們預料的不一樣。
使用SQL語句生成排名值一般須要作兩次計算,例如,須要額外計算一次出演過相同數量電影的演員有哪些。使用變量則可一次完成---這對性能是一個很大的提高。
針對這個案例,另外一個簡單的方案是在FROM子句中使用子查詢生成的一箇中間的臨時表:排序

mysql> SELECT actor_id,
    -> @curr_cnt := cnt AS cnt,
    -> @rank     := IF(@prev_cnt <> @curr_cnt, @rank + 1, @rank) AS rank,
    -> @prev_cnt := @curr_cnt AS dummy
    -> FROM (
    -> SELECT actor_id, COUNT(*) AS cnt
    -> FROM film_actor
    -> GROUP BY actor_id
    -> ORDER BY cnt DESC
    -> LIMIT 10
    -> ) as der;
+----------+-----+------+-------+
| actor_id | cnt | rank | dummy |
+----------+-----+------+-------+
|      107 |  42 |    1 |    42 |
|      102 |  41 |    2 |    41 |
|      198 |  40 |    3 |    40 |
|      181 |  39 |    4 |    39 |
|       23 |  37 |    5 |    37 |
|       81 |  36 |    6 |    36 |
|       37 |  35 |    7 |    35 |
|      106 |  35 |    7 |    35 |
|       60 |  35 |    7 |    35 |
|       13 |  35 |    7 |    35 |
+----------+-----+------+-------+

避免重複查詢剛剛更新的數據

若是在更新行的同窗又但願得到該行的信息,避免重複查詢,能夠用變量巧妙的實現。例如,咱們的一個客戶但願可以更高效地更新一條記錄的時間戳,同時但願查詢當前記錄中存放的時間戳是什麼。簡單地,能夠用下面的代碼來實現:生命週期

UPDATE t1 SET lastUpdated = NOW() WHERE id = 1;
SELECT lastUpdated FROM t1 WHERE id = 1;

使用變量,咱們能夠按以下方式重寫查詢:

UPDATE t1 SET lastUpdated = NOW() WHERE id = 1 AND @now := NOW();
SELECT @now;

上面看起來仍然須要兩個查詢,須要兩次網絡來回,可是這裏第二個查詢無需訪問數據表,因此會快不少。

統計更新和插入的數量

INSERT INTO t1(c1, c2) VALUES(4, 4), (2, 1), (3, 1)
ON DUPLICATE KEY UPDATE
    c1 = VALUES(c1) + (0 * (@x := @x + 1));

當每次因爲衝突致使更新時對變量@x自增一次,而後表達式乘以0讓其不影響更新的內容,另外,MySQL的協議會返回被更改的總行數,因此不須要單獨統計。

肯定取值的順序

使用用戶自定義變量的一個最多見的問題就是沒有注意到在賦值和讀取變量的時候多是在查詢的不一樣階段。例如,在SELECT子句中進行賦值而後再WHERE子句中讀取變量,則可能變量取值並不如你所想:

mysql> SET @rownum := 0;
mysql> SELECT actor_id, @rownum := @rownum + 1 AS cnt
    -> FROM actor
    -> WHERE @rownum <= 1;
+----------+------+
| actor_id | cnt  |
+----------+------+
|       58 |    1 |
|       92 |    2 |
+----------+------+

由於WHERE和SELECT是在查詢執行的不一樣階段被執行的。若是在查詢中再加入ORDER BY的話,結果可能會更不一樣;

mysql> SET @rownum := 0;
mysql> SELECT actor_id, @rownum := @rownum + 1 AS cnt
    -> FROM actor
    -> WHERE @rownum <= 1
    -> ORDER BY first_name;

這是由於ORDER BY 引入了文件排序,而WHERE條件是在文件排序操做以前取值的,因此這條查詢會返回表中的所有記錄。解決這個問題的辦法是讓變量的賦值和取值發生在執行查詢的同一階段:

mysql> SET @rownum := 0;
mysql> SELECT actor_id, @rownum AS rownum
    -> FROM actor
    -> WHERE (@rownum := @rownum + 1) <= 1;
+----------+--------+
| actor_id | rownum |
+----------+--------+
|       58 |      1 |
+----------+--------+

編寫偷懶的UNION

假設須要編寫一個UNION查詢,其第一個子查詢做爲分支條件先執行,若是找到了匹配的行,則跳過第二個分支。例如先在一個頻繁訪問的表查找熱數據,找不到再去另一個較少訪問的表查找冷數據。

SELECT id FROM users WHERE id = 123;
UNION ALL
SELECT id FROM users_archived WHERE id = 123;

上面的查詢能夠工做,可是不管第一個表找沒找到,都會在第二個表再找一次,若是使用變量的話能夠很好地規避這個問題。

SELECT GREATEST(@found := -1, id) AS id, 'users' AS which_tbl
FROM users WHERE id = 1
UNION ALL
    SELECT id, 'users_archived'
    FROM users_archived WHERE id = 1 AND @found IS NULL
UNION ALL   
    SELECT 1, 'reset' FROM DUAL WHERE (@found := NULL) IS NOT NULL;

用戶自定義變量的其餘用處

經過一些實踐,能夠了解全部用戶自定義變量可以作的有趣的事情,例以下面這些用法:

  • 查詢運行時計算總數和平均值
  • 模擬GROUP語句中的函數FIRST()和LAST()
  • S對大量數據作一些數據計算
  • 計算一個大表的MD5散列值
  • 編寫一個樣本處理函數
  • 模擬讀/寫遊標
  • 在SHOW語句的WHERE子句中加入變量值
相關文章
相關標籤/搜索