身份证号出生日期提取需先按长度区分15位和18位:18位取第7–14位转str_to_date('%y%m%d'),15位取第7–12位补'19'后同法转换,统一返回date类型并自动校验合法性。

身份证号第7–14位就是出生日期,但必须校验格式再截取
直接用 SUBSTR() 或 LEFT()/MID() 提取第7–14位看似简单,但实际中常因身份证号长度不一(15位旧号 vs 18位新号)、含字母X、全角空格或补零导致结果错乱。15位旧身份证的出生日期在第7–12位(无年份前两位),且是两位年份(如"92"代表1992),不能统一按18位逻辑处理。
所以第一步永远是过滤掉非标准数据:
- 用
CHAR_LENGTH(id_card) IN (15, 18)排除非标长度 - 用
id_card REGEXP '^[0-9]{17}[0-9Xx]$' OR id_card REGEXP '^[0-9]{15}$'初筛格式(注意X大小写) - 避免用
TRIM()后直接截取——有些脏数据末尾带不可见字符,建议先REPLACE(id_card, '\r', '')和REPLACE(id_card, '\n', '')
18位身份证:用 SUBSTR + STR_TO_DATE 安全转日期类型
18位身份证第7–14位是标准 YYYYMMDD 格式,但直接 SUBSTR(id_card, 7, 8) 得到的是字符串,参与比较或计算时容易隐式转换出错。应立刻转为 DATE 类型。
正确写法:
STR_TO_DATE(SUBSTR(id_card, 7, 8), '%Y%m%d')
这样能天然捕获非法日期(如"20230230")并返回 NULL,比手动判断闰年/每月天数靠谱得多。如果字段允许 NULL,这个行为反而是保护机制。
- 别用
CONVERT(..., DATE)—— MySQL 5.7+ 对非法格式会报错而非静默转NULL - 别拼接
'20'或'19'前缀——18位号年份已完整,硬加会导致2000年前出生者错误(如1901年) - 若需兼容严格模式,可在外层套
IF(STR_TO_DATE(...) IS NULL, NULL, ...)显式控制
15位身份证:必须补全世纪年份再转换
15位号无世纪信息,第7–8位是两位年份(如"92"),默认属于1900年代,但极少数情况(如1900年前出生者极少,可忽略;而2000年后出生者不可能用15位号)——所以统一补 '19' 是安全的。关键在于补全后仍要走 STR_TO_DATE 验证。
推荐写法:
STR_TO_DATE(CONCAT('19', SUBSTR(id_card, 7, 6)), '%Y%m%d')
注意是 SUBSTR(id_card, 7, 6)(取6位:YYMMDD),不是8位。若遇到个别15位号里月份为00或日期为00,STR_TO_DATE 仍会返回 NULL,比强行转成"19920000"更有意义。
- 不要用
LPAD(SUBSTR(...), 4, '19')拼年份——万一原字段是空或异常,会拼出"19"开头的无效串 - 业务上若明确只收18位号,可在入库时就用触发器或应用层拦截15位号,比查询时兼容更彻底
合并15/18位逻辑的单条SQL:用 CASE + 长度分支最稳
一个字段里混存两种身份证号时,不能靠正则判断“是否含X”来区分——15位号不可能含X,但18位号含X只是校验位,主体仍是数字。唯一可靠依据是长度。
最终可用的提取表达式:
CASE
WHEN CHAR_LENGTH(id_card) = 18 THEN STR_TO_DATE(SUBSTR(id_card, 7, 8), '%Y%m%d')
WHEN CHAR_LENGTH(id_card) = 15 THEN STR_TO_DATE(CONCAT('19', SUBSTR(id_card, 7, 6)), '%Y%m%d')
ELSE NULL
END
这个表达式在 WHERE 或 SELECT 中都可直接使用。如果性能敏感(比如大表上频繁计算),建议把结果存入生成列(MySQL 5.7+)或额外维护一个 birthday DATE 字段,避免每次查询都重复解析。
真正容易被忽略的是:即使身份证号格式合法,出生日期也可能超出合理范围(如1800年或2100年)。业务上通常需要加一层 AND birthday BETWEEN '1900-01-01' AND CURDATE() 过滤,否则统计报表里会出现明显异常值。











