MySQL 8.4 Reference Manual  /  ...  /  How MySQL Partitioning Handles NULL

26.2.7 MySQL 分区如何处理 NULL 值

MySQL 的分区功能允许使用 NULL 作为分区表达式的值,无论该表达式是基于列值还是用户提供的表达式。尽管可以将 NULL 用作必须否则产生整数的表达式的值,但请记住,NULL 不是一个数字。MySQL 的分区实现将 NULL 视为任何非 NULL 值都小于的值,就像 ORDER BY 一样。

这意味着不同类型的分区处理 NULL 的方式可能会产生你不期望的行为,如果你没有为此做好准备。因此,本节将讨论每种 MySQL 分区类型在确定行应该存储到哪个分区时是如何处理 NULL 值的,并为每种类型提供示例。

RANGE 分区中的 NULL 处理。  如果您将一个值为 NULL 的行插入到使用范围分区的表中,该列用于确定分区的值是 NULL,那么该行将被插入到最低分区。考虑以下两个在名为 p 的数据库中创建的表:

mysql> CREATE TABLE t1 (
    ->     c1 INT,
    ->     c2 VARCHAR(20)
    -> )
    -> PARTITION BY RANGE(c1) (
    ->     PARTITION p0 VALUES LESS THAN (0),
    ->     PARTITION p1 VALUES LESS THAN (10),
    ->     PARTITION p2 VALUES LESS THAN MAXVALUE
    -> );
Query OK, 0 rows affected (0.09 sec)

mysql> CREATE TABLE t2 (
    ->     c1 INT,
    ->     c2 VARCHAR(20)
    -> )
    -> PARTITION BY RANGE(c1) (
    ->     PARTITION p0 VALUES LESS THAN (-5),
    ->     PARTITION p1 VALUES LESS THAN (0),
    ->     PARTITION p2 VALUES LESS THAN (10),
    ->     PARTITION p3 VALUES LESS THAN MAXVALUE
    -> );
Query OK, 0 rows affected (0.09 sec)

您可以使用以下查询对 PARTITIONS 表中的信息来查看这些两个 CREATE TABLE 语句创建的分区:

mysql> SELECT TABLE_NAME, PARTITION_NAME, TABLE_ROWS, AVG_ROW_LENGTH, DATA_LENGTH
     >   FROM INFORMATION_SCHEMA.PARTITIONS
     >   WHERE TABLE_SCHEMA = 'p' AND TABLE_NAME LIKE 't_';
+------------+----------------+------------+----------------+-------------+
| TABLE_NAME | PARTITION_NAME | TABLE_ROWS | AVG_ROW_LENGTH | DATA_LENGTH |
+------------+----------------+------------+----------------+-------------+
| t1         | p0             |          0 |              0 |           0 |
| t1         | p1             |          0 |              0 |           0 |
| t1         | p2             |          0 |              0 |           0 |
| t2         | p0             |          0 |              0 |           0 |
| t2         | p1             |          0 |              0 |           0 |
| t2         | p2             |          0 |              0 |           0 |
| t2         | p3             |          0 |              0 |           0 |
+------------+----------------+------------+----------------+-------------+
7 rows in set (0.00 sec)

(有关该表的更多信息,请参阅第 28.3.21 节,“INFORMATION_ SCHEMA PARTITIONS 表”。) 现在,让我们为每个表填充一个包含分区键列的 NULL 值的单行,并使用一对 SELECT 语句验证这些行被插入:

mysql> INSERT INTO t1 VALUES (NULL, 'mothra');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t2 VALUES (NULL, 'mothra');
Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM t1;
+------+--------+
| id   | name   |
+------+--------+
| NULL | mothra |
+------+--------+
1 row in set (0.00 sec)

mysql> SELECT * FROM t2;
+------+--------+
| id   | name   |
+------+--------+
| NULL | mothra |
+------+--------+
1 row in set (0.00 sec)

您可以通过重新运行之前的查询并检查输出来查看哪些分区用于存储插入的行:

mysql> SELECT TABLE_NAME, PARTITION_NAME, TABLE_ROWS, AVG_ROW_LENGTH, DATA_LENGTH
     >   FROM INFORMATION_SCHEMA.PARTITIONS
     >   WHERE TABLE_SCHEMA = 'p' AND TABLE_NAME LIKE 't_';
