预计阅读时间:4 分钟
django 设置时区支持导致 mysql 查询报错
背景:为什么要开 USE_TZ
在 django settings 中设置 USE_TZ = True 是官方推荐的做法,也是 django 5.0 之后的默认值(2024 年补注:django 5.0 发布于 2023 年 12 月,写这篇时还没有)。官方文档的说明:
当启用对时区的支持时,Django 在数据库中以 UTC 为单位存储日期时间信息,在内部使用具有时区的日期时间对象,并在模板和表单中将其转换为最终用户的时区。
如果用户居住在多个时区,这会很方便:你要根据用户的时间来显示日期信息。
即使你的网站只在一个时区提供服务,在数据库中用 UTC 存储数据仍然是一个好的做法,主要原因是夏令时(DST)。许多国家都有 DST 系统:春天时钟向前拨,秋天向后拨。如果你直接用当地时间工作,转换发生时就可能遇到一年两次的错误。这对博客也许无所谓,但如果是每年两次向客户多收或少收一小时的费用,就是真问题了。解决办法是在代码中使用 UTC,只在与终端用户交互时使用当地时间。
可以看到,开启时区支持后,时区转换的问题完全交给 django 处理,这显然是更好的。很多文章说的「直接把 USE_TZ 设成 False」属于把结界拆了图省事,不推荐。
问题与修复
但设置之后,django 对 mysql 做按日期截断的查询会报错,比如 QuerySet.datetimes()、TruncDay / TruncMonth 等 Trunc* 函数(博客的按月归档就是这类查询),普通的增删改查不受影响:
ValueError: Database returned an invalid datetime value. Are time zone definitions for your database installed?
这是 mysql 没有安装时区表导致的:开了 USE_TZ 后,django 在 mysql 上做日期截断会用 CONVERT_TZ() 把时间转到当前时区,没有时区表时 CONVERT_TZ() 返回 NULL,django 的 Trunc* 拿到 NULL 就抛出上面的 ValueError(django 4.1 源码 django/db/models/functions/datetime.py 的 TruncBase.convert_value、django/db/backends/mysql/operations.py 的 CONVERT_TZ,2026-10-31 读取;datetimes() 内部就是用 Trunc 实现的)。需要手动同步时区信息到 mysql:
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -D mysql -u root -p
mysql -u root -p -e "flush tables;" mysql
第一条把系统时区表加载进 mysql,第二条刷新 mysql 的表缓存,两条命令各需要输入一次 root 密码。命令要在 mysql 服务器所在的 Linux 主机(或容器)里执行。我当时执行完这两条,django 查询就正常了。
不过 MySQL 官方文档的做法是导入后重启 mysqld,flush tables 不在官方步骤里:mysqld 会缓存查过的时区信息,文档原话是「After running mysql_tzinfo_to_sql, restart the server so that it does not continue to use any previously cached time zone data」(2026-10-31 读取)。所以导入或更新时区表后,按官方建议重启 mysqld。
本文由 aboom 原创,转载请注明出处。