ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL日历应用:公历农历两百年数据表设计与SQL查询实战

MySQL日历应用:公历农历两百年数据表设计与SQL查询实战 简介一份覆盖1900—2100年的MySQL日历数据表压缩包内含公历与农历两套完整数据面向日历应用、事件管理、中国传统节日提醒等开发场景帮助开发者解决公历农历互转和日期信息查询难题。整个压缩包共1个文件为SQL脚本大小仅1.94MB脚本中直接包含建表语句与预置数据。公历表设有年份、月份、日期、星期、节假日标记、描述等字段农历表则额外包含农历月日、农历星期、农历节假日标识以及对应公历日期结构清晰导入后即可用于查询。该资源已有1882人学习其核心价值在于省去手工整理大量日期数据的成本适合PHP、Java、Python等各类后端开发者直接使用。基于此表可实现公历农历日期联动查询、传统节日与节气识别、生日提醒等常用功能并可与用户日程等业务数据关联进一步提高日历类应用的数据完整性和开发效率。1. 公历农历两百年数据值得先聊的其实是 MySQL 里怎么组织它做 MySQL 日历应用真正卡住开发的往往不是函数而是农历。公历可以用DATE_ADD直接推算农历一到闰月、节气、传统节日映射就变成一套没有固定公式的查表逻辑。jp_lunar_solar.sql这个脚本把 1900-2100 年的公历和农历固化成两张数据表省去自己维护天文算历和边界判断的成本适合做节日提醒、日程排班、传统节日公告这类业务。我会按真实使用顺序来拆先看jp_lunar_solar.sql里的表和字段再导入并写公历转农历、农历转公历的 SQL最后聊索引、缓存和 2100 年边界。每一段都会给出可执行的 SQL 和参数说明不绕开常见的坑。2. 拆开 jp_lunar_solar.sql公历表、农历表到底怎么设计2.1 表结构拆解两张表还是三张表解压 ra r 后里面是一个jp_lunar_solar.sql。这类脚本常见的做法是创建两张表solar_calendar存公历lunar_calendar存农历再用solar_date字段把两张表连起来。按这个文件名和摘要描述下面这套字段基本能覆盖实际库里出现的核心列。表字段说明solar_calendarid主键solar_calendarsolar_date公历日期DATE 类型solar_calendaryear / month / day拆分后的公历年月日solar_calendarweekday星期几0-6 或周日到周六solar_calendarholiday_flag是否法定节假日或公众假期solar_calendardescription节日名或备注lunar_calendarid / solar_date主键 / 与公历表连接的外键lunar_calendarlunar_year / lunar_month / lunar_day农历年月日lunar_calendaris_leap是否闰月0 否 1 是lunar_calendarlunar_weekday农历对应的星期几lunar_calendarlunar_holiday农历假节日标记lunar_calendarfestival节日名如春节、端午lunar_calendar里的solar_date应当做成唯一键因为一个公历日只会对应一个农历日。反过来看同一个农历月份如果遇到闰月会出现两个“正月初一”所以不能把lunar_year lunar_month lunar_day当唯一键必须带上is_leap。这个设计决定了后面所有查询怎么走索引。2.2 导入 MySQL 前先做编码与 SQL 模式检查下载下来的.rar在 Windows 上解压后SQL 文件很可能是 GBK 编码直接导入会出现中文乱码或者卡在CREATE TABLE的注释里。我一般的处理顺序是先看文件类型再决定要不要转码。file jp_lunar_solar.sql head -n 30 jp_lunar_solar.sql如果file输出Non-ISO extended-ASCII说明是 GBK 编码。在 Linux 或 Git Bash 里转成 UTF-8iconv -f GBK -t UTF-8 jp_lunar_solar.sql jp_lunar_solar_utf8.sql转码后导入 MySQLmysql -uroot -p --default-character-setutf8mb4 jp_lunar_solar_utf8.sql--default-character-setutf8mb4要放在重定向之前否则客户端和服务器端字符集不一致农历注释和节日名可能写成一堆问号。导入前还可以先看一眼sql_modeSELECT sql_mode;如果里面包含NO_ZERO_DATE或STRICT_TRANS_TABLES而脚本里又有0000-00-00这种占位日期导入会报错。正规日历表不会有零日期但如果遇到报错不要急着改全局sql_mode先确认脚本里是不是真的写了非法日期。2.3 用 COUNT 和抽样查询验证导入结果导入成功不代表数据是对的。我习惯先验证年份范围、总行数和一个关键日期比如 2024 年春节。SELECT COUNT(*) AS total, MIN(solar_date) AS min_date, MAX(solar_date) AS max_date FROM solar_calendar;如果全部日期落满total应该在 73400 行左右因为 1900-01-01 到 2100-12-31 跨越 201 年中间有 73 个闰年总天数大约 73400 天。如果只有 355 行说明脚本可能只存了农历节气的换算表而不是每日一张表。接下来做一次连接查询验证公历和农历的对应关系SELECT s.solar_date, l.lunar_year, l.lunar_month, l.lunar_day, l.is_leap FROM solar_calendar s LEFT JOIN lunar_calendar l ON s.solar_date l.solar_date WHERE s.solar_date IN (2024-02-10, 2024-02-24, 2024-03-10);预期结果solar_datelunar_yearlunar_monthlunar_dayis_leap2024-02-1020241102024-02-24202411502024-03-1020242102024-02-10 是春节2024-02-24 是元宵2024-03-10 是二月初一。抽样结果对上了再继续做业务查询才有意义。2.4 为什么把农历数据表化而不是写算法硬算农历不是像公历那样能用一条公式连续算出来的。闰月规则、大小月分布、节气日期都依赖权威历书和天文观测人工计算很容易在 2050 年之后的某个月份出错。把 1900-2100 年数据直接建成表查询逻辑就退化成一次索引查找比在代码里维护几百行算法更可控。表驱动方案的另一个优点是修正成本低。如果某一年官方历书有调整只需要更新那一年的几十行数据不需要重新发布程序。代价是数据表会占一点空间但 73400 行在 MySQL 里连 10 MB 都不到完全不用担心性能。3. 公历转农历一条索引、一个 JOIN别让 7 万行数据拖垮接口3.1 从公历日期到农历日期的最短 SQL公历转农历最常见的场景是业务表里存了公历日期界面上要显示“农历正月十五”这样的文案。只要lunar_calendar表里有solar_date的唯一索引一条查询就能拿到结果。SELECT l.lunar_year, l.lunar_month, l.lunar_day, l.is_leap, l.festival FROM lunar_calendar l WHERE l.solar_date 2025-01-29;2025-01-29 是春节期望输出lunar_year2025, lunar_month1, lunar_day1, is_leap0, festival春节。注意这里的lunar_year是农历年不等于公历年份。比如公历 2025-01-25 还在农历甲辰年腊月二十六lunar_year就是 2024不能直接拿公历年份去过滤查询。这就是为什么很多新手会写出WHERE lunar_year YEAR(2025-01-29)结果把春节之前两周的数据全过滤掉。正确做法是始终以solar_date为过滤条件农历字段只做展示不做范围判断。3.2 索引怎么建才不白建如果lunar_calendar的主键是id那么solar_date上要单独建唯一索引ALTER TABLE lunar_calendar ADD UNIQUE INDEX uk_solar_date (solar_date);因为每个公历日期只能对应一行农历数据唯一索引既能保证数据质量也能加速等值查询。验证方式是用EXPLAIN看执行计划EXPLAIN SELECT lunar_month, lunar_day FROM lunar_calendar WHERE solar_date 2025-01-29;正常情况下type应该是const或refrows是 1。如果看到typeALL说明索引没建上或者查询条件里把solar_date包了函数导致索引失效。另外一个容易被忽略的点如果应用里经常按农历节日查询比如查“最近三个中秋节分别是几号”还需要给lunar_year, lunar_month, lunar_day建联合索引ALTER TABLE lunar_calendar ADD INDEX idx_lunar_ymd (lunar_year, lunar_month, lunar_day);这个索引在农历转公历的场景里价值更大因为按农历年月日过滤是典型的范围查询。3.3 联表查节日和节假日很多日历应用除了显示农历日期还要标记放假安排。这时把solar_calendar和lunar_calendar连起来按公历年份筛一遍SELECT s.solar_date, l.lunar_month, l.lunar_day, l.festival FROM solar_calendar s JOIN lunar_calendar l ON s.solar_date l.solar_date WHERE s.year 2025 AND l.festival IS NOT NULL ORDER BY s.solar_date;这个 SQL 里s.year走solar_calendar的索引JOIN走lunar_calendar的uk_solar_date扫描范围被限制在 365 行内。如果festival字段是空字符串而不是NULL就把条件改成l.festival 。不同脚本对节日的存储方式不一样。有的把节日塞进description字段有的单独放festival。如果字段命名不同写业务的时候要先查一下表结构避免搬来一段 SQL 发现列不存在。3.4 一个容易翻车的边界日期函数和时区公历日期在表里是DATE类型但业务系统里经常用DATETIME存用户操作时间。查询时如果用l.solar_date NOW()MySQL 会有隐式转换索引可能失效还会带来时区问题。写法问题WHERE l.solar_date NOW()NOW 带时间DATE 和 DATETIME 比较时类型转换WHERE l.solar_date DATE_FORMAT(NOW(), %Y-%m-%d)对 NOW 做格式化函数开销大WHERE l.solar_date CURRENT_DATE()推荐CURRENT_DATE 本身是 DATE我一般建议在代码层把时间转成日期字符串再查或者直接在 SQL 里用CURRENT_DATE()SELECT lunar_month, lunar_day, festival FROM lunar_calendar WHERE solar_date CURRENT_DATE();如果服务端time_zone和业务时区不一致CURRENT_DATE()取的是一天开始时刻所在的日期跨时区部署时要在连接串或配置里先统一时区否则会在凌晨 0 点到 8 点之间出现“显示的是昨天农历”的诡异问题。4. 农历转公历闰月字段与存储过程的正确打开方式4.1 农历转公历为什么也必须查表农历转公历比公历转农历容易踩坑因为农历月份不是固定的 30 天或 29 天交替。闰月更是会打断人的直觉。以 2023 年为例农历日期公历日期is_leap2023 年二月初一2023-02-2002023 年闰二月初一2023-03-221如果不带is_leap条件2023-02-20和2023-03-22都会被归到“二月初一”查询结果就有两条。这就是为什么lunar_calendar表里必须保留is_leap字段而且查询时要把它作为过滤条件的一部分。这个设计问题的本质是农历日期不是一个天然唯一的“日期坐标”必须把lunar_year lunar_month lunar_day is_leap四个值合起来才能定位到唯一的公历日期。4.2 把查询包成 SQL 函数在业务代码里写农历转公历每次都要拼一段WHERE很啰嗦。直接封装成一个 MySQL 函数应用层调用时就像调普通函数一样逻辑全部收敛在数据库端。DELIMITER $$ CREATE FUNCTION lunar_to_solar( p_lunar_year INT, p_lunar_month INT, p_lunar_day INT, p_is_leap TINYINT ) RETURNS DATE DETERMINISTIC READS SQL DATA BEGIN DECLARE v_solar_date DATE; SELECT solar_date INTO v_solar_date FROM lunar_calendar WHERE lunar_year p_lunar_year AND lunar_month p_lunar_month AND lunar_day p_lunar_day AND is_leap p_is_leap LIMIT 1; RETURN v_solar_date; END $$ DELIMITER ;调用方式SELECT lunar_to_solar(2025, 1, 1, 0) AS chun_jie_2025;期望结果是2025-01-29。函数内部用LIMIT 1是为了防止理论上的重复行触发SELECT INTO警告。DETERMINISTIC告诉 MySQL 这个函数在相同输入下一定返回相同结果允许它被用于索引计算和主从复制READS SQL DATA表示函数只读不写符合日历表的使用场景。4.3 处理查不到数据的坑如果把农历日期写成lunar_to_solar(2025, 1, 32, 0)表里没有这条记录SELECT INTO会触发 SQLSTATE02000的NOT FOUND错误函数直接抛异常。这在批量生成节假日的任务里很致命因为一条坏数据会让整个存储过程中断。改进版用CONTINUE HANDLER把查不到转成NULL返回DELIMITER $$ CREATE FUNCTION lunar_to_solar_safe( p_lunar_year INT, p_lunar_month INT, p_lunar_day INT, p_is_leap TINYINT ) RETURNS DATE DETERMINISTIC READS SQL DATA BEGIN DECLARE v_solar_date DATE DEFAULT NULL; DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_solar_date NULL; SELECT solar_date INTO v_solar_date FROM lunar_calendar WHERE lunar_year p_lunar_year AND lunar_month p_lunar_month AND lunar_day p_lunar_day AND is_leap p_is_leap LIMIT 1; RETURN v_solar_date; END $$ DELIMITER ;调用SELECT lunar_to_solar_safe(2025, 1, 32, 0);返回NULL不报错。这种做法适合做数据清洗比如导入一批农历生日先逐条转换能转的就写公历不能转的记入异常表之后再人工检查。4.4 存储过程按年生成所有农历节日节日提醒业务里经常要一次性取出一整年的农历节日而不是逐条调函数。这时用存储过程更直接。DELIMITER $$ CREATE PROCEDURE annual_lunar_festivals(IN p_year INT) BEGIN SELECT lunar_month, lunar_day, festival, solar_date FROM lunar_calendar WHERE lunar_year p_year AND festival IS NOT NULL ORDER BY solar_date; END $$ DELIMITER ;执行CALL annual_lunar_festivals(2024);p_year是农历年份不是公历年份。如果应用侧传入的是公历年份要先在存储过程里做一次范围换算或者干脆用公历年份查solar_calendar再去关联农历表。这个细节决定查出来的节日列表是偏前一年还是偏后一年。5. 从单表查询到线上应用索引、缓存和 2100 年边界5.1 业务表 JOIN 的顺序先收紧数据范围带用户提醒的日历应用一般有一张user_reminder表里面存公历提醒日期。要显示对应的农历别上来就全表 JOIN。SELECT r.remind_id, r.remind_date, l.lunar_month, l.lunar_day, l.festival FROM user_reminder r LEFT JOIN lunar_calendar l ON r.remind_date l.solar_date WHERE r.user_id 1024 AND r.remind_date 2025-01-01 AND r.remind_date 2025-02-01;先按user_id和日期范围过滤把结果集压到几十行再 JOIN 农历表这样lunar_calendar的索引压力几乎为零。LEFT JOIN保证即使某天数据缺失提醒记录依然能查出来只是农历字段是NULL。5.2 静态日历数据适合做缓存1900-2100 的数据在一年内几乎不会变不需要每次都查 MySQL。可以用 Redis 缓存最热的日期映射key 设计成lunar:2025-03-15value 用2025-2-16-0这样紧凑的字符串TTL 设 7 天。缓存的回源逻辑就是执行一次lunar_calendar的等值查询。如果不想引入 Redis也可以建一张冗余宽表CREATE TABLE calendar_daily AS SELECT s.solar_date, l.lunar_year, l.lunar_month, l.lunar_day, l.festival FROM solar_calendar s JOIN lunar_calendar l ON s.solar_date l.solar_date;查询时只查calendar_daily避开每次 JOIN。这张表只需要在数据更新后重建一次平时作为只读表使用。5.3 2100 年边界与自测基准数据表只到 2100-12-31如果应用层不校验用户输入 2101-05-01 会得到空结果或错误提示。MySQL 8.0.16 之后支持 CHECK 约束可以挡住非法范围ALTER TABLE lunar_calendar ADD CONSTRAINT chk_lunar_solar_range CHECK (solar_date BETWEEN 1900-01-01 AND 2100-12-31);旧版本 MySQL 不强制 CHECK 约束需要在应用层判断日期范围。自测时不要只测今天用几个公认的基准日期做回归公历日期农历对应节日2024-02-10正月初一春节2024-02-24正月十五元宵2024-06-10五月初五端午2025-01-29正月初一春节最后再做一次完整性检查确认公历表与农历表没有断档SELECT COUNT(*) AS mismatch_count FROM solar_calendar s LEFT JOIN lunar_calendar l ON s.solar_date l.solar_date WHERE l.solar_date IS NULL;mismatch_count为 0 时两张表才算是完整可用的日历数据。本文还有配套的精品资源点击获取
返回列表