时区表未加载时,CONVERT_TZ 为什么返回 NULL
CONVERT_TZ 把固定偏移转成另一个固定偏移没有问题,换成 Europe/London 却返回 NULL。这组对照很有用:日期本身可以转换,问题更可能出在命名时区的识别和规则数据,不必先去修改所有业务时间列。
MySQL 安装时会建立系统时区表,但并不保证已经装入规则。操作系统知道伦敦在哪个时区,也不代表 mysqld 已经有同一份命名时区数据。容器镜像又可能同时缺系统 tzdata 和数据库时区数据,所以“宿主机时间正常”不是充分证据。
从同一条连接做两组探针
以下按 MySQL 8.4、Linux 上的维护流程编写。先只读检查,不急着导入。所有查询都使用固定日期,避免 NOW() 引入当前会话时区与测试日期变化。
SELECT VERSION(), @@hostname, @@port,
@@GLOBAL.time_zone, @@SESSION.time_zone, @@system_time_zone;
SELECT
CONVERT_TZ('2026-01-15 12:00:00', '+00:00', '+08:00') AS numeric_offset,
CONVERT_TZ('2026-01-15 12:00:00', '+00:00', 'Europe/London') AS named_winter,
CONVERT_TZ('2026-07-15 12:00:00', '+00:00', 'Europe/London') AS named_summer;
固定偏移那列应是 20:00。伦敦冬季应是 12:00,夏季应是 13:00。如果第一列正常、后两列为 NULL,检查时区名拼写和系统表加载情况;如果第一列也不正常,先检查完整参数、输入日期与实例版本,不能只沿着“缺时区表”这一条路径走。
需要维护权限的只读核对如下:
SELECT COUNT(*) AS named_zone_count
FROM mysql.time_zone_name;
SELECT Name, Time_zone_id
FROM mysql.time_zone_name
WHERE Name IN ('Europe/London', 'Asia/Shanghai', 'UTC');
总数为零表明没有装入命名列表。总数大于零也不能证明所需名字存在,更不能证明规则版本足够新。若业务账号无权读取 mysql 系统库,可以由维护账号完成这一步;不要给所有在线账号开放系统表读取,只为了每次请求检查一次环境。
SET time_zone = 'Europe/London' 在名字未知时会报错,而 CONVERT_TZ 遇到无效时区参数可能返回 NULL。两个入口表现不同,但都能指向同一份环境缺口。应用应把这类空结果当成配置错误处理,不能再用原始日期顶替,伪装成转换成功。
把源数据、生成文件和落库分成三个位置
先确认维护机器上有 MySQL 8.4 工具和系统 tzdata。mysql_tzinfo_to_sql 读取运行它的那台机器的 zoneinfo 文件,再生成 SQL;它不是远程服务器规则的自动下载器。如果在旧运维机上生成,再导入新数据库,最终得到的仍可能是一份旧规则。
command -v mysql
command -v mysql_tzinfo_to_sql
command -v mysqldump
mysql --version
test -d /usr/share/zoneinfo
test -f /usr/share/zoneinfo/Europe/London
MySQL 8.4 的 mysql_tzinfo_to_sql 不提供通用的 --version 参数;该参数会被当作目录名,不能借它检查版本,更不能将这种探测输出接到数据库。生成工具版本应通过它所属的安装包记录核对,客户端 mysql 的版本也不能单独证明生成工具来自同一安装。
Linux 上常见目录是 /usr/share/zoneinfo,实际镜像仍要核对。Alpine、Debian 等发行版的 tzdata 安装与版本查询命令不同,应使用当前系统的包管理方式。只有一个 /etc/localtime 文件,不能据此假设整个命名时区数据库都存在。
维护连接可以预先配置为独立 login-path,密码由工具交互读取。下例中的账号和地址是本地演示位置,执行前换成受控维护实例的配置:
mysql_config_editor set --login-path=tz-maint \
--host=127.0.0.1 --port=3306 --user=tz_admin --password
mysql --login-path=tz-maint --batch --skip-column-names \
--execute='SELECT VERSION(), @@hostname, @@port'
第二条连接核验应在实际导入前再次完成。客户端本机时区正确、SQL 文件生成成功、目标实例正确,是三份独立事实,不要只留下一句“导入脚本运行过”。维护账号需要相应系统表写入权限,日常业务连接不承担这项操作。
留住旧规则与本次生成记录
在受控维护窗口内,先保存当前五张时区表的数据,再生成待导入文件。下面使用 Bash,执行前确认当前目录适合保留维护记录。
set -euo pipefail
umask 077
tz_work=$(mktemp -d ./tz-load.XXXXXX)
mysqldump --login-path=tz-maint \
--single-transaction --set-gtid-purged=OFF --no-tablespaces \
--no-create-info --skip-triggers --complete-insert \
mysql time_zone time_zone_name time_zone_transition \
time_zone_transition_type time_zone_leap_second \
> "$tz_work/timezones-before.sql"
mysql_tzinfo_to_sql /usr/share/zoneinfo \
> "$tz_work/timezones-new.sql" \
2> "$tz_work/generation.log"
test -s "$tz_work/timezones-new.sql"
wc -c "$tz_work/timezones-new.sql"
sed -n '1,40p' "$tz_work/generation.log"
printf '本次文件目录:%s\n' "$tz_work"
这里没有把旧表结构从别的版本覆盖回来。MySQL 8.4 的系统时区表由安装与升级流程管理,不要下载一份旧 MyISAM 表定义替换它们。数据备份的恢复也应先在相同版本的隔离实例演练;旧数据插回一份已经部分导入的新数据之前,需要清理和一致性方案,不能把这份备份文件简单追加执行当成完整回滚。
生成日志里可能有某些非时区文件被跳过的提示,要核对具体文件,不能见到 warning 就一概忽略,也不能见到一个 warning 就断言所需全部时区无效。文件非空只能证明工具输出过内容;后面的目标名字、冬夏规则探针才验证业务需要的能力。
维护记录最好同时保存 tzdata 包版本、工具版本和生成文件校验值。未来某个国家调整规则后,单看导入日期并不能知道装入的是哪一份数据。文件来自哪里、由哪个工具转换,应该能够追溯。
执行导入以后,还没有到验收终点
确认生成内容和目标连接后,再把同一个文件提交给 MySQL 客户端:
mysql --login-path=tz-maint --database=mysql \
< "$tz_work/timezones-new.sql" \
> "$tz_work/load.log" 2> "$tz_work/load-errors.log"
保持 set -e,SQL 错误会让流程停下来。不要给客户端加 --force 后继续向下打印成功。导入失败时,先查看退出码和错误记录,核对哪些语句已经生效;不能假定整个生成文件天然是一笔全有或全无的业务事务。
mysqld 会缓存查到的时区规则。更新表内容后,按实例所属服务的正常维护流程重启服务器,确保不继续使用旧缓存。这里只描述维护动作,不给一个对所有部署都通用的 systemctl 命令:容器、托管数据库和多副本实例的重启与切换边界不同。仅断开应用连接再重连,不能替代服务器规则缓存刷新。
重启完成,用新连接重新执行开头的三列探针,再确认名字存在。新旧进程、维护连接和应用入口都应指向实际服务实例。如果前面测试的是直连节点、应用走的是另一个读副本,应用仍可能继续返回 NULL。
行数不是规则正确性的验收
名字存在只能证明目录可识别。以伦敦为例,冬夏转换应分别产生 12:00 和 13:00,而不是全年都固定成同一个偏移。对实际服务地区,选择它近期发生过规则变化的日期做回归,才能发现“表里有名字,但规则已经过时”。
再用同一条应用连接检查会话时区。如果业务明确传入源和目标时区,CONVERT_TZ 不应依赖 @@SESSION.time_zone 猜输入;若查询里混用 TIMESTAMP、NOW() 或未标明时区的参数,仍要审查驱动和连接设置。加载规则只解决命名时区能力,不会自动修正所有列的数据含义。
托管数据库可能不允许直接维护系统表。这时应走该服务支持的时区数据更新方式,先在测试实例验证相同探针。不要因为没有系统表权限,就把每个命名时区改成一份固定数值偏移,夏令时和历史变更会继续被遗漏。
系统 tzdata 后续更新,也不会神奇地重写 MySQL 已装入的规则表;数据库更新、服务器缓存刷新与应用进程使用的规则都要纳入同一次发布验收。应保存真实转换结果,而不只记录一次命令退出码。
完成后,应用对“不认识时区”应有明确错误路径。一个正常日期返回 NULL,应暴露环境配置问题;用 IFNULL(CONVERT_TZ(...), original_time) 掩盖它,用户看到的只是一个看似合法、其实少转了时区的时间。