如何将出生日期字段的错误数据类型从 VARCHAR 更改为 DATE

Lio*_*ith 5 mysql datatypes varchar date mysql-5.7

我在创建带有生日字段的用户表时犯了一个错误,我没有放置 DATE 数据类型,而是放置了一个 VARCHAR!

所以现在我的用户表看起来像这样:

CREATE TABLE IF NOT EXISTS users 
(
  id INT    UNSIGNED NOT NULL,
  birthdate VARCHAR (200) NOT NULL,

  PRIMARY KEY (id)
) DEFAULT CHARSET=utf8;
Run Code Online (Sandbox Code Playgroud)

它填充如下(样本):

INSERT INTO users (id, birthdate) VALUES
  (1,'1991-01-23'),
  (2,'yyyy-01-23'),
  (3,'1991-mm-23'),
  (4,'1991-01-dd'),
  (5,''),
  (6,'1991-01-d3'),
  (7,'1983-05-23'),
  (8,'1991-0m-23'),
  (9,'19yy-01-23'),
  (10,'y991-01-23');
Run Code Online (Sandbox Code Playgroud)

现在我想将每个不正确的生日更新为 NULL,或者设置一个默认值,如 2020-01-01。在这里查看我的 sqlfiddle 。

Vér*_*ace 4

为此,我使用了 dbfiddle.uk(请参阅此处),而不是 sqlfiddle.com - 服务器更多,并且可以更好地保持最新状态。

所以,我所做的如下(设置按照您的sqlfiddle):

CREATE TABLE IF NOT EXISTS users (
  id int(6) unsigned NOT NULL,
  birthdate varchar(200) NOT NULL,
  PRIMARY KEY (id)
)DEFAULT CHARSET=utf8;
Run Code Online (Sandbox Code Playgroud)

填充:

INSERT INTO users (id, birthdate) VALUES
  (1,'1991-01-23'),
  (2,'yyyy-01-23'),
  (3,'1991-mm-23'),
  (4,'1991-01-dd'),
  (5,''),
  (6,'1991-01-d3'),
  (7,'1983-05-23'),
  (8,'1991-0m-23'),
  (9,'19yy-01-23'),
  (10,'y991-01-23');
Run Code Online (Sandbox Code Playgroud)

添加一列来保存有效值:

ALTER TABLE users ADD new_bdate DATE;  -- add column to hold valid values
Run Code Online (Sandbox Code Playgroud)

请注意,新字段可为空。必须如此,除非您想输入一些默认值,例如 01/01/1900 或“0000-00-00”或“2020-01-01” - 我(强烈)建议不要这样做!当优化器计算出计划时,它会让优化器感到困惑,而NULL当您的数据未知时,它是完全有效的!

您在评论中提到尝试使用“0000-00-00”作为默认值会失败。这是因为sql_mode包含STRICT_TRANS_TABLES- 这在 MySQL 5.7 中默认启用(请参阅此处的文档) -此处此处进一步讨论(以及随附的链接和注释)。来自 MySQL 文档:

严格模式会影响服务器是否允许“0000-00-00”作为有效日期:如果未启用严格模式,则允许“0000-00-00”并且插入不会产生警告。如果启用严格模式,则不允许使用“0000-00-00”并且插入会产生错误,除非同时给出 IGNORE。对于 INSERT IGNORE 和 UPDATE IGNORE,允许使用“0000-00-00”并且插入会产生警告。

因此,现在我们更改birthdate字段以接受NULLs - 这在稍后“清理”完成时很重要:

ALTER TABLE users MODIFY birthdate VARCHAR (200) NULL;  
-- make birthdate nullable
-- this is important for the STR_TO_DATE function.
Run Code Online (Sandbox Code Playgroud)

我还使原始birthdate字段可为空。如果不这样做,那么第一个UPDATE就会失败,如下所示TRANSACTION

START TRANSACTION;
UPDATE users SET  birthdate = NULL
WHERE birthdate REGEXP '[a-zA-Z/]' OR birthdate = '';
UPDATE users SET new_bdate = birthdate WHERE birthdate IS NOT NULL;
COMMIT;
Run Code Online (Sandbox Code Playgroud)

在一个事务中执行两个 DML 步骤/查询以避免步骤之间的任何更新非常重要 - 尽管我认为您可以在安静的时间执行此操作 - 或者您可以在更改期间锁定表。

  • 正则表达式解释:
    [az]表示匹配[a,b,c...x,y,z]范围内的所有字符,A-Z大写的意思相同。该/字符将与斜杠匹配 - 可在日期中使用,但对 MySQL 日期无效。

现在,我们清理;