+------------+----------------+------------+----------------+-------------+
| TABLE_NAME | PARTITION_NAME | TABLE_ROWS | AVG_ROW_LENGTH | DATA_LENGTH |
+------------+----------------+------------+----------------+-------------+
| t1         | p0             |          1 |             20 |          20 |
| t1         | p1             |          0 |              0 |           0 |
| t1         | p2             |          0 |              0 |           0 |
| t2         | p0             |          1 |             20 |          20 |
| t2         | p1             |          0 |              0 |           0 |
| t2         | p2             |          0 |              0 |           0 |
| t2         | p3             |          0 |              0 |           0 |
+------------+----------------+------------+----------------+-------------+
7 rows in set (0.01 sec)

您还可以通过删除这些分区并再次运行 SELECT 语句来证明这些行被存储在每个表的最低编号分区中:

mysql> ALTER TABLE t1 DROP PARTITION p0;
Query OK, 0 rows affected (0.16 sec)

mysql> ALTER TABLE t2 DROP PARTITION p0;
Query OK, 0 rows affected (0.16 sec)

mysql> SELECT * FROM t1;
Empty set (0.00 sec)

mysql> SELECT * FROM t2;
Empty set (0.00 sec)

(有关 ALTER TABLE ... DROP PARTITION 的更多信息,请参阅第 15.1.9 节,“ALTER TABLE 语句”。)

NULL 在使用 SQL 函数的分区表达式中也被处理得一样。在我们定义一个使用类似于以下 CREATE TABLE 语句创建的表时:

CREATE TABLE tndate (
    id INT,
    dt DATE
)
PARTITION BY RANGE( YEAR(dt) ) (
    PARTITION p0 VALUES LESS THAN (1990),
    PARTITION p1 VALUES LESS THAN (2000),
    PARTITION p2 VALUES LESS THAN MAXVALUE
);

正如 MySQL 中其他函数一样,YEAR(NULL) 返回 NULL。具有 dt 列值为 NULL 的行将被视为分区表达式评估的值小于任何其他值,因此会被插入到分区 p0 中。

LIST 分区中的 NULL 处理。  使用列表分区的表允许 NULL 值,只要其中一个分区是使用包含 NULL 的值列表定义的。反过来说,使用 LIST 分区的表如果没有明确在值列表中包含 NULL,则拒绝导致分区表达式为 NULL 的行,如下所示:

mysql> CREATE TABLE ts1 (
    ->     c1 INT,
    ->     c2 VARCHAR(20)
    -> )
    -> PARTITION BY LIST(c1) (
    ->     PARTITION p0 VALUES IN (0, 3, 6),
    ->     PARTITION p1 VALUES IN (1, 4, 7),
    ->     PARTITION p2 VALUES IN (2, 5, 8)
    -> );
Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO ts1 VALUES (9, 'mothra');
ERROR 1504 (HY000): Table has no partition for value 9

mysql> INSERT INTO ts1 VALUES (NULL, 'mothra');
ERROR 1504 (HY000): Table has no partition for value NULL

只有具有 c1 值在 0 到 8 之间(含)的行可以被插入到 ts1NULL 就像数字 9 一样,位于这个范围之外。我们可以创建名为 ts2ts3 的表,其中包含值列表中的 NULL,如下所示:

mysql> CREATE TABLE ts2 (
    ->     c1 INT,
    ->     c2 VARCHAR(20)
    -> )
    -> PARTITION BY LIST(c1) (
    ->     PARTITION p0 VALUES IN (0, 3, 6),
    ->     PARTITION p1 VALUES IN (1, 4, 7),
    ->     PARTITION p2 VALUES IN (2, 5, 8),
    ->     PARTITION p3 VALUES IN (NULL)
    -> );
Query OK, 0 rows affected (0.01 sec)

mysql> CREATE TABLE ts3 (
    ->     c1 INT,
    ->     c2 VARCHAR(20)
    -> )
    -> PARTITION BY LIST(c1) (
    ->     PARTITION p0 VALUES IN (0, 3, 6),
    ->     PARTITION p1 VALUES IN (1, 4, 7, NULL),
    ->     PARTITION p2 VALUES IN (2, 5, 8)
    -> );
Query OK, 0 rows affected (0.01 sec)

当定义值列表用于分区时,您可以(应该)将 NULL 视为任何其他值。例如,VALUES IN (NULL)VALUES IN (1, 4, 7, NULL) 都是有效的,同时也包括 VALUES IN (1, NULL, 4, 7)VALUES IN (NULL, 1, 4, 7) 等等。您可以将一个包含 c1 列为 NULL 的行插入到每个表 ts2ts3 中:

mysql> INSERT INTO ts2 VALUES (NULL, 'mothra');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO ts3 VALUES (NULL, 'mothra');
Query OK, 1 row affected (0.00 sec)

