我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

基于MySQL的万年历数据存储与查询方案详解

基于MySQL的万年历数据存储与查询方案详解 简介一份基于MySQL的万年历数据资源覆盖1970至2100年的农历、二十四节气、财神方位、宜忌、星座、天干地支与五行等传统历法信息面向需要快速接入日历数据的开发者、技术爱好者及信息系统项目。资源包共2个文件约1.86MB其中CSV数据文件按日期逐行记录公历、农历、节气、财神方位、宜忌、星座、干支五行等字段SQL脚本文件提供建表与数据导入命令两者配合可快速搭建万年历查询后端。目前已有1608人学习下载具备一定参考热度。使用者可直接获取近130年的完整历法数据免去手工收集与整理成本同时CSV与SQL双格式便于二次开发适用于黄历网站、日程管理、命理分析、知识科普等场景可嵌入多种业务系统支持按日期、节气、吉凶等多维度检索查询。数据字段完整适合直接用于应用开发与算法验证。 最近在整理一套万年历相关的数据服务顺手把整个方案从表结构设计到初始化脚本、再到业务查询都梳理了一遍。这套东西做完之后后面接婚庆择日、搬家选期、甚至是普通的日历组件都非常顺手。所以决定把整个过程拆出来分享给需要用到农历、黄历、节气和干支数据的开发者。需要说明的是这套方案核心是MySQL数据库存储和脚本化处理不依赖任何商业API数据完全本地化。你只要把数据灌进去后续所有查询都是毫秒级返回适合对数据隐私和响应速度有要求的业务场景。1. 项目拆解万年历到底需要哪些数据为什么选MySQL来存先解决一个根本问题万年历不只是“公历转农历”那么简单。一个能真正落到业务里的万年历服务至少需要覆盖四类数据公历日期、农历日期、干支纪年和黄历宜忌。这里面最麻烦的不是公历而是农历的闰月规则、节气计算和每日宜忌的映射关系。1.1 核心数据域拆解我把整个数据域拆成四张基础表来规划日期主表存公历日期、农历年/月/日、是否闰月、生肖、星期、当日节气名称。干支表存年干支、月干支、日干支按日期唯一关联。黄历宜忌表存每日的宜、忌、冲煞、值神、彭祖百忌等文本信息。节气表存二十四节气的精确时间点精确到分钟。为什么拆成多张表而不是一张大宽表因为不同业务模块对数据的使用频率不同。比如日历组件只需要日期主表择日业务才需要黄历宜忌表。拆开之后你可以只索引和缓存高频字段减少无效IO。这是我在实际项目中踩过坑之后总结出来的一开始图省事放一张表里数据量到几万行之后带大文本字段的查询明显变慢。1.2 为什么MySQL是合适的存储方案有人可能会问这种数据量直接用SQLite或者JSON文件不就行了对于个人用的小工具确实可以但一旦牵扯到多端同步、并发写入、按条件筛选比如筛选某个月内适合嫁娶的日期MySQL的优势就很明显。第一MySQL对范围查询和条件过滤的支持非常成熟一个B树索引就能把日期范围查询优化到毫秒级。第二通过脚本批量灌数据时MySQL的事务机制能保证数据一致性不会出现灌了一半崩溃导致数据残缺的情况。第三如果你后面要接后端服务MySQL是生态最成熟的无论是Java、Python还是Node.js驱动都很完善。所以这个项目选型就是数据生成脚本负责把历法数据算出来MySQL负责存储和查询。2. 建库建表一套能应对真实查询的表结构设计表结构设计是整套方案的骨架设计得好不好直接决定后面开发效率。我这里的核心原则是保证日期主表足够精简把大字段和低频字段放到扩展表里。2.1 日期主表结构CREATE DATABASE IF NOT EXISTS calendar_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE calendar_db; CREATE TABLE calendar_date ( id int NOT NULL AUTO_INCREMENT, solar_date date NOT NULL COMMENT 公历日期, lunar_year smallint NOT NULL COMMENT 农历年, lunar_month tinyint NOT NULL COMMENT 农历月, lunar_day tinyint NOT NULL COMMENT 农历日, is_leap_month tinyint NOT NULL DEFAULT 0 COMMENT 是否闰月1是0否, lunar_text varchar(20) NOT NULL COMMENT 农历日文字如初一、十五, zodiac varchar(10) NOT NULL COMMENT 生肖, weekday varchar(10) NOT NULL COMMENT 星期, solar_term varchar(10) DEFAULT NULL COMMENT 当日节气无节气则为NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_solar_date (solar_date), KEY idx_lunar_month_day (lunar_month, lunar_day) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT公历农历日期主表;这里有一个关键设计uk_solar_date唯一索引。因为公历日期是绝对唯一的用它做唯一键可以防止脚本重复灌数据时产生脏数据。如果你要用INSERT INTO ... ON DUPLICATE KEY UPDATE做增量更新这个唯一索引是前提。2.2 黄历宜忌表结构CREATE TABLE almanac_daily ( date_id int NOT NULL COMMENT 关联calendar_date.id, solar_date date NOT NULL COMMENT 冗余公历日期方便直接查询, yi varchar(500) DEFAULT NULL COMMENT 宜多用顿号分隔, ji varchar(500) DEFAULT NULL COMMENT 忌, chong varchar(50) DEFAULT NULL COMMENT 冲煞, zhi_shen varchar(20) DEFAULT NULL COMMENT 值神, peng_zu varchar(100) DEFAULT NULL COMMENT 彭祖百忌, tai_shen varchar(100) DEFAULT NULL COMMENT 胎神占方, ji_shen varchar(200) DEFAULT NULL COMMENT 吉神宜趋, xiong_shen varchar(200) DEFAULT NULL COMMENT 凶煞宜忌, PRIMARY KEY (date_id), KEY idx_solar_date (solar_date), CONSTRAINT fk_almanac_date FOREIGN KEY (date_id) REFERENCES calendar_date (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT每日黄历宜忌;yi和ji字段我直接用了varchar(500)。实际使用中一天的宜忌内容一般不会超过10项每项4个字加上分隔符200个字符完全够用。用varchar而不是text是为了避免text字段带来的额外存储开销和索引限制。外键这里我建议保留因为数据是脚本一次性灌入的不是高频写入场景外键能保证表之间的数据完整性查询性能在这个数据量级也不会有明显影响。3. 数据从哪来初始化脚本与数据清洗实录表结构搭好了现在最核心的问题就是数据准确吗黄历数据最怕的是农历推算出错特别是闰月年份一旦错一天后面全错。3.1 数据源选型与校验策略目前公开的农历数据源大体上有三类一是各个开源项目的农历推算代码基于天文算法二是现成的万年历CSV数据文件三是商业API接口。我的建议是以推算代码生成主干数据再用权威数据源抽样校验。具体方法是在脚本里生成数据后随机抽取30个日期点和已有可靠数据源做交叉比对确保年干支、农历月日、节气三天内完全一致。不要直接下单一数据源。我踩过的坑是某份网上下载的CSV在2023年闰二月的数据直接错了农历日期整体偏了一天。这种错误靠肉眼很难发现但交叉校验很快就能暴露。3.2 Python脚本生成与入库我这里的生成脚本用的是Python因为Python的datetime库和第三方农历库生态最成熟。核心代码如下import pymysql from datetime import date, timedelta from lunardate import LunarDate from sxtwl import Lunar # 连接MySQL conn pymysql.connect( host127.0.0.1, userroot, passwordyour_password, databasecalendar_db, charsetutf8mb4 ) cursor conn.cursor() def get_lunar_info(solar_date): 将公历日期转换为农历信息返回日期主表所需字段 lunar LunarDate.fromSolarDate(solar_date.year, solar_date.month, solar_date.day) # 农历转干支纪年等更详细的信息可以用sxtwl库获取 lunar_detailed Lunar.fromSolar(solar_date.year, solar_date.month, solar_date.day) return lunar, lunar_detailed # 生成从2020年到2030年的数据 start_date date(2020, 1, 1) end_date date(2030, 12, 31) current start_date while current end_date: lunar, lunar_detailed get_lunar_info(current) yi_text get_yi_from_source(current) # 从数据源读取黄历宜 ji_text get_ji_from_source(current) sql INSERT INTO calendar_date (solar_date, lunar_year, lunar_month, lunar_day, is_leap_month, lunar_text, zodiac, weekday, solar_term) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE solar_termVALUES(solar_term) cursor.execute(sql, ( current, lunar.year, lunar.month, lunar.day, 1 if lunar.isLeapMonth else 0, lunar_day_text(lunar.day), zodiac_text(lunar.year), weekday_cn(current.weekday()), get_solar_term(current) )) current timedelta(days1) conn.commit() cursor.close() conn.close()这段代码里有几个关键点值得展开说。ON DUPLICATE KEY UPDATE是幂等写入的核心。脚本可能因为各种原因中断重跑如果每次重跑都插入而不是更新很快表里就满了重复数据。有了这个子句重跑多少次结果都是一样的。lunar_day_text和get_solar_term需要单独实现。农历日文字初一、初二、廿三和节气名称属于强规则数据直接用映射表或计算函数生成即可。特别注意节气不是每天都有当天没有节气时get_solar_term返回None。3.3 Shell脚本一键执行的实战配置Python脚本负责逻辑Shell脚本负责让它跑得省心。我习惯写一个init_calendar.sh把建库、安装依赖、跑数据脚本、做数据校验串成一条命令#!/bin/bash # 万年历数据初始化脚本 MYSQL_HOST127.0.0.1 MYSQL_USERroot MYSQL_PASSWORDyour_password DB_NAMEcalendar_db echo [1/4] 检查Python依赖... pip install pymysql lunardate sxtwl -q echo [2/4] 创建数据库表结构... mysql -h$MYSQL_HOST -u$MYSQL_USER -p$MYSQL_PASSWORD schema.sql echo [3/4] 导入历法数据... python3 generate_calendar_data.py echo [4/4] 数据校验... mysql -h$MYSQL_HOST -u$MYSQL_USER -p$MYSQL_PASSWORD -e SELECT COUNT(*) AS total_days FROM calendar_db.calendar_date; SELECT solar_date, lunar_year, lunar_month, lunar_day, is_leap_month FROM calendar_db.calendar_date WHERE solar_date BETWEEN 2023-01-01 AND 2023-03-31 ORDER BY solar_date;这个脚本执行完你会看到类似下面的输出[1/4] 检查Python依赖... [2/4] 创建数据库表结构... [3/4] 导入历法数据... [4/4] 数据校验... ------------ | total_days | ------------ | 4017 | ------------ ----------------------------------------------------------------- | solar_date | lunar_year | lunar_month | lunar_day | is_leap_month | ----------------------------------------------------------------- | 2023-01-01 | 2022 | 12 | 10 | 0 | ...注意2023年1月1日对应的是农历2022年腊月初十这是对的。如果你发现年份对不上多半是脚本里取农历年时取了公历年份这是最常见的坑。4. 实战查询黄历查询、农历互转的SQL写法与索引优化数据落库之后最关键的就是查询效率。这里分享几个我实际项目中用到的SQL以及对应的索引优化思路。4.1 查询某一天完整的黄历信息这是最基础的查询业务上常见于日历组件点击某一天时展示详情。SELECT c.solar_date, c.lunar_year, c.lunar_month, c.lunar_day, c.is_leap_month, c.lunar_text, c.zodiac, c.solar_term, a.yi, a.ji, a.chong, a.zhi_shen, a.peng_zu, a.tai_shen FROM calendar_date c LEFT JOIN almanac_daily a ON c.id a.date_id WHERE c.solar_date 2024-02-10;这个SQL的关键在于命中uk_solar_date唯一索引理论上走的是点查数据量再大也就几毫秒。LEFT JOIN保证即使某天没有黄历数据也至少会返回日期主表信息前端不会因为某天缺少数据而白屏。4.2 筛选区间内适合某类活动的日期比如用户要查未来三个月内适合“嫁娶”的日子这个查询是高频且典型的SELECT c.solar_date, c.lunar_text, c.weekday, a.yi FROM calendar_date c INNER JOIN almanac_daily a ON c.id a.date_id WHERE c.solar_date BETWEEN 2024-03-01 AND 2024-05-31 AND a.yi LIKE %嫁娶% ORDER BY c.solar_date;这个SQL在数据量达到数万行之后性能的关键在于有没有覆盖索引。我建议在calendar_date表上建一个(solar_date, id)的复合索引这样BETWEEN过滤可以直接走索引避免回表。almanac_daily表上的idx_solar_date索引也很有必要确保JOIN的时候不会全表扫。4.3 公历转农历和农历转公历公历转农历比较简单因为日期主表本身就是“公历日期为主键”的表直接按solar_date查即可拿到农历字段。如果是农历转公历场景比如用户输入“农历2024年正月初五”要求算出对应的公历日期SELECT solar_date FROM calendar_date WHERE lunar_year 2024 AND lunar_month 1 AND lunar_day 5 AND is_leap_month 0;这里就体现出idx_lunar_month_day索引的价值了。如果没有这个索引这条SQL是纯全表扫描。加了索引之后MySQL可以先通过索引快速定位到所有正月初五的记录再过滤年份性能提升非常明显。实测在5万条数据量下全表扫描在60ms左右走索引后在5ms以内差距是数量级的。另外要特别注意农历转公历查询时一定要带is_leap_month条件。因为有闰月的年份同一个月会出现两次如果不区分查询结果会多于一条。业务里如果遇到这类情况需要提示用户明确是否闰月。5. 常见问题与排查技巧速查真实开发中踩过的坑5.1 中文乱码如果你发现MySQL里查出来的黄历数据是乱码不要犹豫首先检查连接串有没有指定charsetutf8mb4。这个参数在Python的pymysql和命令行mysql客户端里都需要显式指定否则可能走默认的latin1。另外一个很多人忽略的是建库时的默认字符集如果不是utf8mb4后续建表时也没有显式指定就会继承库的默认值。所以我强烈建议建库语句、建表语句里都显式声明DEFAULT CHARSETutf8mb4不要依赖默认值。5.2 上下十年数据缺失默认脚本如果只生成了10年数据业务跑着跑着就会遇到“查不到未来某天数据”的报错。这个问题在择日、提醒类业务里特别致命。我的策略是首次跑数时生成长区间之后每年初用定时脚本补跑明年的数据。# 年度补数脚本适合放在crontab里每年1月1日执行 python3 generate_calendar_data.py --start 2025-01-01 --end 2025-12-315.3 农历闰月时间边界错误这是最容易出问题的地方。农历闰月的规则是人定的不是算法天然就能算对的。比如2023年有闰二月2025年有闰六月。如果生成脚本用的库版本老旧或者数据源本身有误闰月前后几天的农历日期可能直接偏移一天。排查时不要只抽查普通月份数据要专门对闰月前后各15天做重点校验。我会写一个单独的校验脚本把已知闰月日和对照数据源逐天比对发现不一致立即修正数据源后再重跑。5.4 节气时间只精确到日还是精确到分钟如果你的业务涉及“某日具体几点几分交节”那日期级存储就不够用了需要在节气表里加上term_time字段存时间戳。如果只是用来展示“今天是立春”那精度到天就够了。6. 这套脚本后续还能扩展什么数据基础搭完之后扩展方向其实很多。如果你在做智能家居或者IoT设备的日历屏可以基于这张表写个定时任务每天凌晨从库中读当天的农历和黄历信息推送到设备端展示。如果是做公众号或小程序可以把它做成每日运势、每日宜忌推送的底层数据服务。再往后做还可以把每日吉时、时辰干支等细化数据扩展进来。6.1 性能再进一步加一层缓存虽然MySQL查询已经很快了但如果是面向C端的高并发查询我建议在MySQL前面加一层Redis缓存。缓存key可以直接用日期字符串value存JSON序列化后的黄历完整信息。不过这里要提醒一下黄历数据是按天变化的缓存过期时间设置成当天凌晨过期会比较合理避免出现第二天数据不更新的问题。6.2 数据订阅与自动更新如果你希望数据库里的黄历数据永远是新赛季可以考虑用Shell脚本定时任务的方式每年自动拉取数据并灌库。用mysqldump备份也是一个不错的选择。备份命令可以写成mysqldump -u root -p calendar_db calendar_db_backup_$(date \%Y\%m\%d).sql备份文件会带日期后缀方便回溯。这样即使误操作把数据搞坏了也能快速恢复。我个人在实际使用中最大的体会是数据基础工程看着不起眼但一旦做扎实了后面接业务能省非常多时间。尤其是MySQL加脚本这套组合既能保证数据一致性又能适配各种业务查询场景。如果预算紧张没有商业API这个方案完全撑得起一个中小心项目。本文还有配套的精品资源点击获取
返回列表