MySQL SQL Mode深度解析:类型、用法与最佳实践

深入了解MySQL SQL Mode,掌握不同模式的用法和最佳实践,提升数据库管理效率。

原文标题:数据库系列之MySQL中的SQL Mode介绍

原文作者:牧羊人的方向

冷月清谈:

本文深入探讨了MySQL中的SQL Mode,解释了不同Mode对SQL语法操作的影响。文章首先介绍了Server SQL Mode的类型,包括ANSI、STRICT_TRANS_TABLES、TRADITIONAL等常用模式,并结合案例讲解了ANSI_QUOTES、ERROR_FOR_DIVISION_BY_ZERO、HIGH_NOT_PRECEDENCE、IGNORE_SPACE、NO_UNSIGNED_SUBTRACTION、ONLY_FULL_GROUP_BY、PAD_CHAR_TO_FULL_LENGTH和TIME_TRUNCATE_FRACTIONAL等具体模式的运作机制。随后,文章详细分析了Strict SQL mode对数据插入和更新的影响,特别是在处理无效或缺失数据时的行为,并比较了STRICT_ALL_TABLES和STRICT_TRANS_TABLES两种模式的差异。此外,文章还对比了IGNORE关键字和Strict SQL mode在错误处理上的不同表现,并列举了IGNORE关键字支持的语句和错误类型。最后,文章总结了核心业务系统中SQL Mode的最佳实践设置,为数据库管理员提供了实用指南。

怜星夜思:

1、文章提到了STRICT_TRANS_TABLES和STRICT_ALL_TABLES两种模式在处理非事务表多行数据插入时的区别,实际应用中,选择哪种模式更合适,分别适用于哪些场景?
2、文章中提到的IGNORE关键字可以忽略多种错误类型,那么在实际应用中,使用IGNORE关键字的潜在风险是什么?如何规避这些风险?
3、文章最后提到了核心业务系统SQL Mode的最佳实践设置,这个设置是通用的吗?在不同的业务场景下,是否需要根据实际情况进行调整?

原文内容

SQL mode定义了不同的SQL语法操作,本文简要介绍了MySQL中的SQL mode类型及使用,结合具体案例使用加深理解,适用于兼容MySQL语法的分布式数据库。

1、Server SQL Mode介绍

MySQL Server可以运行在不同的SQL Modes下,每一类SQL Modes定义MySQL应该支持什么样的SQL语法。DBA可以设置系统级别的SQL mode,每个应用也可以根据需要设置session级别的SQL mode。在MySQL 8.0版本中默认设置以下SQL mode:

mysql> select @@global.sql_mode;
+-----------------------------------------------------------------------------------------------------------------------+

| @@global.sql_mode |
+-----------------------------------------------------------------------------------------------------------------------+

| ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION |
+-----------------------------------------------------------------------------------------------------------------------+

1 row in set (0.00 sec)
在MySQL 8.0版本中支持以下SQL mode:

其中ANSI、STRICT_TRANS_TABLES和TRADITIONAL为最常用的SQL Mode。
  • ANSI更接近于标准SQL,等同于REAL_AS_FLOAT, PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE和ONLY_FULL_GROUP_BY

  • STRICT_TRANS_TABLES:如果无法按给定的方式将值插入到事务表中,请中止该语句。对于非事务表,如果值出现在单行语句或多行语句的第一行中,则中止该语句

  • TRADITIONAL:使MySQL的行为像一个“传统”的SQL数据库系统,在向列中插入错误值时,此模式“给出错误而不是警告”。等同于STRICT_TRANS_TABLES, STRICT_ALL_TABLES, NO_ZERO_IN_DATE, NO_ZERO_DATE, ERROR_FOR_DIVISION_BY_ZERO和NO_ENGINE_SUBSTITUTION

1.1 ANSI_QUOTES

双引号””不能作为引用字符串,而是会解释为标识符。

mysql> set @@session.sql_mode='';
Query OK, 0 rows affected (0.00 sec)

mysql> select @@session.sql_mode;
+--------------------+

| @@session.sql_mode |
+--------------------+

| |
+--------------------+

1 row in set (0.00 sec)
mysql> select "test" from tango.test01;
+------+

| test |
+------+

| test |
| test |
| test |
+------+

3 rows in set (0.02 sec)

