当依靠未记录的行为来确定 MySQL 为 GROUP BY 操作中的隐藏列选择的值时,存在哪些极端情况?

egg*_*yal 6 mysql group-by

GROUP BYHAVING与隐藏列,MySQL手册文件(强调):

MySQL 扩展了 的使用,GROUP BY以便选择列表可以引用GROUP BY子句中未命名的非聚合列。这意味着前面的查询在 MySQL 中是合法的。您可以使用此功能通过避免不必要的列排序和分组来获得更好的性能。但是,这主要在GROUP BY每个组中未命名的每个非聚合列中的所有值都相同时很有用。服务器可以自由地从每个组中选择任何值,因此除非它们相同,否则选择的值是不确定的。

尽管开发人员发出了这个明确的警告,但有些人继续依赖未记录的行为来确定将从隐藏列中选择的值。

特别是,MySQL似乎经常从每个组中选择“第一”记录(其中“第一”的概念本身没有记录,例如某些存储引擎上的最旧记录或根据应用于物化表的某种排序顺序从子查询)。我已经看到这个被利用来检索,例如,分组最大值:

SELECT * FROM (
  SELECT * FROM my_table ORDER BY sort_col DESC
) t GROUP BY group_col
Run Code Online (Sandbox Code Playgroud)

为了完整起见,可以通过简单的连接以标准和文档化的方式完成相同的操作:

SELECT * FROM my_table NATURAL JOIN (
  SELECT   group_col, MAX(sort_col) sort_col
  FROM     my_table
  GROUP BY group_col
) t
Run Code Online (Sandbox Code Playgroud)

我相信人们永远不应该依赖无证行为,因为可能存在无法预见的极端情况导致该行为中断。例如,在GROUP BY用索引满足操作时,MySQL 对结果进行排序,从而可能选择一个意外的值。

还有哪些极端情况可以打破这种行为?或者它对于生产系统是否足够可靠?

Rol*_*DBA 1

我在想NATURAL JOIN你刚才使用的例子

SELECT * FROM my_table NATURAL JOIN (
  SELECT   group_col, MAX(sort_col) sort_col
  FROM     my_table
  GROUP BY group_col
) t
Run Code Online (Sandbox Code Playgroud)

如果你转向另一种类型JOIN并强加WHERE,尽管不明智地依赖无证行为,但订单可能会在没有警告的情况下来来去去。GROUP BY

对于这个例子,我将

  • 使用Windows 7
  • 在 Windows 上使用 MySQL 5.5.12-log
  • 创建一些示例数据
  • 强加一个LEFT JOIN不带WHERE子句的
  • 强加一个LEFT JOIN带有 WHERE 子句的

对于数据库环境

mysql> select version();
+------------+
| version()  |
+------------+
| 5.5.12-log |
+------------+
1 row in set (0.00 sec)

mysql> show variables like '%version_co%';
+-------------------------+------------------------------+
| Variable_name           | Value                        |
+-------------------------+------------------------------+
| version_comment         | MySQL Community Server (GPL) |
| version_compile_machine | x86                          |
| version_compile_os      | Win64                        |
+-------------------------+------------------------------+
3 rows in set (0.00 sec)

mysql>
Run Code Online (Sandbox Code Playgroud)

使用此脚本生成示例数据

DROP DATABASE IF EXISTS eggyal;
CREATE DATABASE eggyal;
USE eggyal
CREATE TABLE groupby
(
    id int not null auto_increment,
    num int,
    primary key (id)
);
INSERT INTO groupby (num) VALUES
(floor(rand() * unix_timestamp())),(floor(rand() * unix_timestamp())),
(floor(rand() * unix_timestamp())),(floor(rand() * unix_timestamp())),
(floor(rand() * unix_timestamp())),(floor(rand() * unix_timestamp())),
(floor(rand() * unix_timestamp())),(floor(rand() * unix_timestamp()));
INSERT INTO groupby (num) SELECT num FROM groupby;
SELECT * FROM groupby;
Run Code Online (Sandbox Code Playgroud)

以及这两个查询用于测试GROUP BY后续使用;

SELECT * FROM groupby A LEFT JOIN
(
    SELECT   num, MAX(id) id
    FROM     groupby
    GROUP BY num
) B USING (id);
SELECT * FROM groupby A LEFT JOIN
(
    SELECT   num, MAX(id) id
    FROM     groupby
    GROUP BY num
) B USING (id) WHERE B.num IS NOT NULL;
Run Code Online (Sandbox Code Playgroud)

让我们测试一下GROUP BY结果的持久性;

步骤 01:创建示例数据

mysql> DROP DATABASE IF EXISTS eggyal;
Query OK, 1 row affected (0.09 sec)

mysql> CREATE DATABASE eggyal;
Query OK, 1 row affected (0.00 sec)

mysql> USE eggyal
Database changed
mysql> CREATE TABLE groupby
    -> (
    ->     id int not null auto_increment,
    ->     num int,
    ->     primary key (id)
    -> );
