
简介这份万年历数据库资源面向需要日期数据支撑的开发与测试人员覆盖1970年1月1日至2100年12月31日的完整日历信息可直接用于考勤统计、排班系统、节假日计算、日程管理等场景省去自行推算农历、星期与节气的繁琐工作。压缩包共3个文件以1个sql脚本为主体内含MySQL建表语句与全量插入语句另附2张png截图用于展示数据表结构与运行效果整体约872KB导入前注意将编码设为utf8即可正常使用。目前已有1433人学习下载说明该数据在同类需求中具备一定参考价值。拿到脚本后可直接建库并拉入执行快速获得一张字段齐全、日期连续的日历表便于二次查询与业务扩展适合初中级开发者及需要快速搭建日期维度的项目使用。1. 万年历数据库从1970到2100一张表扛住两万五千天很多做排班、考勤、金融计息、节假日判断的系统最后都会撞上同一堵墙日期逻辑散落在业务代码里今天加个“工作日”字段明天补个“农历”字段后天又要算“第几周”。等到某天运营说“帮我把2027年所有周一的日期导出来”你才发现自己得写一堆循环去凑。万年历数据库要解决的就是这件事——把1970年1月1日到2100年12月31日这四万七千多天的日期属性提前算好、落库、建索引业务侧只查表不推算。这个方案适合谁适合正在做考勤系统、排班工具、财务计息模块、节假日营销配置的开发者。它不复杂但极其吃细节闰年、周数归属、农历转换、时区边界任何一个算错下游全是脏数据。我见过一个排班系统因为把某年12月31日归到了下一年的第1周导致跨年那周的班次全部错位排查了整整两天。所以这篇不聊虚的直接把建表语句、插入逻辑、生成脚本和踩过的坑摊开讲。2. 表结构怎么设计字段取舍决定查询效率2.1 为什么用一张宽表而不是多张关联表常见做法是把日期基础信息、农历信息、节假日信息拆成三张表用日期做主键关联。我一开始也这么设计后来发现查询时几乎每次都要三表 JOIN而万年历的数据量是固定的四万七千多条宽表的存储成本完全可以接受。一张宽表的好处是业务侧一条SELECT就能拿到全部属性不用关心关联逻辑索引建在日期列上范围查询和单日查询都走同一条路径。代价是插入时需要一次性算好所有字段。但万年历的数据是静态的——1970到2100的日期属性不会变除非政策调整节假日那是另一张配置表的事。所以宽表在这个场景下是更务实的选择。字段设计上我一般会分四组基础日期组、公历属性组、农历属性组、业务标记组。基础日期组放date_keyDATE类型主键、year、month、day公历属性组放day_of_week、day_of_year、week_of_year、quarter、is_weekend农历属性组放lunar_year、lunar_month、lunar_day、is_leap_month、lunar_date_str业务标记组放is_holiday、holiday_name、workday_adjust。后面两组允许为 NULL因为农历和节假日需要额外数据源。2.2 建表语句与索引策略CREATE TABLE calendar_master ( date_key DATE NOT NULL COMMENT 公历日期主键, year SMALLINT UNSIGNED NOT NULL COMMENT 年份, month TINYINT UNSIGNED NOT NULL COMMENT 月份 1-12, day TINYINT UNSIGNED NOT NULL COMMENT 日 1-31, day_of_week TINYINT UNSIGNED NOT NULL COMMENT 星期几 1周一 7周日, day_of_year SMALLINT UNSIGNED NOT NULL COMMENT 一年中的第几天 1-366, week_of_year TINYINT UNSIGNED NOT NULL COMMENT ISO周数 1-53, quarter TINYINT UNSIGNED NOT NULL COMMENT 季度 1-4, is_weekend TINYINT(1) NOT NULL DEFAULT 0 COMMENT 是否周末 1是 0否, lunar_year SMALLINT UNSIGNED DEFAULT NULL COMMENT 农历年, lunar_month TINYINT UNSIGNED DEFAULT NULL COMMENT 农历月 1-12, lunar_day TINYINT UNSIGNED DEFAULT NULL COMMENT 农历日 1-30, is_leap_month TINYINT(1) DEFAULT 0 COMMENT 农历是否闰月, lunar_date_str VARCHAR(20) DEFAULT NULL COMMENT 农历中文描述, is_holiday TINYINT(1) NOT NULL DEFAULT 0 COMMENT 是否法定节假日, holiday_name VARCHAR(50) DEFAULT NULL COMMENT 节假日名称, workday_adjust TINYINT(1) NOT NULL DEFAULT 0 COMMENT 调休上班标记 1需上班, PRIMARY KEY (date_key), KEY idx_year_month (year, month), KEY idx_week (year, week_of_year), KEY idx_weekend (is_weekend), KEY idx_holiday (is_holiday, workday_adjust) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT万年历主表 1970-2100;主键用DATE类型而不是自增 ID是因为业务查询几乎全部围绕日期展开用日期做主键天然去重也避免了一次额外的唯一索引。idx_year_month服务于按月拉取idx_week服务于按周统计idx_weekend和idx_holiday服务于筛选。注意week_of_year用的是 ISO 8601 标准周一为一周起点跨年周归属需要特别处理后面会讲。提示如果业务需要频繁按“农历月日”查询比如生日提醒建议额外加一个idx_lunar索引在lunar_month, lunar_day上但会略微增加插入时间。2.3 字段类型选择的几个边界year用SMALLINT UNSIGNED足够覆盖到 65535但实际只用到 2100。day_of_year最大 366SMALLINT没问题。week_of_year最大 53TINYINT够用。is_weekend、is_holiday这些布尔语义的字段用TINYINT(1)是 MySQL 的惯例不用BIT是因为很多 ORM 对BIT支持不好。农历字段允许 NULL是因为农历转换依赖外部数据源如果暂时没有农历数据插入时留空不影响公历查询。lunar_date_str存中文描述比如“正月初一”方便直接展示避免前端再拼。3. 数据怎么生成从日期循环到批量插入3.1 用 Python 生成插入语句的完整脚本四万七千多条数据手写 INSERT 不现实。常见做法是用脚本生成 SQL 文件再导入 MySQL。我用 Python 写生成脚本因为日期计算库成熟逻辑清晰。import datetime import calendar def iso_week_info(d): 返回 ISO 周数和该周所属的 ISO 年份 iso_year, iso_week, iso_weekday d.isocalendar() return iso_year, iso_week def generate_insert(d): year d.year month d.month day d.day # isoweekday: 周一1 周日7 day_of_week d.isoweekday() day_of_year d.timetuple().tm_yday iso_year, iso_week iso_week_info(d) quarter (month - 1) // 3 1 is_weekend 1 if day_of_week 6 else 0 # 农历字段暂留 NULL由后续农历数据源补充 sql ( fINSERT INTO calendar_master f(date_key, year, month, day, day_of_week, day_of_year, fweek_of_year, quarter, is_weekend) VALUES ( f{d.isoformat()}, {year}, {month}, {day}, {day_of_week}, f{day_of_year}, {iso_week}, {quarter}, {is_weekend}); ) return sql def main(): start datetime.date(1970, 1, 1) end datetime.date(2100, 12, 31) delta datetime.timedelta(days1) lines [] current start while current end: lines.append(generate_insert(current)) current delta with open(calendar_insert.sql, w, encodingutf-8) as f: f.write(SET NAMES utf8mb4;\n) f.write(START TRANSACTION;\n) f.write(\n.join(lines)) f.write(\nCOMMIT;\n) print(f共生成 {len(lines)} 条插入语句) if __name__ __main__: main()这段脚本的核心是isocalendar()它返回 ISO 年份、ISO 周数、ISO 星期几。注意iso_year可能和公历year不同——比如 2021 年 1 月 1 日是周五ISO 归属是 2020 年第 53 周。这就是跨年周问题的根源。脚本里week_of_year存的是 ISO 周数但year存的是公历年查询时如果按year week_of_year组合筛选跨年那几天会漏掉或重复。解决办法是在表里额外加一个iso_year字段或者查询时用date_key的范围来界定。参数说明start和end定义了日期范围改这两个值就能生成不同区间的数据。delta是步长固定一天。输出文件用START TRANSACTION和COMMIT包裹四万多条插入在一个事务里比逐条提交快一个数量级。3.2 批量插入的性能优化与分批策略四万七千条 INSERT 语句直接导入MySQL 默认的max_allowed_packet可能不够。我一般会把脚本改成每 1000 条一组用多值 INSERT 语法def chunk_inserts(dates, chunk_size1000): 把日期列表按 chunk_size 分组生成多值 INSERT for i in range(0, len(dates), chunk_size): chunk dates[i:ichunk_size] values [] for d in chunk: day_of_week d.isoweekday() day_of_year d.timetuple().tm_yday iso_year, iso_week d.isocalendar()[:2] quarter (d.month - 1) // 3 1 is_weekend 1 if day_of_week 6 else 0 values.append( f({d.isoformat()}, {d.year}, {d.month}, {d.day}, f{day_of_week}, {day_of_year}, {iso_week}, {quarter}, {is_weekend}) ) sql ( INSERT INTO calendar_master (date_key, year, month, day, day_of_week, day_of_year, week_of_year, quarter, is_weekend) VALUES ,.join(values) ; ) yield sql多值 INSERT 比单条 INSERT 快 5 到 10 倍因为减少了网络往返和 SQL 解析次数。chunk_size设 1000 是个经验值太大容易撞max_allowed_packet默认 4MB 或 64MB太小则优化效果不明显。导入前可以先SET GLOBAL max_allowed_packet 67108864;放宽限制。导入命令用mysql -u root -p your_db calendar_insert.sql如果文件太大可以先用split切成多个小文件再逐个导入。3.3 农历数据怎么补外部数据源与更新策略公历属性可以纯计算农历不行。农历转换依赖天文算法或预置数据表。常见做法是找一份覆盖 1900 到 2100 的农历数据表按年存储每年的农历月日映射然后写脚本关联更新。我一般会建一张临时表lunar_raw把农历数据导入然后用UPDATE ... JOIN回填主表UPDATE calendar_master c JOIN lunar_raw l ON c.date_key l.solar_date SET c.lunar_year l.lunar_year, c.lunar_month l.lunar_month, c.lunar_day l.lunar_day, c.is_leap_month l.is_leap_month, c.lunar_date_str l.lunar_str;农历数据源的质量直接决定回填结果。我踩过的坑是某份数据源在 2057 年的闰月标记错了导致那一年所有农历日期偏移一个月。所以回填后一定要抽查几个已知日期比如春节、中秋和权威日历对照。4. 查询怎么写高频场景的 SQL 与索引命中4.1 按年、月、周、季度拉取日期列表最常见的查询是“给我 2025 年 3 月的所有日期”SELECT date_key, day_of_week, is_weekend, lunar_date_str, is_holiday FROM calendar_master WHERE year 2025 AND month 3 ORDER BY date_key;这条走idx_year_month命中范围扫描。注意ORDER BY date_key在 InnoDB 里因为主键就是date_key所以排序成本很低。按周拉取要小心跨年周SELECT date_key, day_of_week FROM calendar_master WHERE date_key BETWEEN 2025-12-29 AND 2026-01-04 ORDER BY date_key;这里用日期范围而不是year week_of_year就是为了绕开 ISO 周跨年的问题。如果非要按周号查得同时匹配iso_year但表里没存这个字段所以范围查询是更稳妥的做法。4.2 工作日与节假日筛选的正确姿势“算 2025 年 3 月有多少个工作日”SELECT COUNT(*) AS workday_count FROM calendar_master WHERE year 2025 AND month 3 AND is_weekend 0 AND is_holiday 0 AND workday_adjust 0;这里有个逻辑陷阱调休上班的周末workday_adjust 1应该算工作日法定节假日的周末is_holiday 1不算。所以更准确的写法是SELECT COUNT(*) AS workday_count FROM calendar_master WHERE year 2025 AND month 3 AND ( (is_weekend 0 AND is_holiday 0) OR workday_adjust 1 );idx_holiday索引覆盖is_holiday和workday_adjust但is_weekend不在这个索引里所以查询会回表。如果这类统计非常频繁可以考虑建一个联合索引(year, month, is_weekend, is_holiday, workday_adjust)但会增大写入开销。万年历数据写入是一次性的所以这个代价可以接受。4.3 农历查询与生日提醒场景“查农历八月初五对应的公历日期”SELECT date_key, lunar_date_str FROM calendar_master WHERE lunar_month 8 AND lunar_day 5 AND is_leap_month 0 ORDER BY date_key;如果没建idx_lunar这条会全表扫描四万多行虽然不算慢但并发高了会拖累。建索引后走索引扫描响应时间从几十毫秒降到几毫秒。生日提醒场景通常是“查今天农历对应的公历日期”反过来用SELECT lunar_date_str FROM calendar_master WHERE date_key CURDATE();这条走主键最快。然后业务侧拿农历月日去匹配用户表里的农历生日。5. 避坑与排查那些让我加班到凌晨的细节5.1 跨年周归属错误导致排班错位现象某排班系统在 2024 年 12 月 30 日到 2025 年 1 月 5 日这一周班次全部错位一天。原因脚本用isocalendar()取周数但表里year存的是公历年查询时用WHERE year 2024 AND week_of_year 1去拉这一周结果拉到了 2024 年 1 月的那一周。解决查询跨年周一律用date_key BETWEEN范围不要用year week_of_year组合。如果业务必须按周号查在表里加iso_year字段并建联合索引。5.2 闰年 2 月 29 日插入失败现象导入脚本在 2000 年 2 月 29 日这条报错提示日期无效。原因脚本里用datetime.date(year, month, day)构造日期但某段代码手动拼了f{year}-02-29字符串而 2100 年不是闰年2 月没有 29 日。解决所有日期构造都用datetime.date加timedelta循环生成不要手动拼字符串。2100 年不是闰年能被 100 整除但不能被 400 整除这是很多人会忽略的边界。5.3 时区导致 CURDATE() 与预期差一天现象服务器时区是 UTC业务在東八区用CURDATE()查“今天”的日期晚上 8 点后查到的还是前一天。原因MySQL 的CURDATE()依赖服务器时区。解决连接时设置SET time_zone 08:00;或者业务侧传入日期参数而不是依赖数据库函数。万年历表本身存的是纯日期不涉及时区但查询时的“今天”定义要统一。5.4 批量插入时 max_allowed_packet 超限现象导入 SQL 文件时中途报Packet for query is too large。原因多值 INSERT 拼接的 SQL 语句超过了max_allowed_packet限制。解决把chunk_size从 1000 降到 500或者导入前执行SET GLOBAL max_allowed_packet 67108864;。注意这个参数修改后需要重新连接才生效。5.5 农历数据回填后未验证导致偏移现象某生日提醒功能在 2057 年全部提前了一个月。原因农历数据源在 2057 年有一个闰月标记错误回填时没有校验。解决回填后抽查每年春节、中秋、端午对应的公历日期和权威日历对照。我一般会写一个校验脚本把春节日期和已知列表比对不一致就报警。6. 进阶技巧把万年历用出花来表建好、数据灌进去之后真正的价值在于怎么用。我分享几个实际项目里验证过的技巧。第一个是“工作日偏移计算”。业务常说“三个工作日后”用万年历表可以一条 SQL 搞定SELECT date_key FROM calendar_master WHERE date_key 2025-03-10 AND (is_weekend 0 AND is_holiday 0 OR workday_adjust 1) ORDER BY date_key LIMIT 3;取第三条就是三个工作日后的日期。比在代码里循环判断快得多而且逻辑集中在数据库层不会因为不同服务的实现差异导致结果不一致。第二个是“季度和周的双维度统计”。很多报表需要同时按季度和周汇总万年历表里quarter和week_of_year都有直接GROUP BY即可。但注意跨年周的归属统计时用date_key的范围来界定周而不是用week_of_year数字。第三个是“节假日配置的热更新”。法定节假日每年由相关部门发布万年历表里的is_holiday和workday_adjust需要每年更新。我一般把这两个字段的更新做成独立的配置表主表只存公历和农历基础属性节假日通过视图或 JOIN 关联。这样政策调整时只改配置表不动主表。第四个是“生成日期维度表供 BI 使用”。很多 BI 工具需要一张日期维度表来做时间智能计算万年历表直接导出即可字段齐全比在 BI 里现算靠谱。最后说一个我自己的习惯每次导入完数据先跑三条校验 SQL——总行数是否等于日期差加一、每年 2 月天数是否正确、每周的日期是否连续。这三条能拦住 90% 的导入错误。希望帮到你。本文还有配套的精品资源点击获取