设置sql_mode为ansi_quotes,可以看到test标识为column列名进行查询

mysql> set @@session.sql_mode='ansi_quotes';
Query OK, 0 rows affected (0.00 sec)

mysql> select @@session.sql_mode;
+--------------------+

| @@session.sql_mode |
+--------------------+

| ANSI_QUOTES |
+--------------------+

1 row in set (0.00 sec)

mysql> select "test" from tango.test01;
ERROR 1054 (42S22): Unknown column 'test' in 'field list'
1.2 ERROR_FOR_DIVISION_BY_ZERO
ERROR_FOR_DIVISION_BY_ZERO模式影响insert或update时出现除以0或者以0取模(mod(N,0)),根据是否设置为严格模式有以下几种情况:
  • 当不设置ERROR_FOR_DIVISION_BY_ZERO,除以0或以0取模的结果为null,不产生告警;

  • 当设置ERROR_FOR_DIVISION_BY_ZERO,除以0或以0取模的结果为null,并产生告警;

  • 当设置ERROR_FOR_DIVISION_BY_ZERO和严格模式,除以0或以0取模会产生报错,除非insert和update配合ignore适用;

  • 对于select查询,除以0或以0取模在任何情况下都会返回null,如果设置ERROR_FOR_DIVISION_BY_ZERO,则会产生warnings;

ERROR_FOR_DIVISION_BY_ZERO并不包含在严格模式里,但这个选项即将被废弃,在未来的版本会合并到严格模式里。

1.3 HIGH_NOT_PRECEDENCE

NOT操作的顺序,比如NOT a BETWEEN b AND c被解释为NOT (a BETWEEN b AND c)。

mysql> set @@session.sql_mode='';
mysql> SELECT NOT 1 BETWEEN -5 AND 5;
+------------------------+
| NOT 1 BETWEEN -5 AND 5 |
+------------------------+
| 0 |
+------------------------+

mysql> SET sql_mode = 'HIGH_NOT_PRECEDENCE';
mysql> SELECT NOT 1 BETWEEN -5 AND 5;
+------------------------+
| NOT 1 BETWEEN -5 AND 5 |
+------------------------+
| 1 |
+------------------------+
1 row in set (0.00 sec)
1.4 IGNORE_SPACE

允许函数名和()之间有空格,这会导致内置的函数为关键字。只是对于内置函数而言,对于用户定义的函数,总是允许和()之间有空格。

mysql>  set @@session.sql_mode='';
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE count (i INT);
Query OK, 0 rows affected (0.10 sec)

当设置ignore_space后

mysql> set @@session.sql_mode='ignore_space';
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE count (i INT);
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'count (i INT)' at line 1

mysql> CREATE TABLE `count` (i INT);
Query OK, 0 rows affected (0.10 sec)

由于count是内置函数,创建表名为count时需要加标识符

1.5 NO_UNSIGNED_SUBTRACTION

当两个整数相减时,其中一个unsigned,会默认生成unsigned的结果。但是如果结果是负数,则会出错。

mysql> set @@session.sql_mode='';

mysql> SELECT CAST(0 AS UNSIGNED) - 1;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(cast(0 as unsigned) - 1)'

如果设置了NO_UNSIGNED_SUBTRACTION,结果如下:

mysql> SET sql_mode = 'NO_UNSIGNED_SUBTRACTION';
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT CAST(0 AS UNSIGNED) - 1;
+-------------------------+

| CAST(0 AS UNSIGNED) - 1 |
+-------------------------+

| -1 |
+-------------------------+

1 row in set (0.00 sec)

如果两个整数相减的结果用来更新一个unsigned列,默认情况下是该列数据类型的最大值,如果开启了NO_UNSIGNED_SUBTRACTION,则设置为0。但是在strict SQL mode下,列值不会变化,并且会报错。

mysql> SET sql_mode='';
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE test (c1 BIGINT UNSIGNED NOT NULL);
Query OK, 0 rows affected (0.09 sec)

