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子句中加入变量值
相关文章
相关标签/搜索