【MySQL】使用者自定義變數
阿新 • • 發佈:2022-01-06
使用者自定義變數是一個用來儲存內容的臨時容器,在連線MySQL的整個過程中都存在。可以
SET
和SELECT
語句來定義。
SET @one := 1;
SET @min_actor := (SELECT MIN(actor_id) FROM sakila.actor);
SET @last_week := CURRENT_DATE-INTERNAL 1 WEEK;
MySQL 自定義變數的侷限性:
- 使用自定義變數的查詢,無法使用查詢快取。
- 不能再使用常量或者識別符號的地方使用自定義變數,例如表名、列名和LIMIT子句中。
- 使用者自定義變數的生命週期是在一個連線中有效,所以不能用它們來做連線間的通訊。
- 如果使用連線池或持久連線,自定義變數會導致你的程式碼與互動隔離,這出現的時候可能是程式碼的 bug 或連線池的 bug,但是是可能發生的。
- 在 MySQL 5.0以前的版本中是大小寫敏感的,因此要注意(在 MySQL 5.0以後已經不區分大小寫了)。
- 不能顯式地宣告自定義變數的型別。確定未定義變數的具體型別的時機在不同MySQL版本中也可能不一樣。如果你希望變數是整數型別,那麼最好在初始化的時候就賦值為0,如果希望是浮點型則賦值為0.0,如果希望是字串則賦值為’’,使用者自定義變數的型別在賦值的時候會改變。MySQL的使用者自定義變數是一個動態型別。
- MySQL優化器在某項些場景下可能會將這些變數優化掉,這可能導致程式碼不按預想的方式執行。
- 賦值的順序和賦值的時間點並不總是固定的,這依賴於優化器的決定,實際情況可能很讓人困惑。
- 賦值符號:=的優先順序非常低,所以需要注意,賦值表示式應該使用明確的括號。
- 使用未定義變數不會產生任何語法錯誤,如果沒有意識到這一點,非常容易犯錯。
使用者自定義變數的用法:
- 查詢執行時計算總數和平均值。
- 模擬GROUP BY語句中的函式FIRST()和LAST()。
- 對大量資料做一些資料計算。
- 計算一個大表的MD5雜湊值。
- 編寫一個樣本處理函式,當樣本中的數值超過某個邊界值的時候將其變為0。
- 模擬讀/寫遊標。
- 在SHOW語句的WHERE子句中加入變數值。
案例:
1、優化排名
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 | +----------+--------+
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 |
+----------+-----+
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 |
+----------+-----+------+-------+
2、避免重複查詢剛剛更新的資料
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;
實際專案的案例,由於User_A
關聯的ORGAN_A
可以為空,所以每條資料查詢獲取要提前將變數@lxDm
置空,避免上一條資料影響。
SELECT (case when LEAST('1',@lxDm:='') IS NOT NULL THEN '2021' ELSE '2021' END) ND,
(SELECT @lxDm:=lxDm FROM ORGAN_A WHERE id = organ_id LIMIT 1) as lxDm,
(SELECT dict_name FROM G_DICT WHERE dict_id = @lxDm) as lxDmStr
FROM User_A
WHERE ....
3、統計更新和插入的數量
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的協議會返回被更改的總行數,所以不需要單獨統計。
4、確定取值的順序
使用使用者自定義變數的一個最常見的問題就是沒有注意到在賦值和讀取變數的時候可能是在查詢的不同階段。例如,在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 |
+----------+--------+
在不改變排序的情況下賦值。
mysql> SET @rownum := 0;
mysql> SELECT actor_id,first_name,@rownum AS rownum
-> FROM actor
-> WHERE @rownum <= 1
-> ORDER BY first_name,LEAST(0,@rownum := @rownum + 1);
5、編寫偷懶的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;
好學若飢,謙卑若愚