Query OK, 0 rows affected (0.07 sec)

mysql> INSERT INTO groupby (num) VALUES
    -> (floor(rand() * unix_timestamp())),(floor(rand() * unix_timestamp())),
    -> (floor(rand() * unix_timestamp())),(floor(rand() * unix_timestamp())),
    -> (floor(rand() * unix_timestamp())),(floor(rand() * unix_timestamp())),
    -> (floor(rand() * unix_timestamp())),(floor(rand() * unix_timestamp()));
Query OK, 8 rows affected (0.06 sec)
Records: 8  Duplicates: 0  Warnings: 0

mysql> INSERT INTO groupby (num) SELECT num FROM groupby;
Query OK, 8 rows affected (0.05 sec)
Records: 8  Duplicates: 0  Warnings: 0

mysql> SELECT * FROM groupby;
+----+------------+
| id | num        |
+----+------------+
|  1 |  269529129 |
|  2 |  387090406 |
|  3 | 1126864683 |
|  4 |  411160755 |
|  5 |   29173595 |
|  6 |  266349579 |
|  7 | 1244227156 |
|  8 |    6231766 |
|  9 |  269529129 |
| 10 |  387090406 |
| 11 | 1126864683 |
| 12 |  411160755 |
| 13 |   29173595 |
| 14 |  266349579 |
| 15 | 1244227156 |
| 16 |    6231766 |
+----+------------+
16 rows in set (0.00 sec)
Run Code Online (Sandbox Code Playgroud)

步骤 02:LEFT JOIN不带WHERE子句使用

mysql> SELECT * FROM groupby A LEFT JOIN
    -> (
    ->     SELECT   num, MAX(id) id
    ->     FROM     groupby
    ->     GROUP BY num
    -> ) B USING (id);
+----+------------+------------+
| id | num        | num        |
+----+------------+------------+
|  1 |  269529129 |       NULL |
|  2 |  387090406 |       NULL |
|  3 | 1126864683 |       NULL |
|  4 |  411160755 |       NULL |
|  5 |   29173595 |       NULL |
|  6 |  266349579 |       NULL |
|  7 | 1244227156 |       NULL |
|  8 |    6231766 |       NULL |
|  9 |  269529129 |  269529129 |
| 10 |  387090406 |  387090406 |
| 11 | 1126864683 | 1126864683 |
| 12 |  411160755 |  411160755 |
| 13 |   29173595 |   29173595 |
| 14 |  266349579 |  266349579 |
| 15 | 1244227156 | 1244227156 |
| 16 |    6231766 |    6231766 |
+----+------------+------------+
16 rows in set (0.00 sec)

mysql>
Run Code Online (Sandbox Code Playgroud)

步骤 03:LEFT JOINWHERE子句一起使用

mysql> SELECT * FROM groupby A LEFT JOIN
    -> (
    ->     SELECT   num, MAX(id) id
    ->     FROM     groupby
    ->     GROUP BY num
    -> ) B USING (id) WHERE B.num IS NOT NULL;
+----+------------+------------+
| id | num        | num        |
+----+------------+------------+
| 16 |    6231766 |    6231766 |
| 13 |   29173595 |   29173595 |
| 14 |  266349579 |  266349579 |
|  9 |  269529129 |  269529129 |
| 10 |  387090406 |  387090406 |
| 12 |  411160755 |  411160755 |
| 11 | 1126864683 | 1126864683 |
| 15 | 1244227156 | 1244227156 |
+----+------------+------------+
8 rows in set (0.00 sec)

mysql>
Run Code Online (Sandbox Code Playgroud)

分析

看了上面的结果,这里有两个问题:

  • 为什么 aLEFT JOIN保留排序方式id
  • 为什么使用 a 会WHERE强制重新排序?
    • 是在 JOIN 阶段吗?
    • 查询优化器是否会提前查看子查询的顺序还是忽略它?

没有人预见到任何这些影响,因为显式子句的行为是由查询优化器的隐式行为所依赖的。

结论

从我的角度来看,极端情况只能是外部性质的。有鉴于此,开发人员必须愿意GROUP BY结合以下十二(12)个方面全面评估结果:

  1. 聚合函数
  2. 子查询的使用
  3. JOINs条款
  4. WHERE条款
  5. 没有明确ORDER BY子句的结果排序顺序
  6. 使用旧版 MySQL GA 版本的查询结果
  7. 使用较新的 MySQL 测试版查询结果
  8. 当前的SQL_MODE设置my.cnf
  9. 编译代码的操作系统
  10. 可能是join_buffer_size的大小相对于其对查询优化器的影响
  11. 可能是sort_buffer_size的大小相对于其对查询优化器的影响
  12. 可能正在使用的存储引擎(MyISAM 与 InnoDB)

这里是要记住的关键点:任何适用于特定环境中的查询的 MySQL 实例本身就是一个特殊情况。一旦更改十二 (12) 个评估方面中的一个或多个,极端情况就会崩溃,特别是考虑到前九 (9) 个方面。