ALTER TABLE users DROP COLUMN new_bdate;
Run Code Online (Sandbox Code Playgroud)

最后,我们检查我们的结果:

SELECT * FROM users;
Run Code Online (Sandbox Code Playgroud)

结果:

id  birthdate
1   1991-01-23
2   NULL
3   NULL
4   NULL
5   NULL
6   NULL
7   1983-05-23
8   NULL
9   NULL
10  NULL
10 rows
Run Code Online (Sandbox Code Playgroud)

因此,现在我们有一列具有正确的数据类型和正确的值(这些是已知的)。

您的问题表明了绝不允许在应用程序中输入自由文本的普遍重要性。如果可能的,用户应该有义务从下拉菜单中进行选择,并从一开始就确保 NOT NULL 约束!

此外,它还显示了从第一天起选择正确数据类型的重要性!您的数据库是保护数据的最后堡垒,因此请确保输入的任何内容从一开始就有效- 您将避免出现此类问题!

编辑:

遵循OP的评论,特别是关于在字段中输入的日期VARCHAR(即YYYY/mm/dd分隔符/而不是`-`` - @Akina的(优雅的)REGEXP答案可以修改如下(添加合适的日期后 - 请参阅此处的小提琴) 。

INSERT INTO `users` (`id`, `birthdate`) VALUES 
  (11, '1993/03/20'),
  (12, '2000/09/25');
  (13, '2015.06.30'),
  (14, '2015_04_15');
Run Code Online (Sandbox Code Playgroud)

请注意使用斜杠 ( /)、点 ( .) 或下划线 ( _) 字符作为不同日期子字段的分隔符。

SQL:

UPDATE users
SET DOB = 
        STR_TO_DATE
        (
          CONCAT
          (
            SUBSTRING(birthdate, 1, 4),
            '-',
            SUBSTRING(birthdate, 6, 2),
            '-',
            SUBSTRING(birthdate, 9, 2)
          ), '%Y-%m-%d'
        )
WHERE birthdate REGEXP '[0-9]{4}.[0-9]{2}.[0-9]{2}';
Run Code Online (Sandbox Code Playgroud)

结果(为简洁起见进行了剪裁):

id   birthdate  DOB
...
...
10  y991-01-23  NULL    
11  1993/03/20  1993-03-20
12  2000/09/25  2000-09-25
13  2015.06.30  2015-06-30
14  2015_04_15  2015-04-15
14 rows
Run Code Online (Sandbox Code Playgroud)

这对@Akina的答案略有修改 - 它在正则表达式中使用点('.') - 点是正则表达式元字符(或“特殊”字符),它是一个通配符,可以代表任何单个(即一个且唯一一个)字符。

因此,任何年份后跟任何单个字符,后跟任何月份,后跟任何单个字符,任何一天都将匹配 - 这将覆盖有效的 ISO 日期(使用连字符 ( -) 或其他可能的分隔符,即下划线或文字点字符。日期)正则表达式有些简化 - 真实的日期可能要复杂得多

从字符串中提取日期我经历了一段非常棘手的时间。我使用了 MySQL(非标准 -令人惊讶!)字符串连接运算符(加号 ( +))符号,它开始添加(即以数字方式)年份数字和月份数字。同样的事情也发生在 MySQL 版本的(标准 SQL)双管道 ( ||) 运算符上。只有当我发现这一点时,我才设法找到最终的工作CONCAT解决方案!

正如我的最后一个链接所说,"ya gotta love MySQL“——这不是我第一个产生的情感……这也是我在这个论坛上宣传 PostgreSQL 的另一个原因!

要将您的出生日期字段设置为默认值 2020-01-01(不建议...见上文),请使用 @Akina评论中的代码:

SET DOB = 
  CASE 
    WHEN birthdate REGEXP {pattern 1} 
      THEN {expression 1} 
    WHEN {pattern 2} THEN {expression 2} ... ELSE NULL END
Run Code Online (Sandbox Code Playgroud)

像这样:

UPDATE users
SET DOB = 
  CASE 
    WHEN birthdate REGEXP '[0-9]{4}.[0-9]{2}.[0-9]{2}'
      THEN 
        STR_TO_DATE
        (
          CONCAT
          (
            SUBSTRING(birthdate, 1, 4),
            '-',
            SUBSTRING(birthdate, 6, 2),
            '-',
            SUBSTRING(birthdate, 9, 2)
          ), '%Y-%m-%d'
        )   
      ELSE '2020-01-01'
    END;
Run Code Online (Sandbox Code Playgroud)

请参阅此处的小提琴。+1 对于一个有趣的第一个问题,它看起来(看似)简单,但却让我思考 - 欢迎来到论坛!