通过向 INFORMATION_ SCHEMA.PARTITIONS 发送适当的查询,您可以确定哪些分区用于存储刚插入的行(假设,正如前面的例子中所示,分区表是在 p 数据库中创建的):

mysql> SELECT TABLE_NAME, PARTITION_NAME, TABLE_ROWS, AVG_ROW_LENGTH, DATA_LENGTH
     >   FROM INFORMATION_SCHEMA.PARTITIONS
     >   WHERE TABLE_SCHEMA = 'p' AND TABLE_NAME LIKE 'ts_';
+------------+----------------+------------+----------------+-------------+
| TABLE_NAME | PARTITION_NAME | TABLE_ROWS | AVG_ROW_LENGTH | DATA_LENGTH |
+------------+----------------+------------+----------------+-------------+
| ts2        | p0             |          0 |              0 |           0 |
| ts2        | p1             |          0 |              0 |           0 |
| ts2        | p2             |          0 |              0 |           0 |
| ts2        | p3             |          1 |             20 |          20 |
| ts3        | p0             |          0 |              0 |           0 |
| ts3        | p1             |          1 |             20 |          20 |
| ts3        | p2             |          0 |              0 |           0 |
+------------+----------------+------------+----------------+-------------+
7 rows in set (0.01 sec)

如前文在本节中所示,您还可以通过删除这些分区并执行一个 SELECT 来验证哪些分区用于存储行。

处理 NULL 的 HASH 和 KEY 分区方式。 对于使用 HASHKEY 分区的表,NULL 被处理得不太一样。在这种情况下,如果分区表达式返回一个 NULL 值,它将被视为其返回值为零。我们可以通过查看创建使用 HASH 分区的表并填充包含适当值的记录对文件系统的影响来验证这个行为。假设您在 p 数据库中有一个名为 th 的表,它使用以下语句创建:

mysql> CREATE TABLE th (
    ->     c1 INT,
    ->     c2 VARCHAR(20)
    -> )
    -> PARTITION BY HASH(c1)
    -> PARTITIONS 2;
Query OK, 0 rows affected (0.00 sec)

可以通过以下查询查看属于该表的分区:

mysql> SELECT TABLE_NAME,PARTITION_NAME,TABLE_ROWS,AVG_ROW_LENGTH,DATA_LENGTH
     >   FROM INFORMATION_SCHEMA.PARTITIONS
     >   WHERE TABLE_SCHEMA = 'p' AND TABLE_NAME ='th';
+------------+----------------+------------+----------------+-------------+
| TABLE_NAME | PARTITION_NAME | TABLE_ROWS | AVG_ROW_LENGTH | DATA_LENGTH |
+------------+----------------+------------+----------------+-------------+
| th         | p0             |          0 |              0 |           0 |
| th         | p1             |          0 |              0 |           0 |
+------------+----------------+------------+----------------+-------------+
2 rows in set (0.00 sec)

TABLE_ROWS 为每个分区都是 0。现在,将一个 c1 列值为 NULL 和 0 的行插入到 th 中,并验证这些行被插入,如下所示:

mysql> INSERT INTO th VALUES (NULL, 'mothra'), (0, 'gigan');
Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM th;
+------+---------+
| c1   | c2      |
+------+---------+
| NULL | mothra  |
+------+---------+
|    0 | gigan   |
+------+---------+
2 rows in set (0.01 sec)

请记住,对于任何整数 NNULL MOD N 总是返回 NULL。对于使用 HASHKEY 分区的表,这个结果在确定正确分区时被视为 0。再次查看信息架构中的 PARTITIONS 表,我们可以看到两行都被插入到了分区 p0 中:

mysql> SELECT TABLE_NAME, PARTITION_NAME, TABLE_ROWS, AVG_ROW_LENGTH, DATA_LENGTH
     >   FROM INFORMATION_SCHEMA.PARTITIONS
     >   WHERE TABLE_SCHEMA = 'p' AND TABLE_NAME ='th';
+------------+----------------+------------+----------------+-------------+
| TABLE_NAME | PARTITION_NAME | TABLE_ROWS | AVG_ROW_LENGTH | DATA_LENGTH |
+------------+----------------+------------+----------------+-------------+
| th         | p0             |          2 |             20 |          20 |
| th         | p1             |          0 |              0 |           0 |
+------------+----------------+------------+----------------+-------------+
2 rows in set (0.00 sec)

通过重复最后一个例子,使用 PARTITION BY KEY 而不是 PARTITION BY HASH 在表定义中,您可以验证 NULL 对于这种类型的分区也被视为 0。