mysql> CREATE TABLE t1 SELECT c1 - 1 AS c2 FROM test;
Query OK, 0 rows affected (0.11 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> DESCRIBE t1;
+-------+-----------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-----------------+------+-----+---------+-------+
| c2 | bigint unsigned | NO | | 0 | |
+-------+-----------------+------+-----+---------+-------+
1 row in set (0.00 sec)

mysql> SET sql_mode='NO_UNSIGNED_SUBTRACTION';
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE t2 SELECT c1 - 1 AS c2 FROM test;
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> DESCRIBE t2;
+-------+--------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------+------+-----+---------+-------+
| c2 | bigint | NO | | 0 | |
+-------+--------+------+-----+---------+-------+
1 row in set (0.00 sec)
1.6 ONLY_FULL_GROUP_BY

对于group by聚合操作,如果在select中出现的列没有在group by中出现,那么这种SQL是不合法的。

##创建表并插入数据
mysql> DROP TABLE IF EXISTS mytable;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> CREATE TABLE mytable (
-> id INT UNSIGNED NOT NULL PRIMARY KEY,
-> a VARCHAR(10),
-> b VARCHAR(10),
-> c INT
-> );
Query OK, 0 rows affected (0.10 sec)

mysql> INSERT INTO mytable
-> VALUES (1, 'abc', 'qrs', 1000),
-> (2, 'abc', 'tuv', 2000),
-> (3, 'def', 'qrs', 4000),
-> (4, 'def', 'tuv', 8000),
-> (5, 'abc', 'qrs', 16000),
-> (6, 'def', 'tuv', 32000);
Query OK, 6 rows affected (0.01 sec)
Records: 6 Duplicates: 0 Warnings: 0

##设置SQL MODEONLY_FULL_GROUP_BY
mysql> SET sql_mode='ONLY_FULL_GROUP_BY';
mysql> select a,b,max(c) from mytable group by a;
ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'tango.mytable.b' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
mysql> select a,b,max(c) from mytable group by a,b;
+------+------+--------+
| a | b | max(c) |
+------+------+--------+
| abc | qrs | 16000 |
| abc | tuv | 2000 |
| def | qrs | 4000 |
| def | tuv | 32000 |
+------+------+--------+
4 rows in set (0.00 sec)
1.7 PAD_CHAR_TO_FULL_LENGTH

默认情况下,列类型为char会裁剪掉,但是如果设置了PAD_CHAR_TO_FULL_LENGTH,CHAR类型的列会使用空Ox20进行填充。

mysql> CREATE TABLE t1 (c1 CHAR(10));
Query OK, 0 rows affected (0.02 sec)

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

mysql> SET sql_mode = '';
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT c1, CHAR_LENGTH(c1) FROM t1;
+------+-----------------+

| c1 | CHAR_LENGTH(c1) |
+------+-----------------+

| xy | 2 |
+------+-----------------+

1 row in set (0.00 sec)

设置SQL Mode为PAD_CHAR_TO_FULL_LENGTH后

mysql> SET sql_mode = 'PAD_CHAR_TO_FULL_LENGTH';
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> SELECT c1, CHAR_LENGTH(c1) FROM t1;
+------------+-----------------+

| c1 | CHAR_LENGTH(c1) |
+------------+-----------------+

| xy | 10 |
+------------+-----------------+

1 row in set (0.00 sec)
1.8 TIME_TRUNCATE_FRACTIONAL

当date、time或timestamp中带有飞秒值时根据列的精度是truncate还是rounding,默认是使用rounding,设置该SQL mode时候会使用truncate。

mysql> CREATE TABLE t2 (id INT, tval TIME(1));
Query OK, 0 rows affected (0.02 sec)
mysql> SET sql_mode='';
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO t2 (id, tval) VALUES(1, 1.55);
Query OK, 1 row affected (0.01 sec)
mysql> SET sql_
mode='TIME_TRUNCATE_FRACTIONAL';
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO t2 (id, tval) VALUES(2, 1.55);
Query OK, 1 row affected (0.00 sec)
mysql> SELECT id, tval FROM t2 ORDER BY id;
+------+------------+

| id | tval |
+------+------------+

| 1 | 00:00:01.6 |
| 2 | 00:00:01.5 |
+------+------------+

2 rows in set (0.00 sec)
2、Strict SQL mode

Strict mode控制着MySQL在update/insert时候对于invalid或missing数据的处理。插入或更新的值invalid有多种原因,比如数据类型错误、值越界等;值缺失是因为向NOT NULL列插入数据的时候没有显示的指定DEFAULT语句。Strict mode也会影响到create table语句。而对于strict mode下的select语句,只会生成一个warning,不会报错。

如果没有生效strict mode,MySQL在插入invalid或缺失值的时候会自动调整,并产生warning信息。在Strict mode下,也可以使用INSERT IGNORE或UPDATE IGNORE产生同样的效果。Strict mode在设置了STRICT_ALL_TABLES或STRICT_TRANS_TABLES生效,这两个模式有以下区别:
  • 对于业务的表,在两种模式下当insert/update语句出现了invalid或missing values会报错,语句会中断并回滚

  • 对于非业务的表,对于insert/update第一行数据的时候跟业务表有同样的行为,语句出错并回滚。但是对于多行记录,并且出错的不在第一行记录的时候,会有所不同

    • 对于STRICT_ALL_TABLES,mysql会报错并忽略余下所有的记录数,这样就会造成有部分成功了。为了避免这个问题,可以使用single-row语句。

    • 对于STRICT_TRANS_TABLES,mysql会把invalid值转换为最接近的valid值并插入;如果值缺失了,会插入默认的值。同时mysql会产生一个warning并继续处理下面的操作

2.1 IGNORE和Strict SQL mode对比
下表对比了当默认值是error还是warning时候IGNORE和Strict SQL mode的不同表现:

从以上可以看出,当同时设置strict SQL mode和IGNORE下,IGNORE优先级更高

2.1.1 IGNORE的影响

在mysql中有些语句支持IGNORE关键字,它会将某些类型的错误降级并产生warning来替代。对于multi-row语句,错误降级为warning可以继续处理接下去的语句,同时IGNORE也会跳过出错的rows。

如下表t3中的主键包含唯一性约束,当插入相同的数据时会提示重复键值。

mysql> CREATE TABLE t3 (i INT NOT NULL PRIMARY KEY);
mysql> INSERT INTO t3 (i) VALUES(1),(1);
ERROR 1062 (23000): Duplicate entry '1' for key 't3.PRIMARY'

加入IGNORE参数后,重复值依然不会插入,但是会报一个warning,语句并没有出错中断

mysql> INSERT IGNORE INTO t3 (i) VALUES(1),(1);
Query OK, 1 row affected, 1 warning (0.00 sec)
Records: 2 Duplicates: 1 Warnings: 1

mysql> show warnings;
+---------+------+------------------------------------------+

| Level | Code | Message |
+---------+------+------------------------------------------+

| Warning | 1062 | Duplicate entry '1' for key 't3.PRIMARY' |
+---------+------+------------------------------------------+

1 row in set (0.00 sec)

在MySQL中以下语句支持IGNORE关键字:CREATE TABLE ... SELECT、DELETE、INSERT、LOAD DATA和UPDATE。IGNORE关键字会忽略以下错误:

ER_BAD_NULL_ERROR
ER_DUP_ENTRY
ER_DUP_ENTRY_WITH_KEY_NAME
ER_DUP_KEY
ER_NO_PARTITION_FOR_GIVEN_VALUE
ER_NO_PARTITION_FOR_GIVEN_VALUE_SILENT
ER_NO_REFERENCED_ROW_2
ER_ROW_DOES_NOT_MATCH_GIVEN_PARTITION_SET
ER_ROW_IS_REFERENCED_2
ER_SUBQUERY_NO_1_ROW
ER_VIEW_CHECK_FAILED
2.1.2 Strict SQL Mode的影响

在Strict SQL mode下,会将特定的warning升级为error。比如在non-strict mode下,插入字符串到整数列会将值转为0,并产生warning。

mysql> SET sql_mode = '';
Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO t3 (i) VALUES('abc');
Query OK, 1 row affected, 1 warning (0.00 sec)

mysql> show warnings;
+---------+------+--------------------------------------------------------+

| Level | Code | Message |
+---------+------+--------------------------------------------------------+

| Warning | 1366 | Incorrect integer value: 'abc' for column 'i' at row 1 |
+---------+------+--------------------------------------------------------+

1 row in set (0.00 sec)
mysql> select * from t3;
+---+

| i |
+---+

| 0 |
| 1 |
+---+

2 rows in set (0.00 sec)

如果在strict mode下,invalid value会被拒绝并且报错:

mysql> SET sql_mode = 'STRICT_ALL_TABLES';
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> INSERT INTO t3 (i) VALUES('abc');
ERROR 1366 (HY000): Incorrect integer value: 'abc' for column 'i' at row 1

Strict SQL mode在值invalid或缺失的时候会产生以下错误信息:

ER_BAD_NULL_ERROR
ER_CUT_VALUE_GROUP_CONCAT
ER_DATA_TOO_LONG
ER_DATETIME_FUNCTION_OVERFLOW
ER_DIVISION_BY_ZERO
ER_INVALID_ARGUMENT_FOR_LOGARITHM
ER_NO_DEFAULT_FOR_FIELD
ER_NO_DEFAULT_FOR_VIEW_FIELD
ER_TOO_LONG_KEY
ER_TRUNCATED_WRONG_VALUE
ER_TRUNCATED_WRONG_VALUE_FOR_FIELD
ER_WARN_DATA_OUT_OF_RANGE
ER_WARN_NULL_TO_NOTNULL
ER_WARN_TOO_FEW_RECORDS
ER_WRONG_ARGUMENTS
ER_WRONG_VALUE_FOR_TYPE
WARN_DATA_TRUNCATED

以上是SQL Mode的简单介绍,在核心业务系统中,根据最佳实践SQL mode设置为ONLY_FULL_GROUP_BY, STRICT_TRANS_TABLES, NO_ZERO_IN_DATE, NO_ZERO_DATE, ERROR_FOR_DIVISION_BY_ZERO, NO_ENGINE_SUBSTITUTION, PIPES_AS_CONCAT, IGNORE_SPACE。

参考资料:

  1. https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html

  2. https://blog.csdn.net/weixin_39004901/article/details/89378097

  3. https://www.modb.pro/db/24613

使用IGNORE关键字最大的风险在于可能会掩盖一些潜在的问题。由于错误被忽略了,你可能无法及时发现数据插入或更新的异常情况,从而导致数据不一致或丢失。为了规避风险,建议在使用IGNORE关键字时,仔细检查warning日志,确保所有被忽略的错误都在可控范围内。

对于这个问题,我觉得需要根据具体情况来判断。如果对数据完整性要求非常严格,任何错误都不能容忍,那么STRICT_ALL_TABLES更合适,因为它会立即停止并回滚,避免脏数据进入数据库。但如果允许部分数据插入成功,且后续可以通过其他方式处理错误数据,那么STRICT_TRANS_TABLES可能更灵活。

文章中提到的最佳实践设置是一个比较通用的方案,适用于大多数核心业务系统。但是,在不同的业务场景下,确实需要根据实际情况进行调整。例如,如果你的业务对数据精度要求非常高,那么可能需要启用更严格的模式,例如STRICT_ALL_TABLES。反之,如果你的业务对性能要求更高,那么可以适当放宽一些限制。

就像穿衣服一样,没有一套衣服适合所有人。SQL Mode的设置也需要根据具体情况进行调整。文章中提到的最佳实践设置可以作为一个起点,然后根据实际情况进行微调,最终找到最适合自己业务的设置。

最佳实践只是一个参考,没有一成不变的方案。在实际应用中,需要根据具体的业务需求、数据特点和性能要求等因素进行综合考虑,选择最合适的SQL Mode设置。建议在测试环境中进行充分的测试和验证,确保设置的SQL Mode能够满足业务需求。

STRICT_ALL_TABLES适合数据质量要求非常高的场景,比如金融交易数据。出现错误立即停止可以避免后续的连锁反应。而STRICT_TRANS_TABLES更适合一些对实时性要求较高,且错误数据可以容忍的场景,比如日志记录。即使部分数据插入失败,也不会影响整体功能。

使用IGNORE关键字需要谨慎,因为它可能会导致数据静默错误。为了避免这种情况,最好结合其他机制一起使用,例如在插入或更新数据后进行校验,或者定期进行数据一致性检查。

这个问题其实没有绝对的答案,选择哪种模式最终取决于你的业务需求和容错机制。我个人更倾向于使用STRICT_ALL_TABLES,因为它能最大程度地保证数据质量。当然,如果你的系统能够有效地处理错误数据,那么STRICT_TRANS_TABLES也是一个不错的选择。

我觉得使用IGNORE关键字就像吃止痛药,虽然可以暂时缓解疼痛,但并不能根治问题。如果滥用IGNORE,最终可能会导致更严重的问题。建议只在特定场景下使用IGNORE,例如处理一些已知的、可接受的错误,并且要做好监控和日志记录。