MySQL8-中文参考-二十七-
MySQL8 中文参考(二十七)
15.2.13.2 连接子句
MySQL 支持以下JOIN
语法用于SELECT
语句的table_references
部分以及多表DELETE
和UPDATE
语句:
*table_references:*
*escaped_table_reference* [, *escaped_table_reference*] ...
*escaped_table_reference*: {
*table_reference*
| { OJ *table_reference* }
}
*table_reference*: {
*table_factor*
| *joined_table*
}
*table_factor*: {
*tbl_name* [PARTITION (*partition_names*)]
[[AS] *alias*] [*index_hint_list*]
| [LATERAL] *table_subquery* [AS] *alias* [(*col_list*)]
| ( *table_references* )
}
*joined_table*: {
*table_reference* {[INNER | CROSS] JOIN | STRAIGHT_JOIN} *table_factor* [*join_specification*]
| *table_reference* {LEFT|RIGHT} [OUTER] JOIN *table_reference* *join_specification*
| *table_reference* NATURAL [INNER | {LEFT|RIGHT} [OUTER]] JOIN *table_factor*
}
*join_specification*: {
ON *search_condition*
| USING (*join_column_list*)
}
*join_column_list*:
*column_name* [, *column_name*] ...
*index_hint_list*:
*index_hint* [, *index_hint*] ...
*index_hint*: {
USE {INDEX|KEY}
[FOR {JOIN|ORDER BY|GROUP BY}] ([*index_list*])
| {IGNORE|FORCE} {INDEX|KEY}
[FOR {JOIN|ORDER BY|GROUP BY}] (*index_list*)
}
*index_list*:
*index_name* [, *index_name*] ...
表引用也被称为连接表达式。
表引用(当它引用分区表时)可以包含一个PARTITION
子句,包括一个逗号分隔的分区、子分区列表,或两者。此选项跟随表名之后,并在任何别名声明之前。此选项的效果是仅从列出的分区或子分区中选择行。未在列表中命名的任何分区或子分区将被忽略。有关更多信息和示例,请参见第 26.5 节,“分区选择”。
与标准 SQL 相比,MySQL 中table_factor
的语法得到了扩展。标准只接受table_reference
,而不是在括号中包含它们的列表。
如果将table_reference
项目列表中的每个逗号视为等同于内连接,则这是一种保守的扩展。例如:
SELECT * FROM t1 LEFT JOIN (t2, t3, t4)
ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)
等同于:
SELECT * FROM t1 LEFT JOIN (t2 CROSS JOIN t3 CROSS JOIN t4)
ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)
在 MySQL 中,JOIN
、CROSS JOIN
和INNER JOIN
是语法上的等效(它们可以互相替换)。在标准 SQL 中,它们不是等效的。INNER JOIN
与ON
子句一起使用,否则使用CROSS JOIN
。
通常情况下,在仅包含内连接操作的连接表达式中,括号可以忽略。MySQL 还支持嵌套连接。参见第 10.2.1.8 节,“嵌套连接优化”。
可以指定索引提示以影响 MySQL 优化器如何使用索引。有关更多信息,请参见第 10.9.4 节,“索引提示”。优化器提示和optimizer_switch
系统变量是影响优化器使用索引的其他方法。请参见第 10.9.3 节,“优化器提示”和第 10.9.2 节,“可切换优化”。
下面的列表描述了编写连接时需要考虑的一般因素:
-
可以使用
*
tbl_name* AS *
alias_name*
或tbl_name alias_name
为表引用取别名:SELECT t1.name, t2.salary FROM employee AS t1 INNER JOIN info AS t2 ON t1.name = t2.name; SELECT t1.name, t2.salary FROM employee t1 INNER JOIN info t2 ON t1.name = t2.name;
-
table_subquery
也被称为FROM
子句中的派生表或子查询。参见第 15.2.15.8 节,“派生表”。这样的子查询必须包含一个别名,以给子查询结果一个表名,并且可以选择在括号中包含一个表列名列表。以下是一个简单的示例:SELECT * FROM (SELECT 1, 2, 3) AS t1;
-
在单个连接中引用的最大表数为 61。这包括通过将
FROM
子句中的派生表和视图合并到外部查询块中处理的连接(参见 Section 10.2.2.4, “Optimizing Derived Tables, View References, and Common Table Expressions with Merging or Materialization”)。 -
在没有连接条件的情况下,
INNER JOIN
和,
(逗号)在语义上是等效的:两者都会在指定的表之间产生笛卡尔积(即,第一个表中的每一行都与第二个表中的每一行连接)。然而,逗号运算符的优先级低于
INNER JOIN
、CROSS JOIN
、LEFT JOIN
等。如果在存在连接条件时混合使用逗号连接和其他连接类型,则可能会出现类似Unknown column '*
col_name*' in 'on clause'
的错误。有关处理此问题的信息稍后在本节中给出。 -
与
ON
一起使用的search_condition
是可以在WHERE
子句中使用的任何条件表达式的形式。通常,ON
子句用于指定如何连接表,而WHERE
子句用于限制结果集中包含哪些行。 -
如果在
LEFT JOIN
中右表的ON
或USING
部分中没有匹配的行,则会使用所有列均设置为NULL
的行作为右表。您可以利用这一点找到一个表中没有对应的另一个表中的行:SELECT left_tbl.* FROM left_tbl LEFT JOIN right_tbl ON left_tbl.id = right_tbl.id WHERE right_tbl.id IS NULL;
此示例查找
left_tbl
中所有具有不在right_tbl
中存在的id
值的行(即,所有在right_tbl
中没有对应行的left_tbl
中的所有行)。参见 Section 10.2.1.9, “Outer Join Optimization”。 -
USING(*
join_column_list*)
子句命名了两个表中必须存在的列的列表。如果表a
和b
都包含列c1
、c2
和c3
,则以下连接将比较来自两个表的对应列:a LEFT JOIN b USING (c1, c2, c3)
-
两个表的
NATURAL [LEFT] JOIN
被定义为与使用命名了两个表中所有列的USING
子句的INNER JOIN
或LEFT JOIN
在���义上等效。 -
RIGHT JOIN
的工作方式类似于LEFT JOIN
。为了保持代码在各种数据库中的可移植性,建议您使用LEFT JOIN
而不是RIGHT JOIN
。 -
在连接语法描述中显示的
{ OJ ... }
语法仅用于与 ODBC 的兼容性。语法中的大括号应该按照字面意义写入;它们不是在其他语法描述中使用的元语法。SELECT left_tbl.* FROM { OJ left_tbl LEFT OUTER JOIN right_tbl ON left_tbl.id = right_tbl.id } WHERE right_tbl.id IS NULL;
您可以在
{ OJ ... }
中使用其他类型的连接,例如INNER JOIN
或RIGHT OUTER JOIN
。这有助于与一些第三方应用程序的兼容性,但不是官方的 ODBC 语法。 -
STRAIGHT_JOIN
类似于JOIN
,不同之处在于左表始终在右表之前读取。这可以用于那些(少数)情况下,连接优化器以次优顺序处理表的情况。
一些连接示例:
SELECT * FROM table1, table2;
SELECT * FROM table1 INNER JOIN table2 ON table1.id = table2.id;
SELECT * FROM table1 LEFT JOIN table2 ON table1.id = table2.id;
SELECT * FROM table1 LEFT JOIN table2 USING (id);
SELECT * FROM table1 LEFT JOIN table2 ON table1.id = table2.id
LEFT JOIN table3 ON table2.id = table3.id;
根据 SQL:2003 标准处理NATURAL
连接和带有USING
的连接,包括外连接变体:
-
NATURAL
连接的冗余列不会出现。考虑以下一组语句:CREATE TABLE t1 (i INT, j INT); CREATE TABLE t2 (k INT, j INT); INSERT INTO t1 VALUES(1, 1); INSERT INTO t2 VALUES(1, 1); SELECT * FROM t1 NATURAL JOIN t2; SELECT * FROM t1 JOIN t2 USING (j);
在第一个
SELECT
语句中,列j
出现在两个表中,因此成为连接列,因此,根据标准 SQL,它应该在输出中只出现一次,而不是两次。类似地,在第二个 SELECT 语句中,列j
在USING
子句中命名,应该在输出中只出现一次,而不是两次。因此,这些语句产生这个输出:
+------+------+------+ | j | i | k | +------+------+------+ | 1 | 1 | 1 | +------+------+------+ +------+------+------+ | j | i | k | +------+------+------+ | 1 | 1 | 1 | +------+------+------+
根据标准 SQL 进行冗余列消除和列排序,产生这个显示顺序:
-
首先,按照它们在第一个表中出现的顺序,合并两个连接表的共同列
-
第二,第一个表中独有的列,按照它们在该表中出现的顺序
-
第三,第二个表中独有的列,按照它们在该表中出现的顺序
替换两个共同列的单个结果列是使用合并操作定义的。也就是说,对于两个
t1.a
和t2.a
,生成的单个连接列a
被定义为a = COALESCE(t1.a, t2.a)
,其中:COALESCE(x, y) = (CASE WHEN x IS NOT NULL THEN x ELSE y END)
如果连接操作是任何其他连接,则连接的结果列由连接表的所有列的串联组成。
合并列的定义的一个结果是,对于外连接,如果两个列中的一个始终为
NULL
,则合并列包含非NULL
列的值。如果两个列都不是NULL
或都是NULL
,那么两个共同列具有相同的值,因此选择哪个作为合并列的值并不重要。解释这个的一个简单方法是将外连接的合并列表示为JOIN
的内表的共同列。假设表t1(a, b)
和表t2(a, c)
具有以下内容:t1 t2 ---- ---- 1 x 2 z 2 y 3 w
然后,对于这个连接,列
a
包含t1.a
的值:mysql> SELECT * FROM t1 NATURAL LEFT JOIN t2; +------+------+------+ | a | b | c | +------+------+------+ | 1 | x | NULL | | 2 | y | z | +------+------+------+
相比之下,对于这个连接,列
a
包含t2.a
的值。mysql> SELECT * FROM t1 NATURAL RIGHT JOIN t2; +------+------+------+ | a | c | b | +------+------+------+ | 2 | z | y | | 3 | w | NULL | +------+------+------+
将这些结果与使用
JOIN ... ON
的等效查询进行比较:mysql> SELECT * FROM t1 LEFT JOIN t2 ON (t1.a = t2.a); +------+------+------+------+ | a | b | a | c | +------+------+------+------+ | 1 | x | NULL | NULL | | 2 | y | 2 | z | +------+------+------+------+
mysql> SELECT * FROM t1 RIGHT JOIN t2 ON (t1.a = t2.a); +------+------+------+------+ | a | b | a | c | +------+------+------+------+ | 2 | y | 2 | z | | NULL | NULL | 3 | w | +------+------+------+------+
-
-
USING
子句可以重写为比较相应列的ON
子句。然而,尽管USING
和ON
类似,但它们并不完全相同。考虑以下两个查询:a LEFT JOIN b USING (c1, c2, c3) a LEFT JOIN b ON a.c1 = b.c1 AND a.c2 = b.c2 AND a.c3 = b.c3
就确定哪些行满足连接条件而言,这两个连接在语义上是相同的。
关于确定要显示哪些列进行
SELECT *
扩展,这两个连接在语义上并不相同。USING
连接选择对应列的合并值,而ON
连接选择所有表中的所有列。对于USING
连接,SELECT *
选择这些值:COALESCE(a.c1, b.c1), COALESCE(a.c2, b.c2), COALESCE(a.c3, b.c3)
对于
ON
连接,SELECT *
选择这些值:a.c1, a.c2, a.c3, b.c1, b.c2, b.c3
在内连接中,
COALESCE(a.c1, b.c1)
与a.c1
或b.c1
相同,因为两列的值相同。在外连接(如LEFT JOIN
)中,两列中的一个可以是NULL
。该列将从结果中省略。 -
ON
子句只能引用其操作数。示例:
CREATE TABLE t1 (i1 INT); CREATE TABLE t2 (i2 INT); CREATE TABLE t3 (i3 INT); SELECT * FROM t1 JOIN t2 ON (i1 = i3) JOIN t3;
该语句因为
i3
是t3
中的列,而不是ON
子句的操作数而失败,会出现Unknown column 'i3' in 'on clause'
错误。要使连接能够被处理,请将语句重写如下:SELECT * FROM t1 JOIN t2 JOIN t3 ON (i1 = i3);
-
JOIN
比逗号运算符(,
)具有更高的优先级,因此连接表达式t1, t2 JOIN t3
被解释为(t1, (t2 JOIN t3))
,而不是((t1, t2) JOIN t3)
。这会影响使用ON
子句的语句,因为该子句只能引用连接操作数中的列,而优先级会影响这些操作数的解释。示例:
CREATE TABLE t1 (i1 INT, j1 INT); CREATE TABLE t2 (i2 INT, j2 INT); CREATE TABLE t3 (i3 INT, j3 INT); INSERT INTO t1 VALUES(1, 1); INSERT INTO t2 VALUES(1, 1); INSERT INTO t3 VALUES(1, 1); SELECT * FROM t1, t2 JOIN t3 ON (t1.i1 = t3.i3);
JOIN
优先于逗号运算符,因此ON
子句的操作数为t2
和t3
。因为t1.i1
不是任何操作数中的列,结果是一个Unknown column 't1.i1' in 'on clause'
错误。要使连接能够被处理,可以使用以下策略之一:
-
使用括号明确地将前两个表分组,以便
ON
子句的操作数为(t1, t2)
和t3
:SELECT * FROM (t1, t2) JOIN t3 ON (t1.i1 = t3.i3);
-
避免使用逗号运算符,改用
JOIN
代替:SELECT * FROM t1 JOIN t2 JOIN t3 ON (t1.i1 = t3.i3);
相同的优先级解释也适用于混合逗号运算符与
INNER JOIN
、CROSS JOIN
、LEFT JOIN
和RIGHT JOIN
的语句,所有这些连接比逗号运算符具有更高的优先级。 -
-
与 SQL:2003 标准相比,MySQL 的一个扩展是允许您对
NATURAL
或USING
连接的共同(合并的)列进行限定,而标准则不允许。
15.2.14 使用 UNION、INTERSECT 和 EXCEPT 的集合操作
-
结果集列名和数据类型
-
使用 TABLE 和 VALUES 语句进行集合操作
-
使用 DISTINCT 和 ALL 的集合操作
-
使用 ORDER BY 和 LIMIT 的集合操作
-
集合操作的限制
SQL 集合操作将多个查询块的结果合并为单个结果。查询块,有时也称为简单表,是任何返回结果集的 SQL 语句,例如 SELECT
。MySQL 8.0(8.0.19 及更高版本)还支持 TABLE
和 VALUES
语句。有关这些语句的详细信息,请参见本章其他部分中的各自描述。
SQL 标准定义了以下三种集合操作:
-
UNION
:将两个查询块的所有结果合并为单个结果,省略任何重复项。 -
INTERSECT
:仅合并两个查询块结果中共有的行,省略任何重复项。 -
EXCEPT
:对于两个查询块A
和B
,返回A
中不在B
中出现的所有结果,省略任何重复项。(一些数据库系统,如 Oracle,使用
MINUS
作为此运算符的名称。MySQL 不支持此功能。)
MySQL 长期支持 UNION
;MySQL 8.0 添加了对 INTERSECT
和 EXCEPT
的支持(MySQL 8.0.31 及更高版本)。
每个集合运算符都支持 ALL
修饰符。当 ALL
关键字跟随一个集合运算符时,这会导致结果中包含重复项。有关更多信息和示例,请参阅涵盖各个运算符的以下部分。
所有三个集合运算符还支持 DISTINCT
关键字,用于在结果中消除重复项。由于这是集合运算符的默认行为,通常不需要显式指定 DISTINCT
。
一般来说,查询块和集合操作可以以任意数量和顺序组合。这里展示了一个大大简化的表示:
*query_block* [*set_op* *query_block*] [*set_op* *query_block*] ...
*query_block*:
SELECT | TABLE | VALUES
*set_op*:
UNION | INTERSECT | EXCEPT
这可以更准确地表示,并更详细地描述如下:
*query_expression*:
[*with_clause*] /* WITH clause */
*query_expression_body*
[*order_by_clause*] [*limit_clause*] [*into_clause*]
*query_expression_body*:
*query_term*
| *query_expression_body* UNION [ALL | DISTINCT] *query_term*
| *query_expression_body* EXCEPT [ALL | DISTINCT] *query_term*
*query_term*:
*query_primary*
| *query_term* INTERSECT [ALL | DISTINCT] *query_primary*
*query_primary*:
*query_block*
| '(' *query_expression_body* [*order_by_clause*] [*limit_clause*] [*into_clause*] ')'
*query_block*: /* also known as a simple table */
*query_specification* /* SELECT statement */
| *table_value_constructor* /* VALUES statement */
| *explicit_table* /* TABLE statement */
您应该知道INTERSECT
在UNION
或EXCEPT
之前进行评估。这意味着,例如,TABLE x UNION TABLE y INTERSECT TABLE z
总是被评估为TABLE x UNION (TABLE y INTERSECT TABLE z)
。有关更多信息,请参见第 15.2.8 节,“INTERSECT 子句”。
此外,您应该记住,虽然UNION
和INTERSECT
集合运算符是可交换的(顺序不重要),但EXCEPT
不是(操作数的顺序会影响结果)。换句话说,以下所有语句都是正确的:
-
TABLE x UNION TABLE y
和TABLE y UNION TABLE x
产生相同的结果,尽管行的排序可能不同。您可以使用ORDER BY
强制它们相同;请参见联合中的 ORDER BY 和 LIMIT。 -
TABLE x INTERSECT TABLE y
和TABLE y INTERSECT TABLE x
返回相同的结果。 -
TABLE x EXCEPT TABLE y
和TABLE y EXCEPT TABLE x
不会产生相同的结果。请参见第 15.2.4 节,“EXCEPT 子句”,以获取示例。
更多信息和示例可以在接下来的章节中找到。
结果集列名和数据类型
集合操作的结果的列名取自第一个查询块的列名。示例:
mysql> CREATE TABLE t1 (x INT, y INT);
Query OK, 0 rows affected (0.04 sec)
mysql> INSERT INTO t1 VALUES ROW(4,-2), ROW(5,9);
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> CREATE TABLE t2 (a INT, b INT);
Query OK, 0 rows affected (0.04 sec)
mysql> INSERT INTO t2 VALUES ROW(1,2), ROW(3,4);
Query OK, 2 rows affected (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> TABLE t1 UNION TABLE t2;
+------+------+
| x | y |
+------+------+
| 4 | -2 |
| 5 | 9 |
| 1 | 2 |
| 3 | 4 |
+------+------+
4 rows in set (0.00 sec)
mysql> TABLE t2 UNION TABLE t1;
+------+------+
| a | b |
+------+------+
| 1 | 2 |
| 3 | 4 |
| 4 | -2 |
| 5 | 9 |
+------+------+
4 rows in set (0.00 sec)
对于UNION
、EXCEPT
和INTERSECT
查询都是如此。
每个查询块中列的选定位置应具有相同的数据类型。例如,第一个语句选择的第一列应与其他语句选择的第一列具有相同的类型。如果相应结果列的数据类型不匹配,则结果中的列的类型和长度将考虑所有查询块检索的值。例如,结果集中的列长度不受限于第一个语句中的值的长度,如下所示:
mysql> SELECT REPEAT('a',1) UNION SELECT REPEAT('b',20);
+----------------------+
| REPEAT('a',1) |
+----------------------+
| a |
| bbbbbbbbbbbbbbbbbbbb |
+----------------------+
使用TABLE
和VALUES
语句进行集合操作
从 MySQL 8.0.19 开始,您还可以在可以使用等效的SELECT
语句的地方使用TABLE
语句或VALUES
语句。假设表t1
和t2
如下所示创建和填充:
CREATE TABLE t1 (x INT, y INT);
INSERT INTO t1 VALUES ROW(4,-2),ROW(5,9);
CREATE TABLE t2 (a INT, b INT);
INSERT INTO t2 VALUES ROW(1,2),ROW(3,4);
在忽略以VALUES
开头的查询输出中的列名的情况下,以下所有UNION
查询都产生相同的结果:
SELECT * FROM t1 UNION SELECT * FROM t2;
TABLE t1 UNION SELECT * FROM t2;
VALUES ROW(4,-2), ROW(5,9) UNION SELECT * FROM t2;
SELECT * FROM t1 UNION TABLE t2;
TABLE t1 UNION TABLE t2;
VALUES ROW(4,-2), ROW(5,9) UNION TABLE t2;
SELECT * FROM t1 UNION VALUES ROW(4,-2),ROW(5,9);
TABLE t1 UNION VALUES ROW(4,-2),ROW(5,9);
VALUES ROW(4,-2), ROW(5,9) UNION VALUES ROW(4,-2),ROW(5,9);
要强制列名相同,请将左侧的查询块包装在SELECT
语句中,并使用别名,如下所示:
mysql> SELECT * FROM (TABLE t2) AS t(x,y) UNION TABLE t1;
+------+------+
| x | y |
+------+------+
| 1 | 2 |
| 3 | 4 |
| 4 | -2 |
| 5 | 9 |
+------+------+
4 rows in set (0.00 sec)
使用DISTINCT
和ALL
进行集合操作
默认情况下,集合操作的结果中会删除重复行。可选的DISTINCT
关键字具有相同的效果,但使其显式化。使用可选的ALL
关键字,不会删除重复行,结果将包含联合中所有查询的所有匹配行。
你可以在同一查询中混合使用ALL
和DISTINCT
。混合类型的处理方式是,使用DISTINCT
的集合操作会覆盖左侧使用ALL
的任何操作。可以通过在UNION
、INTERSECT
或EXCEPT
后显式地使用DISTINCT
,或者在没有跟随DISTINCT
或ALL
关键字的情况下隐式地使用集合操作来生成DISTINCT
集合。
在 MySQL 8.0.19 及更高版本中,当一个或多个TABLE
语句、VALUES
语句或两者用于生成集合时,集合操作的工作方式相同。
使用ORDER BY
和LIMIT
的集合操作
要对作为联合、交集或其他集合操作的一部分使用的单个查询块应用ORDER BY
或LIMIT
子句,请将查询块括在括号中,并将子句放在括号内,就像这样:
(SELECT a FROM t1 WHERE a=10 AND b=1 ORDER BY a LIMIT 10)
UNION
(SELECT a FROM t2 WHERE a=11 AND b=2 ORDER BY a LIMIT 10);
(TABLE t1 ORDER BY x LIMIT 10)
INTERSECT
(TABLE t2 ORDER BY a LIMIT 10);
对于单个查询块或语句使用ORDER BY
并不意味着结果中行的顺序,因为默认情况下,集合操作生成的行是无序的。因此,在这种情况下,ORDER BY
通常与LIMIT
结合使用,以确定要检索的所选行的子集,即使它并不一定影响这些行在最终结果中的顺序。如果在查询块中没有LIMIT
出现ORDER BY
,则会被优化掉,因为在任何情况下都没有影响。
要对整个集合操作的结果进行排序或限制,请将ORDER BY
或LIMIT
放在最后一个语句之后:
SELECT a FROM t1
EXCEPT
SELECT a FROM t2 WHERE a=11 AND b=2
ORDER BY a LIMIT 10;
TABLE t1
UNION
TABLE t2
ORDER BY a LIMIT 10;
如果一个或多个单独的语句使用了ORDER BY
、LIMIT
或两者,并且另外,你希望对整个结果应用ORDER BY
、LIMIT
或两者,则必须将每个这样的单独语句括在括号中。
(SELECT a FROM t1 WHERE a=10 AND b=1)
EXCEPT
(SELECT a FROM t2 WHERE a=11 AND b=2)
ORDER BY a LIMIT 10;
(TABLE t1 ORDER BY a LIMIT 10)
UNION
TABLE t2
ORDER BY a LIMIT 10;
没有ORDER BY
或LIMIT
子句的语句不需要括号;在刚刚显示的两个语句的第二个语句中用(TABLE t2)
替换TABLE t2
不会改变UNION
的结果。
你也可以在集合操作中使用ORDER BY
和LIMIT
,就像在这个使用mysql客户端的示例中所示的那样:
mysql> VALUES ROW(4,-2), ROW(5,9), ROW(-1,3)
-> UNION
-> VALUES ROW(1,2), ROW(3,4), ROW(-1,3)
-> ORDER BY column_0 DESC LIMIT 3;
+----------+----------+
| column_0 | column_1 |
+----------+----------+
| 5 | 9 |
| 4 | -2 |
| 3 | 4 |
+----------+----------+
3 rows in set (0.00 sec)
(请记住,TABLE
语句和VALUES
语句都不接受WHERE
子句。)
这种类型的ORDER BY
不能使用包含表名的列引用(即以tbl_name
.col_name
格式的名称)。相反,在第一个查询块中提供一个列别名,并在ORDER BY
子句中引用该别名。 (你也可以在ORDER BY
子句中使用列位置引用该列,但这种列位置的使用已被弃用,因此可能在未来的 MySQL 版本中被移除。)
如果要排序的列被别名,ORDER BY
子句必须引用别名,而不是列名。以下两个语句中第一个是允许的,但第二个会因为Unknown column 'a' in 'order clause'
错误而失败:
(SELECT a AS b FROM t) UNION (SELECT ...) ORDER BY b;
(SELECT a AS b FROM t) UNION (SELECT ...) ORDER BY a;
为了使UNION
结果的行由每个查询块检索的行集合依次组成,需要在每个查询块中选择一个额外的列作为排序列,并在最后一个查询块后添加一个按照该列排序的ORDER BY
子句:
(SELECT 1 AS sort_col, col1a, col1b, ... FROM t1)
UNION
(SELECT 2, col2a, col2b, ... FROM t2) ORDER BY sort_col;
为了在各个结果中保持排序顺序,向ORDER BY
子句添加一个次要列:
(SELECT 1 AS sort_col, col1a, col1b, ... FROM t1)
UNION
(SELECT 2, col2a, col2b, ... FROM t2) ORDER BY sort_col, col1a;
使用额外的列还可以让你确定每行来自哪个查询块。额外的列还可以提供其他标识信息,比如指示表名的字符串。
集合操作的限制
MySQL 中的集合操作受到一些限制,这些限制在接下来的几段中描述。
包括SELECT
语句在内的集合操作有以下限制:
-
第一个
SELECT
中的HIGH_PRIORITY
没有效果。任何后续SELECT
中的HIGH_PRIORITY
都会产生语法错误。 -
仅最后一个
SELECT
语句可以使用INTO
子句。然而,整个UNION
结果将被写入INTO
输出目的地。
截至 MySQL 8.0.20,这两个包含INTO
的UNION
变体已被弃用;你应该期待它们在未来的 MySQL 版本中被移除的支持:
-
在查询表达式的尾随查询块中,在
FROM
之前使用INTO
会产生警告。例如:... UNION SELECT * INTO OUTFILE '*file_name*' FROM *table_name*;
-
在查询表达式的括号尾随块中,使用
INTO
(无论其相对于FROM
的位置如何)都会产生警告。例如:... UNION (SELECT * INTO OUTFILE '*file_name*' FROM *table_name*);
这些变体已经被弃用,因为它们很令人困惑,好像它们收集的信息来自命名表而不是整个查询表达式(
UNION
)。
在ORDER BY
子句中使用聚合函数的集合操作将被拒绝,并显示ER_AGGREGATE_ORDER_FOR_UNION
。虽然错误名称可能暗示这仅适用于UNION
查询,但前述情况也适用于EXCEPT
和INTERSECT
查询,如下所示:
mysql> TABLE t1 INTERSECT TABLE t2 ORDER BY MAX(x);
ERROR 3028 (HY000): Expression #1 of ORDER BY contains aggregate function and applies to a UNION, EXCEPT or INTERSECT
锁定子句(比如FOR UPDATE
或LOCK IN SHARE MODE
)适用于其后的查询块。这意味着,在与集合操作一起使用的SELECT
语句中,只有在查询块和锁定子句被括号括起来时才能使用锁定子句。
15.2.15 子查询
15.2.15.1 子查询作为标量操作数
15.2.15.2 使用子查询进行比较
15.2.15.3 使用 ANY、IN 或 SOME 的子查询
15.2.15.4 使用 ALL 的子查询
15.2.15.5 行子查询
15.2.15.6 使用 EXISTS 或 NOT EXISTS 的子查询
15.2.15.7 相关子查询
15.2.15.8 派生表
15.2.15.9 横向派生表
15.2.15.10 子查询错误
15.2.15.11 优化子查询
15.2.15.12 子查询的限制
子查询是另一个语句内的SELECT
语句。
所有 SQL 标准要求的子查询形式和操作都得到支持,以及一些 MySQL 特有的功能。
这里是一个子查询的示例:
SELECT * FROM t1 WHERE column1 = (SELECT column1 FROM t2);
在这个示例中,SELECT * FROM t1 ...
是外部查询(或外部语句),(SELECT column1 FROM t2)
是子查询。我们说子查询嵌套在外部查询中,实际上可以在其他子查询中嵌套子查询,深度相当大。子查询必须始终出现在括号内。
子查询的主要优点是:
-
它们允许查询结构化,以便可以隔离语句的每个部分。
-
它们提供了执行通常需要复杂连接和联合的操作的替代方法。
-
许多人发现子查询比复杂的连接或联合更易读。事实上,正是子查询的创新给人们最初的想法,称早期的 SQL 为“结构化查询语言”。
这里是一个示例语句,展示了 SQL 标准规定的子查询语法的主要要点,并在 MySQL 中得到支持:
DELETE FROM t1
WHERE s11 > ANY
(SELECT COUNT(*) /* no hint */ FROM t2
WHERE NOT EXISTS
(SELECT * FROM t3
WHERE ROW(5*t2.s1,77)=
(SELECT 50,11*s1 FROM t4 UNION SELECT 50,77 FROM
(SELECT * FROM t5) AS t5)));
子查询可以返回标量(单个值)、单行、单列或表(一个或多个列的一个或多行)。这些称为标量、列、行和表子查询。通常只能在特定上下文中使用返回特定类型结果的子查询,如下节所述。
子查询可以在哪些类型的语句中使用没有太多限制。子查询可以包含许多普通SELECT
可以包含的关键字或子句:DISTINCT
、GROUP BY
、ORDER BY
、LIMIT
、连接、索引提示、UNION
构造、注释、函数等等。
从 MySQL 8.0.19 开始,TABLE
和VALUES
语句可以在子查询中使用。使用VALUES
的子查询通常是更冗长的子查询版本,可以使用集合表示法更简洁地重写,或者使用SELECT
或TABLE
语法;假设表ts
是使用语句CREATE TABLE ts VALUES ROW(2), ROW(4), ROW(6)
创建的,这里显示的语句都是等效的:
SELECT * FROM tt
WHERE b > ANY (VALUES ROW(2), ROW(4), ROW(6));
SELECT * FROM tt
WHERE b > ANY (SELECT * FROM ts);
SELECT * FROM tt
WHERE b > ANY (TABLE ts);
TABLE
子查询的示例将在接下来的章节中展示。
子查询的外部语句可以是任何一个:SELECT
, INSERT
, UPDATE
, DELETE
, SET
, 或 DO
。
有关优化器如何处理子查询的信息,请参阅第 10.2.2 节,“优化子查询、派生表、视图引用和公共表达式”。有关子查询使用的限制讨论,包括某些形式子查询语法的性能问题,请参阅第 15.2.15.12 节,“子查询的限制”。
15.2.15.1 标量操作数的子查询
在其最简单的形式中,子查询是返回单个值的标量子查询。标量子查询是一个简单的操作数,你几乎可以在任何地方使用它,只要单列值或字面值是合法的,并且你可以期望它具有所有操作数具有的特征:数据类型、长度、可以为NULL
的指示等。例如:
CREATE TABLE t1 (s1 INT, s2 CHAR(5) NOT NULL);
INSERT INTO t1 VALUES(100, 'abcde');
SELECT (SELECT s2 FROM t1);
这个SELECT
中的子查询返回一个单一值('abcde'
),其数据类型为CHAR
,长度为 5,字符集和排序规则等于CREATE TABLE
时生效的默认值,并指示列中的值可以为NULL
。标量子查询选择的值的可空性不会被复制,因为如果子查询结果为空,结果就是NULL
。对于刚刚显示的子查询,如果t1
为空,结果将是NULL
,即使s2
是NOT NULL
。
在一些情况下,标量子查询无法使用。如果语句只允许使用字面值,你就不能使用子查询。例如,LIMIT
需要字面整数参数,而LOAD DATA
需要字面字符串文件名。你不能使用子查询来提供这些值。
当你在以下部分看到包含相当简陋结构(SELECT column1 FROM t1)
的示例时,请想象你自己的代码包含更加多样化和复杂的结构。
假设我们创建了两个表:
CREATE TABLE t1 (s1 INT);
INSERT INTO t1 VALUES (1);
CREATE TABLE t2 (s1 INT);
INSERT INTO t2 VALUES (2);
然后执行一个SELECT
:
SELECT (SELECT s1 FROM t2) FROM t1;
结果是2
,因为t2
中有一行包含一个值为2
的列s1
。
在 MySQL 8.0.19 及更高版本中,前面的查询也可以这样写,使用TABLE
:
SELECT (TABLE t2) FROM t1;
标量子查询可以是表达式的一部分,但记住括号,即使子查询是为函数提供参数的操作数。例如:
SELECT UPPER((SELECT s1 FROM t1)) FROM t2;
在 MySQL 8.0.19 及更高版本中,可以使用SELECT UPPER((TABLE t1)) FROM t2
获得相同的结果。
原文:
dev.mysql.com/doc/refman/8.0/en/comparisons-using-subqueries.html
15.2.15.2 使用子查询进行比较
子查询最常见的用法是形式:
*non_subquery_operand* *comparison_operator* (*subquery*)
其中comparison_operator
是这些运算符之一:
= > < >= <= <> != <=>
例如:
... WHERE 'a' = (SELECT column1 FROM t1)
MySQL 也允许这种结构:
*non_subquery_operand* LIKE (*subquery*)
曾经,子查询的唯一合法位置是在比较的右侧,您可能仍然会发现一些坚持这一点的旧 DBMS。
这是一个常见形式的子查询比较的例子,使用连接无法完成。它查找表t1
中所有column1
值等于表t2
中最大值的行:
SELECT * FROM t1
WHERE column1 = (SELECT MAX(column2) FROM t2);
这里是另一个例子,这个例子再次使用连接是不可能的,因为它涉及对其中一个表进行聚合。它查找表t1
中包含在给定列中出现两次值的所有行:
SELECT * FROM t1 AS t
WHERE 2 = (SELECT COUNT(*) FROM t1 WHERE t1.id = t.id);
对于将子查询与标量进行比较,子查询必须返回一个标量。对于将子查询与行构造函数进行比较,子查询必须是返回与行构造函数相同数量值的行子查询。参见第 15.2.15.5 节,“行子查询”。
原文:
dev.mysql.com/doc/refman/8.0/en/any-in-some-subqueries.html
15.2.15.3 带有 ANY、IN 或 SOME 的子查询
语法:
*operand* *comparison_operator* ANY (*subquery*)
*operand* IN (*subquery*)
*operand* *comparison_operator* SOME (*subquery*)
其中comparison_operator
是以下这些运算符之一:
= > < >= <= <> !=
ANY
关键字必须跟在比较运算符后面,意思是“如果子查询返回的列中的任何值对比较为TRUE
,则返回TRUE
”。例如:
SELECT s1 FROM t1 WHERE s1 > ANY (SELECT s1 FROM t2);
假设表t1
中有一行包含(10)
。如果表t2
包含(21,14,7)
,则表达式为TRUE
,因为t2
中有一个值7
小于10
。如果表t2
包含(20,10)
,或者表t2
为空,则表达式为FALSE
。如果表t2
包含(NULL,NULL,NULL)
,则表达式为unknown(即NULL
)。
在与子查询一起使用时,IN
是= ANY
的别名。因此,这两个语句是相同的:
SELECT s1 FROM t1 WHERE s1 = ANY (SELECT s1 FROM t2);
SELECT s1 FROM t1 WHERE s1 IN (SELECT s1 FROM t2);
当与表达式列表一起使用时,IN
和= ANY
不是同义词。IN
可以接受表达式列表,但= ANY
不能。参见 Section 14.4.2, “Comparison Functions and Operators”。
NOT IN
不是<> ANY
的别名,而是<> ALL
的别名。参见 Section 15.2.15.4, “Subqueries with ALL”。
SOME
这个词是ANY
的别名。因此,这两个语句是相同的:
SELECT s1 FROM t1 WHERE s1 <> ANY (SELECT s1 FROM t2);
SELECT s1 FROM t1 WHERE s1 <> SOME (SELECT s1 FROM t2);
使用SOME
这个词的情况很少,但这个例子说明了为什么它可能有用。对大多数人来说,英语短语“a is not equal to any b”意味着“没有 b 等于 a”,但这并不是 SQL 语法的意思。该语法的含义是“有一些 b 不等于 a”。使用<> SOME
可以确保每个人都理解查询的真正含义。
从 MySQL 8.0.19 开始,您可以在标量IN
、ANY
或SOME
子查询中使用TABLE
,前提是表只包含一列。如果t2
只有一列,那么本节中先前显示的语句可以写成这样,在每种情况下用TABLE t2
替换SELECT s1 FROM t2
:
SELECT s1 FROM t1 WHERE s1 > ANY (TABLE t2);
SELECT s1 FROM t1 WHERE s1 = ANY (TABLE t2);
SELECT s1 FROM t1 WHERE s1 IN (TABLE t2);
SELECT s1 FROM t1 WHERE s1 <> ANY (TABLE t2);
SELECT s1 FROM t1 WHERE s1 <> SOME (TABLE t2);
15.2.15.4 带有 ALL 的子查询
语法:
*operand* *comparison_operator* ALL (*subquery*)
单词 ALL
必须跟在比较运算符后面,意思是“如果比较对子查询返回的列中的所有值都为 TRUE
,则返回 TRUE
”。例如:
SELECT s1 FROM t1 WHERE s1 > ALL (SELECT s1 FROM t2);
假设表 t1
中包含一行 (10)
。如果表 t2
包含 (-5,0,+5)
,表达式为 TRUE
,因为 10
大于 t2
中的所有三个值。如果表 t2
包含 (12,6,NULL,-100)
,表达式为 FALSE
,因为 t2
中有一个值 12
大于 10
。如果表 t2
包含 (0,NULL,1)
,表达式为 unknown(即 NULL
)。
最后,如果表 t2
是空的,表达式为 TRUE
。因此,当表 t2
为空时,以下表达式为 TRUE
:
SELECT * FROM t1 WHERE 1 > ALL (SELECT s1 FROM t2);
但是当表 t2
为空时,此表达式为 NULL
:
SELECT * FROM t1 WHERE 1 > (SELECT s1 FROM t2);
此外,当表 t2
为空时,以下表达式为 NULL
:
SELECT * FROM t1 WHERE 1 > ALL (SELECT MAX(s1) FROM t2);
一般来说,包含 NULL
值的表 和 空表 是“边缘情况”。在编写子查询时,始终考虑是否考虑了这两种可能性。
NOT IN
是 <> ALL
的别名。因此,这两个语句是相同的:
SELECT s1 FROM t1 WHERE s1 <> ALL (SELECT s1 FROM t2);
SELECT s1 FROM t1 WHERE s1 NOT IN (SELECT s1 FROM t2);
MySQL 8.0.19 支持 TABLE
语句。与 IN
、ANY
和 SOME
一样,你可以在 TABLE
中使用 ALL
和 NOT IN
,前提是满足以下两个条件:
-
子查询中只包含一列
-
子查询不依赖于列表达式
例如,假设表 t2
只包含一列,前面显示的最后两个语句可以这样使用 TABLE t2
编写:
SELECT s1 FROM t1 WHERE s1 <> ALL (TABLE t2);
SELECT s1 FROM t1 WHERE s1 NOT IN (TABLE t2);
无法使用 TABLE t2
编写诸如 SELECT * FROM t1 WHERE 1 > ALL (SELECT MAX(s1) FROM t2);
这样的查询,因为子查询依赖于列表达式。
15.2.15.5 行子查询
标量或列子查询返回单个值或一列值。行子查询是一种返回单行的子查询变体,因此可以返回多个列值。行子查询比较的合法运算符有:
= > < >= <= <> != <=>
以下是两个示例:
SELECT * FROM t1
WHERE (col1,col2) = (SELECT col3, col4 FROM t2 WHERE id = 10);
SELECT * FROM t1
WHERE ROW(col1,col2) = (SELECT col3, col4 FROM t2 WHERE id = 10);
对于这两个查询,如果表t2
包含一个id = 10
的单行,子查询返回一个单行。如果此行的col3
和col4
值等于任何t1
行的col1
和col2
值,则WHERE
表达式为TRUE
,每个查询都返回这些t1
行。如果t2
行的col3
和col4
值不等于任何t1
行的col1
和col2
值,则表达式为FALSE
,查询返回一个空结果集。如果子查询未产生行,则表达式为未知(即NULL
)。如果子查询产生多行,则会出现错误,因为行子查询最多只能返回一行。
有关每个运算符如何用于行比较的信息,请参阅第 14.4.2 节,“比较函数和运算符”。
表达式(1,2)
和ROW(1,2)
有时被称为行构造器。这两者是等价的。子查询返回的行构造器和行必须包含相同数量的值。
行构造器用于与返回两个或更多列的子查询进行比较。当子查询返回单列时,这被视为标量值而不是行,因此不能将行构造器与不返回至少两列的子查询一起使用。因此,以下查询由于语法错误而失败:
SELECT * FROM t1 WHERE ROW(1) = (SELECT column1 FROM t2)
行构造器在其他情境下也是合法的。例如,以下两个语句在语义上是等价的(并且由优化器以相同方式处理):
SELECT * FROM t1 WHERE (column1,column2) = (1,1);
SELECT * FROM t1 WHERE column1 = 1 AND column2 = 1;
以下查询回答了请求,“找到表t1
中存在于表t2
中的所有行”:
SELECT column1,column2,column3
FROM t1
WHERE (column1,column2,column3) IN
(SELECT column1,column2,column3 FROM t2);
有关优化器和行构造器的更多信息,请参阅第 10.2.1.22 节,“行构造器表达式优化”
原文:
dev.mysql.com/doc/refman/8.0/en/exists-and-not-exists-subqueries.html
15.2.15.6 带有 EXISTS 或 NOT EXISTS 的子查询
如果子查询返回任何行,EXISTS *
子查询*
是 TRUE
,而 NOT EXISTS *
子查询*
是 FALSE
。例如:
SELECT column1 FROM t1 WHERE EXISTS (SELECT * FROM t2);
传统上,EXISTS
子查询以 SELECT *
开头,但也可以以 SELECT 5
或 SELECT column1
或任何其他内容开头。MySQL 会忽略这种子查询中的 SELECT
列表,因此不会有任何区别。
对于前面的示例,如果 t2
包含任何行,即使行中只有 NULL
值,EXISTS
条件也为 TRUE
。这实际上是一个不太可能的示例,因为 [NOT] EXISTS
子查询几乎总是包含相关性。以下是一些更现实的示例:
-
一个或多个城市中存在什么样的商店?
SELECT DISTINCT store_type FROM stores WHERE EXISTS (SELECT * FROM cities_stores WHERE cities_stores.store_type = stores.store_type);
-
没有城市中存在什么样的商店?
SELECT DISTINCT store_type FROM stores WHERE NOT EXISTS (SELECT * FROM cities_stores WHERE cities_stores.store_type = stores.store_type);
-
所有城市中存在什么样的商店?
SELECT DISTINCT store_type FROM stores WHERE NOT EXISTS ( SELECT * FROM cities WHERE NOT EXISTS ( SELECT * FROM cities_stores WHERE cities_stores.city = cities.city AND cities_stores.store_type = stores.store_type));
最后一个示例是一个双重嵌套的 NOT EXISTS
查询。也就是说,它在一个 NOT EXISTS
子句中有一个 NOT EXISTS
子句。形式上,它回答了“是否存在一个城市有一个不在 Stores
中的商店”这个问题?但更容易说的是,嵌套的 NOT EXISTS
回答了“对于所有 y
,x
是否都为 TRUE
?”
在 MySQL 8.0.19 及更高版本中,您还可以在子查询中使用 NOT EXISTS
或 NOT EXISTS
与 TABLE
,就像这样:
SELECT column1 FROM t1 WHERE EXISTS (TABLE t2);
结果与在子查询中没有 WHERE
子句的情况下使用 SELECT *
相同。
原文:
dev.mysql.com/doc/refman/8.0/en/correlated-subqueries.html
15.2.15.7 相关子查询
相关子查询 是一个包含对外部查询中也出现的表的引用的子查询。例如:
SELECT * FROM t1
WHERE column1 = ANY (SELECT column1 FROM t2
WHERE t2.column2 = t1.column2);
注意,子查询包含对 t1
列的引用,即使子查询的 FROM
子句没有提及表 t1
。因此,MySQL 查找子查询外部,在外部查询中找到 t1
。
假设表 t1
包含一行,其中 column1 = 5
和 column2 = 6
;同时,表 t2
包含一行,其中 column1 = 5
和 column2 = 7
。简单表达式 ... WHERE column1 = ANY (SELECT column1 FROM t2)
将是 TRUE
,但在这个例子中,子查询中的 WHERE
子句是 FALSE
(因为 (5,6)
不等于 (5,7)
),因此整个表达式是 FALSE
。
作用域规则: MySQL 从内到外进行评估。例如:
SELECT column1 FROM t1 AS x
WHERE x.column1 = (SELECT column1 FROM t2 AS x
WHERE x.column1 = (SELECT column1 FROM t3
WHERE x.column2 = t3.column1));
在这个语句中,x.column2
必须是表 t2
中的一列,因为 SELECT column1 FROM t2 AS x ...
重命名了 t2
。它不是表 t1
中的一列,因为 SELECT column1 FROM t1 ...
是一个更远处的外部查询。
从 MySQL 8.0.24 开始,当 optimizer_switch
变量的 subquery_to_derived
标志启用时,优化器可以将相关标量子查询转换为派生表。考虑这里显示的查询:
SELECT * FROM t1
WHERE ( SELECT a FROM t2
WHERE t2.a=t1.a ) > 0;
为了避免为给定的派生表多次实例化,我们可以代替多次实例化一个派生表,该派生表在内部查询中引用的表(t2.a
)上添加一个分组,然后在提升的谓词(t1.a = derived.a
)上进行外连接,以选择正确的组与外部行匹配。 (如果子查询已经有明确的分组,则额外的分组将添加到分组列表的末尾。)因此,先前显示的查询可以像这样重写:
SELECT t1.* FROM t1
LEFT OUTER JOIN
(SELECT a, COUNT(*) AS ct FROM t2 GROUP BY a) AS derived
ON t1.a = derived.a
AND
REJECT_IF(
(ct > 1),
"ERROR 1242 (21000): Subquery returns more than 1 row"
)
WHERE derived.a > 0;
在重写的查询中,REJECT_IF()
表示一个内部函数,用于测试给定条件(这里是比较 ct > 1
)并在条件为真时引发给定错误(在本例中是 ER_SUBQUERY_NO_1_ROW
)。这反映了优化器在评估 JOIN
或 WHERE
子句之前执行的基数检查,之后才评估任何提升的谓词,只有在子查询不返回多于一行时才执行。
只有满足以下条件时,才能执行这种类型的转换:
-
子查询可以是
SELECT
列表、WHERE
条件或HAVING
条件的一部分,但不能是JOIN
条件的一部分,并且不能包含LIMIT
或OFFSET
子句。此外,子查询不能包含任何集合操作,如UNION
。 -
WHERE
子句可以包含一个或多个谓词,并用AND
组合。如果WHERE
子句包含一个OR
子句,则无法进行转换。WHERE
子句中至少有一个谓词必须符合转换条件,且没有一个谓词可以拒绝转换。 -
要符合转换的条件,
WHERE
子句谓词必须是一个等式谓词,其中每个操作数都应该是一个简单的列引用。没有其他谓词—包括其他比较谓词—符合转换条件。该谓词必须使用等号操作符=
进行比较;在这种情况下,不支持空安全≪=>
操作符。 -
只包含内部引用的
WHERE
子句谓词不符合转换条件,因为它可以在分组之前进行评估。只包含外部引用的WHERE
子句谓词符合转换条件,即使它可以提升到外部查询块。这是通过在派生表中添加一个不带分组的基数检查来实现的。 -
要符合条件,
WHERE
子句谓词必须有一个操作数仅包含内部引用,另一个操作数仅包含外部引用。如果由于此规则而使谓词不符合条件,则拒绝转换查询。 -
相关列只能存在于子查询的
WHERE
子句中(而不是SELECT
列表、JOIN
或ORDER BY
子句、GROUP BY
列表或HAVING
子句)。子查询的FROM
列表中也不能有任何相关列。 -
相关列不能包含在聚合函数的参数列表中。
-
相关列必须在直接包含待转换子查询的查询块中解析。
-
在
WHERE
子句中的嵌套标量子查询中不能存在相关列。 -
子查询不能包含任何窗口函数,并且不能包含在子查询外部的查询块中聚合的任何聚合函数。如果
SELECT
列表元素中包含COUNT()
聚合函数,则必须在最高级别,并且不能是表达式的一部分。
另请参阅第 15.2.15.8 节,“派生表”。
15.2.15.8 派生表
本节讨论了派生表的一般特性。有关由LATERAL
关键字引导的横向派生表的信息,请参见 Section 15.2.15.9,“横向派生表”。
派生表是在查询FROM
子句的范围内生成表的表达式。例如,在SELECT
语句的FROM
子句中的子查询是一个派生表:
SELECT ... FROM (*subquery*) [AS] *tbl_name* ...
JSON_TABLE()
函数生成一个表,并提供了创建派生表的另一种方法:
SELECT * FROM JSON_TABLE(*arg_list*) [AS] *tbl_name* ...
[AS] *
tbl_name*
子句是必需的,因为FROM
子句中的每个表都必须有一个名称。派生表中的任何列必须具有唯一的名称。或者,tbl_name
`后面可以跟着一个括号括起来的列名列表:
SELECT ... FROM (*subquery*) [AS] *tbl_name* (*col_list*) ...
列名的数量必须与表列的数量相同。
为了说明问题,假设您有这个表:
CREATE TABLE t1 (s1 INT, s2 CHAR(5), s3 FLOAT);
下面是如何在FROM
子句中使用子查询,使用示例表:
INSERT INTO t1 VALUES (1,'1',1.0);
INSERT INTO t1 VALUES (2,'2',2.0);
SELECT sb1,sb2,sb3
FROM (SELECT s1 AS sb1, s2 AS sb2, s3*2 AS sb3 FROM t1) AS sb
WHERE sb1 > 1;
结果:
+------+------+------+
| sb1 | sb2 | sb3 |
+------+------+------+
| 2 | 2 | 4 |
+------+------+------+
这里是另一个例子:假设您想知道一个分组表的一组求和的平均值。这不起作用:
SELECT AVG(SUM(column1)) FROM t1 GROUP BY column1;
然而,这个查询提供了所需的信息:
SELECT AVG(sum_column1)
FROM (SELECT SUM(column1) AS sum_column1
FROM t1 GROUP BY column1) AS t1;
注意,在子查询中使用的列名(sum_column1
)在外部查询中被识别。
派生表的列名来自其选择列表:
mysql> SELECT * FROM (SELECT 1, 2, 3, 4) AS dt;
+---+---+---+---+
| 1 | 2 | 3 | 4 |
+---+---+---+---+
| 1 | 2 | 3 | 4 |
+---+---+---+---+
要明确提供列名,请在派生表名称后面跟着一个括号括起来的列名列表:
mysql> SELECT * FROM (SELECT 1, 2, 3, 4) AS dt (a, b, c, d);
+---+---+---+---+
| a | b | c | d |
+---+---+---+---+
| 1 | 2 | 3 | 4 |
+---+---+---+---+
派生表可以返回标量、列、行或表。
派生表受到以下限制:
-
派生表不能包含对同一
SELECT
的其他表的引用(使用LATERAL
派生表进行处理;请参见 Section 15.2.15.9,“横向派生表”)。 -
在 MySQL 8.0.14 之前,派生表不能包含外部引用。这是 MySQL 在 MySQL 8.0.14 中解除的限制,而不是 SQL 标准的限制。例如,以下查询中的派生表
dt
包含对外部查询中表t1
的引用t1.b
:SELECT * FROM t1 WHERE t1.d > (SELECT AVG(dt.a) FROM (SELECT SUM(t2.a) AS a FROM t2 WHERE t2.b = t1.b GROUP BY t2.c) dt WHERE dt.a > 10);
该查询在 MySQL 8.0.14 及更高版本中有效。在 8.0.14 之前,它会产生一个错误:
Unknown column 't1.b' in 'where clause'
优化器以一种不需要将派生表实例化的方式确定有关派生表的信息,因此EXPLAIN
不需要将其实例化。请参见 Section 10.2.2.4,“使用合并或实例化优化派生表、视图引用和通用表达式”。
在某些情况下,使用EXPLAIN SELECT
可能会修改表数据。如果外部查询访问任何表,并且内部查询调用修改表中一个或多个行的存储函数,则可能会发生这种情况。假设数据库d1
中有两个表t1
和t2
,以及一个修改t2
的存储函数f1
,创建如下所示:
CREATE DATABASE d1;
USE d1;
CREATE TABLE t1 (c1 INT);
CREATE TABLE t2 (c1 INT);
CREATE FUNCTION f1(p1 INT) RETURNS INT
BEGIN
INSERT INTO t2 VALUES (p1);
RETURN p1;
END;
直接在EXPLAIN SELECT
中引用函数对t2
没有影响,如下所示:
mysql> SELECT * FROM t2;
Empty set (0.02 sec)
mysql> EXPLAIN SELECT f1(5)\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: NULL
partitions: NULL
type: NULL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: NULL
filtered: NULL
Extra: No tables used 1 row in set (0.01 sec)
mysql> SELECT * FROM t2;
Empty set (0.01 sec)
这是因为SELECT
语句没有引用任何表,可以在输出的table
和Extra
列中看到。这也适用于以下嵌套的SELECT
:
mysql> EXPLAIN SELECT NOW() AS a1, (SELECT f1(5)) AS a2\G
*************************** 1\. row ***************************
id: 1
select_type: PRIMARY
table: NULL
type: NULL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: NULL
filtered: NULL
Extra: No tables used 1 row in set, 1 warning (0.00 sec)
mysql> SHOW WARNINGS;
+-------+------+------------------------------------------+
| Level | Code | Message |
+-------+------+------------------------------------------+
| Note | 1249 | Select 2 was reduced during optimization |
+-------+------+------------------------------------------+
1 row in set (0.00 sec)
mysql> SELECT * FROM t2;
Empty set (0.00 sec)
但是,如果外部SELECT
引用任何表,优化器也会执行子查询中的语句,结果导致t2
被修改:
mysql> EXPLAIN SELECT * FROM t1 AS a1, (SELECT f1(5)) AS a2\G
*************************** 1\. row ***************************
id: 1
select_type: PRIMARY
table: <derived2>
partitions: NULL
type: system
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1
filtered: 100.00
Extra: NULL
*************************** 2\. row ***************************
id: 1
select_type: PRIMARY
table: a1
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1
filtered: 100.00
Extra: NULL
*************************** 3\. row ***************************
id: 2
select_type: DERIVED
table: NULL
partitions: NULL
type: NULL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: NULL
filtered: NULL
Extra: No tables used 3 rows in set (0.00 sec)
mysql> SELECT * FROM t2;
+------+
| c1 |
+------+
| 5 |
+------+
1 row in set (0.00 sec)
衍生表优化也可以与许多相关的(标量)子查询一起使用(MySQL 8.0.24 及更高版本)。有关更多信息和示例,请参见第 15.2.15.7 节,“相关子查询”。
原文:
dev.mysql.com/doc/refman/8.0/en/lateral-derived-tables.html
15.2.15.9 侧向派生表
通常,派生表不能引用同一FROM
子句中之前表的列。从 MySQL 8.0.14 开始,可以将派生表定义为侧向派生表,以指定允许这种引用。
非侧向派生表使用第 15.2.15.8 节“派生表”中讨论的语法来指定。侧向派生表的语法与非侧向派生表相同,只是在派生表规范之前指定了关键字LATERAL
。LATERAL
关键字必须在每个要用作侧向派生表的表之前。
侧向派生表受到以下限制:
-
侧向派生表只能出现在
FROM
子句中,可以是用逗号分隔的表列表,也可以是连接规范(JOIN
、INNER JOIN
、CROSS JOIN
、LEFT [OUTER] JOIN
或RIGHT [OUTER] JOIN
)中。 -
如果一个侧向派生表在连接子句的右操作数中,并且包含对左操作数的引用,则连接操作必须是
INNER JOIN
、CROSS JOIN
或LEFT [OUTER] JOIN
。如果表在左操作数中,并且包含对右操作数的引用,则连接操作必须是
INNER JOIN
、CROSS JOIN
或RIGHT [OUTER] JOIN
。 -
如果一个侧向派生表引用了一个聚合函数,则该函数的聚合查询不能是包含侧向派生表的
FROM
子句所属的查询。 -
根据 SQL 标准,MySQL 始终将与表函数(如
JSON_TABLE()
)的连接视为已使用LATERAL
。这在 MySQL 的任何版本中都是正确的,这就是为什么即使在 MySQL 8.0.14 之前的版本中也可以针对此函数进行连接。在 MySQL 8.0.14 及更高版本中,LATERAL
关键字是隐式的,并且不允许在JSON_TABLE()
之前使用。这也符合 SQL 标准。
以下讨论显示了侧向派生表如何使得某些 SQL 操作成为可能,这些操作无法通过非侧向派生表完成,或者需要更低效的解决方法。
假设我们想解决这个问题:给定一个销售团队成员的表(其中每行描述一个销售团队成员),以及所有销售的表(其中每行描述一笔销售:销售人员、客户、金额、日期),确定每个销售人员的最大销售额及其客户。这个问题可以有两种方法来解决。
解决问题的第一种方法:对于每个销售人员,计算最大销售额,并找到提供此最大销售额的客户。在 MySQL 中,可以这样做:
SELECT
salesperson.name,
-- find maximum sale size for this salesperson
(SELECT MAX(amount) AS amount
FROM all_sales
WHERE all_sales.salesperson_id = salesperson.id)
AS amount,
-- find customer for this maximum size
(SELECT customer_name
FROM all_sales
WHERE all_sales.salesperson_id = salesperson.id
AND all_sales.amount =
-- find maximum size, again
(SELECT MAX(amount) AS amount
FROM all_sales
WHERE all_sales.salesperson_id = salesperson.id))
AS customer_name
FROM
salesperson;
那个查询是低效的,因为它在每个销售人员中计算最大尺寸两次(在第一个子查询中一次,在第二个子查询中一次)。
我们可以尝试通过在每个销售人员中计算最大值并在派生表中“缓存”它来实现效率提升,如这个修改后的查询所示:
SELECT
salesperson.name,
max_sale.amount,
max_sale_customer.customer_name
FROM
salesperson,
-- calculate maximum size, cache it in transient derived table max_sale
(SELECT MAX(amount) AS amount
FROM all_sales
WHERE all_sales.salesperson_id = salesperson.id)
AS max_sale,
-- find customer, reusing cached maximum size
(SELECT customer_name
FROM all_sales
WHERE all_sales.salesperson_id = salesperson.id
AND all_sales.amount =
-- the cached maximum size
max_sale.amount)
AS max_sale_customer;
然而,在 SQL-92 中,该查询是非法的,因为派生表不能依赖于同一 FROM
子句中的其他表。派生表必须在查询的持续时间内保持恒定,不能包含对其他 FROM
子句表列的引用。如此编写的查询会产生以下错误:
ERROR 1054 (42S22): Unknown column 'salesperson.id' in 'where clause'
在 SQL:1999 中,如果派生表前面有 LATERAL
关键字(表示“这个派生表依赖于其左侧的先前表”),则查询变得合法:
SELECT
salesperson.name,
max_sale.amount,
max_sale_customer.customer_name
FROM
salesperson,
-- calculate maximum size, cache it in transient derived table max_sale
LATERAL
(SELECT MAX(amount) AS amount
FROM all_sales
WHERE all_sales.salesperson_id = salesperson.id)
AS max_sale,
-- find customer, reusing cached maximum size
LATERAL
(SELECT customer_name
FROM all_sales
WHERE all_sales.salesperson_id = salesperson.id
AND all_sales.amount =
-- the cached maximum size
max_sale.amount)
AS max_sale_customer;
一个侧向派生表不需要是恒定的,并且每当依赖的前一个表中的新行被顶层查询处理时,它就会被更新。
解决问题的第二种方法:如果 SELECT
列表中的子查询可以返回多列,则可以使用不同的解决方案:
SELECT
salesperson.name,
-- find maximum size and customer at same time
(SELECT amount, customer_name
FROM all_sales
WHERE all_sales.salesperson_id = salesperson.id
ORDER BY amount DESC LIMIT 1)
FROM
salesperson;
那是高效的但是非法的。它不起作用,因为这样的子查询只能返回单列:
ERROR 1241 (21000): Operand should contain 1 column(s)
重写查询的一种尝试是从派生表中选择多列:
SELECT
salesperson.name,
max_sale.amount,
max_sale.customer_name
FROM
salesperson,
-- find maximum size and customer at same time
(SELECT amount, customer_name
FROM all_sales
WHERE all_sales.salesperson_id = salesperson.id
ORDER BY amount DESC LIMIT 1)
AS max_sale;
然而,那也不起作用。派生表依赖于 salesperson
表,因此在没有 LATERAL
的情况下失败:
ERROR 1054 (42S22): Unknown column 'salesperson.id' in 'where clause'
添加 LATERAL
关键字使查询合法:
SELECT
salesperson.name,
max_sale.amount,
max_sale.customer_name
FROM
salesperson,
-- find maximum size and customer at same time
LATERAL
(SELECT amount, customer_name
FROM all_sales
WHERE all_sales.salesperson_id = salesperson.id
ORDER BY amount DESC LIMIT 1)
AS max_sale;
简而言之,LATERAL
是刚刚讨论的两种方法中所有缺点的高效解决方案。
15.2.15.10 子查询错误
有一些错误仅适用于子查询。本节描述了这些错误。
-
不支持的子查询语法:
ERROR 1235 (ER_NOT_SUPPORTED_YET) SQLSTATE = 42000 Message = "This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'"
这意味着 MySQL 不支持以下类似语句:
SELECT * FROM t1 WHERE s1 IN (SELECT s2 FROM t2 ORDER BY s1 LIMIT 1)
-
子查询返回的列数不正确:
ERROR 1241 (ER_OPERAND_COL) SQLSTATE = 21000 Message = "Operand should contain 1 column(s)"
在这种情况下会发生此错误:
SELECT (SELECT column1, column2 FROM t2) FROM t1;
如果子查询返回多列用于行比较,则可以使用子查询。在其他情境下,子查询必须是标量操作数。参见第 15.2.15.5 节,“行子查询”。
-
子查询返回的行数不正确:
ERROR 1242 (ER_SUBSELECT_NO_1_ROW) SQLSTATE = 21000 Message = "Subquery returns more than 1 row"
对于子查询必须返回最多一行但返回多行的语句会发生此错误。考虑以下示例:
SELECT * FROM t1 WHERE column1 = (SELECT column1 FROM t2);
如果
SELECT column1 FROM t2
只返回一行,则前面的查询有效。如果子查询返回多于一行,则会出现错误 1242。在这种情况下,查询应重写为:SELECT * FROM t1 WHERE column1 = ANY (SELECT column1 FROM t2);
-
子查询中错误使用表:
Error 1093 (ER_UPDATE_TABLE_USED) SQLSTATE = HY000 Message = "You can't specify target table 'x' for update in FROM clause"
在尝试在子查询中修改表并从同一表中进行选择的情况下会发生此错误:
UPDATE t1 SET column2 = (SELECT MAX(column1) FROM t1);
您可以使用公共表达式或派生表来解决此问题。参见第 15.2.15.12 节,“子查询限制”。
在 MySQL 8.0.19 及更高版本中,当在子查询中使用TABLE
时,本节描述的所有错误也适用。
对于事务性存储引擎,子查询失败会导致整个语句失败。对于非事务性存储引擎,在遇到错误之前进行的数据修改会被保留。
原文:
dev.mysql.com/doc/refman/8.0/en/optimizing-subqueries.html
15.2.15.11 优化子查询
开发仍在进行中,因此长期来看,没有可靠的优化提示。以下列表提供了一些有趣的技巧,您可能想尝试一下。另请参阅 Section 10.2.2,“优化子查询、派生表、视图引用和公共表达式”。
-
将子查询外部的子句移到内部。例如,使用这个查询:
SELECT * FROM t1 WHERE s1 IN (SELECT s1 FROM t1 UNION ALL SELECT s1 FROM t2);
而不是这个查询:
SELECT * FROM t1 WHERE s1 IN (SELECT s1 FROM t1) OR s1 IN (SELECT s1 FROM t2);
举个例子,使用这个查询:
SELECT (SELECT column1 + 5 FROM t1) FROM t2;
而不是这个查询:
SELECT (SELECT column1 FROM t1) + 5 FROM t2;
译文:
dev.mysql.com/doc/refman/8.0/en/subquery-restrictions.html
15.2.15.12 子查询的限制
-
一般来说,你不能修改一个表并在子查询中从同一个表中选择。例如,这个限制适用于以下形式的语句:
DELETE FROM t WHERE ... (SELECT ... FROM t ...); UPDATE t ... WHERE col = (SELECT ... FROM t ...); {INSERT|REPLACE} INTO t (SELECT ... FROM t ...);
例外:前述禁令不适用于如果对修改的表使用了派生表,并且该派生表是实现而不是合并到外部查询中。 (参见 Section 10.2.2.4, “使用合并或实现优化派生表、视图引用和公共表达式”.) 例如:
UPDATE t ... WHERE col = (SELECT * FROM (SELECT ... FROM t...) AS dt ...);
这里派生表的结果被实现为一个临时表,因此在对
t
进行更新时,相关行已经被选择。一般来说,你可以通过添加
NO_MERGE
优化器提示来影响优化器实现一个派生表。参见 Section 10.9.3, “优化器提示”。 -
行比较操作只部分支持:
-
对于
*
expr* [NOT] IN *
subquery*
,expr
可以是一个n
元组(使用行构造器语法指定),子查询可以返回n
元组行。因此,允许的语法更具体地表达为*
row_constructor* [NOT] IN *
table_subquery*
-
对于
*
expr* *
op* {ALL|ANY|SOME} *
subquery*
,expr
必须是一个标量值,子查询必须是一个列子查询;它不能返回多列行。
换句话说,对于返回
n
元组行的子查询,这是支持的:(*expr_1*, ..., *expr_n*) [NOT] IN *table_subquery*
但是这是不支持的:
(*expr_1*, ..., *expr_n*) *op* {ALL|ANY|SOME} *subquery*
之所以支持
IN
的行比较而不支持其他操作的原因是,IN
是通过将其重写为一系列=
比较和AND
操作来实现的。这种方法不能用于ALL
、ANY
或SOME
。 -
-
在 MySQL 8.0.14 之前,
FROM
子句中的子查询不能是相关子查询。它们在查询执行期间被整体实现(评估以产生结果集),因此不能针对外部查询的每一行进行评估。优化器延迟实现直到结果需要,这可能允许避免实现。参见 Section 10.2.2.4, “使用合并或实现优化派生表、视图引用和公共表达式”。 -
MySQL 不支持在某些子查询操作符中的子查询中使用
LIMIT
:mysql> SELECT * FROM t1 WHERE s1 IN (SELECT s2 FROM t2 ORDER BY s1 LIMIT 1); ERROR 1235 (42000): This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'
参见第 15.2.15.10 节,“子查询错误”。
-
MySQL 允许子查询引用具有插入行等数据修改副作用的存储函数。例如,如果
f()
插入行,则以下查询可以修改数据:SELECT ... WHERE x IN (SELECT f() ...);
这种行为是对 SQL 标准的扩展。在 MySQL 中,它可能产生不确定的结果,因为
f()
可能在给定查询的不同执行中由优化器选择处理的方式而执行不同次数。对于基于语句或混合格式的复制,这种不确定性的一个影响是这样的查询可能在源数据库和其副本上产生不同的结果。
15.2.16 TABLE 语句
TABLE
是 MySQL 8.0.19 中引入的 DML 语句,返回命名表的行和列。
TABLE *table_name* [ORDER BY *column_name*] [LIMIT *number* [OFFSET *number*]]
TABLE
语句在某些方面类似于SELECT
。给定名为t
的表存在,以下两个语句产生相同的输出:
TABLE t;
SELECT * FROM t;
您可以使用ORDER BY
和LIMIT
子句对TABLE
生成的行数进行排序和限制。这些与在SELECT
中使用相同的子句完全相同(包括与LIMIT
一起使用的可选OFFSET
子句),如下所示:
mysql> TABLE t;
+----+----+
| a | b |
+----+----+
| 1 | 2 |
| 6 | 7 |
| 9 | 5 |
| 10 | -4 |
| 11 | -1 |
| 13 | 3 |
| 14 | 6 |
+----+----+
7 rows in set (0.00 sec)
mysql> TABLE t ORDER BY b;
+----+----+
| a | b |
+----+----+
| 10 | -4 |
| 11 | -1 |
| 1 | 2 |
| 13 | 3 |
| 9 | 5 |
| 14 | 6 |
| 6 | 7 |
+----+----+
7 rows in set (0.00 sec)
mysql> TABLE t LIMIT 3;
+---+---+
| a | b |
+---+---+
| 1 | 2 |
| 6 | 7 |
| 9 | 5 |
+---+---+
3 rows in set (0.00 sec)
mysql> TABLE t ORDER BY b LIMIT 3;
+----+----+
| a | b |
+----+----+
| 10 | -4 |
| 11 | -1 |
| 1 | 2 |
+----+----+
3 rows in set (0.00 sec)
mysql> TABLE t ORDER BY b LIMIT 3 OFFSET 2;
+----+----+
| a | b |
+----+----+
| 1 | 2 |
| 13 | 3 |
| 9 | 5 |
+----+----+
3 rows in set (0.00 sec)
TABLE
在两个关键方面与SELECT
不同:
-
TABLE
总是显示表的所有列。例外:
TABLE
的输出不包括不可见列。查看第 15.1.20.10 节,“不可见列”。 -
TABLE
不允许对行进行任意过滤;也就是说,TABLE
不支持任何WHERE
子句。
为了限制返回的表列,过滤超出ORDER BY
和LIMIT
所能实现的行,或两者都使用,使用SELECT
。
TABLE
可以与临时表一起使用。
TABLE
也可以用于取代SELECT
在许多其他结构中,包括以下列出的结构:
-
使用诸如
UNION
之类的集合运算符,如下所示:mysql> TABLE t1; +---+----+ | a | b | +---+----+ | 2 | 10 | | 5 | 3 | | 7 | 8 | +---+----+ 3 rows in set (0.00 sec) mysql> TABLE t2; +---+---+ | a | b | +---+---+ | 1 | 2 | | 3 | 4 | | 6 | 7 | +---+---+ 3 rows in set (0.00 sec) mysql> TABLE t1 UNION TABLE t2; +---+----+ | a | b | +---+----+ | 2 | 10 | | 5 | 3 | | 7 | 8 | | 1 | 2 | | 3 | 4 | | 6 | 7 | +---+----+ 6 rows in set (0.00 sec)
刚刚显示的
UNION
等效于以下语句:mysql> SELECT * FROM t1 UNION SELECT * FROM t2; +---+----+ | a | b | +---+----+ | 2 | 10 | | 5 | 3 | | 7 | 8 | | 1 | 2 | | 3 | 4 | | 6 | 7 | +---+----+ 6 rows in set (0.00 sec)
TABLE
还可以与SELECT
语句、VALUES
语句或两者一起在集合操作中使用。查看第 15.2.18 节,“UNION 子句”、第 15.2.4 节,“EXCEPT 子句”和第 15.2.8 节,“INTERSECT 子句”,获取更多信息和示例。另请参阅第 15.2.14 节,“使用 UNION、INTERSECT 和 EXCEPT 的集合操作”。 -
使用
INTO
填充用户变量,并使用INTO OUTFILE
或INTO DUMPFILE
将表数据写入文件。查看第 15.2.13.1 节,“SELECT ... INTO 语句”,获取更具体的信息和示例。 -
在许多情况下,您可以使用子查询。给定具有名为
a
的列的任何表t1
,以及具有单列的第二个表t2
,以下语句是可能的:SELECT * FROM t1 WHERE a IN (TABLE t2);
假设表
t1
的单列命名为x
,前述与以下各语句等效(在任一情况下产生完全相同的结果):SELECT * FROM t1 WHERE a IN (SELECT x FROM t2); SELECT * FROM t1 WHERE a IN (SELECT * FROM t2);
查看第 15.2.15 节,“子查询”,获取更多信息。
-
在
INSERT
和REPLACE
语句中,您原本会使用SELECT *
。有关更多信息和示例,请参阅第 15.2.7.1 节,“INSERT ... SELECT 语句”。 -
TABLE
在许多情况下也可以代替SELECT
在CREATE TABLE ... SELECT
或CREATE VIEW ... SELECT
中使用。有关这些语句的更多信息和示例,请参阅其描述。
15.2.17 UPDATE 语句
UPDATE
是修改表中行的 DML 语句。
一个UPDATE
语句可以以WITH
")子句开头,以定义在UPDATE
中可访问的常用表达式。请参阅第 15.2.20 节,“WITH (Common Table Expressions)”")。
单表语法:
UPDATE [LOW_PRIORITY] [IGNORE] *table_reference*
SET *assignment_list*
[WHERE *where_condition*]
[ORDER BY ...]
[LIMIT *row_count*]
*value*:
{*expr* | DEFAULT}
*assignment*:
*col_name* = *value*
*assignment_list*:
*assignment* [, *assignment*] ...
多表语法:
UPDATE [LOW_PRIORITY] [IGNORE] *table_references*
SET *assignment_list*
[WHERE *where_condition*]
对于单表语法,UPDATE
语句使用新值更新命名表中现有行的列。SET
子句指示要修改的列以及它们应该被赋予的值。每个值可以作为表达式给出,或者使用关键字DEFAULT
将列明确设置为其默认值。如果给出WHERE
子句,则指定标识要更新的行的条件。如果没有WHERE
子句,则更新所有行。如果指定了ORDER BY
子句,则按指定的顺序更新行。LIMIT
子句限制可以更新的行数。
对于多表语法,UPDATE
更新满足条件的每个表中的行。每个匹配的行只更新一次,即使它多次匹配条件。对于多表语法,不能使用ORDER BY
和LIMIT
。
对于分区表,此语句的单表和多表形式都支持PARTITION
子句作为表引用的一部分。此选项接受一个或多个分区或子分区(或两者)的列表。仅检查列出的分区(或子分区)是否匹配,并且不在任何这些分区或子分区中的行不会被更新,无论它是否满足where_condition
。
注意
与在INSERT
或REPLACE
语句中使用PARTITION
时不同,即使列出的分区(或子分区)中没有行与where_condition
匹配,否则有效的UPDATE ... PARTITION
语句也被认为是成功的。
有关更多信息和示例,请参见第 26.5 节,“分区选择”。
where_condition
是一个对每个要更新的行求值为 true 的表达式。有关表达式语法,请参见第 11.5 节,“表达式”。
table_references
和where_condition
的指定如第 15.2.13 节,“SELECT 语句”所述。
只有在实际上被更新的UPDATE
中引用的列才需要UPDATE
权限。对于只读取但不修改的任何列,只需要SELECT
权限。
UPDATE
语句支持以下修饰符:
-
使用
LOW_PRIORITY
修饰符,UPDATE
的执行会延迟,直到没有其他客户端从表中读取数据。这仅影响仅使用表级锁定的存储引擎(如MyISAM
,MEMORY
和MERGE
)。 -
使用
IGNORE
修饰符,即使在更新过程中发生错误,更新语句也不会中止。对于唯一键值上发生重复键冲突的行不会被更新。将更新为会导致数据转换错误的值的行将被更新为最接近的有效值。更多信息,请参见 The Effect of IGNORE on Statement Execution。
UPDATE IGNORE
语句,包括具有ORDER BY
子句的语句,被标记为不安全的用于基于语句的复制。(这是因为更新行的顺序决定了哪些行被忽略。)这样的语句在使用基于语句模式时会在错误日志中产生警告,并在使用MIXED
模式时以基于行的格式写入二进制日志。(Bug #11758262, Bug #50439)更多信息请参见 Section 19.2.1.3, “Determination of Safe and Unsafe Statements in Binary Logging”。
如果在表达式中访问要更新的表中的列,UPDATE
会使用列的当前值。例如,以下语句将col1
设置为比其当前值多一的值:
UPDATE t1 SET col1 = col1 + 1;
以下语句中的第二个赋值将col2
设置为当前(更新后)的col1
值,而不是原始的col1
值。结果是col1
和col2
具有相同的值。这种行为与标准 SQL 不同。
UPDATE t1 SET col1 = col1 + 1, col2 = col1;
单表UPDATE
赋值通常从左到右进行评估。对于多表更新,不能保证赋值按任何特定顺序执行。
如果将列设置为其当前值,MySQL 会注意到这一点并且不会对其进行更新。
如果您通过将已声明为NOT NULL
的列设置为NULL
来更新列,并且启用了严格的 SQL 模式,则会发生错误;否则,该列将设置为列数据类型的隐式默认值,并且警告计数会增加。对于数值类型,隐式默认值为0
,对于字符串类型,为空字符串(''
),对于日期和时间类型,为“零”值。请参见第 13.6 节,“数据类型默认值”。
如果显式更新生成列,则唯一允许的值是DEFAULT
。有关生成列的信息,请参见第 15.1.20.8 节,“CREATE TABLE and Generated Columns”。
UPDATE
返回实际更改的行数。mysql_info()
C API 函数返回匹配并更新的行数以及在UPDATE
过程中发生的警告数。
您可以使用LIMIT *
row_count*
来限制UPDATE
的范围。LIMIT
子句是一个匹配行数的限制。一旦找到满足WHERE
子句的row_count
行,无论它们是否实际更改,语句都会停止。
如果UPDATE
语句包含ORDER BY
子句,则按照子句指定的顺序更新行。在某些情况下,这可能会避免错误。假设表t
包含具有唯一索引的列id
。以下语句可能会因为更新行的顺序不同而导致重复键错误:
UPDATE t SET id = id + 1;
例如,如果表中id
列包含 1 和 2,并且在将 1 更新为 2 之前将 2 更新为 3,则会发生错误。为避免此问题,请添加ORDER BY
子句,以使具有较大id
值的行在具有较小值的行之前更新:
UPDATE t SET id = id + 1 ORDER BY id DESC;
您还可以执行涵盖多个表的UPDATE
操作。但是,不能在多表UPDATE
中使用ORDER BY
或LIMIT
。table_references
子句列出了参与连接的表。其语法在第 15.2.13.2 节,“JOIN Clause”中描述。以下是一个示例:
UPDATE items,month SET items.price=month.price
WHERE items.id=month.id;
前面的示例显示了使用逗号运算符的内连接,但是多表UPDATE
语句可以使用在SELECT
语句中允许的任何类型的连接,例如LEFT JOIN
。
如果您使用涉及InnoDB
表的多表UPDATE
语句,并且这些表存在外键约束,MySQL 优化器可能会以与它们的父/子关系不同的顺序处理表。在这种情况下,该语句将失败并回滚。相反,更新单个表,并依赖InnoDB
提供的ON UPDATE
功能,以使其他表相应地进行修改。请参阅第 15.1.20.5 节,“外键约束”。
您不能在子查询中直接更新表并从同一表中进行选择。您可以通过使用多表更新来解决此问题,其中一个表是从您实际希望更新的表派生的,并使用别名引用派生表。假设您希望更新一个名为items
的表,该表是使用以下语句定义的:
CREATE TABLE items (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
wholesale DECIMAL(6,2) NOT NULL DEFAULT 0.00,
retail DECIMAL(6,2) NOT NULL DEFAULT 0.00,
quantity BIGINT NOT NULL DEFAULT 0
);
要减少任何标记幅度为 30%或更高且库存少于一百的任何商品的零售价格,您可以尝试使用类似以下的UPDATE
语句,其中在WHERE
子句中使用了子查询。如下所示,此语句不起作用:
mysql> UPDATE items
> SET retail = retail * 0.9
> WHERE id IN
> (SELECT id FROM items
> WHERE retail / wholesale >= 1.3 AND quantity > 100);
ERROR 1093 (HY000): You can't specify target table 'items' for update in FROM clause
相反,您可以使用多表更新,其中子查询移动到要更新的表列表中,并使用别名在最外层WHERE
子句中引用它,就像这样:
UPDATE items,
(SELECT id FROM items
WHERE id IN
(SELECT id FROM items
WHERE retail / wholesale >= 1.3 AND quantity < 100))
AS discounted
SET items.retail = items.retail * 0.9
WHERE items.id = discounted.id;
因为优化器默认尝试将派生表discounted
合并到最外层查询块中,所以只有在强制实体化派生表时才起作用。您可以通过在运行更新之前将optimizer_switch
系统变量的derived_merge
标志设置为off
,或者使用NO_MERGE
优化提示来实现这一点,如下所示:
UPDATE /*+ NO_MERGE(discounted) */ items,
(SELECT id FROM items
WHERE retail / wholesale >= 1.3 AND quantity < 100)
AS discounted
SET items.retail = items.retail * 0.9
WHERE items.id = discounted.id;
在这种情况下使用优化提示的优势在于它仅在使用它的查询块内部应用,因此在执行UPDATE
后不需要再次更改optimizer_switch
的值。
另一种可能性是重写子查询,使其不使用IN
或EXISTS
,就像这样:
UPDATE items,
(SELECT id, retail / wholesale AS markup, quantity FROM items)
AS discounted
SET items.retail = items.retail * 0.9
WHERE discounted.markup >= 1.3
AND discounted.quantity < 100
AND items.id = discounted.id;
在这种情况下,默认情况下子查询是实体化的,而不是合并的,因此不需要禁用派生表的合并。
15.2.18 UNION 子句
*query_expression_body* UNION [ALL | DISTINCT] *query_block*
[UNION [ALL | DISTINCT] *query_expression_body*]
[...]
*query_expression_body*:
*See Section 15.2.14, “Set Operations with UNION, INTERSECT, and EXCEPT”*
UNION
将多个查询块的结果合并为单个结果集。此示例使用 SELECT
语句:
mysql> SELECT 1, 2;
+---+---+
| 1 | 2 |
+---+---+
| 1 | 2 |
+---+---+
mysql> SELECT 'a', 'b';
+---+---+
| a | b |
+---+---+
| a | b |
+---+---+
mysql> SELECT 1, 2 UNION SELECT 'a', 'b';
+---+---+
| 1 | 2 |
+---+---+
| 1 | 2 |
| a | b |
+---+---+
MySQL 8.0 中的 UNION 处理与 MySQL 5.7 相比
在 MySQL 8.0 中,SELECT
和 UNION
的解析器规则进行了重构,以使其更一致(在每个上下文中统一应用相同的 SELECT
语法)并减少重复。与 MySQL 5.7 相比,这项工作产生了几个用户可见的效果,可能需要重写某些语句:
-
NATURAL JOIN
允许可选的INNER
关键字(NATURAL INNER JOIN
),符合标准 SQL。 -
允许不带括号的右深度连接(例如,
... JOIN ... JOIN ... ON ... ON
),符合标准 SQL。 -
STRAIGHT_JOIN
现在允许USING
子句,类似于其他内连接。 -
解析器接受围绕查询表达式的括号。例如,
(SELECT ... UNION SELECT ...)
是允许的。另请参阅 第 15.2.11 节,“带括号的查询表达式”。 -
解析器更好地符合文档中允许放置
SQL_CACHE
和SQL_NO_CACHE
查询修饰符的规定。 -
左侧嵌套联合,以前仅在子查询中允许,现在在顶层语句中也允许。例如,此语句现在被接受为有效:
(SELECT 1 UNION SELECT 1) UNION SELECT 1;
-
锁定子句(
FOR UPDATE
,LOCK IN SHARE MODE
)仅允许在非UNION
查询中使用。这意味着必须对包含锁定子句的SELECT
语句使用括号。此语句不再被接受为有效:SELECT 1 FOR UPDATE UNION SELECT 1 FOR UPDATE;
相反,应该这样写陈述:
(SELECT 1 FOR UPDATE) UNION (SELECT 1 FOR UPDATE);
15.2.19 VALUES Statement
VALUES
是 MySQL 8.0.19 中引入的 DML 语句,它将一组一个或多个行作为表返回。换句话说,它是一个表值构造函数,也可以作为独立的 SQL 语句。
VALUES *row_constructor_list* [ORDER BY *column_designator*] [LIMIT *number*]
*row_constructor_list*:
ROW(*value_list*)[, ROW(*value_list*)][, ...]
*value_list*:
*value*[, *value*][, ...]
*column_designator*:
column_*index*
VALUES
语句由VALUES
关键字后跟一个或多个行构造函数列表组成,用逗号分隔。行构造函数由ROW()
行构造函数子句组成,其值列表由一个或多个标量值包含在括号中。一个值可以是任何 MySQL 数据类型的文字值或解析为标量值的表达式。
ROW()
不能是空的(但提供的每个标量值可以是NULL
)。在同一个VALUES
语句中,每个ROW()
在其值列表中必须具有相同数量的值。
DEFAULT
关键字不受VALUES
支持,并导致语法错误,除非它用于在INSERT
语句中提供值。
VALUES
的输出是一个表:
mysql> VALUES ROW(1,-2,3), ROW(5,7,9), ROW(4,6,8);
+----------+----------+----------+
| column_0 | column_1 | column_2 |
+----------+----------+----------+
| 1 | -2 | 3 |
| 5 | 7 | 9 |
| 4 | 6 | 8 |
+----------+----------+----------+
3 rows in set (0.00 sec)
从VALUES
输出的表的列具有隐式命名的列column_0
,column_1
,column_2
等,始终从0
开始。这个事实可以用来使用可选的ORDER BY
子句按列对行进行排序,就像这个子句在SELECT
语句中的工作方式一样,如下所示:
mysql> VALUES ROW(1,-2,3), ROW(5,7,9), ROW(4,6,8) ORDER BY column_1;
+----------+----------+----------+
| column_0 | column_1 | column_2 |
+----------+----------+----------+
| 1 | -2 | 3 |
| 4 | 6 | 8 |
| 5 | 7 | 9 |
+----------+----------+----------+
3 rows in set (0.00 sec)
在 MySQL 8.0.21 及更高版本中,VALUES
语句还支持LIMIT
子句以限制输出中的行数。(以前,LIMIT
是允许的,但不起作用。)
VALUES
语句在列值的数据类型方面是宽松的;您可以在同一列中混合类型,如下所示:
mysql> VALUES ROW("q", 42, '2019-12-18'),
-> ROW(23, "abc", 98.6),
-> ROW(27.0002, "Mary Smith", '{"a": 10, "b": 25}');
+----------+------------+--------------------+
| column_0 | column_1 | column_2 |
+----------+------------+--------------------+
| q | 42 | 2019-12-18 |
| 23 | abc | 98.6 |
| 27.0002 | Mary Smith | {"a": 10, "b": 25} |
+----------+------------+--------------------+
3 rows in set (0.00 sec)
重要提示
具有一个或多个ROW()
实例的VALUES
充当表值构造函数;尽管它可以用于在INSERT
或REPLACE
语句中提供值,但不要将其与也用于此目的的VALUES
关键字混淆。您也不要将其与指代INSERT ... ON DUPLICATE KEY UPDATE
中列值的VALUES()
函数混淆。
您还应该记住ROW()
是一个行值构造函数(参见 Section 15.2.15.5, “Row Subqueries”),而VALUES ROW()
是一个表值构造函数;这两者不能互换使用。
VALUES
可以在许多情况下使用 SELECT
,包括以下情况:
-
使用
UNION
,如下所示:mysql> SELECT 1,2 UNION SELECT 10,15; +----+----+ | 1 | 2 | +----+----+ | 1 | 2 | | 10 | 15 | +----+----+ 2 rows in set (0.00 sec) mysql> VALUES ROW(1,2) UNION VALUES ROW(10,15); +----------+----------+ | column_0 | column_1 | +----------+----------+ | 1 | 2 | | 10 | 15 | +----------+----------+ 2 rows in set (0.00 sec)
您可以将多行构建的表联合在一起,就像这样:
mysql> VALUES ROW(1,2), ROW(3,4), ROW(5,6) > UNION VALUES ROW(10,15),ROW(20,25); +----------+----------+ | column_0 | column_1 | +----------+----------+ | 1 | 2 | | 3 | 4 | | 5 | 6 | | 10 | 15 | | 20 | 25 | +----------+----------+ 5 rows in set (0.00 sec)
在这种情况下,您也可以(通常更可取地)完全省略
UNION
并使用单个VALUES
语句,就像这样:mysql> VALUES ROW(1,2), ROW(3,4), ROW(5,6), ROW(10,15), ROW(20,25); +----------+----------+ | column_0 | column_1 | +----------+----------+ | 1 | 2 | | 3 | 4 | | 5 | 6 | | 10 | 15 | | 20 | 25 | +----------+----------+
VALUES
也可以与SELECT
语句、TABLE
语句或两者一起在联合中使用。UNION
中的构建表必须包含相同数量的列,就像您使用SELECT
一样。有关更多示例,请参见 Section 15.2.18, “UNION Clause”。在 MySQL 8.0.31 及更高版本中,您可以像使用
UNION
一样使用EXCEPT
和INTERSECT
与VALUES
,如下所示:mysql> VALUES ROW(1,2), ROW(3,4), ROW(5,6) -> INTERSECT -> VALUES ROW(10,15), ROW(20,25), ROW(3,4); +----------+----------+ | column_0 | column_1 | +----------+----------+ | 3 | 4 | +----------+----------+ 1 row in set (0.00 sec) mysql> VALUES ROW(1,2), ROW(3,4), ROW(5,6) -> EXCEPT -> VALUES ROW(10,15), ROW(20,25), ROW(3,4); +----------+----------+ | column_0 | column_1 | +----------+----------+ | 1 | 2 | | 5 | 6 | +----------+----------+ 2 rows in set (0.00 sec)
有关更多信息,请参见 Section 15.2.4, “EXCEPT Clause” 和 Section 15.2.8, “INTERSECT Clause”。
-
在连接中。有关更多信息和示例,请参见 Section 15.2.13.2, “JOIN Clause”。
-
在
INSERT
或REPLACE
语句中代替VALUES()
,在这种情况下,其语义与此处描述的略有不同。有关详细信息,请参见 Section 15.2.7, “INSERT Statement”。 -
在
CREATE TABLE ... SELECT
和CREATE VIEW ... SELECT
中代替源表。有关更多信息和示例,请参见这些语句的描述。
15.2.20 WITH(公共表达式)
公共表达式(CTE)是存在于单个语句范围内的命名临时结果集,可以在该语句中稍后引用,可能多次。以下讨论描述了如何编写使用 CTE 的语句。
-
公共表达式
-
递归公共表达式
-
限制公共表达式递归
-
递归公共表达式示例
-
与类似结构比较的公共表达式
有关 CTE 优化的信息,请参见第 10.2.2.4 节,“使用合并或材料化优化派生表、视图引用和公共表达式”。
其他资源
这些文章包含有关在 MySQL 中使用 CTEs 的其他信息,包括许多示例:
公共表达式
要指定公共表达式,请使用具有一个或多个逗号分隔子句的WITH
")子句。每个子句提供一个生成结果集的子查询,并将一个名称与子查询关联。以下示例在WITH
")子句中定义了名为cte1
和cte2
的 CTE,并在跟随WITH
")子句的顶层SELECT
中引用它们:
WITH
cte1 AS (SELECT a, b FROM table1),
cte2 AS (SELECT c, d FROM table2)
SELECT b, d FROM cte1 JOIN cte2
WHERE cte1.a = cte2.c;
在包含WITH
")子句的语句中,每个 CTE 名称都可以被引用以访问相应的 CTE 结果集。
CTE 名称可以在其他 CTE 中引用,使得 CTE 可以基于其他 CTE 定义。
CTE 可以引用自身来定义递归 CTE。递归 CTE 的常见应用包括序列生成和遍历分层或树形数据。
公共表达式是 DML 语句语法的可选部分。它们使用WITH
")子句定义:
*with_clause*:
WITH [RECURSIVE]
*cte_name* [(*col_name* [, *col_name*] ...)] AS (*subquery*)
[, *cte_name* [(*col_name* [, *col_name*] ...)] AS (*subquery*)] ...
cte_name
指定一个单个公共表达式,并且可以在包含WITH
")子句的语句中作为表引用。
AS (*
子查询*)
部分中的子查询
称为“CTE 的子查询”,并生成 CTE 结果集。AS
后面的括号是必需的。
如果 CTE 的子查询引用其自身的名称,则该公共表达式是递归的。如果WITH
")子句中的任何 CTE 是递归的,则必须包含RECURSIVE
关键字。有关更多信息,请参见递归公共表达式。
给定 CTE 的列名确定如下:
-
如果 CTE 名称后跟着一个括号括起的名称列表,则这些名称是列名:
WITH cte (col1, col2) AS ( SELECT 1, 2 UNION ALL SELECT 3, 4 ) SELECT col1, col2 FROM cte;
名称列表中的名称数量必须与结果集中的列数相同。
-
否则,列名来自
AS (*
子查询*)
部分中第一个SELECT
的选择列表:WITH cte AS ( SELECT 1 AS col1, 2 AS col2 UNION ALL SELECT 3, 4 ) SELECT col1, col2 FROM cte;
在以下情况下允许使用WITH
")子句:
-
在
SELECT
、UPDATE
和DELETE
语句的开头。WITH ... SELECT ... WITH ... UPDATE ... WITH ... DELETE ...
-
在子查询(包括派生表子查询)的开头:
SELECT ... WHERE id IN (WITH ... SELECT ...) ... SELECT * FROM (WITH ... SELECT ...) AS dt ...
-
在包含
SELECT
语句的语句之前:INSERT ... WITH ... SELECT ... REPLACE ... WITH ... SELECT ... CREATE TABLE ... WITH ... SELECT ... CREATE VIEW ... WITH ... SELECT ... DECLARE CURSOR ... WITH ... SELECT ... EXPLAIN ... WITH ... SELECT ...
同一级别只允许一个WITH
")子句。不允许在同一级别后跟WITH
")再跟WITH
"),因此这是不合法的:
WITH cte1 AS (...) WITH cte2 AS (...) SELECT ...
要使语句合法,使用一个单独的WITH
")子句,通过逗号分隔子句:
WITH cte1 AS (...), cte2 AS (...) SELECT ...
但是,如果它们出现在不同级别,则语句可以包含多个WITH
")子句:
WITH cte1 AS (SELECT 1)
SELECT * FROM (WITH cte2 AS (SELECT 2) SELECT * FROM cte2 JOIN cte1) AS dt;
WITH
")子句可以定义一个或多个公共表达式,但每个 CTE 名称必须对该子句唯一。这是不合法的:
WITH cte1 AS (...), cte1 AS (...) SELECT ...
要使语句合法,定义具有唯一名称的 CTE:
WITH cte1 AS (...), cte2 AS (...) SELECT ...
CTE 可以引用自身或其他 CTE:
-
自引用 CTE 是递归的。
-
CTE 可以引用在同一
WITH
")子句中较早定义的 CTE,但不能引用稍后定义的 CTE。这个约束排除了相互递归的 CTE,其中
cte1
引用cte2
,而cte2
引用cte1
。这些引用中的一个必须是对稍后定义的 CTE 的引用,这是不允许的。 -
在给定查询块中,CTE 可以引用更外层级别的查询块中定义的 CTE,但不能引用更内层级别的查询块中定义的 CTE。
对于解析具有相同名称的对象引用,派生表隐藏 CTE;而 CTE 隐藏基本表、TEMPORARY
表和视图。名称解析通过在同一查询块中搜索对象,然后依次在外部块中查找,直到找到具有该名称的对象为止。
与派生表类似,在 MySQL 8.0.14 之前,CTE 不能包含外部引用。这是 MySQL 在 MySQL 8.0.14 中解除的限制,而不是 SQL 标准的限制。有关递归 CTE 的特定语法考虑事项,请参见递归公共表达式。
递归公共表达式
递归公共表达式是具有引用其自身名称的子查询的表达式。例如:
WITH RECURSIVE cte (n) AS
(
SELECT 1
UNION ALL
SELECT n + 1 FROM cte WHERE n < 5
)
SELECT * FROM cte;
当执行时,该语句产生这样的结果,一个包含简单线性序列的单列:
+------+
| n |
+------+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
+------+
递归 CTE 具有以下结构:
-
如果
WITH
子句中的任何 CTE 引用自身,则WITH
子句必须以WITH RECURSIVE
开头。(如果没有 CTE 引用自身,则允许使用RECURSIVE
但不是必需的。)如果忘记为递归 CTE 添加
RECURSIVE
,则可能会出现以下错误:ERROR 1146 (42S02): Table '*cte_name*' doesn't exist
-
递归 CTE 子查询有两部分,由
UNION ALL
或UNION [DISTINCT]
分隔:SELECT ... -- return initial row set UNION ALL SELECT ... -- return additional row sets
第一个
SELECT
生成 CTE 的初始行或行,并不引用 CTE 名称。第二个SELECT
生成额外的行并通过在其FROM
子句中引用 CTE 名称进行递归。当这部分不再生成新行时,递归结束。因此,递归 CTE 由一个非递归的SELECT
部分后跟一个递归的SELECT
部分组成。每个
SELECT
部分本身可以是多个SELECT
语句的联合。 -
CTE 结果列的类型是从非递归
SELECT
部分的列类型推断出来的,所有列都是可空的。对于类型确定,递归SELECT
部分将被忽略。 -
如果非递归部分和递归部分由
UNION DISTINCT
分隔,重复行将被消除。这对执行传递闭包的查询很有用,以避免无限循环。 -
递归部分的每次迭代仅在前一次迭代生成的行上操作。如果递归部分有多个查询块,每个查询块的迭代按照未指定的顺序进行安排,并且每个查询块操作的行是由其前一次迭代或自其前一次迭代结束以来其他查询块生成的行。
之前显示的递归 CTE 子查询有这个非递归部分,用于检索单行以生成初始行集合:
SELECT 1
CTE 子查询也有这个递归部分:
SELECT n + 1 FROM cte WHERE n < 5
在每次迭代中,SELECT
会生成一个新值,比上一行集合中的 n
的值大 1
。第一次迭代操作初始行集合(1
)并生成 1+1=2
;第二次迭代操作第一次迭代的行集合(2
)并生成 2+1=3
;依此类推。直到递归结束,即当 n
不再小于 5
时。
如果 CTE 的递归部分为某列生成了比非递归部分更宽的值,可能需要扩展非递归部分中的列以避免数据截断。考虑以下语句:
WITH RECURSIVE cte AS
(
SELECT 1 AS n, 'abc' AS str
UNION ALL
SELECT n + 1, CONCAT(str, str) FROM cte WHERE n < 3
)
SELECT * FROM cte;
在非严格 SQL 模式下,该语句会产生如下输出:
+------+------+
| n | str |
+------+------+
| 1 | abc |
| 2 | abc |
| 3 | abc |
+------+------+
str
列的值都是 'abc'
,因为非递归 SELECT
确定了列宽度。因此,递归 SELECT
生成的更宽的 str
值会被截断。
在严格 SQL 模式下,该语句会产生错误:
ERROR 1406 (22001): Data too long for column 'str' at row 1
为了解决这个问题,使语句不产生截断或错误,可以在非递归的SELECT
中使用CAST()
来使str
列变宽:
WITH RECURSIVE cte AS
(
SELECT 1 AS n, CAST('abc' AS CHAR(20)) AS str
UNION ALL
SELECT n + 1, CONCAT(str, str) FROM cte WHERE n < 3
)
SELECT * FROM cte;
现在该语句产生了这个结果,没有截断:
+------+--------------+
| n | str |
+------+--------------+
| 1 | abc |
| 2 | abcabc |
| 3 | abcabcabcabc |
+------+--------------+
列通过名称访问,而不是位置,这意味着递归部分中的列可以访问非递归部分中位置不同的列,正如这个 CTE 所示:
WITH RECURSIVE cte AS
(
SELECT 1 AS n, 1 AS p, -1 AS q
UNION ALL
SELECT n + 1, q * 2, p * 2 FROM cte WHERE n < 5
)
SELECT * FROM cte;
因为一行中的p
是从前一行中的q
派生的,反之亦然,所以输出的每一行中正负值交换位置:
+------+------+------+
| n | p | q |
+------+------+------+
| 1 | 1 | -1 |
| 2 | -2 | 2 |
| 3 | 4 | -4 |
| 4 | -8 | 8 |
| 5 | 16 | -16 |
+------+------+------+
一些语法约束适用于递归 CTE 子查询:
-
递归
SELECT
部分不能包含以下结构:-
聚合函数如
SUM()
-
窗口函数
-
GROUP BY
-
ORDER BY
-
DISTINCT
在 MySQL 8.0.19 之前,递归 CTE 的递归
SELECT
部分也不能使用LIMIT
子句。这个限制在 MySQL 8.0.19 中被解除,现在在这种情况下支持LIMIT
,还有一个可选的OFFSET
子句。结果集的效果与在最外层SELECT
中使用LIMIT
时相同,但更有效,因为在递归SELECT
中使用它会在生成请求的行数后立即停止生成行。这些约束不适用于递归 CTE 的非递归
SELECT
部分。对DISTINCT
的禁止仅适用于UNION
成员;允许UNION DISTINCT
。 -
-
递归的
SELECT
部分必须仅在其FROM
子句中引用 CTE 一次,而且不能在任何子查询中引用。它可以引用除 CTE 之外的其他表,并将它们与 CTE 连接起来。如果在这样的连接中使用,CTE 不能位于LEFT JOIN
的右侧。
这些约束来自 SQL 标准,除了 MySQL 特定的排除ORDER BY
、LIMIT
(MySQL 8.0.18 及更早版本)和DISTINCT
之外。
对于递归 CTE,EXPLAIN
输出的递归SELECT
部分在Extra
列中显示Recursive
。
EXPLAIN
显示的成本估计表示每次迭代的成本,这可能与总成本有很大差异。优化器无法预测迭代次数,因为它无法预测WHERE
子句何时变为 false。
CTE 的实际成本也可能受到结果集大小的影响。产生许多行的 CTE 可能需要一个足够大的内部临时表,以便从内存转换为磁盘格式,并可能遭受性能损失。如果是这样,增加允许的内存临时表大小可能会提高性能;参见第 10.4.4 节,“MySQL 中的内部临时表使用”。
限制公共表达式递归
对于递归 CTE 非常重要的是,递归的SELECT
部分包含一个终止递归的条件。作为一种开发技术,防止递归 CTE 失控的方法是通过设置执行时间限制来强制终止:
-
cte_max_recursion_depth
系统变量为 CTE 的递归级别数量设置了限制。服务器会终止任何递归超过此变量值的 CTE 的执行。 -
max_execution_time
系统变量强制执行当前会话中执行的SELECT
语句的执行超时。 -
MAX_EXECUTION_TIME
优化提示为其中出现的SELECT
语句强制执行每个查询的执行超时。
假设递归 CTE 被错误地编写为没有递归执行终止条件:
WITH RECURSIVE cte (n) AS
(
SELECT 1
UNION ALL
SELECT n + 1 FROM cte
)
SELECT * FROM cte;
默认情况下,cte_max_recursion_depth
的值为 1000,当递归超过 1000 级时,CTE 会终止。应用程序可以更改会话值以满足其需求:
SET SESSION cte_max_recursion_depth = 10; -- permit only shallow recursion
SET SESSION cte_max_recursion_depth = 1000000; -- permit deeper recursion
你也可以设置全局cte_max_recursion_depth
值,以影响随后开始的所有会话。
对于执行缓慢且因此递归的查询,或者有理由将cte_max_recursion_depth
值设置得非常高的情况,另一种防止深度递归的方法是设置每个会话的超时时间。为此,在执行 CTE 语句之前执行类似以下语句:
SET max_execution_time = 1000; -- impose one second timeout
或者,在 CTE 语句本身中包含一个优化提示:
WITH RECURSIVE cte (n) AS
(
SELECT 1
UNION ALL
SELECT n + 1 FROM cte
)
SELECT /*+ SET_VAR(cte_max_recursion_depth = 1M) */ * FROM cte;
WITH RECURSIVE cte (n) AS
(
SELECT 1
UNION ALL
SELECT n + 1 FROM cte
)
SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM cte;
从 MySQL 8.0.19 开始,你也可以在递归查询中使用LIMIT
来对最外层的SELECT
返回的最大行数进行限制,例如:
WITH RECURSIVE cte (n) AS
(
SELECT 1
UNION ALL
SELECT n + 1 FROM cte LIMIT 10000
)
SELECT * FROM cte;
你可以在设置时间限制之外或代替设置时间限制。因此,以下 CTE 在返回一万行或运行一秒钟(1000 毫秒)后终止:
WITH RECURSIVE cte (n) AS
(
SELECT 1
UNION ALL
SELECT n + 1 FROM cte LIMIT 10000
)
SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM cte;
如果没有执行时间限制的递归查询进入无限循环,您可以从另一个会话中使用KILL QUERY
终止它。在会话本身中,用于运行查询的客户端程序可能提供了终止查询的方法。例如,在mysql中,键入Control+C会中断当前语句。
递归公共表达式示例
如前所述,递归公共表达式(CTEs)经常用于生成系列和遍历分层或树形结构数据。本节展示了这些技术的一些简单示例。
-
斐波那契数列生成
-
日期系列生成
-
分层数据遍历
斐波那契数列生成
斐波那契数列以数字 0 和 1(或 1 和 1)开始,之后每个数字都是前两个数字的和。如果递归SELECT
生成的每行都可以访问系列中前两个数字,则递归公共表达式可以生成斐波那契数列。以下 CTE 使用 0 和 1 作为前两个数字生成了一个包含 10 个数字的系列:
WITH RECURSIVE fibonacci (n, fib_n, next_fib_n) AS
(
SELECT 1, 0, 1
UNION ALL
SELECT n + 1, next_fib_n, fib_n + next_fib_n
FROM fibonacci WHERE n < 10
)
SELECT * FROM fibonacci;
CTE 生成了这个结果:
+------+-------+------------+
| n | fib_n | next_fib_n |
+------+-------+------------+
| 1 | 0 | 1 |
| 2 | 1 | 1 |
| 3 | 1 | 2 |
| 4 | 2 | 3 |
| 5 | 3 | 5 |
| 6 | 5 | 8 |
| 7 | 8 | 13 |
| 8 | 13 | 21 |
| 9 | 21 | 34 |
| 10 | 34 | 55 |
+------+-------+------------+
CTE 的工作原理如下:
-
n
是一个显示列,表示该行包含第n
个斐波那契数。例如,第 8 个斐波那契数是 13。 -
fib_n
列显示斐波那契数n
。 -
next_fib_n
列显示数字n
后面的下一个斐波那契数。该列为下一行提供了下一个系列值,以便该行可以在其fib_n
列中生成前两个系列值的和。 -
当
n
达到 10 时,递归结束。这是一个任意选择,以限制输出为一小组行。
前面的输出显示了整个 CTE 结果。要仅选择其中的一部分,请在顶层SELECT
中添加适当的WHERE
子句。例如,要选择第 8 个斐波那契数,执行以下操作:
mysql> WITH RECURSIVE fibonacci ...
...
SELECT fib_n FROM fibonacci WHERE n = 8;
+-------+
| fib_n |
+-------+
| 13 |
+-------+
日期系列生成
公共表达式可以生成一系列连续的日期,这对于生成包含系列中所有日期的行的摘要非常有用,包括未在摘要数据中表示的日期。
假设销售数字表包含以下行:
mysql> SELECT * FROM sales ORDER BY date, price;
+------------+--------+
| date | price |
+------------+--------+
| 2017-01-03 | 100.00 |
| 2017-01-03 | 200.00 |
| 2017-01-06 | 50.00 |
| 2017-01-08 | 10.00 |
| 2017-01-08 | 20.00 |
| 2017-01-08 | 150.00 |
| 2017-01-10 | 5.00 |
+------------+--------+
此查询总结了每天的销售情况:
mysql> SELECT date, SUM(price) AS sum_price
FROM sales
GROUP BY date
ORDER BY date;
+------------+-----------+
| date | sum_price |
+------------+-----------+
| 2017-01-03 | 300.00 |
| 2017-01-06 | 50.00 |
| 2017-01-08 | 180.00 |
| 2017-01-10 | 5.00 |
+------------+-----------+
然而,该结果对于表跨越的日期范围中未表示的日期存在“空洞”。可以使用递归 CTE 生成该日期集合,然后与销售数据进行LEFT JOIN
连接以生成表示范围内所有日期的结果。
这是生成日期范围系列的 CTE:
WITH RECURSIVE dates (date) AS
(
SELECT MIN(date) FROM sales
UNION ALL
SELECT date + INTERVAL 1 DAY FROM dates
WHERE date + INTERVAL 1 DAY <= (SELECT MAX(date) FROM sales)
)
SELECT * FROM dates;
CTE 生成此结果:
+------------+
| date |
+------------+
| 2017-01-03 |
| 2017-01-04 |
| 2017-01-05 |
| 2017-01-06 |
| 2017-01-07 |
| 2017-01-08 |
| 2017-01-09 |
| 2017-01-10 |
+------------+
CTE 的工作原理:
-
非递归
SELECT
生成sales
表跨度日期范围内最早的日期。 -
递归
SELECT
生成的每行将日期增加一天到前一行生成的日期。 -
当日期达到
sales
表跨度的日期范围内的最高日期时,递归结束。
将 CTE 与sales
表进行LEFT JOIN
连接,生成每个日期范围内的销售摘要:
WITH RECURSIVE dates (date) AS
(
SELECT MIN(date) FROM sales
UNION ALL
SELECT date + INTERVAL 1 DAY FROM dates
WHERE date + INTERVAL 1 DAY <= (SELECT MAX(date) FROM sales)
)
SELECT dates.date, COALESCE(SUM(price), 0) AS sum_price
FROM dates LEFT JOIN sales ON dates.date = sales.date
GROUP BY dates.date
ORDER BY dates.date;
输出如下所示:
+------------+-----------+
| date | sum_price |
+------------+-----------+
| 2017-01-03 | 300.00 |
| 2017-01-04 | 0.00 |
| 2017-01-05 | 0.00 |
| 2017-01-06 | 50.00 |
| 2017-01-07 | 0.00 |
| 2017-01-08 | 180.00 |
| 2017-01-09 | 0.00 |
| 2017-01-10 | 5.00 |
+------------+-----------+
一些需要注意的要点:
-
查询是否低效,特别是对于递归
SELECT
中每行执行的包含MAX()
子查询?EXPLAIN
显示包含MAX()
的子查询仅评估一次,并且结果被缓存。 -
使用
COALESCE()
避免在sales
表中没有销售数据的日期中在sum_price
列中显示NULL
。
分层数据遍历
递归通用表达式对于遍历形成层次结构的数据非常有用。考虑以下创建一个小数据集的语句,该数据集显示公司中每个员工的员工姓名和 ID 号,以及员工的经理的 ID。顶层员工(CEO)的经理 ID 为NULL
(没有经理)。
CREATE TABLE employees (
id INT PRIMARY KEY NOT NULL,
name VARCHAR(100) NOT NULL,
manager_id INT NULL,
INDEX (manager_id),
FOREIGN KEY (manager_id) REFERENCES employees (id)
);
INSERT INTO employees VALUES
(333, "Yasmina", NULL), # Yasmina is the CEO (manager_id is NULL)
(198, "John", 333), # John has ID 198 and reports to 333 (Yasmina)
(692, "Tarek", 333),
(29, "Pedro", 198),
(4610, "Sarah", 29),
(72, "Pierre", 29),
(123, "Adil", 692);
结果数据集如下所示:
mysql> SELECT * FROM employees ORDER BY id;
+------+---------+------------+
| id | name | manager_id |
+------+---------+------------+
| 29 | Pedro | 198 |
| 72 | Pierre | 29 |
| 123 | Adil | 692 |
| 198 | John | 333 |
| 333 | Yasmina | NULL |
| 692 | Tarek | 333 |
| 4610 | Sarah | 29 |
+------+---------+------------+
要为每个员工生成组织结构图和每个员工的管理链(即从 CEO 到员工的路径),请使用递归 CTE:
WITH RECURSIVE employee_paths (id, name, path) AS
(
SELECT id, name, CAST(id AS CHAR(200))
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, CONCAT(ep.path, ',', e.id)
FROM employee_paths AS ep JOIN employees AS e
ON ep.id = e.manager_id
)
SELECT * FROM employee_paths ORDER BY path;
CTE 生成此输出:
+------+---------+-----------------+
| id | name | path |
+------+---------+-----------------+
| 333 | Yasmina | 333 |
| 198 | John | 333,198 |
| 29 | Pedro | 333,198,29 |
| 4610 | Sarah | 333,198,29,4610 |
| 72 | Pierre | 333,198,29,72 |
| 692 | Tarek | 333,692 |
| 123 | Adil | 333,692,123 |
+------+---------+-----------------+
CTE 的工作原理:
-
非递归
SELECT
生成 CEO 的行(具有NULL
管理者 ID 的行)。path
列扩展为CHAR(200)
,以确保递归SELECT
生成的较长path
值有足够的空间。 -
递归
SELECT
生成的每行找到所有直接向前一行生成的员工报告的员工。对于每个这样的员工,行包括员工 ID 和姓名,以及员工的管理链。链是经理的链,员工 ID 添加到末尾。 -
当员工没有其他人向他们报告时,递归结束。
要查找特定员工或员工的路径,请在顶层SELECT
添加WHERE
子句。例如,要显示 Tarek 和 Sarah 的结果,请修改SELECT
如下:
mysql> WITH RECURSIVE ...
...
SELECT * FROM employees_extended
WHERE id IN (692, 4610)
ORDER BY path;
+------+-------+-----------------+
| id | name | path |
+------+-------+-----------------+
| 4610 | Sarah | 333,198,29,4610 |
| 692 | Tarek | 333,692 |
+------+-------+-----------------+
与类似结构比较的通用表达式
通用表达式(CTEs)在某些方面类似于派生表:
-
两种结构都有名称。
-
两种结构存在于单个语句的范围内。
由于这些相似之处,CTE 和派生表通常可以互换使用。作为一个简单的例子,以下语句是等价的:
WITH cte AS (SELECT 1) SELECT * FROM cte;
SELECT * FROM (SELECT 1) AS dt;
然而,CTE 相对于派生表有一些优势:
-
派生表只能在查询中引用一次。而 CTE 可以被多次引用。要使用派生表结果的多个实例,必须多次派生结果。
-
一个 CTE 可以是自引用的(递归的)。
-
一个 CTE 可以引用另一个 CTE。
-
一个 CTE 在语句中的定义出现在开头时可能更容易阅读,而不是嵌入其中。
CTE 类似于使用CREATE [TEMPORARY] TABLE
创建的表,但不需要显式定义或删除。对于 CTE,您无需拥有创建表的权限。
15.3 事务和锁定语句
原文:
dev.mysql.com/doc/refman/8.0/en/sql-transactional-statements.html
15.3.1 START TRANSACTION、COMMIT 和 ROLLBACK 语句
15.3.2 无法回滚的语句
15.3.3 导致隐式提交的语句
15.3.4 SAVEPOINT、ROLLBACK TO SAVEPOINT 和 RELEASE SAVEPOINT 语句
15.3.5 LOCK INSTANCE FOR BACKUP 和 UNLOCK INSTANCE 语句
15.3.6 LOCK TABLES 和 UNLOCK TABLES 语句
15.3.7 SET TRANSACTION 语句
15.3.8 XA 事务
MySQL 通过诸如SET autocommit
、START TRANSACTION
、COMMIT
和 ROLLBACK
等语句支持本地事务(在给定的客户端会话中)。请参阅第 15.3.1 节,“START TRANSACTION, COMMIT, and ROLLBACK Statements”。XA 事务支持使 MySQL 能够参与分布式事务。请参阅第 15.3.8 节,“XA Transactions”。
15.3.1 START TRANSACTION
、COMMIT
和ROLLBACK
语句
START TRANSACTION
[*transaction_characteristic* [, *transaction_characteristic*] ...]
*transaction_characteristic*: {
WITH CONSISTENT SNAPSHOT
| READ WRITE
| READ ONLY
}
BEGIN [WORK]
COMMIT [WORK] [AND [NO] CHAIN] [[NO] RELEASE]
ROLLBACK [WORK] [AND [NO] CHAIN] [[NO] RELEASE]
SET autocommit = {0 | 1}
这些语句提供了对事务的控制:
-
START TRANSACTION
或BEGIN
开始一个新事务。 -
COMMIT
提交当前事务,使其更改永久生效。 -
ROLLBACK
回滚当前事务,取消其更改。 -
SET autocommit
禁用或启用当前会话的默认自动提交模式。
默认情况下,MySQL 运行时启用自动提交模式。这意味着,当不在事务内时,每个语句都是原子的,就好像被 START TRANSACTION
和 COMMIT
包围。您无法使用 ROLLBACK
撤消效果;但是,如果在执行语句时发生错误,则会回滚该语句。
要隐式禁用单个语句序列的自动提交模式,请使用 START TRANSACTION
语句:
START TRANSACTION;
SELECT @A:=SUM(salary) FROM table1 WHERE type=1;
UPDATE table2 SET summary=@A WHERE type=1;
COMMIT;
使用 START TRANSACTION
,自动提交保持禁用,直到您使用 COMMIT
或 ROLLBACK
结束事务。然后自动提交模式恢复到先前的状态。
START TRANSACTION
允许多个修饰符控制事务特性。要指定多个修饰符,请用逗号分隔它们。
-
WITH CONSISTENT SNAPSHOT
修饰符为能够执行此操作的存储引擎启动一致性读取。这仅适用于InnoDB
。其效果与发出START TRANSACTION
然后从任何InnoDB
表中进行SELECT
相同。请参阅 Section 17.7.2.3, “Consistent Nonlocking Reads”。WITH CONSISTENT SNAPSHOT
修饰符不会更改当前事务的隔离级别,因此仅在当前隔离级别允许一致性读取时才提供一致的快照。唯一允许一致性读取的隔离级别是REPEATABLE READ
。对于所有其他隔离级别,WITH CONSISTENT SNAPSHOT
子句将被忽略。当忽略WITH CONSISTENT SNAPSHOT
子句时会生成警告。 -
READ WRITE
和READ ONLY
修饰符设置事务访问模式。它们允许或禁止对事务中使用的表进行更改。READ ONLY
限制阻止事务修改或锁定对其他事务可见的事务和非事务表;事务仍然可以修改或锁定临时表。当事务已知为只读时,MySQL 在
InnoDB
表上的查询会启用额外的优化。指定READ ONLY
可确保在无法自动确定只读状态的情况下应用这些优化。有关更多信息,请参阅第 10.5.3 节,“优化 InnoDB 只读事务”。如果未指定访问模式,则应用默认模式。除非默认值已更改,否则为读/写。在同一语句中不允许同时指定
READ WRITE
和READ ONLY
。在只读模式下,仍然可以使用 DML 语句更改使用
TEMPORARY
关键字创建的表。使用 DDL 语句进行的更改是不允许的,就像永久表一样。有关事务访问模式的其他信息,包括更改默认模式的方法,请参阅第 15.3.7 节,“SET TRANSACTION 语句”。
如果启用了
read_only
系统变量,则使用START TRANSACTION READ WRITE
显式启动事务需要CONNECTION_ADMIN
权限(或已弃用的SUPER
权限)。
重要提示
许多用于编写 MySQL 客户端应用程序的 API(如 JDBC)提供了自己的启动事务方法,可以(有时应该)代替从客户端发送START TRANSACTION
语句。有关更多信息,请参阅第三十一章,“连接器和 API”,或您的 API 文档。
要显式禁用自动提交模式,请使用以下语句:
SET autocommit=0;
将autocommit
变量设置为零以禁用自动提交模式后,对事务安全表(例如InnoDB
或NDB
)的更改不会立即生效。您必须使用COMMIT
将更改存储到磁盘,或使用ROLLBACK
忽略更改。
autocommit
是一个会话变量,必须为每个会话设置。要为每个新连接禁用自动提交模式,请参阅autocommit
系统变量在第 7.1.8 节,“服务器系统变量”的描述。
BEGIN
和BEGIN WORK
可作为START TRANSACTION
的别名来启动事务。START TRANSACTION
是标准 SQL 语法,是启动临时事务的推荐方式,并允许BEGIN
不支持的修饰符。
BEGIN
语句与使用BEGIN
关键字开始BEGIN ... END
复合语句的方式不同。后者不会开始事务。请参见第 15.6.1 节,“BEGIN ... END 复合语句”。
注意
在所有存储程序(存储过程和函数、触发器和事件)中,解析器将BEGIN [WORK]
视为BEGIN ... END
块的开始。在这种情况下使用START TRANSACTION
开始一个事务。
可选的WORK
关键字支持COMMIT
和ROLLBACK
,以及CHAIN
和RELEASE
子句。CHAIN
和RELEASE
可用于对事务完成进行额外控制。completion_type
系统变量的值确定默认完成行为。请参见第 7.1.8 节,“服务器系统变量”。
AND CHAIN
子句导致新事务在当前事务结束后立即开始,并且新事务具有与刚终止事务相同的隔离级别。新事务还使用与刚终止事务相同的访问模式(READ WRITE
或READ ONLY
)。RELEASE
子句导致服务器在终止当前事务后断开当前客户端会话。包括NO
关键字可以抑制CHAIN
或RELEASE
完成,如果completion_type
系统变量设置为默认导致链接或释放完成时,这可能很有用。
开始事务会导致任何待处理的事务被提交。更多信息请参见第 15.3.3 节,“导致隐式提交的语句”。
开始事务还会导致使用LOCK TABLES
获取的表锁被释放,就好像执行了UNLOCK TABLES
一样。开始事务不会释放使用FLUSH TABLES WITH READ LOCK
获取的全局读锁。
为了获得最佳结果,事务应仅使用单个事务安全存储引擎管理的表执行。否则,可能会出现以下问题:
-
如果使用来自多个事务安全存储引擎(如
InnoDB
)的表,并且事务隔离级别不是SERIALIZABLE
,那么当一个事务提交时,使用相同表的另一个正在进行的事务可能只看到第一个事务所做的一部分更改。也就是说,混合引擎的事务的原子性不能得到保证,可能会导致不一致性。(如果混合引擎事务不频繁,您可以使用SET TRANSACTION ISOLATION LEVEL
根据需要在每个事务基础上将隔离级别设置为SERIALIZABLE
。) -
如果在事务中使用非事务安全的表,那么对这些表的更改将立即存储,而不考虑自动提交模式的状态。
-
如果在事务中更新非事务表后发出
ROLLBACK
语句,将会出现ER_WARNING_NOT_COMPLETE_ROLLBACK
警告。事务安全表的更改将被回滚,但非事务安全表的更改不会被回滚。
每个事务在COMMIT
时以一个块的形式存储在二进制日志中。被回滚的事务不会被记录。(例外:对非事务表的修改无法回滚。如果被回滚的事务包括对非事务表的修改,则整个事务将在结尾处使用ROLLBACK
语句记录,以确保对非事务表的修改被复制。)请参阅 Section 7.4.4, “The Binary Log”。
你可以使用SET TRANSACTION
语句更改事务的隔离级别或访问模式。请参阅 Section 15.3.7, “SET TRANSACTION Statement”。
回滚可能是一个缓慢的操作,可能会在用户没有明确要求的情况下隐式发生(例如,当发生错误时)。因此,SHOW PROCESSLIST
在会话的State
列中显示Rolling back
,不仅适用于使用ROLLBACK
语句执行的显式回滚,还适用于隐式回滚。
注意
在 MySQL 8.0 中,BEGIN
、COMMIT
和ROLLBACK
不受--replicate-do-db
或--replicate-ignore-db
规则的影响。
当InnoDB
执行事务的完全回滚时,事务设置的所有锁都会被释放。如果事务中的一个 SQL 语句由于错误(如重复键错误)而回滚,那么该语句设置的锁将在事务保持活动状态时保留。这是因为InnoDB
以一种格式存储行锁,以至于事后无法知道哪个锁是由哪个语句设置的。
如果事务中的一个SELECT
语句调用了一个存储函数,并且存储函数中的一个语句失败,那么该语句将会回滚。如果随后为该事务执行ROLLBACK
,整个事务将会回滚。
15.3.2 无法回滚的语句
有些语句是无法回滚的。一般来说,这些包括数据定义语言(DDL)语句,比如创建或删除数据库的语句,创建、删除或修改表或存储过程的语句。
你应该设计你的事务不包括这样的语句。如果你在事务早期发出一个无法回滚的语句,然后稍后另一个语句失败,那么在这种情况下,通过发出一个ROLLBACK
语句无法完全回滚事务的效果。
15.3.3 导致隐式提交的语句
本节列出的语句(以及它们的任何同义词)会隐式结束当前会话中的任何活动事务,就好像在执行该语句之前已经执行了一个COMMIT
。
大多数这些语句在执行后也会导致隐式提交。其目的是在其自己的特殊事务中处理每个这样的语句。事务控制和锁定语句是例外情况:如果在执行之前发生隐式提交,则在之后不会再发生另一个提交。
-
数据定义语言(DDL)语句用于定义或修改数据库对象。
ALTER EVENT
,ALTER FUNCTION
,ALTER PROCEDURE
,ALTER SERVER
,ALTER TABLE
,ALTER TABLESPACE
,ALTER VIEW
,CREATE DATABASE
,CREATE EVENT
,CREATE FUNCTION
,CREATE INDEX
,CREATE PROCEDURE
,CREATE ROLE
,CREATE SERVER
,CREATE SPATIAL REFERENCE SYSTEM
,CREATE TABLE
,CREATE TABLESPACE
,CREATE TRIGGER
,CREATE VIEW
,DROP DATABASE
,DROP EVENT
,DROP FUNCTION
,DROP INDEX
,DROP PROCEDURE
,DROP ROLE
,DROP SERVER
,DROP SPATIAL REFERENCE SYSTEM
,DROP TABLE
,DROP TABLESPACE
,DROP TRIGGER
,DROP VIEW
,INSTALL PLUGIN
,RENAME TABLE
,TRUNCATE TABLE
,UNINSTALL PLUGIN
.使用
TEMPORARY
关键字的CREATE TABLE
和DROP TABLE
语句不会提交事务。(这不适用于对临时表的其他操作,如ALTER TABLE
和CREATE INDEX
,这些操作会导致提交。)然而,虽然没有隐式提交,但也不能回滚该语句,这意味着使用这些语句会违反事务的原子性。例如,如果使用CREATE TEMPORARY TABLE
然后回滚事务,表仍然存在。在
InnoDB
中,CREATE TABLE
语句被处理为单个事务。这意味着用户的ROLLBACK
不会撤消用户在该事务期间进行的CREATE TABLE
语句。当创建非临时表时,
CREATE TABLE ... SELECT
在执行语句之前和之后会导致隐式提交。(对于CREATE TEMPORARY TABLE ... SELECT
不会发生提交。) -
隐式使用或修改
mysql
数据库中表的语句。ALTER USER
,CREATE USER
,DROP USER
,GRANT
,RENAME USER
,REVOKE
,SET PASSWORD
. -
事务控制和锁定语句。
BEGIN
,LOCK TABLES
, 如果值尚未为 1,则SET autocommit = 1
,START TRANSACTION
,UNLOCK TABLES
.只有当使用
LOCK TABLES
获取非事务表锁时,UNLOCK TABLES
才会提交事务。对于跟随FLUSH TABLES WITH READ LOCK
的UNLOCK TABLES
不会发生提交,因为后者不会获取表级锁。事务不能嵌套。这是在发出
START TRANSACTION
语句或其同义词时为当前事务执行的隐式提交的结果。导致隐式提交的语句不能在事务处于
ACTIVE
状态时用于 XA 事务。BEGIN
语句与开始BEGIN ... END
复合语句的BEGIN
关键字的使用不同。后者不会导致隐式提交。请参阅 Section 15.6.1, “BEGIN ... END Compound Statement”。 -
数据加载语句。
LOAD DATA
。LOAD DATA
仅对使用NDB
存储引擎的表造成隐式提交。 -
管理语句。
ANALYZE TABLE
,CACHE INDEX
,CHECK TABLE
,FLUSH
,LOAD INDEX INTO CACHE
,OPTIMIZE TABLE
,REPAIR TABLE
,RESET
(但不包括RESET PERSIST
)。 -
复制控制语句。
START REPLICA
,STOP REPLICA
,RESET REPLICA
,CHANGE REPLICATION SOURCE TO
,CHANGE MASTER TO
。在 MySQL 8.0.22 中,SLAVE 关键字被 REPLICA 替换。
15.3.4 保存点、回滚到保存点和释放保存点语句
SAVEPOINT *identifier*
ROLLBACK [WORK] TO [SAVEPOINT] *identifier*
RELEASE SAVEPOINT *identifier*
InnoDB
支持 SQL 语句SAVEPOINT
、ROLLBACK TO SAVEPOINT
、RELEASE SAVEPOINT
以及用于ROLLBACK
的可选WORK
关键字。
SAVEPOINT
语句使用identifier
设置具有名称的事务保存点。如果当前事务具有相同名称的保存点,则旧保存点将被删除,并设置一个新的保存点。
ROLLBACK TO SAVEPOINT
语句将事务回滚到指定的保存点,而不终止事务。在回滚中,当前事务在设置保存点后对行所做的修改将被撤消,但InnoDB
不会释放保存点后存储在内存中的行锁。(对于新插入的行,锁信息由存储在行中的事务 ID 携带;锁不会单独存储在内存中。在这种情况下,行锁在撤消时被释放。)比指定保存点设置的保存点将被删除。
如果ROLLBACK TO SAVEPOINT
语句返回以下错误,则表示不存在具有指定名称的保存点:
ERROR 1305 (42000): SAVEPOINT *identifier* does not exist
RELEASE SAVEPOINT
语句从当前事务的保存点集中移除指定的保存点。不会发生提交或回滚。如果保存点不存在,则会报错。
如果执行COMMIT
或未命名保存点的ROLLBACK
,则会删除当前事务的所有保存点。
当调用存储函数或触发器被激活时,将创建一个新的保存点级别。之前级别上的保存点变得不可用,因此不会与新级别上的保存点发生冲突。当函数或触发器终止时,它创建的任何保存点都会被释放,并且之前的保存点级别会被恢复。
15.3.5 LOCK INSTANCE FOR BACKUP 和 UNLOCK INSTANCE 语句
原文:
dev.mysql.com/doc/refman/8.0/en/lock-instance-for-backup.html
LOCK INSTANCE FOR BACKUP
UNLOCK INSTANCE
LOCK INSTANCE FOR BACKUP
获取一个实例级别的备份锁,允许在在线备份期间进行 DML 操作,同时阻止可能导致不一致快照的操作。
执行LOCK INSTANCE FOR BACKUP
语句需要BACKUP_ADMIN
权限。当从早期版本升级到 MySQL 8.0 时,具有RELOAD
权限的用户会自动被授予BACKUP_ADMIN
权限。
多个会话可以同时持有备份锁。
UNLOCK INSTANCE
释放当前会话持有的备份锁。如果会话终止,会话持有的备份锁也会被释放。
LOCK INSTANCE FOR BACKUP
阻止创建、重命名或删除文件。REPAIR TABLE
TRUNCATE TABLE
、OPTIMIZE TABLE
和账户管理语句被阻止。参见第 15.7.1 节,“账户管理语句”。还会阻止修改未记录在InnoDB
重做日志中的InnoDB
文件的操作。
LOCK INSTANCE FOR BACKUP
允许执行仅影响用户创建的临时表的 DDL 操作。实际上,在持有备份锁时,可以创建、重命名或删除属于用户创建的临时表的文件。也允许创建二进制日志文件。
在实例上有效的LOCK INSTANCE FOR BACKUP
语句生效期间,不应发出PURGE BINARY LOGS
语句,因为这违反了备份锁的规则,会从服务器中删除文件。从 MySQL 8.0.28 开始,这是不允许的。
由LOCK INSTANCE FOR BACKUP
获取的备份锁独立于事务锁和由FLUSH TABLES *
tbl_name* [, *
tbl_name*] ... WITH READ LOCK
获取的锁,并允许以下语句序列:
LOCK INSTANCE FOR BACKUP;
FLUSH TABLES *tbl_name* [, *tbl_name*] ... WITH READ LOCK;
UNLOCK TABLES;
UNLOCK INSTANCE;
FLUSH TABLES *tbl_name* [, *tbl_name*] ... WITH READ LOCK;
LOCK INSTANCE FOR BACKUP;
UNLOCK INSTANCE;
UNLOCK TABLES;
lock_wait_timeout
设置定义了LOCK INSTANCE FOR BACKUP
语句在放弃之前等待获取锁的时间。
15.3.6 LOCK TABLES
和 UNLOCK TABLES
语句
LOCK {TABLE | TABLES}
*tbl_name* [[AS] *alias*] *lock_type*
[, *tbl_name* [[AS] *alias*] *lock_type*] ...
*lock_type*: {
READ [LOCAL]
| [LOW_PRIORITY] WRITE
}
UNLOCK {TABLE | TABLES}
MySQL 允许客户端会话明确为协作访问表的其他会话或在会话需要独占访问表时阻止其他会话修改表而获取表锁。一个会话只能为自己获取或释放锁。一个会话不能为另一个会话获取锁或释放另一个会话持有的锁。
锁可以用于模拟事务或在更新表时获得更快的速度。这在 Table-Locking Restrictions and Conditions 中有更详细的解释。
LOCK TABLES
明确为当前客户端会话获取表锁。表锁可以用于基表或视图。你必须拥有 LOCK TABLES
权限,并且对每个要锁定的对象都必须拥有 SELECT
权限。
对于视图锁定,LOCK TABLES
会将视图中使用的所有基表添加到要锁定的表集合中,并自动锁定它们。对于任何被锁定视图下的表,LOCK TABLES
会检查视图定义者(对于 SQL SECURITY DEFINER
视图)或调用者(对于所有视图)对表具有适当的权限。
如果你使用 LOCK TABLES
明确锁定一个表,那么任何触发器中使用的表也会隐式锁定,如 LOCK TABLES and Triggers 中所述。
如果你使用 LOCK TABLES
明确锁定一个表,那么任何通过外键约束相关的表会被隐式打开并锁定。对于外键检查,相关表会被采取共享只读锁(LOCK TABLES READ
)。对于级联更新,涉及操作的相关表会被采取共享无写锁(LOCK TABLES WRITE
)。
UNLOCK TABLES
明确释放当前会话持有的任何表锁。在获取新锁之前,LOCK TABLES
会隐式释放当前会话持有的任何表锁。
UNLOCK TABLES
的另一个用途是释放使用FLUSH TABLES WITH READ LOCK
语句获取的全局读锁,该语句使您可以锁定所有数据库中的所有表。请参见第 15.7.8.3 节,“FLUSH 语句”。(如果您有可以在某个时间点进行快照的文件系统(如 Veritas),这是一个非常方便的备份方式。)
LOCK TABLE
是LOCK TABLES
的同义词;UNLOCK TABLE
是UNLOCK TABLES
的同义词。
表锁仅防止其他会话进行不当读取或写入。持有WRITE
锁的会话可以执行诸如DROP TABLE
或TRUNCATE TABLE
等表级操作。对于持有READ
锁的会话,不允许执行DROP TABLE
和TRUNCATE TABLE
操作。
以下讨论仅适用于非TEMPORARY
表。对于TEMPORARY
表,允许(但会被忽略)使用LOCK TABLES
。该表可以在创建它的会话中自由访问,而不受其他锁定的影响。不需要锁定,因为没有其他会话可以看到该表。
-
表锁定获取
-
表锁定释放
-
表锁定和事务的交互
-
LOCK TABLES 和触发器
-
表锁定的限制和条件
表锁定获取
要在当前会话中获取表锁,请使用LOCK TABLES
语句,该语句获取元数据锁(参见第 10.11.4 节,“元数据锁定”)。
可用的锁类型如下:
READ [LOCAL]
锁:
-
拥有锁的会话可以读取表(但不能写入)。
-
多个会话可以同时为表获取
READ
锁。 -
其他会话可以在不显式获取
READ
锁的情况下读取表。 -
LOCAL
修饰符允许其他会话在持有锁的情况下执行非冲突的INSERT
语句(并发插入)。但是,如果在持有锁的同时使用外部进程操作数据库,则不能使用READ LOCAL
。对于InnoDB
表,READ LOCAL
与READ
相同。
[LOW_PRIORITY] WRITE
锁:
-
持有锁的会话可以读取和写入表。
-
只有持有锁的会话才能访问表。在锁被释放之前,其他会话无法访问它。
-
当持有
WRITE
锁时,其他会话对表的锁请求将被阻塞。 -
LOW_PRIORITY
修饰符没有影响。在 MySQL 的早期版本中,它会影响锁定行为,但现在不再有效。它现在已被弃用,使用WRITE
而不带LOW_PRIORITY
会产生警告。
WRITE
锁通常比 READ
锁具有更高的优先级,以确保更新尽快处理。这意味着如果一个会话获取了 READ
锁,然后另一个会话请求 WRITE
锁,后续的 READ
锁请求将等待,直到请求 WRITE
锁的会话获取并释放锁。(对于 max_write_lock_count
系统变量的小值,可能会有例外情况;请参见 Section 10.11.4, “Metadata Locking”.)
如果由于其他会话在任何表上持有的锁而导致 LOCK TABLES
语句必须等待,它将阻塞直到所有锁都可以被获取。
需要锁定表的会话必须在单个 LOCK TABLES
语句中获取所有需要的锁。在持有这些锁的情况下,会话只能访问被锁定的表。例如,在以下语句序列中,尝试访问 t2
会导致错误,因为它在 LOCK TABLES
语句中未被锁定:
mysql> LOCK TABLES t1 READ;
mysql> SELECT COUNT(*) FROM t1;
+----------+
| COUNT(*) |
+----------+
| 3 |
+----------+
mysql> SELECT COUNT(*) FROM t2;
ERROR 1100 (HY000): Table 't2' was not locked with LOCK TABLES
INFORMATION_SCHEMA
数据库中的表是一个例外。即使一个会话持有使用 LOCK TABLES
获取的表锁,也可以在不显式锁定的情况下访问它们。
不能在单个查询中多次引用具有相同名称的锁定表。请改用别名,并为表和每个别名获取单独的锁:
mysql> LOCK TABLE t WRITE, t AS t1 READ;
mysql> INSERT INTO t SELECT * FROM t;
ERROR 1100: Table 't' was not locked with LOCK TABLES
mysql> INSERT INTO t SELECT * FROM t AS t1;
第一个INSERT
出现错误是因为对于一个被锁定的表有两个相同名称的引用。第二个INSERT
成功是因为对表的引用使用了不同的名称。
如果您的语句通过别名引用表,那么必须使用相同的别名锁定表。不能不指定别名而锁定表:
mysql> LOCK TABLE t READ;
mysql> SELECT * FROM t AS myalias;
ERROR 1100: Table 'myalias' was not locked with LOCK TABLES
相反,如果您使用别名锁定表,那么在语句中必须使用该别名引用它:
mysql> LOCK TABLE t AS myalias READ;
mysql> SELECT * FROM t;
ERROR 1100: Table 't' was not locked with LOCK TABLES
mysql> SELECT * FROM t AS myalias;
表锁定释放
当会话持有的表锁被释放时,它们会同时被释放。一个会话可以显式释放其锁,或者在某些条件下锁可能会隐式释放。
-
一个会话可以通过
UNLOCK TABLES
显式释放其锁。 -
如果一个会话在已经持有锁的情况下发出
LOCK TABLES
语句以获取锁,那么在授予新锁之前,现有的锁会被隐式释放。 -
如果一个会话开始一个事务(例如,使用
START TRANSACTION
),会执行一个隐式的UNLOCK TABLES
,导致现有的锁被释放。(有关表锁定和事务之间的交互的更多信息,请参阅表锁定和事务的交互.)
如果客户端会话的连接终止,无论是正常还是异常,服务器会隐式释放会话持有的所有表锁(事务性和非事务性)。如果客户端重新连接,那么这些锁将不再生效。此外,如果客户端有一个活动事务,在断开连接时服务器会回滚该事务,如果重新连接发生,则新会话将以自动提交启用的方式开始。因此,客户端可能希望禁用自动重新连接。启用自动重新连接时,如果重新连接发生,客户端不会收到通知,但任何表锁或当前事务都会丢失。禁用自动重新连接时,如果连接断开,下一个发出的语句将出现错误。客户端可以检测错误并采取适当的措施,如重新获取锁或重做事务。请参阅自动重新连接控制。
注意
如果你在一个被锁定的表上使用 ALTER TABLE
,它可能会变为未锁定状态。例如,如果你尝试第二次 ALTER TABLE
操作,结果可能是一个错误 Table '*
tbl_name*' was not locked with LOCK TABLES
。为了处理这个问题,在第二次修改之前再次锁定表。另请参阅 Section B.3.6.1, “ALTER TABLE 存在的问题”。
表锁定和事务的交互
LOCK TABLES
和 UNLOCK TABLES
与事务的使用交互如下:
-
LOCK TABLES
不是事务安全的,并在尝试锁定表之前隐式提交任何活动事务。 -
UNLOCK TABLES
隐式提交任何活动事务,但仅在使用LOCK TABLES
获取表锁时。例如,在以下一组语句中,UNLOCK TABLES
释放全局读锁但不提交事务,因为没有表锁生效:FLUSH TABLES WITH READ LOCK; START TRANSACTION; SELECT ... ; UNLOCK TABLES;
-
开始一个事务(例如,使用
START TRANSACTION
)隐式提交任何当前事务并释放现有的表锁。 -
FLUSH TABLES WITH READ LOCK
获取全局读锁而不是表锁,因此它不受LOCK TABLES
和UNLOCK TABLES
关于表锁定和隐式提交的相同行为影响。例如,START TRANSACTION
不会释放全局读锁。请参见 Section 15.7.8.3, “FLUSH 语句”。 -
其他隐式导致事务提交的语句不会释放现有的表锁。有关此类语句的列表,请参见 Section 15.3.3, “导致隐式提交的语句”。
-
使用事务表(如
InnoDB
表)的正确方法是使用SET autocommit = 0
(而不是START TRANSACTION
)开始事务,然后使用LOCK TABLES
,直到显式提交事务之前不要调用UNLOCK TABLES
。例如,如果你需要向表t1
写入并从表t2
读取,你可以这样做:SET autocommit=0; LOCK TABLES t1 WRITE, t2 READ, ...; *... do something with tables t1 and t2 here ...* COMMIT; UNLOCK TABLES;
当你调用
LOCK TABLES
时,InnoDB
内部会获取自己的表锁,而 MySQL 会获取自己的表锁。InnoDB
在下一次提交时释放其内部表锁,但要释放 MySQL 的表锁,你必须调用UNLOCK TABLES
。你不应该设置autocommit = 1
,因为这样InnoDB
会在调用LOCK TABLES
后立即释放其内部表锁,很容易发生死锁。如果设置autocommit = 1
,InnoDB
根本不会获取内部表锁,以帮助旧应用程序避免不必要的死锁。 -
ROLLBACK
不会释放表锁。
表锁和触发器
如果使用LOCK TABLES
显式锁定一个表,那么触发器中使用的任何表也会隐式锁定:
-
这些锁与使用
LOCK TABLES
语句显式获取的锁同时获取。 -
触发器中使用的表的锁取决于表是否仅用于读取。如果是,那么读取锁就足够了。否则,会使用写入锁。
-
如果一个表被使用
LOCK TABLES
显式锁定以供读取,但由于可能在触发器中被修改而需要写入锁定,那么会获取写入锁定而不是读取锁定。(也就是说,由于表在触发器中的出现导致对表的显式读取锁请求被转换为写入锁请求。)
假设你使用这个语句锁定了两个表,t1
和t2
:
LOCK TABLES t1 WRITE, t2 READ;
如果t1
或t2
有任何触发器,触发器中使用的表也会被锁定。假设t1
有一个定义如下的触发器:
CREATE TRIGGER t1_a_ins AFTER INSERT ON t1 FOR EACH ROW
BEGIN
UPDATE t4 SET count = count+1
WHERE id = NEW.id AND EXISTS (SELECT a FROM t3);
INSERT INTO t2 VALUES(1, 2);
END;
LOCK TABLES
语句的结果是,t1
和t2
被锁定,因为它们出现在语句中,而t3
和t4
被锁定,因为它们在触发器中使用:
-
根据
WRITE
锁请求,t1
被锁定为写入。 -
即使请求是读取锁,
t2
也被锁定为写入。这是因为在触发器内插入了t2
,所以读取请求被转换为写入请求。 -
因为只在触发器内部读取,所以
t3
被锁定为只读。 -
因为可能在触发器内更新,所以
t4
被锁定为写入。
表锁定的限制和条件
您可以安全地使用KILL
来终止等待表锁定的会话。请参阅第 15.7.8.4 节,“KILL 语句”。
不能在存储程序中使用LOCK TABLES
和UNLOCK TABLES
。
不能使用LOCK TABLES
锁定performance_schema
数据库中的表,除了setup_*
xxx*
表。
由LOCK TABLES
生成的锁的范围是单个 MySQL 服务器。它与 NDB Cluster 不兼容,NDB Cluster 无法跨多个mysqld实例强制执行 SQL 级别的锁。您可以在 API 应用程序中强制执行锁定。有关更多信息,请参阅第 25.2.7.10 节,“与多个 NDB Cluster 节点相关的限制”。
在LOCK TABLES
语句生效期间,禁止以下语句:CREATE TABLE
,CREATE TABLE ... LIKE
,CREATE VIEW
,DROP VIEW
以及对存储函数、存储过程和事件的 DDL 语句。
对于一些操作,必须访问mysql
数据库中的系统表。例如,HELP
语句需要服务器端帮助表的内容,而CONVERT_TZ()
可能需要读取时区表。服务器会根据需要隐式锁定系统表以进行读取,因此您无需显式锁定它们。这些表被视为刚刚描述的方式:
mysql.help_category
mysql.help_keyword
mysql.help_relation
mysql.help_topic
mysql.time_zone
mysql.time_zone_leap_second
mysql.time_zone_name
mysql.time_zone_transition
mysql.time_zone_transition_type
如果您想要在任何表格上明确放置一个WRITE
锁定,并使用LOCK TABLES
语句,那么该表格必须是唯一被锁定的;没有其他表格可以与相同语句一起被锁定。
通常情况下,您不需要锁定表格,因为所有单个UPDATE
语句都是原子的;没有其他会话可以干扰任何其他当前执行的 SQL 语句。然而,在一些情况下,锁定表格可能会带来优势:
-
如果您将在一组
MyISAM
表上运行许多操作,锁定您将要使用的表格会更快。锁定MyISAM
表格会加快在其上插入、更新或删除的速度,因为 MySQL 在调用UNLOCK TABLES
之前不会刷新被锁定表格的键缓存。通常情况下,每个 SQL 语句后都会刷新键缓存。锁定表格的缺点是,没有会话可以更新一个
READ
-锁定的表格(包括持有锁的表格),也没有会话可以访问一个WRITE
-锁定的表格,除了持有锁的表格。 -
如果您正在使用非事务性存储引擎来创建表格,如果您希望确保在
SELECT
和UPDATE
之间没有其他会话修改表格,您必须使用LOCK TABLES
。这里展示的示例需要使用LOCK TABLES
来安全执行:LOCK TABLES trans READ, customer WRITE; SELECT SUM(value) FROM trans WHERE customer_id=*some_id*; UPDATE customer SET total_value=*sum_from_previous_statement* WHERE customer_id=*some_id*; UNLOCK TABLES;
没有
LOCK TABLES
,可能会导致另一个会话在执行SELECT
和UPDATE
语句之间插入新行到trans
表中。
在许多情况下,您可以通过使用相对更新(UPDATE customer SET *
value*=*
value*+*
new_value*
)或LAST_INSERT_ID()
函数来避免使用LOCK TABLES
。
在某些情况下,您也可以通过使用用户级别的咨询锁函数GET_LOCK()
和RELEASE_LOCK()
来避免锁定表格。这些锁保存在服务器的哈希表中,并且使用pthread_mutex_lock()
和pthread_mutex_unlock()
实现高速。请参见第 14.14 节,“锁定函数”。
有关锁定策略的更多信息,请参见第 10.11.1 节,“内部锁定方法”。
15.3.7 SET TRANSACTION 语句
SET [GLOBAL | SESSION] TRANSACTION
*transaction_characteristic* [, *transaction_characteristic*] ...
*transaction_characteristic*: {
ISOLATION LEVEL *level*
| *access_mode*
}
*level*: {
REPEATABLE READ
| READ COMMITTED
| READ UNCOMMITTED
| SERIALIZABLE
}
*access_mode*: {
READ WRITE
| READ ONLY
}
此语句指定了事务 特性。它接受一个由逗号分隔的一个或多个特性值的列表。每个特性值设置事务的隔离级别 或访问模式。隔离级别用于对InnoDB
表进行操作。访问模式指定事务是以读/写模式还是只读模式运行。
此外,SET TRANSACTION
可以包括一个可选的GLOBAL
或SESSION
关键字,以指示语句的范围。
-
事务隔离级别
-
事务访问模式
-
事务特性范围
事务隔离级别
要设置事务隔离级别,请使用ISOLATION LEVEL *
level*
子句。不允许在同一SET TRANSACTION
语句中指定多个ISOLATION LEVEL
子句。
默认隔离级别是REPEATABLE READ
。其他允许的值是READ COMMITTED
、READ UNCOMMITTED
和SERIALIZABLE
。有关这些隔离级别的信息,请参见第 17.7.2.1 节,“事务隔离级别”。
事务访问模式
要设置事务访问模式,请使用READ WRITE
或READ ONLY
子句。不允许在同一SET TRANSACTION
语句中指定多个访问模式子句。
默认情况下,事务以读/写模式进行,允许对事务中使用的表进行读取和写入。可以使用具有READ WRITE
访问模式的SET TRANSACTION
明确指定此模式。
如果事务访问模式设置为READ ONLY
,则禁止对表进行更改。这可能使存储引擎能够进行性能改进,这些改进在不允许写入时可能是可能的。
在只读模式下,仍然可以使用 DML 语句更改使用 TEMPORARY
关键字创建的表。使用 DDL 语句进行的更改是不允许的,就像永久表一样。
也可以使用 START TRANSACTION
语句为单个事务指定 READ WRITE
和 READ ONLY
访问模式。
事务特性范围
您可以全局设置事务特性,为当前会话或仅限于下一个事务:
-
使用
GLOBAL
关键字:-
该语句全局适用于所有后续会话。
-
现有会话不受影响。
-
-
使用
SESSION
关键字:-
该语句适用于当前会话中执行的所有后续事务。
-
该语句允许在事务内部执行,但不影响当前正在进行的事务。
-
如果在事务之间执行,则该语句将覆盖设置下一个事务值的任何先前语句。
-
-
没有任何
SESSION
或GLOBAL
关键字:-
该语句仅适用于会话中执行的下一个单个事务。
-
后续事务将恢复使用命名特性的会话值。
-
该语句不允许在事务内部执行:
mysql> START TRANSACTION; Query OK, 0 rows affected (0.02 sec) mysql> SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; ERROR 1568 (25001): Transaction characteristics can't be changed while a transaction is in progress
-
更改全局事务特性需要 CONNECTION_ADMIN
权限(或已弃用的 SUPER
权限)。任何会话都可以自由更改其会话特性(即使在事务中间),或者更改其下一个事务的特性(在该事务开始之前)。
要在服务器启动时设置全局隔离级别,请在命令行或选项文件中使用 --transaction-isolation=*
level*
选项。此选项的 level
值使用破折号而不是空格,因此可接受的值为 READ-UNCOMMITTED
、READ-COMMITTED
、REPEATABLE-READ
或 SERIALIZABLE
。
同样,要在服务器启动时设置全局事务访问模式,请使用 --transaction-read-only
选项。默认值为 OFF
(读/写模式),但该值可以设置为 ON
以进行只读模式。
例如,要将隔离级别设置为 REPEATABLE READ
并将访问模式设置为 READ WRITE
,请在选项文件的 [mysqld]
部分中使用以下行:
[mysqld]
transaction-isolation = REPEATABLE-READ
transaction-read-only = OFF
在运行时,可以间接地使用SET TRANSACTION
语句设置全局、会话和下一个事务范围级别的特征,如前所述。也可以直接使用SET
语句为transaction_isolation
和transaction_read_only
系统变量赋值来直接设置它们:
-
SET TRANSACTION
允许在不同范围级别设置事务特征时使用可选的GLOBAL
和SESSION
关键字。 -
用于为
transaction_isolation
和transaction_read_only
系统变量赋值的SET
语句具有在不同范围级别设置这些变量的语法。
以下表格显示了每个SET TRANSACTION
和变量赋值语法设置的特征范围级别。
表 15.9 事务特征的 SET TRANSACTION 语法
语法 | 受影响的特征范围 |
---|---|
SET GLOBAL TRANSACTION * transaction_characteristic* |
全局 |
SET SESSION TRANSACTION * transaction_characteristic* |
会话 |
SET TRANSACTION * transaction_characteristic* |
仅适用于下一个事务 |
表 15.10 事务特征的 SET 语法
语法 | 受影响的特征范围 |
---|---|
SET GLOBAL * var_name* = * value* |
全局 |
SET @@GLOBAL.* var_name* = * value* |
全局 |
SET PERSIST * var_name* = * value* |
全局 |
SET @@PERSIST.* var_name* = * value* |
全局 |
SET PERSIST_ONLY * var_name* = * value* |
无运行时效果 |
SET @@PERSIST_ONLY.* var_name* = * value* |
无运行时效果 |
SET SESSION * var_name* = * value* |
会话 |
SET @@SESSION.* var_name* = * value* |
会话 |
SET * var_name* = * value* |
会话 |
SET @@* var_name* = * value* |
仅适用于下一个事务 |
语法 | 受影响的特征范围 |
可以在运行时检查事务特征的全局和会话值:
SELECT @@GLOBAL.transaction_isolation, @@GLOBAL.transaction_read_only;
SELECT @@SESSION.transaction_isolation, @@SESSION.transaction_read_only;
15.3.8 XA 事务
15.3.8.1 XA 事务 SQL 语句
15.3.8.2 XA 事务状态
15.3.8.3 XA 事务的限制
InnoDB
存储引擎支持 XA 事务。MySQL 的 XA 实现基于 X/Open CAE 文档Distributed Transaction Processing: The XA Specification。该文档由 The Open Group 发布,可在www.opengroup.org/public/pubs/catalog/c193.htm
获取。当前 XA 实现的限制在第 15.3.8.3 节“XA 事务的限制”中描述。
在客户端方面,没有特殊要求。MySQL 服务器的 XA 接口由以XA
关键字开头的 SQL 语句组成。MySQL 客户端程序必须能够发送 SQL 语句并理解 XA 语句接口的语义。它们不需要链接到最新的客户端库。旧的客户端库也可以工作。
在 MySQL Connectors 中,MySQL Connector/J 5.0.0 及更高版本直接支持 XA,通过一个处理 XA SQL 语句接口的类接口。
XA 支持分布式事务,即允许多个独立的事务资源参与全局事务。事务资源通常是关系型数据库,但也可能是其他类型的资源。
一个全局事务涉及几个本身是事务性的操作,但所有这些操作必须作为一个组要么全部成功完成,要么全部回滚。实质上,这将 ACID 属性“提升”到一个更高的级别,以便多个 ACID 事务可以作为全局操作的组成部分一起执行,并且也具有 ACID 属性。(与非分布式事务一样,如果您的应用程序对读现象敏感,可能更喜欢SERIALIZABLE
。REPEATABLE READ
可能对分布式事务不够。)
一些分布式事务的例子:
-
一个应用程序可以充当一个集成工具,将消息服务与关系型数据库结合在一起。该应用程序确保处理涉及消息发送、检索和处理的事务,同时涉及事务性数据库,都发生在一个全局事务中。你可以将其视为“事务性电子邮件”。
-
一个应用程序执行涉及不同数据库服务器的操作,例如 MySQL 服务器和 Oracle 服务器(或多个 MySQL 服务器),其中涉及多个服务器的操作必须作为全局事务的一部分进行,而不是作为每个服务器本地的单独事务。
-
一家银行在关系数据库管理系统(RDBMS)中保留账户信息,并通过自动取款机(ATM)分发和接收资金。必须确保 ATM 操作在账户中正确反映,但这不能仅通过 RDBMS 完成。全局事务管理器整合 ATM 和数据库资源,以确保金融交易的整体一致性。
使用全局事务的应用程序涉及一个或多个资源管理器和一个事务管理器:
-
资源管理器(RM)提供对事务资源的访问。数据库服务器是资源管理器的一种。必须能够提交或回滚由 RM 管理的事务。
-
事务管理器(TM)协调作为全局事务一部分的事务。它与处理每个事务的 RM 进行通信。全局事务及其分支由稍后描述的命名方案标识。
MySQL 对 XA 的实现使 MySQL 服务器能够充当处理全局事务中的 XA 事务的资源管理器。连接到 MySQL 服务器的客户端程序充当事务管理器。
要执行全局事务,必须知道涉及哪些组件,并使每个组件达到可以提交或回滚的状态。根据每个组件报告的关于其成功能力的情况,它们必须作为一个原子组提交或回滚。也就是说,要么所有组件都必须提交,要么所有组件都必须回滚。要管理全局事务,必须考虑到任何组件或连接网络可能会失败。
执行全局事务的过程使用两阶段提交(2PC)。这发生在执行全局事务的分支执行的操作之后。
-
在第一阶段,所有分支都被准备好了。也就是说,它们被 TM 告知准备好提交。通常,这意味着每个管理分支的 RM 在稳定存储中记录了分支的操作。这些分支指示它们是否能够这样做,这些结果将用于第二阶段。
-
在第二阶段,TM 告诉 RM 是否提交或回滚。如果所有分支在准备时指示它们能够提交,那么所有分支都被告知提交。如果任何分支在准备时指示它不能提交,那么所有分支都被告知回滚。
在某些情况下,全局事务可能会使用一阶段提交(1PC)。例如,当事务管理器发现全局事务仅包含一个事务资源(即单个分支)时,可以要求该资源同时准备和提交。
15.3.8.1 XA 事务 SQL 语句
要在 MySQL 中执行 XA 事务,请使用以下语句:
XA {START|BEGIN} *xid* [JOIN|RESUME]
XA END *xid* [SUSPEND [FOR MIGRATE]]
XA PREPARE *xid*
XA COMMIT *xid* [ONE PHASE]
XA ROLLBACK *xid*
XA RECOVER [CONVERT XID]
对于 XA START
,JOIN
和 RESUME
子句被识别但没有效果。
对于 XA END
,SUSPEND [FOR MIGRATE]
子句被识别但没有效果。
每个 XA 语句都以 XA
关键字开头,大多数需要一个 xid
值。xid
是一个 XA 事务标识符。它指示语句适用于哪个事务。xid
值由客户端提供,或由 MySQL 服务器生成。xid
值有一到三部分:
*xid*: *gtrid* [, *bqual* [, *formatID* ]]
gtrid
是全局事务标识符,bqual
是分支限定符,formatID
是标识 gtrid
和 bqual
值使用的格式的数字。如语法所示,bqual
和 formatID
是可选的。如果未给出,默认 bqual
值为 ''
。如果未给出,默认 formatID
值为 1。
gtrid
和 bqual
必须是字符串字面量,每个最多 64 字节(而非字符)长。gtrid
和 bqual
可以以几种方式指定。您可以使用带引号的字符串('ab'
)、十六进制字符串(X'6162'
、0x6162
)或位值(b'*
nnnn*'
)。
formatID
是一个无符号整数。
gtrid
和 bqual
值由 MySQL 服务器底层的 XA 支持程序解释为字节。然而,在解析包含 XA 语句的 SQL 语句时,服务器使用特定的字符集。为了安全起见,将 gtrid
和 bqual
写成十六进制字符串。
xid
值通常由事务管理器生成。一个事务管理器生成的值必须与其他事务管理器生成的值不同。给定的事务管理器必须能够在 XA RECOVER
语句返回的值列表中识别出自己的 xid
值。
XA START *
xid*
使用给定的 xid
值启动一个 XA 事务。每个 XA 事务必须具有唯一的 xid
值,因此该值不能当前被另一个 XA 事务使用。使用 gtrid
和 bqual
值来评估唯一性。随后的所有 XA 语句必须使用与 XA START
语句中给定的 xid
值相同的 xid
值指定。如果您使用这些语句之一,但指定的 xid
值与某个现有 XA 事务不对应,则会发生错误。
从 MySQL 8.0.31 开始,当服务器运行时带有 --replicate-do-db
或 --replicate-ignore-db
时,XA START
、XA BEGIN
、XA END
、XA COMMIT
和 XA ROLLBACK
语句不会被默认数据库过滤。
一个或多个 XA 事务可以是同一个全局事务的一部分。给定全局事务中的所有 XA 事务必须在 xid
值中使用相同的 gtrid
值。因此,gtrid
值必须是全局唯一的,以确保对于给定 XA 事务是哪个全局事务的一部分没有歧义。 xid
值的 bqual
部分必须对于全局事务中的每个 XA 事务都不同。 (bqual
值必须不同的要求是当前 MySQL XA 实现的限制。它不是 XA 规范的一部分。)
XA RECOVER
语句返回 MySQL 服务器上处于PREPARED
状态的 XA 事务的信息。(参见 Section 15.3.8.2, “XA 事务状态”.)输出包括服务器上每个这样的 XA 事务的行,无论是哪个客户端启动的。
XA RECOVER
需要 XA_RECOVER_ADMIN
权限。这个权限要求阻止用户发现除了自己之外的未完成准备好的 XA 事务的 XID 值。它不影响 XA 事务的正常提交或回滚,因为启动它的用户知道它的 XID。
XA RECOVER
输出行如下(对于由部分 'abc'
、'def'
和 7
组成的 xid
值的示例):
mysql> XA RECOVER;
+----------+--------------+--------------+--------+
| formatID | gtrid_length | bqual_length | data |
+----------+--------------+--------------+--------+
| 7 | 3 | 3 | abcdef |
+----------+--------------+--------------+--------+
输出列的含义如下:
-
formatID
是事务xid
的formatID
部分 -
gtrid_length
是xid
的gtrid
部分的字节长度 -
bqual_length
是xid
的bqual
部分的字节长度 -
data
是xid
的gtrid
和bqual
部分的串联
XID 值可能包含不可打印字符。XA RECOVER
允许一个可选的 CONVERT XID
子句,以便客户端可以请求十六进制的 XID 值。
15.3.8.2 XA 事务状态
一个 XA 事务会经历以下状态:
-
使用
XA START
来启动一个 XA 事务并将其置于ACTIVE
状态。 -
对于一个
ACTIVE
的 XA 事务,发出构成事务的 SQL 语句,然后发出XA END
语句。XA END
将事务置于IDLE
状态。 -
对于一个
IDLE
的 XA 事务,可以发出XA PREPARE
语句或XA COMMIT ... ONE PHASE
语句:-
XA PREPARE
将事务置于PREPARED
状态。此时,XA RECOVER
语句会在输出中包含事务的xid
值,因为XA RECOVER
列出所有处于PREPARED
状态的 XA 事务。 -
XA COMMIT ... ONE PHASE
准备并提交事务。xid
值不会被XA RECOVER
列出,因为事务已经终止。
-
-
对于一个
PREPARED
的 XA 事务,可以发出XA COMMIT
语句来提交并终止事务,或者发出XA ROLLBACK
来回滚并终止事务。
下面是一个简单的 XA 事务,作为全局事务的一部分向表中插入一行:
mysql> XA START 'xatest';
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO mytable (i) VALUES(10);
Query OK, 1 row affected (0.04 sec)
mysql> XA END 'xatest';
Query OK, 0 rows affected (0.00 sec)
mysql> XA PREPARE 'xatest';
Query OK, 0 rows affected (0.00 sec)
mysql> XA COMMIT 'xatest';
Query OK, 0 rows affected (0.00 sec)
在 MySQL 8.0.28 及更早版本中,在给定客户端连接的上下文中,XA 事务和本地(非 XA)事务是互斥的。例如,如果已经发出 XA START
来开始一个 XA 事务,那么在 XA 事务提交或回滚之前不能启动本地事务。反之,如果已经使用 START TRANSACTION
开始了本地事务,那么在事务提交或回滚之前不能使用 XA 语句。
MySQL 8.0.29 及更高版本支持分离的 XA 事务,通过xa_detach_on_prepare
系统变量启用(默认为ON
)。分离的事务在执行XA PREPARE
后与当前会话断开连接(可以由另一个连接提交或回滚)。这意味着当前会话可以自由开始新的本地事务或 XA 事务,而无需等待准备的 XA 事务被提交或回滚。
当 XA 事务被分离时,一个连接对其准备的任何 XA 事务没有特殊知识。如果当前会话尝试提交或回滚给定的 XA 事务(即使是它准备的),在另一个连接已经这样做之后,请求将被拒绝,并显示无效的 XID 错误(ER_XAER_NOTA
),因为请求的xid
不再存在。
注意
分离的 XA 事务不能使用临时表。
当禁用分离的 XA 事务(xa_detach_on_prepare
设置为OFF
)时,一个 XA 事务会保持连接,直到由发起连接提交或回滚,就像之前描述的 MySQL 8.0.28 及更早版本一样。不建议为用于组复制的 MySQL 服务器实例禁用分离的 XA 事务;有关更多信息,请参阅服务器实例配置。
如果一个 XA 事务处于ACTIVE
状态,你不能发出任何导致隐式提交的语句。这将违反 XA 协议,因为你无法回滚 XA 事务。尝试执行这样的语句会引发以下错误:
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed
when global transaction is in the ACTIVE state
适用于前述备注的语句列在第 15.3.3 节,“导致隐式提交的语句”中。
15.3.8.3 XA 事务的限制
XA 事务支持仅限于InnoDB
存储引擎。
对于“外部 XA”,一个 MySQL 服务器充当资源管理器,客户端程序充当事务管理器。对于“内部 XA”,MySQL 服务器内的存储引擎充当 RMs,服务器本身充当 TM。内部 XA 支持受到各个存储引擎能力的限制。处理涉及多个存储引擎的 XA 事务需要内部 XA。实现内部 XA 要求存储引擎在表处理程序级别支持两阶段提交,目前只有InnoDB
符合这一要求。
对于XA START
,识别JOIN
和RESUME
子句但不起作用。
对于XA END
,识别SUSPEND [FOR MIGRATE]
子句但不起作用。
要求xid
值的bqual部分在全局事务中的每个 XA 事务中不同是当前 MySQL XA 实现的限制。这不是 XA 规范的一部分。
XA 事务分两部分写入二进制日志。当发出XA PREPARE
时,事务的第一部分直到XA PREPARE
使用初始 GTID 写入。XA_prepare_log_event
用于在二进制日志中标识这类事务。当发出XA COMMIT
或XA ROLLBACK
时,包含仅XA COMMIT
或XA ROLLBACK
语句的事务的第二部分使用第二个 GTID 写入。请注意,由XA_prepare_log_event
标识的事务的初始部分不一定会跟随其XA COMMIT
或XA ROLLBACK
,这可能导致任意两个 XA 事务的二进制日志交错记录。XA 事务的两部分甚至可以出现在不同的二进制日志文件中。这意味着处于PREPARED
状态的 XA 事务现在持久存在,直到发出显式的XA COMMIT
或XA ROLLBACK
语句,确保 XA 事务与复制兼容。
在副本上,在 XA 事务准备完成后,它会从复制应用程序线程中分离,并可以由副本上的任何线程提交或回滚。这意味着相同的 XA 事务可以在不同线程上以不同状态出现在events_transactions_current
表中。events_transactions_current
表显示线程上最近监视的事务事件的当前状态,并在线程空闲时不更新此状态。因此,即使在被另一个线程处理后,XA 事务仍可能以PREPARED
状态显示在原始应用程序线程中。要明确识别仍处于PREPARED
状态且需要恢复的 XA 事务,请使用XA RECOVER
语句,而不是性能模式事务表。
使用 XA 事务存在以下限制:
-
在 MySQL 8.0.30 之前,XA 事务在意外停机时对于二进制日志不是完全具有弹性。如果在服务器正在执行
XA PREPARE
、XA COMMIT
、XA ROLLBACK
或XA COMMIT ... ONE PHASE
语句时发生意外停机,服务器可能无法恢复到正确状态,导致服务器和二进制日志处于不一致状态。在这种情况下,二进制日志可能包含未应用的额外 XA 事务,或者缺少已应用的 XA 事务。此外,如果启用了 GTIDs,在恢复后,@@GLOBAL.GTID_EXECUTED
可能无法正确描述已应用的事务。请注意,如果在XA PREPARE
之前、在XA PREPARE
和XA COMMIT
(或XA ROLLBACK
)之间,或在XA COMMIT
(或XA ROLLBACK
)之后发生意外停机,则服务器和二进制日志将正确恢复并处于一致状态。从 MySQL 8.0.30 开始,这不再是一个问题;服务器将
XA PREPARE
实现为一个两阶段操作,它在存储引擎和服务器之间维护准备操作的状态,并在存储引擎和二进制日志之间强制执行执行顺序,以便在服务器节点上的状态在一致和持久之前不进行广播。请注意,当使用相同的事务 XID 依次执行 XA 事务并且在处理
XA COMMIT ... ONE PHASE
过程中发生中断时,可能无法再同步二进制日志和存储引擎之间的状态。如果在此事务在存储引擎中准备后发生了刚描述的一系列事件,而XA COMMIT
语句仍在执行,则可能会发生这种情况。这是一个已知问题。 -
不支持在 XA 事务中使用复制过滤器或二进制日志过滤器。对表的过滤可能导致副本上的 XA 事务为空,并且不支持空的 XA 事务。此外,副本的连接元数据存储库和应用程序元数据存储库存储在
InnoDB
表中,这在 MySQL 8.0 中成为默认设置,过滤的 XA 事务会改变数据引擎事务的内部状态,并可能与复制事务上下文状态不一致。当 XA 事务受到复制过滤器的影响时,无论事务是否为空,都会记录错误
ER_XA_REPLICATION_FILTERS
。如果事务不为空,则副本可以继续运行,但您应该采取措施停止使用 XA 事务的复制过滤器以避免潜在问题。如果事务为空,则副本将停止。在这种情况下,副本可能处于一个不确定的状态,其中复制过程的一致性可能会受到损害。特别是,副本的gtid_executed
集合可能与源端不一致。要解决这种情况,请隔离源端并停止所有复制,然后检查复制拓扑结构中的 GTID 一致性。撤消生成错误消息的 XA 事务,然后重新启动复制。 -
XA 事务被认为对于基于语句的复制是不安全的。如果在源端并行提交了两个 XA 事务,并且在副本上以相反的顺序准备这些事务,可能会发生无法安全解决的锁定依赖关系,并且复制可能会因副本上的死锁而失败。这种情况可能发生在单线程或多线程副本上。当设置
binlog_format=STATEMENT
时,对于 XA 事务中的 DML 语句会发出警告。当设置binlog_format=MIXED
或binlog_format=ROW
时,XA 事务中的 DML 语句将使用基于行的复制进行记录,潜在问题将不再存在。
15.4 复制语句
原文:
dev.mysql.com/doc/refman/8.0/en/sql-replication-statements.html
15.4.1 控制源服务器的 SQL 语句
15.4.2 控制副本服务器的 SQL 语句
15.4.3 控制组复制的 SQL 语句
通过本节描述的语句,可以通过 SQL 接口控制复制。语句分为控制源服务器的组,控制副本服务器的组,以及可应用于任何复制服务器的组。
15.4.1 用于控制源服务器的 SQL 语句
原文:
dev.mysql.com/doc/refman/8.0/en/replication-statements-master.html
15.4.1.1 清除二进制日志语句
15.4.1.2 重置主服务器语句
15.4.1.3 设置 sql_log_bin 语句
本节讨论了用于管理复制源服务器的语句。第 15.4.2 节,“用于控制副本服务器的 SQL 语句”,讨论了用于管理副本服务器的语句。
除了这里描述的语句外,以下SHOW
语句用于在复制中与源服务器一起使用。有关这些语句的信息,请参阅第 15.7.7 节,“显示语句”。
-
SHOW BINARY LOGS
-
SHOW BINLOG EVENTS
-
SHOW MASTER STATUS
-
SHOW REPLICAS
(或在 MySQL 8.0.22 之前,SHOW SLAVE HOSTS
)
15.4.1.1 PURGE BINARY LOGS Statement
PURGE { BINARY | MASTER } LOGS {
TO '*log_name*'
| BEFORE *datetime_expr*
}
二进制日志是一组文件,其中包含 MySQL 服务器进行的数据修改的信息。该日志由一组二进制日志文件和一个索引文件组成(参见 第 7.4.4 节,“二进制日志”)。
PURGE BINARY LOGS
语句删除日志索引文件中指定日志文件名或日期之前列出的所有二进制日志文件。BINARY
和 MASTER
是同义词。删除的日志文件也会从索引文件中记录的列表中移除,因此给定的日志文件将成为列表中的第一个。
运行 PURGE BINARY LOGS
需要 BINLOG_ADMIN
权限。如果服务器没有使用 --log-bin
选项启动以启用二进制日志记录,则此语句无效。
示例:
PURGE BINARY LOGS TO 'mysql-bin.010';
PURGE BINARY LOGS BEFORE '2019-04-02 22:46:26';
BEFORE
变体的 datetime_expr
参数应该评估为一个 DATETIME
值(一个以 '*
YYYY-MM-DD hh:mm:ss*'
格式表示的值)。
当副本正在复制时,可以安全地运行 PURGE BINARY LOGS
。你不需要停止它们。如果你有一个活动的副本,当前正在读取你尝试删除的日志文件之一,这个语句不会删除正在使用的日志文件或比它更晚的任何日志文件,但会删除任何更早的日志文件。在这种情况下会发出警告消息。然而,如果一个副本没有连接,并且你碰巧清除了它尚未读取的日志文件之一,那么在重新连接后,该副本无法复制。
在实例生效 LOCK INSTANCE FOR BACKUP
语句时,不应该执行 PURGE BINARY LOGS
,因为这违反了备份锁的规则,会从服务器中删除文件。从 MySQL 8.0.28 开始,这是不允许的。
要安全地清除二进制日志文件,请按照以下步骤进行:
-
在每个副本上,使用
SHOW REPLICA STATUS
来检查它正在读取哪个日志文件。 -
使用
SHOW BINARY LOGS
在源上获取二进制日志文件的列表。 -
确定所有副本中最早的日志文件。这是目标文件。如果所有副本都是最新的,那么这是列表中的最后一个日志文件。
-
备份即将删除的所有日志文件。(此步骤是可选的,但始终建议执行。)
-
清除所有日志文件,但不包括目标文件。
PURGE BINARY LOGS TO
和 PURGE BINARY LOGS BEFORE
在二进制日志文件列表中的文件已经通过其他方式(比如在 Linux 上使用rm)被移除系统时会出现错误(Bug #18199, Bug #18453)。为了处理这种错误,需要手动编辑.index
文件(这是一个简单的文本文件),确保它只列出实际存在的二进制日志文件,然后重新运行失败的PURGE BINARY LOGS
语句。
服务器的二进制日志到期后会自动删除二进制日志文件。文件的删除可以在启动时和二进制日志刷新时进行。默认的二进制日志到期时间为 30 天。您可以使用binlog_expire_logs_seconds
系统变量指定替代的到期时间。如果您正在使用复制,应该指定一个到期时间,不低于从源头落后的最长时间。
15.4.1.2 RESET MASTER Statement
RESET MASTER [TO *binary_log_file_index_number*]
警告
谨慎使用此语句,以确保不会丢失任何需要的二进制日志文件数据和 GTID 执行历史记录。
RESET MASTER
需要RELOAD
权限。
对于启用了二进制日志记录的服务器(log_bin
为ON
),RESET MASTER
会删除所有现有的二进制日志文件并重置二进制日志索引文件,将服务器重置为启动二进制日志记录之前的状态。会创建一个新的空二进制日志文件,以便重新启动二进制日志记录。
对于使用 GTIDs 的服务器(gtid_mode
为ON
),发出RESET MASTER
会重置 GTID 执行历史记录。gtid_purged
系统变量的值设置为空字符串(''
),gtid_executed
系统变量的全局值(但不是会话值)设置为空字符串,并清除mysql.gtid_executed
表(参见 mysql.gtid_executed Table)。如果启用了 GTID 的服务器启用了二进制日志记录,RESET MASTER
也会像上面描述的那样重置二进制日志。请注意,即使启用了 GTID 的服务器是一个禁用了二进制日志记录的副本,RESET MASTER
也是重置 GTID 执行历史记录的方法;RESET REPLICA
对 GTID 执行历史记录没有影响。有关重置 GTID 执行历史记录的更多信息,请参阅重置 GTID 执行历史记录。
发出不带可选TO
子句的RESET MASTER
会删除索引文件中列出的所有二进制日志文件,将二进制日志索引文件重置为空,并创建一个从1
开始的新二进制日志文件。在重置后,可以使用可选的TO
子句从不是1
的数字开始二进制日志文件索引。
确保您使用了合理的索引号值。如果输入了不正确的值,您可以通过发出带有或不带有TO
子句的另一个RESET MASTER
语句来更正。如果不更正超出范围的值,服务器将无法重新启动。
以下示例演示了TO
子句的用法:
RESET MASTER TO 1234;
SHOW BINARY LOGS;
+-------------------+-----------+-----------+
| Log_name | File_size | Encrypted |
+-------------------+-----------+-----------+
| source-bin.001234 | 154 | No |
+-------------------+-----------+-----------+
重要提示
不带TO
子句的RESET MASTER
的效果与PURGE BINARY LOGS
的效果有 2 个关键区别:
-
RESET MASTER
会删除索引文件中列出的所有二进制日志文件,只留下一个带有数字后缀.000001
的空二进制日志文件,而编号不会被PURGE BINARY LOGS
重置。 -
当任何副本正在运行时,不应使用
RESET MASTER
。在副本正在运行时使用RESET MASTER
的行为是未定义的(因此不受支持),而可以在副本正在运行时安全地使用PURGE BINARY LOGS
。
另请参阅 Section 15.4.1.1, “PURGE BINARY LOGS Statement”。
在首次设置源和副本时,执行不带TO
子句的RESET MASTER
可能会很有用,以便您可以验证设置如下:
-
启动源和副本,并开始复制(参见 Section 19.1.2, “Setting Up Binary Log File Position Based Replication”)。
-
在源上执行几个测试查询。
-
检查查询是否已被复制到副本。
-
当复制正常运行时,在副本上依次执行
STOP REPLICA
,然后执行RESET REPLICA
,然后验证副本上不存在来自测试查询的不需要的数据。 -
从源中删除不需要的数据,然后执行
RESET MASTER
以清除与其关联的任何二进制日志条目和标识符。
在验证设置、重置源和副本并确保源或副本上没有任何不需要的数据或测试生成的二进制日志文件后,您可以启动副本并开始复制。
15.4.1.3 SET sql_log_bin 语句
SET sql_log_bin = {OFF|ON}
sql_log_bin
变量控制当前会话是否启用二进制日志记录(假设二进制日志本身已启用)。默认值为ON
。要为当前会话禁用或启用二进制日志记录,请将会话sql_log_bin
变量设置为OFF
或ON
。
将此变量设置为OFF
,以便在对源进行更改时暂时禁用二进制日志记录,而这些更改不希望被复制到副本中。
设置此系统变量的会话值是受限制的操作。会话用户必须具有足够的特权来设置受限制的会话变量。请参阅第 7.1.9.1 节,“系统变量特权”。
不可能在事务或子查询中设置sql_log_bin
的会话值。
将此变量设置为OFF
会阻止新的 GTID 分配给二进制日志中的事务。如果您正在使用 GTID 进行复制,这意味着即使稍后重新启用二进制日志记录,从此时点开始写入日志的 GTID 也不会考虑期间发生的任何事务,因此实际上这些事务将丢失。
mysqldump向使用 GTID 的服务器的转储文件中添加SET @@SESSION.sql_log_bin=0
语句,这会在重新加载转储文件时禁用二进制日志记录。该语句阻止生成新的 GTID 并将其分配给执行时的转储文件中的事务,以便使用事务的原始 GTID。
15.4.2 用于控制 REPLICA 服务器的 SQL 语句
原文:
dev.mysql.com/doc/refman/8.0/en/replication-statements-replica.html
15.4.2.1 更改主服务器 TO 语句
15.4.2.2 更改复制过滤器语句
15.4.2.3 更改复制源 TO 语句
15.4.2.4 重置 REPLICA 语句
15.4.2.5 重置 SLAVE 语句
15.4.2.6 启动 REPLICA 语句
15.4.2.7 启动 SLAVE 语句
15.4.2.8 停止 REPLICA 语句
15.4.2.9 停止 SLAVE 语句
本节讨论了用于管理 REPLICA 服务器的语句。第 15.4.1 节,“用于控制源服务器的 SQL 语句”,讨论了用于管理源服务器的语句。
除了这里描述的语句外,SHOW REPLICA STATUS
和 SHOW RELAYLOG EVENTS
也与 REPLICA 一起使用。有关这些语句的信息,请参阅 第 15.7.7.35 节,“显示 REPLICA 状态语句” 和 第 15.7.7.32 节,“显示 RELAYLOG 事件语句”。
15.4.2.1 更改主服务器语句
CHANGE MASTER TO *option* [, *option*] ... [ *channel_option* ]
*option*: {
MASTER_BIND = '*interface_name*'
| MASTER_HOST = '*host_name*'
| MASTER_USER = '*user_name*'
| MASTER_PASSWORD = '*password*'
| MASTER_PORT = *port_num*
| PRIVILEGE_CHECKS_USER = {'*account*' | NULL}
| REQUIRE_ROW_FORMAT = {0|1}
| REQUIRE_TABLE_PRIMARY_KEY_CHECK = {STREAM | ON | OFF}
| ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS = {OFF | LOCAL | *uuid*}
| MASTER_LOG_FILE = '*source_log_name*'
| MASTER_LOG_POS = *source_log_pos*
| MASTER_AUTO_POSITION = {0|1}
| RELAY_LOG_FILE = '*relay_log_name*'
| RELAY_LOG_POS = *relay_log_pos*
| MASTER_HEARTBEAT_PERIOD = *interval*
| MASTER_CONNECT_RETRY = *interval*
| MASTER_RETRY_COUNT = *count*
| SOURCE_CONNECTION_AUTO_FAILOVER = {0|1}
| MASTER_DELAY = *interval*
| MASTER_COMPRESSION_ALGORITHMS = '*algorithm*[,*algorithm*][,*algorithm*]'
| MASTER_ZSTD_COMPRESSION_LEVEL = *level*
| MASTER_SSL = {0|1}
| MASTER_SSL_CA = '*ca_file_name*'
| MASTER_SSL_CAPATH = '*ca_directory_name*'
| MASTER_SSL_CERT = '*cert_file_name*'
| MASTER_SSL_CRL = '*crl_file_name*'
| MASTER_SSL_CRLPATH = '*crl_directory_name*'
| MASTER_SSL_KEY = '*key_file_name*'
| MASTER_SSL_CIPHER = '*cipher_list*'
| MASTER_SSL_VERIFY_SERVER_CERT = {0|1}
| MASTER_TLS_VERSION = '*protocol_list*'
| MASTER_TLS_CIPHERSUITES = '*ciphersuite_list*'
| MASTER_PUBLIC_KEY_PATH = '*key_file_name*'
| GET_MASTER_PUBLIC_KEY = {0|1}
| NETWORK_NAMESPACE = '*namespace*'
| IGNORE_SERVER_IDS = (*server_id_list*),
| GTID_ONLY = {0|1}
}
*channel_option*:
FOR CHANNEL *channel*
*server_id_list*:
[*server_id* [, *server_id*] ... ]
更改主服务器为
更改了副本服务器用于连接到源和从源读取数据的参数。它还更新了复制元数据存储库的内容(参见第 19.2.4 节,“中继日志和复制元数据存储库”)。从 MySQL 8.0.23 开始,请使用更改复制源为
代替更改主服务器为
,该语句从该版本开始已弃用。在 MySQL 8.0.23 之前的版本中,请使用更改主服务器为
。
更改主服务器为
需要REPLICATION_SLAVE_ADMIN
权限(或已弃用的SUPER
权限)。
在更改主服务器为
语句中未指定的选项保留其值,除非在以下讨论中另有说明。因此,在大多数情况下,没有必要指定不更改的选项。
用于SOURCE_HOST
和其他更改复制源为
选项的值将检查换行符(\n
或0x0A
)字符。这些值中存在这些字符会导致语句失败并显示错误。
可选的FOR CHANNEL *
channel*
子句使您能够命名语句适用于哪个复制通道。提供FOR CHANNEL *
channel*
子句将CHANGE MASTER TO
语句应用于特定的复制通道,并用于添加新通道或修改现有通道。例如,要添加名为channel2
的新通道:
CHANGE MASTER TO MASTER_HOST=host1, MASTER_PORT=3002 FOR CHANNEL 'channel2'
如果未命名任何子句且不存在额外通道,则更改主服务器为
语句适用于默认通道,其名称为空字符串("")。当设置了多个复制通道时,每个更改主服务器为
语句必须使用FOR CHANNEL *
channel*
子句命名通道。有关更多信息,请参见第 19.2.2 节,“复制通道”。
对于CHANGE MASTER TO
语句的某些选项,您必须在发出CHANGE MASTER TO
语句之前(以及之后发出START SLAVE
语句)发出STOP SLAVE
语句。有时,您只需要停止复制 SQL(应用程序)线程或复制 I/O(接收器)线程,而不是两者同时停止:
-
当应用程序线程停止时,即使复制接收器线程正在运行,您也可以使用
RELAY_LOG_FILE
、RELAY_LOG_POS
和MASTER_DELAY
选项的任何允许组合执行CHANGE MASTER TO
。当接收器线程正在运行时,不得使用此语句的其他选项。 -
当接收器线程停止时,即使应用程序线程正在运行,您也可以使用此语句的任何选项(以任何允许的组合)执行
CHANGE MASTER TO
,除了RELAY_LOG_FILE
、RELAY_LOG_POS
、MASTER_DELAY
或MASTER_AUTO_POSITION = 1
。 -
在发出使用
MASTER_AUTO_POSITION = 1
、GTID_ONLY = 1
或ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS
的CHANGE MASTER TO
语句之前,必须停止接收线程和应用线程。
您可以使用SHOW SLAVE STATUS
检查复制应用程序线程和复制接收器线程的当前状态。请注意,Group Replication 应用程序通道(group_replication_applier
)没有接收器线程,只有一个应用程序线程。
CHANGE MASTER TO
语句具有许多副作用和交互作用,您应该事先了解:
-
CHANGE MASTER TO
导致正在进行的事务的隐式提交。请参阅第 15.3.3 节,“导致隐式提交的语句”。 -
CHANGE MASTER TO
会将先前的MASTER_HOST
、MASTER_PORT
、MASTER_LOG_FILE
和MASTER_LOG_POS
值写入错误日志,以及在执行之前有关副本状态的其他信息。 -
如果您正在使用基于语句的复制和临时表,可能会出现在
STOP SLAVE
语句后跟随CHANGE MASTER TO
语句时在副本上留下临时表的情况。每当发生这种情况时,都会发出警告(ER_WARN_OPEN_TEMP_TABLES_MUST_BE_ZERO
)。在这种情况下,您可以通过确保在执行此类CHANGE MASTER TO
语句之前,Replica_open_temp_tables
或Slave_open_temp_tables
系统状态变量的值等于 0 来避免这种情况。 -
当使用多线程副本(
replica_parallel_workers
> 0 或slave_parallel_workers
> 0)时,停止副本可能会导致已从中继日志执行的事务序列中存在间隙,无论副本是有意还是无意地停止。当存在这样的间隙时,发出CHANGE MASTER TO
将失败。在这种情况下的解决方案是发出START SLAVE UNTIL SQL_AFTER_MTS_GAPS
,以确保关闭这些间隙。从 MySQL 8.0.26 开始,在使用基于 GTID 的复制和 GTID 自动定位时,完全跳过检查事务序列中的间隙的过程,因为可以使用 GTID 自动定位解决事务间隙。在这种情况下,仍然可以使用CHANGE MASTER TO
。
CHANGE MASTER TO
语句提供以下选项:
ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS = {OFF | LOCAL | *
uuid*}
使复制通道为没有 GTID 的复制事务分配一个 GTID,从而实现从不使用基于 GTID 的复制的源到使用的副本的复制。对于多源副本,您可以同时使用使用ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS
和不使用的通道。默认值为OFF
,表示未使用该功能。
LOCAL
分配一个包括副本自己的 UUID(server_uuid
设置)的 GTID。*
uuid*
分配一个包括指定 UUID 的 GTID,例如用于复制源服务器的server_uuid
设置。使用非本地 UUID 可以让您区分在副本上发起的事务和在源上发起的事务,以及对于多源副本,区分在不同源上发起的事务。您选择的 UUID 仅对副本自身的使用具有意义。如果源发送的任何事务已经具有 GTID,则保留该 GTID。
专用于组复制的通道不能使用ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS
,但是服务器实例上另一个源的异步复制通道可以这样做。在这种情况下,不要将组复制组名称指定为用于创建 GTIDs 的 UUID。
要将ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS
设置为LOCAL
或*
uuid*
,副本必须设置gtid_mode=ON
,且此后不能更改。此选项用于与基于二进制日志文件位置的复制的源一起使用,因此不能为通道设置MASTER_AUTO_POSITION=1
。在设置此选项之前,必须停止复制 SQL 线程和复制 I/O(接收器)线程。
重要提示
在任何通道上设置了ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS
的副本都不能晋升为替换复制源服务器的副本,如果需要故障转移,则无法使用从副本获取的备份来恢复复制源服务器。相同的限制也适用于替换或恢复其他使用任何通道上的ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS
的副本。
有关更多限制和信息,请参见第 19.1.3.6 节,“从没有 GTIDs 的源复制到具有 GTIDs 的副本”。
GET_MASTER_PUBLIC_KEY = {0|1}
通过从源请求公钥启用基于 RSA 密钥对的密码交换。此选项默认禁用。
此选项适用于使用caching_sha2_password
认证插件进行身份验证的副本。对于使用此插件进行身份验证的帐户的连接,源不会发送公钥,除非请求,因此必须请求或在客户端中指定。如果给出MASTER_PUBLIC_KEY_PATH
并指定有效的公钥文件,则它优先于GET_MASTER_PUBLIC_KEY
。如果您使用使用caching_sha2_password
插件进行身份验证的复制用户帐户(从 MySQL 8.0 开始为默认设置),并且不使用安全连接,则必须指定此选项或MASTER_PUBLIC_KEY_PATH
选项之一以向副本提供 RSA 公钥。
GTID_ONLY = {0|1}
停止复制通道在复制元数据存储库中持久化文件名和文件位置。GTID_ONLY
从 MySQL 8.0.27 版本开始提供。对于异步复制通道,默认情况下禁用GTID_ONLY
选项,但对于 Group Replication 通道,默认情况下启用,并且无法禁用。
对于具有此设置的复制通道,仍然会跟踪内存中的文件位置,并且可以通过错误消息和诸如SHOW REPLICA STATUS
语句等接口观察文件位置以进行调试(如果过时,则显示为无效)。但是,在 GTID 基础复制实际上不需要它们的情况下,包括事务排队和应用过程中,避免了持久化和检查文件位置所需的写入和读取。
仅当复制 SQL(applier)线程和复制 I/O(receiver)线程都停止时才能使用此选项。要为复制通道设置GTID_ONLY = 1
,服务器必须使用 GTID(gtid_mode = ON
),并且源必须使用基于行的二进制日志记录(不支持基于语句的复制)。必须为复制通道设置选项REQUIRE_ROW_FORMAT = 1
和SOURCE_AUTO_POSITION = 1
。
当设置GTID_ONLY = 1
时,如果服务器的replica_parallel_workers=1
系统变量设置为零,则副本使用单线程 applier,因此在技术上始终是多线程 applier。这是因为多线程 applier 使用保存的位置而不是复制元数据存储库来定位需要重新应用的事务的起始位置。
如果在设置后禁用GTID_ONLY
,则会删除现有的中继日志,并持久化现有的已知二进制日志文件位置,即使它们已过时。复制元数据存储库中的二进制日志和中继日志的文件位置可能无效,如果是这种情况,则会返回警告。只要SOURCE_AUTO_POSITION
仍然启用,就会使用 GTID 自动定位来提供正确的定位。
如果还禁用SOURCE_AUTO_POSITION
,则在复制元数据存储库中,如果二进制日志和中继日志的文件位置有效,则用于定位。如果它们标记为无效,则必须提供有效的二进制日志文件名和位置(SOURCE_LOG_FILE
和SOURCE_LOG_POS
)。如果还提供中继日志文件名和位置(RELAY_LOG_FILE
和RELAY_LOG_POS
),则中继日志将被保留,并且应用程序位置将设置为指定位置。GTID 自动跳过确保任何已应用的事务即使最终的应用程序位置不正确也会被跳过。
IGNORE_SERVER_IDS = (*
server_id_list*)
使复制品忽略来自指定服务器的事件。该选项接受一个逗号分隔的 0 或多个服务器 ID 的列表。来自服务器的日志轮换和删除事件不会被忽略,并记录在中继日志中。
在循环复制中,原始服务器通常充当其自己事件的终结者,以便它们不会被应用多次。因此,在循环复制中,当圈中的一个服务器被移除时,此选项很有用。假设您有一个带有服务器 ID 1、2、3 和 4 的 4 个服务器的循环复制设置,并且服务器 3 失效。在从服务器 2 开始到服务器 4 的复制中填补间隙时,您可以在服务器 4 上发出的CHANGE MASTER TO
语句中包含IGNORE_SERVER_IDS = (3)
,告诉它使用服务器 2 而不是服务器 3 作为其源。这样做会导致它忽略并不传播任何源自不再使用的服务器的语句。
如果IGNORE_SERVER_IDS
包含服务器自己的 ID,并且服务器启动时启用了--replicate-same-server-id
选项,则会导致错误。
注意
当全局事务标识符(GTID)用于复制时,已经应用的事务会自动被忽略,因此不需要使用IGNORE_SERVER_IDS
功能,该功能已被弃用。如果为服务器设置了gtid_mode=ON
,则在CHANGE MASTER TO
语句中包含IGNORE_SERVER_IDS
选项时会发出弃用警告。
源元数据存储库和SHOW REPLICA STATUS
的输出提供了当前被忽略的服务器列表。有关更多信息,请参阅 Section 19.2.4.2, “Replication Metadata Repositories”和 Section 15.7.7.35, “SHOW REPLICA STATUS Statement”。
如果发出不带任何IGNORE_SERVER_IDS
选项的CHANGE MASTER TO
语句,则会保留任何现有的忽略服务器列表。要清除忽略服务器列表,必须使用带有空列表的选项:
CHANGE MASTER TO IGNORE_SERVER_IDS = ();
RESET REPLICA ALL
会清除IGNORE_SERVER_IDS
。
注意
如果在任何通道已设置带有IGNORE_SERVER_IDS
的现有服务器 ID 时发出SET GTID_MODE=ON
,则会发出弃用警告。在启动基于 GTID 的复制之前,请检查并清除涉及的服务器上的所有忽略服务器 ID 列表。SHOW REPLICA STATUS
语句显示忽略 ID 列表(如果有)。如果收到弃用警告,您仍然可以通过发出包含带有空列表的IGNORE_SERVER_IDS
选项的CHANGE MASTER TO
语句来清除列表。
MASTER_AUTO_POSITION = {0|1}
使复制品尝试使用基于 GTID 的自动定位功能连接到源,而不是基于二进制日志文件的位置。此选项用于启动使用基于 GTID 的复制的复制品。默认值为 0,表示不使用 GTID 自动定位和基于 GTID 的复制。只有在停止复制 SQL(应用程序)线程和复制 I/O(接收器)线程时,才能将此选项与CHANGE MASTER TO
一起使用。
复制品和源都必须启用 GTIDs(GTID_MODE=ON
,ON_PERMISSIVE
或OFF_PERMISSIVE
在复制品上,以及源上的GTID_MODE=ON
)。MASTER_LOG_FILE
、MASTER_LOG_POS
、RELAY_LOG_FILE
和RELAY_LOG_POS
不能与MASTER_AUTO_POSITION = 1
一起指定。如果在复制品上启用了多源复制,您需要为每个适用的复制通道设置MASTER_AUTO_POSITION = 1
选项。
设置MASTER_AUTO_POSITION = 1
后,在初始连接握手中,复制品发送包含其已接收、已提交或两者都有的事务的 GTID 集。源会响应通过发送其二进制日志中的所有事务,其 GTID 未包含在复制品发送的 GTID 集中。此交换确保源仅发送复制品尚未记录或提交的具有 GTID 的事务。如果复制品从多个源接收事务,如钻石拓扑结构的情况下,自动跳过功能确保不会应用事务两次。有关复制品发送的 GTID 集如何计算的详细信息,请参见 Section 19.1.3.3, “GTID Auto-Positioning”。
如果应该由源发送的任何事务已从源的二进制日志中清除,或者通过其他方法添加到gtid_purged
系统变量的 GTID 集合中,源将向副本发送错误ER_MASTER_HAS_PURGED_REQUIRED_GTIDS
,并且复制不会启动。缺失的已清除事务的 GTID 将在源的错误日志中被识别并列在警告消息ER_FOUND_MISSING_GTIDS
中。此外,如果在交换事务期间发现副本已记录或提交具有 GTID 中源 UUID 的事务,但源本身尚未提交它们,则源将向副本发送错误ER_SLAVE_HAS_MORE_GTIDS_THAN_MASTER
,并且复制不会启动。有关如何处理这些情况的信息,请参见第 19.1.3.3 节,“GTID 自动定位”。
您可以通过检查 Performance Schema replication_connection_status
表或SHOW REPLICA STATUS
的输出来查看是否启用了 GTID 自动定位的复制正在运行。再次禁用MASTER_AUTO_POSITION
选项会使副本恢复到基于文件的复制。
MASTER_BIND = '*
interface_name*'
确定副本的哪个网络接口被选择用于连接到源,用于具有多个网络接口的副本。指定网络接口的 IP 地址。字符串值的最大长度为 255 个字符。
配置此选项的 IP 地址(如果有)可以在SHOW REPLICA STATUS
输出的Master_Bind
列中看到。在源元数据存储库表mysql.slave_master_info
中,该值可以在Master_bind
列中看到。将副本绑定到特定网络接口的能力也受到 NDB 集群的支持。
MASTER_COMPRESSION_ALGORITHMS = '*
algorithm*[,*
algorithm*][,*
algorithm*]'
指定连接到复制源服务器的允许压缩算法中的一个、两个或三个,用逗号分隔。字符串值的最大长度为 99 个字符。默认值为uncompressed
。
可用的算法是zlib
、zstd
和uncompressed
,与protocol_compression_algorithms
系统变量相同。算法可以以任何顺序指定,但这不是优先顺序 - 算法协商过程尝试使用zlib
,然后zstd
,然后uncompressed
,如果它们被指定。MASTER_COMPRESSION_ALGORITHMS
自 MySQL 8.0.18 起可用。
MASTER_COMPRESSION_ALGORITHMS
的值仅在replica_compressed_protocol
或slave_compressed_protocol
系统变量被禁用时适用。如果replica_compressed_protocol
或slave_compressed_protocol
被启用,则优先于MASTER_COMPRESSION_ALGORITHMS
,并且如果源和副本都支持该算法,则连接到源使用zlib
压缩。有关更多信息,请参见第 6.2.8 节,“连接压缩控制”。
二进制日志事务压缩(自 MySQL 8.0.20 起可用),由binlog_transaction_compression
系统变量激活,也可用于节省带宽。如果与连接压缩结合使用,则连接压缩的机会减少作用于数据,但仍可以压缩标头以及未压缩的事件和事务有效载荷。有关二进制日志事务压缩的更多信息,请参见第 7.4.4.5 节,“二进制日志事务压缩”。
MASTER_CONNECT_RETRY = *
interval*
指定副本在连接到源超时后进行重新连接尝试之间的秒数间隔。默认间隔为 60 秒。
尝试次数受MASTER_RETRY_COUNT
选项限制。如果使用默认设置,副本在重新连接尝试之间等待 60 秒(MASTER_CONNECT_RETRY=60
),并以这个速率继续尝试重新连接 60 天(MASTER_RETRY_COUNT=86400
)。这些值记录在源元数据存储库中,并显示在replication_connection_configuration
性能模式表中。
MASTER_DELAY = *
interval*
指定副本必须滞后源多少秒。从源接收的事件要在至少interval
秒后才会在副本上执行。interval
必须是一个非负整数,范围从 0 到 2³¹−1。默认值为 0。更多信息,请参阅 Section 19.4.11, “Delayed Replication”。
当复制 SQL 线程停止时,可以在运行中的副本上执行使用MASTER_DELAY
选项的CHANGE MASTER TO
语句。
MASTER_HEARTBEAT_PERIOD = *
interval*
控制心跳间隔,防止在连接仍然良好的情况下发生连接超时。在经过那么多秒后,会向副本发送一个心跳信号,并且每当源的二进制日志更新为一个事件时,等待时间就会被重置。因此,只有在二进制日志文件中有一段时间没有发送的事件时,源才会发送心跳。
心跳间隔interval
是一个十进制值,范围为 0 到 4294967 秒,分辨率为毫秒;最小的非零值为 0.001。将interval
设置为 0 会完全禁用心跳。心跳间隔默认为replica_net_timeout
或slave_net_timeout
系统变量值的一半。它记录在源元数据存储库中,并显示在replication_connection_configuration
性能模式表中。
系统变量replica_net_timeout
(从 MySQL 8.0.26 开始)或slave_net_timeout
(在 MySQL 8.0.26 之前)指定了复制等待源端发送更多数据或心跳信号的秒数,超过这个时间,复制将认为连接已断开,中止读取,并尝试重新连接。默认值为 60 秒(一分钟)。请注意,对replica_net_timeout
或slave_net_timeout
的值或默认设置的更改不会自动更改心跳间隔,无论是显式设置还是使用先前计算的默认值。如果将全局值replica_net_timeout
或slave_net_timeout
设置为小于当前心跳间隔的值,将发出警告。如果更改了replica_net_timeout
或slave_net_timeout
,还必须发出CHANGE MASTER TO
以将心跳间隔调整为适当的值,以便在连接超时之前发生心跳信号。如果不这样做,心跳信号将不起作用,如果没有从源端接收到数据,复制可能会进行重复的重新连接尝试,创建僵尸转储线程。
MASTER_HOST = '*
host_name*'
复制源服务器的主机名或 IP 地址。复制使用此信息连接到源端。字符串值的最大长度为 255 个字符。在 MySQL 8.0.17 之前为 60 个字符。
如果指定了MASTER_HOST
或MASTER_PORT
,则复制将假定源服务器与以前不同(即使选项值与当前值相同)。在这种情况下,源端二进制日志文件名和位置的旧值被认为不再适用,因此如果在语句中不指定MASTER_LOG_FILE
和MASTER_LOG_POS
,则会自动附加MASTER_LOG_FILE=''
和MASTER_LOG_POS=4
。
将MASTER_HOST=''
设置为(即将其值显式设置为空字符串)不等同于根本不设置MASTER_HOST
。尝试将MASTER_HOST
设置为空字符串将导致错误。
MASTER_LOG_FILE = '*
source_log_name*'
,MASTER_LOG_POS = *
source_log_pos*
二进制日志文件名以及复制 I/O(接收器)线程从源端二进制日志中读取的位置,下次线程启动时开始读取。如果使用基于二进制日志文件位置的复制,请指定这些选项。
MASTER_LOG_FILE
必须包括源服务器上可用的特定二进制日志文件的数字后缀,例如,MASTER_LOG_FILE='binlog.000145'
。字符串值的最大长度为 511 个字符。
MASTER_LOG_POS
是副本在该文件中开始读取的数字位置。MASTER_LOG_POS=4
表示二进制日志文件中事件的开始。
如果指定了MASTER_LOG_FILE
或MASTER_LOG_POS
中的任一项,则不能指定MASTER_AUTO_POSITION = 1
,该选项用于基于 GTID 的复制。
如果未指定MASTER_LOG_FILE
或MASTER_LOG_POS
中的任一项,则副本使用复制 SQL(应用程序)线程在发出CHANGE MASTER TO
之前的最后坐标。这确保即使复制 SQL(应用程序)线程相对于复制 I/O(接收器)线程较晚,也不会在复制中出现不连续。
MASTER_PASSWORD = '*
password*'
用于连接到复制源服务器的复制用户帐户的密码。字符串值的最大长度为 32 个字符。如果指定了MASTER_PASSWORD
,则还需要MASTER_USER
。
在CHANGE MASTER TO
语句中用于复制用户帐户的密码长度限制为 32 个字符。尝试使用超过 32 个字符的密码会导致CHANGE MASTER TO
失败。
密码在 MySQL 服务器的日志、性能模式表和SHOW PROCESSLIST
语句中被掩码。
MASTER_PORT = *
port_num*
副本用于连接到复制源服务器的 TCP/IP 端口号。
注意
复制不能使用 Unix 套接字文件。您必须能够使用 TCP/IP 连接到复制源服务器。
如果指定了MASTER_HOST
或MASTER_PORT
,则副本会假定源服务器与之前不同(即使选项值与当前值相同)。在这种情况下,源二进制日志文件名和位置的旧值被视为不再适用,因此如果在语句中未指定MASTER_LOG_FILE
和MASTER_LOG_POS
,则会自动附加MASTER_LOG_FILE=''
和MASTER_LOG_POS=4
。
MASTER_PUBLIC_KEY_PATH = '*
key_file_name*'
通过提供包含源端所需的公钥副本的文件的路径名,启用基于 RSA 密钥对的密码交换。该文件必须采用 PEM 格式。字符串值的最大长度为 511 个字符。
此选项适用于使用sha256_password
或caching_sha2_password
认证插件进行身份验证的复制品。(对于sha256_password
,只有在使用 OpenSSL 构建 MySQL 时才能使用MASTER_PUBLIC_KEY_PATH
。)如果您使用的是使用caching_sha2_password
插件进行身份验证的复制用户帐户(这是 MySQL 8.0 的默认设置),并且您没有使用安全连接,则必须指定此选项或GET_MASTER_PUBLIC_KEY=1
选项以向复制品提供 RSA 公钥。
MASTER_RETRY_COUNT = *
count*
设置复制品在连接到源之后超时的最大重连尝试次数,由replica_net_timeout
或slave_net_timeout
系统变量确定。如果复制品确实需要重新连接,则第一次重试会在超时后立即发生。默认值为 86400 次尝试。
尝试之间的间隔由MASTER_CONNECT_RETRY
选项指定。如果使用默认设置,复制品在重新连接尝试之间等待 60 秒(MASTER_CONNECT_RETRY=60
),并以此速率继续尝试重新连接 60 天(MASTER_RETRY_COUNT=86400
)。将MASTER_RETRY_COUNT
设置为 0 意味着没有限制重新连接尝试次数,因此复制品将无限尝试重新连接。
MASTER_CONNECT_RETRY
和MASTER_RETRY_COUNT
的值记录在源元数据存储库中,并显示在replication_connection_configuration
性能模式表中。MASTER_RETRY_COUNT
取代了--master-retry-count
服务器启动选项。
MASTER_SSL = {0|1}
指定复制品是否加密复制连接。默认值为 0,表示复制品不加密复制连接。如果设置MASTER_SSL=1
,则可以使用MASTER_SSL_*
xxx*
和MASTER_TLS_*
xxx*
选项配置加密。
为复制连接设置MASTER_SSL=1
,然后不设置更多的MASTER_SSL_*
xxx*
选项相当于为客户端设置--ssl-mode=REQUIRED
,如加密连接的命令选项中所述。使用MASTER_SSL=1
,连接尝试仅在可以建立加密连接时成功。复制连接不会退回到未加密连接,因此没有与复制相对应的--ssl-mode=PREFERRED
设置。如果设置了MASTER_SSL=0
,则相当于--ssl-mode=DISABLED
。
重要
为了帮助防止复杂的中间人攻击,重要的是副本要验证服务器的身份。您可以指定额外的MASTER_SSL_*
xxx*
选项,对应设置--ssl-mode=VERIFY_CA
和--ssl-mode=VERIFY_IDENTITY
,这比默认设置更好,有助于防止这种类型的攻击。使用这些设置,副本会检查服务器的证书是否有效,并检查副本正在使用的主机名是否与服务器证书中的身份匹配。要实施这些验证级别之一,必须首先确保服务器的 CA 证书可靠地可用于副本,否则会导致可用性问题。因此,它们不是默认设置。
MASTER_SSL_*
xxx*
,MASTER_TLS_*
xxx*
指定副本如何使用加密和密码来保护复制连接。即使在没有 SSL 支持的副本上也可以更改这些选项。它们保存在源元数据存储库中,但如果副本没有启用 SSL 支持,则会被忽略。字符串值的MASTER_SSL_*
xxx*
和MASTER_TLS_*
xxx*
选项的值的最大长度为 511 个字符,MASTER_TLS_CIPHERSUITES
除外,其长度为 4000 个字符。
MASTER_SSL_*
xxx*
和MASTER_TLS_*
xxx*
选项执行与加密连接的命令选项中描述的--ssl-*
xxx*
和--tls-*
xxx*
客户端选项相同的功能。两组选项之间的对应关系,以及使用MASTER_SSL_*
xxx*
和MASTER_TLS_*
xxx*
选项建立安全连接的方法,在第 19.3.1 节,“设置复制使用加密连接”中有解释。
MASTER_USER = '*
user_name*'
用于连接到复制源服务器的复制用户帐户的用户名。字符串值的最大长度为 96 个字符。
对于组复制,此帐户必须存在于复制组的每个成员中。如果 XCom 通信堆栈用于组,则用于分布式恢复,如果 MySQL 通信堆栈用于组,则用于组通信连接。对于 MySQL 通信堆栈,帐户必须具有GROUP_REPLICATION_STREAM
权限。
可以通过指定MASTER_USER=''
来设置空用户名称,但不能使用空用户名称启动复制通道。在 MySQL 8.0.21 之前的版本中,只有在出于安全目的需要清除先前使用的凭据时才设置空的MASTER_USER
用户名称。不要在之后使用通道,因为在这些版本中存在一个错误,如果从存储库中读取到空用户名称(例如,在自动重新启动组复制通道期间),则可能会替换默认用户名称。从 MySQL 8.0.21 开始,可以设置空的MASTER_USER
用户名称,并在之后使用通道,只要始终使用START REPLICA
语句或START GROUP_REPLICATION
语句提供用户凭据启动复制通道。这种方法意味着复制通道始终需要操作员干预才能重新启动,但用户凭据不会记录在复制元数据存储库中。
重要
要使用使用caching_sha2_password
插件进行身份验证的复制用户帐户连接到源,必须设置安全连接,如 Section 19.3.1, “Setting Up Replication to Use Encrypted Connections”中所述,或启用不加密的连接以支持使用 RSA 密钥对进行密码交换。caching_sha2_password
身份验证插件是从 MySQL 8.0 开始新用户的默认插件(有关详细信息,请参见 Section 8.4.1.2, “Caching SHA-2 Pluggable Authentication”)。如果您创建或使用用于复制的用户帐户使用此身份验证插件,并且未使用安全连接,则必须启用基于 RSA 密钥对的密码交换才能成功连接。您可以使用此语句的MASTER_PUBLIC_KEY_PATH
选项或GET_MASTER_PUBLIC_KEY=1
选项来执行此操作。
MASTER_ZSTD_COMPRESSION_LEVEL = *
level*
用于使用zstd
压缩算法的与复制源服务器连接的压缩级别。允许的级别为 1 到 22,较大的值表示较高级别的压缩。默认级别为 3。MASTER_ZSTD_COMPRESSION_LEVEL
自 MySQL 8.0.18 起可用。
压缩级别设置对不使用zstd
压缩的连接没有影响。有关更多信息,请参见 Section 6.2.8, “Connection Compression Control”。
NETWORK_NAMESPACE = '*
namespace*'
用于 TCP/IP 连接到复制源服务器或者如果使用 MySQL 通信堆栈,则用于 Group Replication 的组通信连接的网络命名空间。字符串值的最大长度为 64 个字符。如果省略此选项,则副本的连接将使用默认(全局)命名空间。在不实现网络命名空间支持的平台上,当副本尝试连接到源时会发生故障。有关网络命名空间的信息,请参见 第 7.1.14 节,“网络命名空间支持”。NETWORK_NAMESPACE
自 MySQL 8.0.22 版本起可用。
PRIVILEGE_CHECKS_USER = {NULL | '*
account*'}
为指定通道提供安全上下文的用户帐户名称。NULL
是默认值,表示不使用安全上下文。PRIVILEGE_CHECKS_USER
自 MySQL 8.0.18 版本起可用。
用户帐户的用户名和主机名必须遵循 第 8.2.4 节,“指定帐户名称” 中描述的语法,并且用户不能是匿名用户(具有空白用户名)或 CURRENT_USER
。该帐户必须具有 REPLICATION_APPLIER
权限,以及执行在通道上复制的事务所需的权限。有关帐户所需权限的详细信息,请参见 第 19.3.3 节,“复制权限检查”。重新启动复制通道时,权限检查将从那一点开始应用。如果您不指定通道且没有其他通道存在,则该语句将应用于默认通道。
当设置了 PRIVILEGE_CHECKS_USER
时,强烈建议使用基于行的二进制日志记录,并且您可以设置 REQUIRE_ROW_FORMAT
来强制执行此操作。例如,要在运行中的副本上启动通道 channel_1
上的权限检查,请执行以下语句:
mysql> STOP REPLICA FOR CHANNEL 'channel_1';
mysql> CHANGE MASTER TO
PRIVILEGE_CHECKS_USER = '*priv_repl*'@'*%.example.com*',
REQUIRE_ROW_FORMAT = 1,
FOR CHANNEL 'channel_1';
mysql> START REPLICA FOR CHANNEL 'channel_1';
对于 MySQL 8.0.22 版本及更高版本,请使用 START REPLICA
和 STOP REPLICA
,对于 MySQL 8.0.22 版本之前的版本,请使用 START SLAVE
和 STOP SLAVE
。这些语句的工作方式相同,只是术语发生了变化。
RELAY_LOG_FILE = '*
relay_log_file*'
,RELAY_LOG_POS = '*
relay_log_pos*'
中继日志文件名以及在该文件中的位置,复制 SQL 线程在下次启动时从副本的中继日志中开始读取的位置。RELAY_LOG_FILE
可以使用绝对路径或相对路径,并且使用与 MASTER_LOG_FILE
相同的基本名称。字符串值的最大长度为 511 个字符。
在运行副本时,可以在停止复制 SQL 线程时执行使用RELAY_LOG_FILE
、RELAY_LOG_POS
或两个选项的CHANGE MASTER TO
语句。如果至少有一个复制 SQL(应用程序)线程和复制 I/O(接收器)线程正在运行,则保留中继日志。如果两个线程都停止,则除非至少指定RELAY_LOG_FILE
或RELAY_LOG_POS
中的一个,否则所有中继日志文件都将被删除。对于仅具有应用程序线程而没有接收器线程的 Group Replication 应用程序通道(group_replication_applier
),如果应用程序线程停止,那么这种情况就是,但是对于该通道,您不能使用RELAY_LOG_FILE
和RELAY_LOG_POS
选项。
REQUIRE_ROW_FORMAT = {0|1}
仅允许通过复制通道处理基于行的复制事件。此选项防止复制应用程序执行诸如创建临时表和执行LOAD DATA INFILE
请求等操作,从而增加通道的安全性。默认情况下,异步复制通道禁用REQUIRE_ROW_FORMAT
选项,但默认情况下启用 Group Replication 通道,并且不能禁用它们。有关更多信息,请参见第 19.3.3 节,“复制权限检查”。REQUIRE_ROW_FORMAT
自 MySQL 8.0.19 起可用。
REQUIRE_TABLE_PRIMARY_KEY_CHECK = {STREAM | ON | OFF}
启用副本选择自己的主键检查策略。默认值为STREAM
。REQUIRE_TABLE_PRIMARY_KEY_CHECK
自 MySQL 8.0.20 起可用。
当为复制通道设置选项为ON
时,副本始终在复制操作中使用sql_require_primary_key
系统变量的值为ON
,需要主键。当选项设置为OFF
时,副本始终在复制操作中使用sql_require_primary_key
系统变量的值为OFF
,因此即使源需要主键,也不需要主键。当REQUIRE_TABLE_PRIMARY_KEY_CHECK
选项设置为STREAM
时,这是默认值,副本使用从源复制的每个事务的任何值。
对于多源复制,将REQUIRE_TABLE_PRIMARY_KEY_CHECK
设置为ON
或OFF
使副本能够规范不同源的复制通道的行为,并保持sql_require_primary_key
系统变量的一致设置。使用ON
可以防止在多个源更新相同的表集时意外丢失主键。使用OFF
允许可以操作主键的源与不能操作主键的源一起工作。
当设置PRIVILEGE_CHECKS_USER
时,将REQUIRE_TABLE_PRIMARY_KEY_CHECK
设置为ON
或OFF
意味着用户帐户无需会话管理级别权限即可设置受限制的会话变量,这些变量是必需的,以便更改sql_require_primary_key
的值以匹配每个事务的源设置。有关更多信息,请参见第 19.3.3 节,“复制权限检查”。
SOURCE_CONNECTION_AUTO_FAILOVER = {0|1}
如果有一个或多个备用复制源服务器可用(即有多个共享复制数据的 MySQL 服务器或服务器组),则为复制通道激活异步连接故障转移机制。SOURCE_CONNECTION_AUTO_FAILOVER
从 MySQL 8.0.22 版本开始可用。默认值为 0,表示未激活该机制。有关完整信息和设置此功能的说明,请参见第 19.4.9.2 节,“副本的异步连接故障转移”。
异步连接故障转移机制在由MASTER_CONNECT_RETRY
和MASTER_RETRY_COUNT
控制的重连尝试耗尽后接管。它会将副本重新连接到一个从指定源列表中选择的备用源,您可以使用asynchronous_connection_failover_add_source
和asynchronous_connection_failover_delete_source
函数来管理这些源。要添加和删除受管理的服务器组,请改用asynchronous_connection_failover_add_managed
和asynchronous_connection_failover_delete_managed
函数。有关更多信息,请参见第 19.4.9 节,“使用异步连接故障转移切换源和副本”。
重要提示
-
只有在使用 GTID 自动定位(
MASTER_AUTO_POSITION = 1
)时才能设置SOURCE_CONNECTION_AUTO_FAILOVER = 1
。 -
当你设置
SOURCE_CONNECTION_AUTO_FAILOVER = 1
时,将MASTER_RETRY_COUNT
和MASTER_CONNECT_RETRY
设置为最小值,只允许在短时间内对同一源进行几次重试尝试,以防连接故障是由瞬时网络中断引起的。否则,异步连接故障转移机制无法及时激活。适当的值为MASTER_RETRY_COUNT=3
和MASTER_CONNECT_RETRY=10
,这使得副本在每次连接尝试之间间隔 10 秒的情况下重试连接 3 次。 -
当你设置
SOURCE_CONNECTION_AUTO_FAILOVER = 1
时,复制元数据存储库必须包含用于连接到复制通道源列表上的所有服务器的复制用户帐户的凭据。可以使用带有MASTER_USER
和MASTER_PASSWORD
选项的CHANGE REPLICATION SOURCE TO
语句来设置这些凭据。更多信息,请参见第 19.4.9 节,“使用异步连接故障转移切换源和副本”。 -
从 MySQL 8.0.27 开始,当你设置
SOURCE_CONNECTION_AUTO_FAILOVER = 1
时,如果此复制通道位于单主模式中的组复制主服务器上,则自动激活副本的异步连接故障转移。启用此功能后,如果正在复制的主服务器下线或进入错误状态,新的主服务器在选举时会在同一通道上开始复制。如果要使用此功能,此复制通道还必须在复制组中的所有辅助服务器上设置,并在任何新加入的成员上设置。(如果使用 MySQL 的克隆功能进行服务器配置,则所有这些都会自动完成。)如果不想使用此功能,请使用group_replication_disable_member_action
函数来禁用默认启用的 Group Replication 成员操作mysql_start_failover_channels_if_primary
。更多信息,请参见第 19.4.9.2 节,“副本的异步连接故障转移”。
示例
当您拥有源的快照并记录了与快照时间对应的源二进制日志坐标时,CHANGE MASTER TO
对于设置副本非常有用。在将快照加载到副本以与源同步之后,您可以在副本上运行CHANGE MASTER TO MASTER_LOG_FILE='*
log_name*', MASTER_LOG_POS=*
log_pos*
来指定副本应开始读取源二进制日志的坐标。以下示例更改了副本使用的源服务器,并建立了副本开始读取源二进制日志的源二进制日志坐标:
CHANGE MASTER TO
MASTER_HOST='source2.example.com',
MASTER_USER='replication',
MASTER_PASSWORD='*password*',
MASTER_PORT=3306,
MASTER_LOG_FILE='source2-bin.001',
MASTER_LOG_POS=4,
MASTER_CONNECT_RETRY=10;
要在故障切换期间将现有副本切换到新源的过程,请参见第 19.4.8 节,“故障切换期间切换源”。
当源和副本上使用 GTID 时,请指定 GTID 自动定位,而不是给出二进制日志文件位置,如下例所示。有关在新服务器、已停止服务器、在线服务器或其他副本上配置和启动基于 GTID 的复制的完整说明,请参见第 19.1.3 节,“具有全局事务标识符的复制”。
CHANGE MASTER TO
MASTER_HOST='source3.example.com',
MASTER_USER='replication',
MASTER_PASSWORD='*password*',
MASTER_PORT=3306,
MASTER_AUTO_POSITION = 1,
FOR CHANNEL "source_3";
在此示例中,使用了多源复制,并且CHANGE MASTER TO
语句应用于连接副本与指定主机的复制通道"source_3"
。有关设置多源复制的指导,请参见第 19.1.5 节,“MySQL 多源复制”。
下一个示例显示了如何使副本应用要重复的中继日志文件中的事务。为此,源不需要可达。您可以使用CHANGE MASTER TO
来定位副本应开始重新应用事务的中继日志位置,然后启动 SQL 线程:
CHANGE MASTER TO
RELAY_LOG_FILE='replica-relay-bin.006',
RELAY_LOG_POS=4025;
START SLAVE SQL_THREAD;
CHANGE MASTER TO
也可用于跳过导致复制停止的二进制日志中的事务。执行此操作的适当方法取决于是否使用 GTID。有关使用CHANGE MASTER TO
或其他方法跳过事务的说明,请参见第 19.1.7.3 节,“跳过事务”。
原文:
dev.mysql.com/doc/refman/8.0/en/change-replication-filter.html
15.4.2.2 CHANGE REPLICATION FILTER Statement
CHANGE REPLICATION FILTER *filter*[, *filter*]
[, ...] [FOR CHANNEL *channel*]
*filter*: {
REPLICATE_DO_DB = (*db_list*)
| REPLICATE_IGNORE_DB = (*db_list*)
| REPLICATE_DO_TABLE = (*tbl_list*)
| REPLICATE_IGNORE_TABLE = (*tbl_list*)
| REPLICATE_WILD_DO_TABLE = (*wild_tbl_list*)
| REPLICATE_WILD_IGNORE_TABLE = (*wild_tbl_list*)
| REPLICATE_REWRITE_DB = (*db_pair_list*)
}
*db_list*:
*db_name*[, *db_name*][, ...]
*tbl_list*:
*db_name.table_name*[, *db_name.table_name*][, ...]
*wild_tbl_list*:
'*db_pattern.table_pattern*'[, '*db_pattern.table_pattern*'][, ...]
*db_pair_list*:
(*db_pair*)[, (*db_pair*)][, ...]
*db_pair*:
*from_db*, *to_db*
CHANGE REPLICATION FILTER
设置一个或多个复制过滤规则,与使用复制过滤选项(如--replicate-do-db
或--replicate-wild-ignore-table
的方式相同。使用此语句设置的过滤器与使用服务器选项设置的过滤器在两个关键方面有所不同:
-
该语句不需要重新启动服务器才能生效,只需首先停止复制 SQL 线程使用
STOP REPLICA SQL_THREAD
(然后使用START REPLICA SQL_THREAD
重新启动)。 -
该语句的效果不是持久的;使用
CHANGE REPLICATION FILTER
设置的任何过滤器在重启复制mysqld后会丢失。
CHANGE REPLICATION FILTER
需要REPLICATION_SLAVE_ADMIN
权限(或已弃用的SUPER
权限)。
使用FOR CHANNEL *
channel*
子句使复制过滤器特定于复制通道,例如在多源副本上。未附加特定FOR CHANNEL
子句应用的过滤器被视为全局过滤器,这意味着它们适用于所有复制通道。
注意
不能在配置为组复制的 MySQL 服务器实例上设置全局复制过滤器,因为在某些服务器上过滤事务会使组无法达成一致状态。可以在未直接涉及组复制的复制通道上设置特定通道的复制过滤器,例如,其中一个组成员同时充当到组外源的副本。不能在group_replication_applier
或group_replication_recovery
通道上设置。
以下列表显示了CHANGE REPLICATION FILTER
选项及其与--replicate-*
服务器选项的关系:
-
REPLICATE_DO_DB
: 根据数据库名称包含更新。等同于--replicate-do-db
。 -
REPLICATE_IGNORE_DB
: 根据数据库名称排除更新。等同于--replicate-ignore-db
。 -
REPLICATE_DO_TABLE
: 根据表名包含更新。等同于--replicate-do-table
。 -
REPLICATE_IGNORE_TABLE
: 根据表名排除更新。等同于--replicate-ignore-table
。 -
REPLICATE_WILD_DO_TABLE
: 根据通配符模式匹配表名包含更新。等同于--replicate-wild-do-table
。 -
REPLICATE_WILD_IGNORE_TABLE
: 根据通配符模式匹配表名排除更新。等同于--replicate-wild-ignore-table
。 -
REPLICATE_REWRITE_DB
: 在源端替换指定数据库的名称后,在副本上执行更新。等同于--replicate-rewrite-db
。
REPLICATE_DO_DB
和REPLICATE_IGNORE_DB
过滤器的确切效果取决于是基于语句还是基于行的复制。有关更多信息,请参见第 19.2.5 节,“服务器如何评估复制过滤规则”。
可以通过在单个CHANGE REPLICATION FILTER
语句中用逗号分隔规则来创建多个复制过滤规则,如下所示:
CHANGE REPLICATION FILTER
REPLICATE_DO_DB = (d1), REPLICATE_IGNORE_DB = (d2);
发出刚才显示的语句等同于使用选项--replicate-do-db=d1
--replicate-ignore-db=d2
启动副本mysqld。
在使用多个复制通道处理来自不同源的事务的多源复制中,使用FOR CHANNEL *
channel*
子句在复制通道上设置复制过滤器:
CHANGE REPLICATION FILTER REPLICATE_DO_DB = (d1) FOR CHANNEL channel_1;
这使您可以创建特定通道的复制过滤器,以从源中筛选出选定的数据。提供FOR CHANNEL
子句时,复制过滤器语句将作用于该复制通道,删除具有与指定复制过滤器相同过滤器类型的任何现有复制过滤器,并用指定的过滤器替换它们。未在语句中明确列出的过滤器类型不会被修改。如果针对未配置的复制通道发出该语句,则该语句将失败,并显示 ER_SLAVE_CONFIGURATION 错误。如果针对 Group Replication 通道发出该语句,则该语句将失败,并显示 ER_SLAVE_CHANNEL_OPERATION_NOT_ALLOWED 错误。
在配置了多个复制通道的副本中,发出不带FOR CHANNEL
子句的CHANGE REPLICATION FILTER
会为每个配置的复制通道以及全局复制过滤器配置复制过滤器。对于每种过滤器类型,如果语句中列出了过滤器类型,则该类型的任何现有过滤规则都将被最近发出的语句中指定的过滤规则替换,否则过滤器类型的旧值将被保留。有关更多信息,请参见 Section 19.2.5.4, “Replication Channel Based Filters”。
如果同一过滤规则被指定多次,则实际上只使用最后一条规则。例如,这里显示的两个语句具有完全相同的效果,因为第一个语句中的第一条REPLICATE_DO_DB
规则被忽略:
CHANGE REPLICATION FILTER
REPLICATE_DO_DB = (db1, db2), REPLICATE_DO_DB = (db3, db4);
CHANGE REPLICATION FILTER
REPLICATE_DO_DB = (db3, db4);
注意
这种行为与--replicate-*
过滤选项的行为不同,其中多次指定相同选项会导致创建多个过滤规则。
不包含任何特殊字符的表和数据库名称无需加引号。与REPLICATION_WILD_TABLE
和REPLICATION_WILD_IGNORE_TABLE
一起使用的值是字符串表达式,可能包含(特殊)通配符字符,因此必须加引号。以下示例语句中显示了这一点:
CHANGE REPLICATION FILTER
REPLICATE_WILD_DO_TABLE = ('db1.old%');
CHANGE REPLICATION FILTER
REPLICATE_WILD_IGNORE_TABLE = ('db1.new%', 'db2.new%');
与REPLICATE_REWRITE_DB
一起使用的值表示数据库名称的对;每个这样的值必须用括号括起来。以下语句将源数据库db1
上发生的语句重写为副本数据库db2
:
CHANGE REPLICATION FILTER REPLICATE_REWRITE_DB = ((db1, db2));
刚刚显示的语句包含两组括号,一组括住数据库名称的对,另一组括住整个列表。这在下面的示例中可能更容易看到,该示例创建了两个rewrite-db
规则,一个将数据库dbA
重写为dbB
,另一个将数据库dbC
重写为dbD
:
CHANGE REPLICATION FILTER
REPLICATE_REWRITE_DB = ((dbA, dbB), (dbC, dbD));
CHANGE REPLICATION FILTER
语句仅替换受语句影响的过滤类型和复制通道的复制过滤规则,而保持其他规则和通道不变。如果要取消给定类型的所有过滤器,请将过滤器的值设置为显式空列表,如此示例所示,它删除了所有现有的REPLICATE_DO_DB
和REPLICATE_IGNORE_DB
规则:
CHANGE REPLICATION FILTER
REPLICATE_DO_DB = (), REPLICATE_IGNORE_DB = ();
以这种方式将过滤器设置为空会删除所有现有规则,不会创建任何新规则,并且不会恢复在启动时使用--replicate-*
选项在命令行或配置文件中设置的任何规则。
RESET REPLICA ALL
语句会移除在被语句删除的通道上设置的特定通道复制过滤器。当被删除的通道或通道重新创建时,任何为副本指定的全局复制过滤器都会被复制到它们,而不会应用任何特定通道的复制过滤器。
欲了解更多信息,请参阅第 19.2.5 节,“服务器如何评估复制过滤规则”。
原文:
dev.mysql.com/doc/refman/8.0/en/change-replication-source-to.html
15.4.2.3 CHANGE REPLICATION SOURCE TO 语句
CHANGE REPLICATION SOURCE TO *option* [, *option*] ... [ *channel_option* ]
*option*: {
SOURCE_BIND = '*interface_name*'
| SOURCE_HOST = '*host_name*'
| SOURCE_USER = '*user_name*'
| SOURCE_PASSWORD = '*password*'
| SOURCE_PORT = *port_num*
| PRIVILEGE_CHECKS_USER = {NULL | '*account*'}
| REQUIRE_ROW_FORMAT = {0|1}
| REQUIRE_TABLE_PRIMARY_KEY_CHECK = {STREAM | ON | OFF | GENERATE}
| ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS = {OFF | LOCAL | *uuid*}
| SOURCE_LOG_FILE = '*source_log_name*'
| SOURCE_LOG_POS = *source_log_pos*
| SOURCE_AUTO_POSITION = {0|1}
| RELAY_LOG_FILE = '*relay_log_name*'
| RELAY_LOG_POS = *relay_log_pos*
| SOURCE_HEARTBEAT_PERIOD = *interval*
| SOURCE_CONNECT_RETRY = *interval*
| SOURCE_RETRY_COUNT = *count*
| SOURCE_CONNECTION_AUTO_FAILOVER = {0|1}
| SOURCE_DELAY = *interval*
| SOURCE_COMPRESSION_ALGORITHMS = '*algorithm*[,*algorithm*][,*algorithm*]'
| SOURCE_ZSTD_COMPRESSION_LEVEL = *level*
| SOURCE_SSL = {0|1}
| SOURCE_SSL_CA = '*ca_file_name*'
| SOURCE_SSL_CAPATH = '*ca_directory_name*'
| SOURCE_SSL_CERT = '*cert_file_name*'
| SOURCE_SSL_CRL = '*crl_file_name*'
| SOURCE_SSL_CRLPATH = '*crl_directory_name*'
| SOURCE_SSL_KEY = '*key_file_name*'
| SOURCE_SSL_CIPHER = '*cipher_list*'
| SOURCE_SSL_VERIFY_SERVER_CERT = {0|1}
| SOURCE_TLS_VERSION = '*protocol_list*'
| SOURCE_TLS_CIPHERSUITES = '*ciphersuite_list*'
| SOURCE_PUBLIC_KEY_PATH = '*key_file_name*'
| GET_SOURCE_PUBLIC_KEY = {0|1}
| NETWORK_NAMESPACE = '*namespace*'
| IGNORE_SERVER_IDS = (*server_id_list*),
| GTID_ONLY = {0|1}
}
*channel_option*:
FOR CHANNEL *channel*
*server_id_list*:
[*server_id* [, *server_id*] ... ]
CHANGE REPLICATION SOURCE TO
更改副本服务器用于连接到源并从源读取数据的参数。它还更新了复制元数据存储库的内容(参见Section 19.2.4, “Relay Log and Replication Metadata Repositories”)。在 MySQL 8.0.23 及更高版本中,请使用CHANGE REPLICATION SOURCE TO
代替已弃用的CHANGE MASTER TO
语句。
CHANGE REPLICATION SOURCE TO
需要REPLICATION_SLAVE_ADMIN
权限(或已弃用的SUPER
权限)。
在CHANGE REPLICATION SOURCE TO
语句中未指定的选项保留其值,除非在以下讨论中另有说明。因此,在大多数情况下,无需指定不更改的选项。
用于SOURCE_HOST
和其他CHANGE REPLICATION SOURCE TO
选项的值会检查换行符(\n
或0x0A
)。这些值中存在这些字符会导致语句失败并出现错误。
可选的FOR CHANNEL *
channel*
子句允许您指定语句适用于哪个复制通道。提供FOR CHANNEL *
channel*
子句将CHANGE REPLICATION SOURCE TO
语句应用于特定的复制通道,并用于添加新通道或修改现有通道。例如,要添加一个名为channel2
的新通道:
CHANGE REPLICATION SOURCE TO SOURCE_HOST=host1, SOURCE_PORT=3002 FOR CHANNEL 'channel2';
如果未命名子句且不存在额外通道,则CHANGE REPLICATION SOURCE TO
语句适用于默认通道,其名称为空字符串("")。当设置了多个复制通道时,每个CHANGE REPLICATION SOURCE TO
语句必须使用FOR CHANNEL *
channel*
子句命名通道。有关更多信息,请参见Section 19.2.2, “Replication Channels”。
对于CHANGE REPLICATION SOURCE TO
语句的某些选项,您必须在发出CHANGE REPLICATION SOURCE TO
语句之前发出STOP REPLICA
语句(以及之后发出START REPLICA
语句)。有时,您只需要停止复制 SQL(应用程序)线程或复制 I/O(接收)线程,而不是两者都要停止:
-
当应用程序线程停止时,您可以使用
RELAY_LOG_FILE
、RELAY_LOG_POS
和SOURCE_DELAY
选项的任何允许组合执行CHANGE REPLICATION SOURCE TO
,即使复制接收线程正在运行。当接收线程正在运行时,不得使用此语句的其他选项。 -
当接收线程停止时,您可以使用此语句的任何选项(以任何允许的组合)执行
CHANGE REPLICATION SOURCE TO
,除了RELAY_LOG_FILE
、RELAY_LOG_POS
、SOURCE_DELAY
或SOURCE_AUTO_POSITION = 1
,即使应用程序线程正在运行。 -
在发出使用
SOURCE_AUTO_POSITION = 1
、GTID_ONLY = 1
或ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS
的CHANGE REPLICATION SOURCE TO
语句之前,必须停止接收线程和应用程序线程。
您可以使用SHOW REPLICA STATUS
检查复制应用程序线程和复制接收线程的当前状态。请注意,Group Replication 应用程序通道(group_replication_applier
)没有接收线程,只有一个应用程序线程。
CHANGE REPLICATION SOURCE TO
语句具有许多您在执行之前应该了解的副作用和交互作用:
-
CHANGE REPLICATION SOURCE TO
会导致正在进行的事务隐式提交。请参阅第 15.3.3 节,“导致隐式提交的语句”。 -
CHANGE REPLICATION SOURCE TO
会将SOURCE_HOST
、SOURCE_PORT
、SOURCE_LOG_FILE
和SOURCE_LOG_POS
的先前值写入错误日志,以及在执行之前有关副本状态的其他信息。 -
如果您正在使用基于语句的复制和临时表,可能会出现在
CHANGE REPLICATION SOURCE TO
语句之后的STOP REPLICA
语句在副本上留下临时表的情况。每当发生这种情况时,都会发出警告(ER_WARN_OPEN_TEMP_TABLES_MUST_BE_ZERO
)。在这种情况下,您可以通过确保在执行CHANGE REPLICATION SOURCE TO
语句之前,Replica_open_temp_tables
或Slave_open_temp_tables
系统状态变量的值等于 0 来避免这种情况。 -
当使用多线程副本(
replica_parallel_workers
> 0)时,停止副本可能导致已从中继日志执行的事务序列中存在间隙,无论副本是有意停止还是其他情况。当存在这样的间隙时,发出CHANGE REPLICATION SOURCE TO
将失败。在这种情况下的解决方案是发出START REPLICA UNTIL SQL_AFTER_MTS_GAPS
,以确保关闭这些间隙。从 MySQL 8.0.26 开始,当使用基于 GTID 的复制和 GTID 自动定位时,完全跳过检查事务序列中的间隙的过程,因为可以使用 GTID 自动定位解决事务间隙。在这种情况下,仍然可以使用CHANGE REPLICATION SOURCE TO
。
下列选项适用于CHANGE REPLICATION SOURCE TO
语句:
-
ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS = {OFF | LOCAL | *
uuid*}
使复制通道为没有 GTID 的复制事务分配一个 GTID,从不使用基于 GTID 的复制的源复制到使用基于 GTID 的副本。对于多源副本,您可以同时使用
ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS
和不使用该选项的通道。默认值为OFF
,表示不使用该功能。LOCAL
分配一个包括副本自身 UUID(server_uuid
设置)的 GTID。*
uuid*
分配一个包括指定 UUID 的 GTID,例如复制源服务器的server_uuid
设置。使用非本地 UUID 可以区分在副本上发起的事务和在源上发起的事务,对于多源副本,还可以区分在不同源上发起的事务。您选择的 UUID 仅对副本自身使用有意义。如果源发送的任何事务已经具有 GTID,则保留该 GTID。专用于组复制的通道不能使用
ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS
,但是在作为组复制组成员的服务器实例上的另一个源的异步复制通道可以这样做。在这种情况下,不要将组复制组名称指定为创建 GTID 的 UUID。要将
ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS
设置为LOCAL
或*
uuid*
,副本必须设置gtid_mode=ON
,并且之后不能更改。此选项用于与具有基于二进制日志文件位置的复制的源一起使用,因此不能为通道设置SOURCE_AUTO_POSITION=1
。在设置此选项之前,必须停止复制 SQL 线程和复制 I/O(接收器)线程。重要提示
使用任何通道上的
ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS
设置的副本不能在需要故障转移时晋升为替换复制源服务器,并且不能使用从副本中获取的备份来恢复复制源服务器。相同的限制也适用于替换或恢复其他使用任何通道上的ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS
的副本。有关更多限制和信息,请参阅第 19.1.3.6 节,“从没有 GTID 的源复制到具有 GTID 的副本”。
-
GET_SOURCE_PUBLIC_KEY = {0|1}
通过请求源的公钥启用基于 RSA 密钥对的密码交换。此选项默认禁用。
此选项适用于使用
caching_sha2_password
认证插件进行身份验证的复制品。对于使用此插件进行身份验证的帐户的连接,源不会发送公钥,除非请求,因此必须请求或在客户端中指定。如果给出SOURCE_PUBLIC_KEY_PATH
并指定有效的公钥文件,则它优先于GET_SOURCE_PUBLIC_KEY
。如果您正在使用使用caching_sha2_password
插件进行身份验证的复制用户帐户(这是从 MySQL 8.0 开始的默认设置),并且您没有使用安全连接,则必须指定此选项或SOURCE_PUBLIC_KEY_PATH
选项以向复制品提供 RSA 公钥。 -
GTID_ONLY = {0|1}
停止复制通道在复制元数据存储库中持久化文件名和文件位置。
GTID_ONLY
从 MySQL 8.0.27 版本开始提供。GTID_ONLY
选项在异步复制通道中默认禁用,但在组复制通道中默认启用,并且无法禁用。对于具有此设置的复制通道,仍会跟踪内存中的文件位置,并且出于调试目的,文件位置仍然可以在错误消息和通过诸如
SHOW REPLICA STATUS
语句(如果它们过时,则显示为无效)等接口中观察到。但是,在 GTID 基础复制实际上不需要它们的情况下,包括事务排队和应用程序过程中,避免了持久化和检查文件位置所需的写入和读取。只有在停止复制 SQL(应用程序)线程和复制 I/O(接收器)线程时才能使用此选项。要为复制通道设置
GTID_ONLY = 1
,必须在服务器上使用 GTID(gtid_mode = ON
),并且源上必须使用基于行的二进制日志记录(不支持基于语句的复制)。必须为复制通道设置选项REQUIRE_ROW_FORMAT = 1
和SOURCE_AUTO_POSITION = 1
。当设置
GTID_ONLY = 1
时,如果服务器上的系统变量replica_parallel_workers
设置为零,则复制品会使用replica_parallel_workers=1
,因此它在技术上始终是一个多线程应用程序。这是因为多线程应用程序使用保存的位置而不是复制元数据存储库来定位需要重新应用的事务的起始位置。如果在设置
GTID_ONLY
后禁用它,现有的中继日志将被删除,并且现有的已知的二进制日志文件位置将被保留,即使它们已经过时。在复制元数据存储库中,二进制日志和中继日志的文件位置可能是无效的,如果是这种情况,则会返回警告。只要SOURCE_AUTO_POSITION
仍然启用,GTID 自动定位将被用来提供正确的定位。如果你也禁用了
SOURCE_AUTO_POSITION
,则在复制元数据存储库中使用二进制日志和中继日志的文件位置进行定位,如果它们是有效的。如果它们被标记为无效,你必须提供一个有效的二进制日志文件名和位置(SOURCE_LOG_FILE
和SOURCE_LOG_POS
)。如果你还提供了一个中继日志文件名和位置(RELAY_LOG_FILE
和RELAY_LOG_POS
),则中继日志将被保留,而应用程序位置将被设置为指定位置。 GTID 自动跳过确保任何已经应用的事务都会被跳过,即使最终的应用程序位置不正确。 -
IGNORE_SERVER_IDS = (*
server_id_list*)
使复制忽略来自指定服务器的事件。该选项接受一个逗号分隔的 0 个或多个服务器 ID 的列表。来自服务器的日志旋转和删除事件不会被忽略,并且会记录在中继日志中。
在循环复制中,原始服务器通常充当其自己事件的终结者,以便它们不会被应用多次。因此,在循环复制中,当圈中的一个服务器被移除时,这个选项是有用的。假设你有一个带有服务器 ID 为 1、2、3 和 4 的 4 台服务器的循环复制设置,并且服务器 3 失败了。在通过从服务器 2 开始到服务器 4 启动复制来填补这个空白时,你可以在服务器 4 上发出的
CHANGE REPLICATION SOURCE TO
语句中包含IGNORE_SERVER_IDS = (3)
,告诉��使用服务器 2 作为其源,而不是服务器 3。这样做会导致它忽略并不传播任何源自不再使用的服务器的语句。如果
IGNORE_SERVER_IDS
包含服务器自己的 ID,并且服务器是使用--replicate-same-server-id
选项启动的,则会导致错误。注意
当全局事务标识符(GTID)用于复制时,已经应用的事务会自动被忽略,因此不需要使用
IGNORE_SERVER_IDS
函数,该函数已被弃用。如果为服务器设置了gtid_mode=ON
,则如果在CHANGE REPLICATION SOURCE TO
语句中包含IGNORE_SERVER_IDS
选项,则会发出弃用警告。源元数据存储库和
SHOW REPLICA STATUS
的输出提供当前被忽略的服务器列表。有关更多信息,请参见第 19.2.4.2 节,“复制元数据存储库”和第 15.7.7.35 节,“显示 REPLICA 状态语句”。如果发出不带任何
IGNORE_SERVER_IDS
选项的CHANGE REPLICATION SOURCE TO
语句,则会保留任何现有列表。要清除被忽略服务器的列表,必须使用带有空列表的选项:CHANGE REPLICATION SOURCE TO IGNORE_SERVER_IDS = ();
RESET REPLICA ALL
清除IGNORE_SERVER_IDS
。注意
如果在任何通道存在使用
IGNORE_SERVER_IDS
设置的现有服务器 ID 时发出SET GTID_MODE=ON
,则会发出弃用警告。在启动基于 GTID 的复制之前,请检查并清除涉及服务器上的所有被忽略的服务器 ID 列表。SHOW REPLICA STATUS
语句显示被忽略的 ID 列表(如果有)。如果收到弃用警告,仍然可以通过发出包含带有空列表的IGNORE_SERVER_IDS
选项的CHANGE REPLICATION SOURCE TO
语句来清除列表。 -
NETWORK_NAMESPACE = '*
namespace*'
用于 TCP/IP 连接到复制源服务器的网络命名空间,或者如果使用 MySQL 通信堆栈,则用于 Group Replication 的组通信连接。字符串值的最大长度为 64 个字符。如果省略此选项,则副本的连接将使用默认(全局)命名空间。在不实现网络命名空间支持的平台上,当副本尝试连接到源时会发生故障。有关网络命名空间的信息,请参见第 7.1.14 节,“网络命名空间支持”。
NETWORK_NAMESPACE
在 MySQL 8.0.22 中可用。 -
PRIVILEGE_CHECKS_USER = {NULL | '*
account*'}
为指定通道提供安全上下文的用户帐户名称。
NULL
是默认值,表示不使用安全上下文。PRIVILEGE_CHECKS_USER
在 MySQL 8.0.18 中可用。用户帐户的用户名和主机名必须遵循 第 8.2.4 节“指定帐户名称” 中描述的语法,并且用户不能是匿名用户(具有空白用户名)或
CURRENT_USER
。帐户必须具有REPLICATION_APPLIER
权限,以及执行在通道上复制的事务所需的权限。有关帐户所需权限的详细信息,请参见 第 19.3.3 节“复制权限检查”。当重新启动复制通道时,权限检查将从那一点开始应用。如果您不指定通道且没有其他通道存在,则该语句将应用于默认通道。当设置了
PRIVILEGE_CHECKS_USER
时,强烈建议使用基于行的二进制日志记录,并且您可以设置REQUIRE_ROW_FORMAT
来强制执行此操作。例如,要在运行中的副本上启动通道channel_1
上的权限检查,请发出以下语句:STOP REPLICA FOR CHANNEL 'channel_1'; CHANGE REPLICATION SOURCE TO PRIVILEGE_CHECKS_USER = '*user*'@'*host*', REQUIRE_ROW_FORMAT = 1, FOR CHANNEL 'channel_1'; START REPLICA FOR CHANNEL 'channel_1';
-
RELAY_LOG_FILE = '*
relay_log_file*'
,RELAY_LOG_POS = '*
relay_log_pos*'
中继日志文件名及文件中的位置,在该位置,复制 SQL 线程在下次启动时开始从副本的中继日志中读取的位置。
RELAY_LOG_FILE
可以使用绝对路径或相对路径,并且使用与SOURCE_LOG_FILE
相同的基本名称。字符串值的最大长度为 511 个字符。当复制 SQL(应用程序)线程停止时,可以在运行中的副本上执行一个使用
RELAY_LOG_FILE
、RELAY_LOG_POS
或两者选项的CHANGE REPLICATION SOURCE TO
语句。如果至少有一个复制应用程序线程和复制 I/O(接收器)线程正在运行,则中继日志将被保留。如果两个线程都停止,则除非至少指定一个RELAY_LOG_FILE
或RELAY_LOG_POS
,否则所有中继日志文件都将被删除。对于仅具有应用程序线程而没有接收器线程的 Group Replication 应用程序通道(group_replication_applier
),如果应用程序线程停止,但是对于该通道,您不能使用RELAY_LOG_FILE
和RELAY_LOG_POS
选项。 -
REQUIRE_ROW_FORMAT = {0|1}
仅允许处理基于行的复制事件的复制通道。此选项阻止复制应用程序执行诸如创建临时表和执行
LOAD DATA INFILE
请求等操作,从而增加了通道的安全性。对于异步复制通道,默认情况下禁用REQUIRE_ROW_FORMAT
选项,但对于组复制通道,默认情况下启用,并且无法禁用。有关更多信息,请参见第 19.3.3 节,“复制权限检查”。REQUIRE_ROW_FORMAT
从 MySQL 8.0.19 开始提供。 -
REQUIRE_TABLE_PRIMARY_KEY_CHECK = {STREAM | ON | OFF | GENERATE}
从 MySQL 8.0.20 开始提供,此选项允许复制品设置自己的主键检查策略,如下:
-
ON
:复制品设置sql_require_primary_key = ON
;任何复制的CREATE TABLE
或ALTER TABLE
语句必须生成包含主键的表。 -
OFF
:复制品设置sql_require_primary_key = OFF
;不会检查任何复制的CREATE TABLE
或ALTER TABLE
语句是否存在主键。 -
STREAM
:复制品使用从源复制的sql_require_primary_key
的任何值来处理每个事务。这是默认值和默认行为。 -
GENERATE
:在 MySQL 8.0.32 中添加,这会导致复制品为任何缺少主键的InnoDB
表生成一个不可见的主键。有关更多信息,请参见第 15.1.20.11 节,“生成的不可见主键”。GENERATE
与组复制不兼容;您可以使用ON
、OFF
或STREAM
。
基于源或复制品表上仅存在生成的不可见主键的差异得到 MySQL 复制支持,只要源支持 GIPKs(MySQL 8.0.30 及更高版本),而复制品使用 MySQL 版本 8.0.32 或更高版本。如果在复制品上使用 GIPKs 并从使用 MySQL 8.0.29 或更早版本的源进行复制,则应注意,在这种情况下,除了复制品上的额外 GIPK 之外,不支持模式中的这种差异,并且可能导致复制错误。
对于多源复制,将
REQUIRE_TABLE_PRIMARY_KEY_CHECK
设置为ON
或OFF
可以使副本在不同源的复制通道之间规范化行为,并为sql_require_primary_key
保持一致的设置。使用ON
可以防止在多个源更新相同的表集时意外丢失主键。使用OFF
可以让可以操作主键的源与不能操作主键的源一起工作。在多个副本的情况下,当
REQUIRE_TABLE_PRIMARY_KEY_CHECK
设置为GENERATE
时,给定副本添加的生成的不可见主键与其他副本上添加的任何此类键是独立的。这意味着,如果使用生成的不可见主键,不同副本上生成的主键列中的值不能保证相同。当故障转移到这样的副本时,这可能是一个问题。当
PRIVILEGE_CHECKS_USER
为NULL
(默认值)时,用户帐户不需要管理级别权限来设置受限制的会话变量。将此选项设置为NULL
以外的值意味着,当REQUIRE_TABLE_PRIMARY_KEY_CHECK
为ON
,OFF
或GENERATE
时,用户帐户不需要会话管理级别权限来设置受限制的会话变量,如sql_require_primary_key
,避免需要授予帐户此类权限。有关更多信息,请参见第 19.3.3 节,“复制权限检查”。 -
-
SOURCE_AUTO_POSITION = {0|1}
使副本尝试使用基于 GTID 的复制的自动定位功能连接到源,而不是基于二进制日志文件的位置。此选项用于启动使用基于 GTID 的复制的副本。默认值为 0,表示不使用 GTID 自动定位和基于 GTID 的复制。只有在停止复制 SQL(应用程序)线程和复制 I/O(接收器)线程时,才能将此选项与
CHANGE REPLICATION SOURCE TO
一起使用。副本和源都必须启用 GTIDs(
GTID_MODE=ON
,在副本上为ON_PERMISSIVE
或OFF_PERMISSIVE
,在源上为GTID_MODE=ON
)。SOURCE_LOG_FILE
,SOURCE_LOG_POS
,RELAY_LOG_FILE
和RELAY_LOG_POS
不能与SOURCE_AUTO_POSITION = 1
一起指定。如果在副本上启用了多源复制,则需要为每个适用的复制通道设置SOURCE_AUTO_POSITION = 1
选项。设置
SOURCE_AUTO_POSITION = 1
后,在初始连接握手中,副本发送一个包含其已接收、已提交或两者都包含的事务的 GTID 集。源端通过发送其二进制日志中所有 GTID 不包含在副本发送的 GTID 集中的事务来做出响应。这种交换确保源端只发送副本尚未记录或提交的具有 GTID 的事务。如果副本从多个源接收事务,如钻石拓扑结构的情况,自动跳过功能确保事务不会被应用两次。有关副本发送的 GTID 集如何计算的详细信息,请参见第 19.1.3.3 节,“GTID 自动定位”。如果应该由源端发送的任何事务已从源端的二进制日志中清除,或者通过其他方法添加到
gtid_purged
系统变量的 GTID 集中,源端将向副本发送错误ER_MASTER_HAS_PURGED_REQUIRED_GTIDS
,并且复制不会启动。缺失的已清除事务的 GTID 将在源端的错误日志中被识别并列在警告消息ER_FOUND_MISSING_GTIDS
中。此外,如果在事务交换过程中发现副本已记录或提交具有源端 UUID 的 GTID 的事务,但源端本身尚未提交它们,则源端将向副本发送错误ER_SLAVE_HAS_MORE_GTIDS_THAN_MASTER
,并且复制不会启动。有关如何处理这些情况的信息,请参见第 19.1.3.3 节,“GTID 自动定位”。您可以通过检查性能模式
replication_connection_status
表或SHOW REPLICA STATUS
语句的输出来查看是否启用了 GTID 自动定位的复制。再次禁用SOURCE_AUTO_POSITION
选项会使副本恢复到基于文件的复制。 -
SOURCE_BIND = '*
interface_name*'
确定副本的哪个网络接口被选择用于连接到源端,适用于具有多个网络接口的副本。指定网络接口的 IP 地址。字符串值的最大长度为 255 个字符。
配置此选项的 IP 地址(如果有)可以在
SHOW REPLICA STATUS
输出的Source_Bind
列中看到。在源元数据存储库表mysql.slave_master_info
中,该值可以在Source_bind
列中看到。将副本绑定到特定网络接口的能力也受到 NDB Cluster 的支持。 -
SOURCE_COMPRESSION_ALGORITHMS = '*
algorithm*[,*
algorithm*][,*
algorithm*]'
指定连接到复制源服务器的允许压缩算法之一、两个或三个,用逗号分隔。字符串值的最大长度为 99 个字符。默认值为
uncompressed
。可用的算法是
zlib
、zstd
和uncompressed
,与protocol_compression_algorithms
系统变量相同。算法可以以任何顺序指定,但这不是优先顺序 - 算法协商过程尝试使用zlib
,然后zstd
,然后uncompressed
,如果它们被指定。SOURCE_COMPRESSION_ALGORITHMS
自 MySQL 8.0.18 起可用。SOURCE_COMPRESSION_ALGORITHMS
的值仅在禁用replica_compressed_protocol
或slave_compressed_protocol
系统变量时适用。如果启用了replica_compressed_protocol
或slave_compressed_protocol
,它优先于SOURCE_COMPRESSION_ALGORITHMS
,并且如果源和副本都支持该算法,则连接到源使用zlib
压缩。有关更多信息,请参见第 6.2.8 节,“连接压缩控制”。二进制日志事务压缩(自 MySQL 8.0.20 起可用),由
binlog_transaction_compression
系统变量激活,也可用于节省带宽。如果与连接压缩结合使用,连接压缩的机会减少,但仍可压缩标头以及未压缩的事件和事务有效载荷。有关二进制日志事务压缩的更多信息,请参见第 7.4.4.5 节,“二进制日志事务压缩”。 -
SOURCE_CONNECT_RETRY = *
interval*
指定副本在连接到源超时后重新连接尝试之间的秒数间隔。默认间隔为 60。
尝试次数受
SOURCE_RETRY_COUNT
选项限制。如果使用默认设置,副本在重新连接尝试之间等待 60 秒(SOURCE_CONNECT_RETRY=60
),并以这个速率持续尝试重新连接 60 天(SOURCE_RETRY_COUNT=86400
)。这些值记录在源元数据存储库中,并显示在replication_connection_configuration
性能模式表中。 -
SOURCE_CONNECTION_AUTO_FAILOVER = {0|1}
如果有一个或多个备用复制源服务器可用(即有多个共享复制数据的 MySQL 服务器或服务器组),则为复制通道激活异步连接故障转移机制。
SOURCE_CONNECTION_AUTO_FAILOVER
从 MySQL 8.0.22 版本开始提供。默认值为 0,表示未激活该机制。有关完整信息和设置此功能的说明,请参阅第 19.4.9.2 节,“副本的异步连接故障转移”。在由
SOURCE_CONNECT_RETRY
和SOURCE_RETRY_COUNT
控制的重新连接尝试耗尽后,异步连接故障转移机制接管。它将副本重新连接到从指定源列表中选择的备用源,您可以使用asynchronous_connection_failover_add_source
和asynchronous_connection_failover_delete_source
函数进行管理。要添加和删除受管理的服务器组,请改用asynchronous_connection_failover_add_managed
和asynchronous_connection_failover_delete_managed
函数。有关更多信息,请参阅第 19.4.9 节,“使用异步连接故障转移切换源和副本”。重要提示
-
只有在使用 GTID 自动定位时(
SOURCE_AUTO_POSITION = 1
)才能设置SOURCE_CONNECTION_AUTO_FAILOVER = 1
。 -
当你设置
SOURCE_CONNECTION_AUTO_FAILOVER = 1
时,将SOURCE_RETRY_COUNT
和SOURCE_CONNECT_RETRY
设置为最小值,只允许几次重试尝试与相同源进行连接,以防连接失败是由瞬时网络中断引起的。否则,异步连接故障转移机制无法及时激活。适当的值是SOURCE_RETRY_COUNT=3
和SOURCE_CONNECT_RETRY=10
,这使得副本在每次连接尝试之间间隔 10 秒的情况下重试连接 3 次。 -
当你设置
SOURCE_CONNECTION_AUTO_FAILOVER = 1
时,复制元数据存储库必须包含用于连接到复制通道源列表中所有服务器的复制用户帐户的凭据。该帐户还必须对性能模式表具有SELECT
权限。这些凭据可以使用带有SOURCE_USER
和SOURCE_PASSWORD
选项的CHANGE REPLICATION SOURCE TO
语句进行设置。有关更多信息,请参见 第 19.4.9 节,“使用异步连接故障转移切换源和副本”。 -
从 MySQL 8.0.27 开始,当你设置
SOURCE_CONNECTION_AUTO_FAILOVER = 1
时,如果这个复制通道在单主模式下的组复制主节点上,副本的异步连接故障转移将自动激活。启用此功能后,如果正在复制的主节点下线或进入错误状态,新的主节点在选举时将在同一通道上开始复制。如果要使用此功能,这个复制通道还必须在复制组中的所有辅助服务器上设置,并在任何新加入的成员上设置。(如果使用 MySQL 的克隆功能进行服务器配置,则所有这些都会自动发生。)如果不想使用此功能,请使用group_replication_disable_member_action()
函数禁用默认启用的 Group Replication 成员操作mysql_start_failover_channels_if_primary
。有关更多信息,请参见 第 19.4.9.2 节,“副本的异步连接故障转移”。
-
-
SOURCE_DELAY = *
interval*
指定副本必须滞后源多少秒。从源接收的事件直到至少延迟
interval
秒后才执行。interval
必须是 0 到 2³¹−1 范围内的非负整数。默认值为 0。更多信息,请参见 Section 19.4.11, “Delayed Replication”。使用
SOURCE_DELAY
选项的CHANGE REPLICATION SOURCE TO
语句可以在运行中的副本上执行,当复制 SQL 线程停止时。 -
SOURCE_HEARTBEAT_PERIOD = *
interval*
控制心跳间隔,防止在没有数据的情况下发生连接超时,如果连接仍然良好。在那么多秒后向副本发送心跳信号,并且每当源的二进制日志更新为一个事件时,等待时间就会重置。因此,只有在二进制日志文件中有一段时间没有发送的事件时,源才会发送心跳。
心跳间隔
interval
是一个十进制值,范围为 0 到 4294967 秒,分辨率为毫秒;最小的非零值为 0.001。将interval
设置为 0 会完全禁用心跳。心跳间隔默认为replica_net_timeout
或slave_net_timeout
系统变量值的一半。它记录在源元数据存储库中,并显示在replication_connection_configuration
性能模式表中。系统变量
replica_net_timeout
(从 MySQL 8.0.26 开始)或slave_net_timeout
(MySQL 8.0.26 之前)指定副本等待来自源的更多数据或心跳信号的秒数,超过该时间副本将认为连接已中断,中止读取并尝试重新连接。默认值为 60 秒(一分钟)。请注意,对replica_net_timeout
或slave_net_timeout
的值或默认设置的更改不会自动更改心跳间隔,无论是明确设置还是使用先前计算的默认值。如果将全局值replica_net_timeout
或slave_net_timeout
设置为小于当前心跳间隔的值,将发出警告。如果更改了replica_net_timeout
或slave_net_timeout
,还必须发出CHANGE REPLICATION SOURCE TO
以将心跳间隔调整为适当的值,以便心跳信号在连接超时之前发生。如果不这样做,心跳信号将不起作用,如果未从源接收到数据,则副本可能会进行重复的重新连接尝试,从而创建僵尸转储线程。 -
SOURCE_HOST = '*
host_name*'
复制源服务器的主机名或 IP 地址。副本使用此信息连接到源。字符串值的最大长度为 255 个字符。
如果您指定了
SOURCE_HOST
或SOURCE_PORT
,则副本会假定源服务器与之前不同(即使选项值与当前值相同)。在这种情况下,源的二进制日志文件名和位置的旧值被认为不再适用,因此如果您在语句中未指定SOURCE_LOG_FILE
和SOURCE_LOG_POS
,则会自动追加SOURCE_LOG_FILE=''
和SOURCE_LOG_POS=4
。将
SOURCE_HOST=''
设置为(即,将其值明确设置为空字符串)不等同于根本不设置SOURCE_HOST
。尝试将SOURCE_HOST
设置为空字符串会导致错误。 -
SOURCE_LOG_FILE = '*
source_log_name*'
,SOURCE_LOG_POS = *
source_log_pos*
二进制日志文件名以及在该文件中的位置,复制 I/O(接收器)线程在下次启动时从源二进制日志中开始读取的位置。如果使用基于二进制日志文件位置的复制,请指定这些选项。
SOURCE_LOG_FILE
必须包括源服务器上可用的特定二进制日志文件的数字后缀,例如,SOURCE_LOG_FILE='binlog.000145'
。字符串值的最大长度为 511 个字符。SOURCE_LOG_POS
是副本在该文件中开始读取的数字位置。SOURCE_LOG_POS=4
表示二进制日志文件中事件的开始。如果指定了
SOURCE_LOG_FILE
或SOURCE_LOG_POS
中的任一个,则不能指定SOURCE_AUTO_POSITION = 1
,该选项用于基于 GTID 的复制。如果未指定
SOURCE_LOG_FILE
或SOURCE_LOG_POS
中的任何一个,则副本使用复制 SQL 线程在发出CHANGE REPLICATION SOURCE TO
之前的最后坐标。这确保了即使复制 SQL(应用程序)线程比复制 I/O(接收器)线程晚,复制也不会出现不连续。 -
SOURCE_PASSWORD = '*
password*'
用于连接到复制源服务器的复制用户帐户的密码。字符串值的最大长度为 32 个字符。如果指定了
SOURCE_PASSWORD
,则还需要SOURCE_USER
。在
CHANGE REPLICATION SOURCE TO
语句中用于复制用户帐户的密码长度限制为 32 个字符。尝试使用超过 32 个字符的密码会导致CHANGE REPLICATION SOURCE TO
失败。密码在 MySQL Server 的日志、性能模式表和
SHOW PROCESSLIST
语句中被掩盖。 -
SOURCE_PORT = *
port_num*
副本用于连接到复制源服务器的 TCP/IP 端口号。
注意
复制不能使用 Unix 套接字文件。您必须能够使用 TCP/IP 连接到复制源服务器。
如果指定了
SOURCE_HOST
或SOURCE_PORT
,则副本会假定源服务器与以前不同(即使选项值与当前值相同)。在这种情况下,源二进制日志文件名和位置的旧值被认为不再适用,因此如果在语句中不指定SOURCE_LOG_FILE
和SOURCE_LOG_POS
,则会自动附加SOURCE_LOG_FILE=''
和SOURCE_LOG_POS=4
。 -
SOURCE_PUBLIC_KEY_PATH = '*
key_file_name*'
通过提供包含源端所需的公钥副本的文件路径名,启用基于 RSA 密钥对的密码交换。文件必须采用 PEM 格式。字符串值的最大长度为 511 个字符。
此选项适用于使用
sha256_password
或caching_sha2_password
认证插件进行身份验证的副本。(对于sha256_password
,只有在使用 OpenSSL 构建 MySQL 时才能使用SOURCE_PUBLIC_KEY_PATH
。)如果您正在使用使用caching_sha2_password
插件进行身份验证的复制用户帐户(这是 MySQL 8.0 的默认设置),并且您没有使用安全连接,则必须指定此选项或GET_SOURCE_PUBLIC_KEY=1
选项之一以向副本提供 RSA 公钥。 -
SOURCE_RETRY_COUNT = *
count*
设置副本在与源的连接超时后进行的重新连接尝试的最大次数,由
replica_net_timeout
或slave_net_timeout
系统变量确定。如果副本确实需要重新连接,则第一次重试会在超时后立即发生。默认值为 86400 次尝试。尝试之间的间隔由
SOURCE_CONNECT_RETRY
选项指定。如果使用默认设置,副本在重新连接尝试之间等待 60 秒(SOURCE_CONNECT_RETRY=60
),并以此速率继续尝试重新连接 60 天(SOURCE_RETRY_COUNT=86400
)。将SOURCE_RETRY_COUNT
设置为 0 意味着没有重新连接尝试次数限制,因此副本将无限尝试重新连接。SOURCE_CONNECT_RETRY
和SOURCE_RETRY_COUNT
的值记录在源元数据存储库中,并显示在replication_connection_configuration
性能模式表中。SOURCE_RETRY_COUNT
取代了--master-retry-count
服务器启动选项。 -
SOURCE_SSL = {0|1}
指定副本是否加密复制连接。默认值为 0,表示副本不加密复制连接。如果设置
SOURCE_SSL=1
,则可以使用SOURCE_SSL_*
xxx*
和SOURCE_TLS_*
xxx*
选项配置加密。为复制连接设置
SOURCE_SSL=1
,然后不设置更多的SOURCE_SSL_*
xxx*
选项相当于为客户端设置--ssl-mode=REQUIRED
,如加密连接的命令选项中所述。使用SOURCE_SSL=1
,只有在可以建立加密连接时连接尝试才会成功。复制连接不会退回到未加密连接,因此没有与复制的--ssl-mode=PREFERRED
设置相对应的设置。如果设置SOURCE_SSL=0
,则相当于--ssl-mode=DISABLED
。重要
为了帮助防止复杂的中间人攻击,副本验证服务器的身份非常重要。您可以指定额外的
SOURCE_SSL_*
xxx*
选项对应于设置--ssl-mode=VERIFY_CA
和--ssl-mode=VERIFY_IDENTITY
,这比默认设置更好地帮助防止这种类型的攻击。使用这些设置,副本会检查服务器的证书是否有效,并检查副本正在使用的主机名是否与服务器证书中的标识匹配。要实施这些验证级别之一,您必须首先确保服务器的 CA 证书可靠地可用于副本,否则将导致可用性问题。因此,它们不是默认设置。 -
SOURCE_SSL_*
xxx*
,SOURCE_TLS_*
xxx*
指定副本如何使用加密和密码来保护复制连接。即使在没有 SSL 支持的编译副本上也可以更改这些选项。它们保存在源元数据存储库中,但如果副本没有启用 SSL 支持,则会被忽略。字符串值为
SOURCE_SSL_*
xxx*
和SOURCE_TLS_*
xxx*
选项的最大长度为 511 个字符,例外是SOURCE_TLS_CIPHERSUITES
,其长度为 4000 个字符。SOURCE_SSL_*
xxx*
和SOURCE_TLS_*
xxx*
选项执行与加密连接的命令选项中描述的--ssl-*
xxx*
和--tls-*
xxx*
客户端选项相同的功能。两组选项之间的对应关系,以及使用SOURCE_SSL_*
xxx*
和SOURCE_TLS_*
xxx*
选项建立安全连接的方法,在第 19.3.1 节,“设置复制使用加密连接”中有解释。 -
SOURCE_USER = '*
user_name*'
复制用户帐户的用户名,用于连接到复制源服务器。字符串值的最大长度为 96 个字符。
对于组复制,此帐户必须存在于复制组的每个成员中。如果 XCom 通信堆栈用于组,则用于分布式恢复,如果 MySQL 通信堆栈用于组,则用于组通信连接。对于 MySQL 通信堆栈,帐户必须具有
GROUP_REPLICATION_STREAM
权限。可以通过指定
SOURCE_USER=''
来设置空用户名称,但不能使用空用户名称启动复制通道。在 MySQL 8.0.21 之前的版本中,仅在需要出于安全目的清除先前使用的凭据时才设置空的SOURCE_USER
用户名称。不要在此之后使用通道,因为在这些版本中存在一个错误,如果从存储库中读取空用户名称(例如,在组复制通道的自动重启期间),则可能会替换默认用户名称。从 MySQL 8.0.21 开始,可以设置空的SOURCE_USER
用户名称,并且可以在之后使用通道,如果始终使用START REPLICA
语句或START GROUP_REPLICATION
语句提供用户凭据以启动复制通道。这种方法意味着复制通道始终需要操作员干预才能重新启动,但用户凭据不会记录在复制元数据存储库中。重要提示
要使用使用
caching_sha2_password
插件进行身份验证的复制用户帐户连接到源,您必须设置安全连接,如 Section 19.3.1, “Setting Up Replication to Use Encrypted Connections”中所述,或启用不加密的连接以支持使用 RSA 密钥对进行密码交换。caching_sha2_password
身份验证插件是从 MySQL 8.0 开始的新用户的默认插件(请参阅 Section 8.4.1.2, “Caching SHA-2 Pluggable Authentication”)。如果您创建或使用用于复制的用户帐户使用此身份验证插件,并且未使用安全连接,则必须启用基于 RSA 密钥对的密码交换以成功连接。您可以使用此语句的SOURCE_PUBLIC_KEY_PATH
选项或GET_SOURCE_PUBLIC_KEY=1
选项来执行此操作。 -
SOURCE_ZSTD_COMPRESSION_LEVEL = *
level*
用于使用
zstd
压缩算法的连接到复制源服务器的压缩级别。SOURCE_ZSTD_COMPRESSION_LEVEL
自 MySQL 8.0.18 起可用。允许的级别为 1 到 22,较大的值表示较高级别的压缩。默认级别为 3。压缩级别设置对不使用
zstd
压缩的连接没有影响。有关更多信息,请参见第 6.2.8 节,“连接压缩控制”。
示例
CHANGE REPLICATION SOURCE TO
对于在具有源快照并记录了快照时间对应的源二进制日志坐标的情况下设置副本非常有用。在将快照加载到副本以与源同步之后,您可以在副本上运行CHANGE REPLICATION SOURCE TO SOURCE_LOG_FILE='*
log_name*', SOURCE_LOG_POS=*
log_pos*
来指定副本应开始读取源二进制日志的坐标。以下示例更改了副本使用的源服务器并建立了副本开始读取的源二进制日志坐标:
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='source2.example.com',
SOURCE_USER='replication',
SOURCE_PASSWORD='*password*',
SOURCE_PORT=3306,
SOURCE_LOG_FILE='source2-bin.001',
SOURCE_LOG_POS=4,
SOURCE_CONNECT_RETRY=10;
有关在故障切换期间将现有副本切换到新源的过程,请参见第 19.4.8 节,“故障切换期间切换源”。
当源和副本上使用 GTID 时,请指定 GTID 自动定位,而不是提供二进制日志文件位置,如下例所示。有关在新服务器或停止的服务器、在线服务器或其他副本上配置和启动基于 GTID 的复制的完整说明,请参见第 19.1.3 节,“具有全局事务标识符的复制”。
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='source3.example.com',
SOURCE_USER='replication',
SOURCE_PASSWORD='*password*',
SOURCE_PORT=3306,
SOURCE_AUTO_POSITION = 1,
FOR CHANNEL "source_3";
在此示例中,使用了多源复制,并且CHANGE REPLICATION SOURCE TO
语句应用于连接副本到指定主机的复制通道"source_3"
。有关设置多源复制的指导,请参见第 19.1.5 节,“MySQL 多源复制”。
下一个示例显示了如何使副本应用您想要重复的中继日志文件中的事务。为此,源不需要可达。您可以使用CHANGE REPLICATION SOURCE TO
来定位您希望副本开始重新应用事务的中继日志位置,然后启动 SQL 线程:
CHANGE REPLICATION SOURCE TO
RELAY_LOG_FILE='replica-relay-bin.006',
RELAY_LOG_POS=4025;
START REPLICA SQL_THREAD;
更改复制源为
也可以用于跳过导致复制停止的二进制日志中的事务。执行此操作的适当方法取决于是否使用 GTID。有关使用更改复制源为
或其他方法跳过事务的说明,请参见第 19.1.7.3 节,“跳过事务”。