日歷落庫:Java 批量生成工作日/周末/節(jié)假日與調(diào)休覆蓋的工程實踐)
簡介一份面向Java開發(fā)者的年度日期詳情入庫方案解決IT系統(tǒng)中維護(hù)每年工作日、周末與節(jié)假日數(shù)據(jù)的常見需求。資源將核心代碼封裝為單個Java文件經(jīng)7z壓縮后僅2KB便于快速查看與遷移。實現(xiàn)基于java.time的LocalDate遍歷全年日期借助DayOfWeek判斷周末并預(yù)留HolidayCalendar等節(jié)假日庫接口通過JDBC連接數(shù)據(jù)庫設(shè)計日期、工作日標(biāo)識、周末標(biāo)識、節(jié)假日標(biāo)識等字段表結(jié)構(gòu)同時考慮批量插入與事務(wù)回滾確保大數(shù)據(jù)量下的寫入效率和一致性。代碼層次清晰包含連接初始化、日期列表構(gòu)建、批量持久化及異常回滾等模塊并留有數(shù)據(jù)庫連接與自定義節(jié)假日規(guī)則的配置擴(kuò)展空間。適合需要構(gòu)建考勤、排班、運(yùn)營日歷等模塊的中級開發(fā)者可借鑒其表設(shè)計、批量提交和邊界測試如閏年、補(bǔ)班等思路。已有3738人學(xué)習(xí)下載可直接作為年度工作日歷數(shù)據(jù)初始化的參考腳本。1. 把節(jié)假日、周末、工作日三態(tài)日歷落庫這個需求解決的是“每天是不是工作日”做考勤、排班、租金屬性計費(fèi)或者任務(wù)調(diào)度的后臺系統(tǒng)時幾乎每個業(yè)務(wù)都要回答同一個問題某一天是不是工作日前幾年我維護(hù)過一個考勤統(tǒng)計模塊早期寫法是每次請求都現(xiàn)算——先判斷周幾再去查節(jié)假日表最后還要處理補(bǔ)班結(jié)果同一套規(guī)則散落在三個服務(wù)里每年年底節(jié)假日安排一公布就要熬夜改代碼。后來換成了標(biāo)題里這個思路用 Java 把每年的節(jié)假日、周末、工作日詳情按日期批量生成提前落進(jìn)一張數(shù)據(jù)庫表業(yè)務(wù)查詢只做一次主鍵點(diǎn)查。這篇筆記就把這套方案從表結(jié)構(gòu)、生成器、調(diào)休覆蓋到運(yùn)維習(xí)慣完整拆開講適合正在做日歷類基礎(chǔ)數(shù)據(jù)維護(hù)、或者想讓考勤邏輯從“算”變成“查”的開發(fā)者。2. 日歷表結(jié)構(gòu)設(shè)計字段明細(xì)、唯一主鍵、覆蓋規(guī)則表怎么搭配2.1 三態(tài)日歷事實表為什么主鍵用日期而不是自增ID先看結(jié)果表。這張表要回答的核心問題是“某天是三種狀態(tài)中的哪一種”所以它在建模上是一張事實表每一行代表一個日期只保留與該日期身份相關(guān)的屬性不摻業(yè)務(wù)狀態(tài)。我常用的建表語句如下CREATE TABLE t_calendar_detail ( calendar_date DATE NOT NULL COMMENT 日期主鍵, year SMALLINT NOT NULL COMMENT 年份, month TINYINT NOT NULL COMMENT 月份, day TINYINT NOT NULL COMMENT 日, weekday TINYINT NOT NULL COMMENT 星期幾1周一7周日, day_type TINYINT NOT NULL COMMENT 1工作日2周末3節(jié)假日, holiday_name VARCHAR(64) DEFAULT NULL COMMENT 節(jié)假日名稱工作日/周末為NULL, remark VARCHAR(128) DEFAULT NULL COMMENT 備注如“調(diào)休補(bǔ)班”, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (calendar_date), KEY idx_year_month (year, month) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT日歷三態(tài)事實表;主鍵我沒有用自增 ID直接拿calendar_date做主鍵。原因是這張表的查詢模式非常固定要么按某個日期精確查要么按年份/月份范圍掃。日期本身全局唯一用它做物理主鍵還能讓按時間范圍的掃描走聚簇索引順序讀比自增主鍵再回表快一個量級。year/month/day/weekday這些冗余字段看著多余但實際報表經(jīng)常按“某年某月的工作日數(shù)”分組統(tǒng)計直接從行里取比DATE_FORMAT包一層函數(shù)高效得多也避免函數(shù)導(dǎo)致索引失效。2.2 覆蓋規(guī)則表調(diào)休補(bǔ)班只改這張表事實表只負(fù)責(zé)“最終結(jié)果”它不應(yīng)該是人直接改的地方。真正需要人工維護(hù)的是規(guī)則表也就是每年年底公布的調(diào)休與法定節(jié)假日安排。我一般會再建一張覆蓋規(guī)則表CREATE TABLE t_rule_override ( target_date DATE NOT NULL COMMENT 被覆蓋的日期, override_type TINYINT NOT NULL COMMENT 1調(diào)休補(bǔ)班周末變工作日3法定節(jié)假日工作日變節(jié)假日, holiday_name VARCHAR(64) DEFAULT NULL COMMENT 節(jié)假日名稱, source_year SMALLINT NOT NULL COMMENT 規(guī)則歸屬年份, effective_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 同一天多條規(guī)則時取最新, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (target_date), KEY idx_source_year (source_year) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT節(jié)假日/調(diào)休覆蓋規(guī)則表;這張表只保存“與默認(rèn)規(guī)則不一致”的日子。默認(rèn)規(guī)則在代碼里寫死周一至周五是工作日周六周日是周末。與默認(rèn)規(guī)則一致的日期根本不需要進(jìn)規(guī)則表這樣每年要維護(hù)的記錄通常只有三四十條。target_date做主鍵很關(guān)鍵——同一天的最終狀態(tài)只能有一種這個約束從表結(jié)構(gòu)上就杜絕了“既放假又補(bǔ)班”的矛盾數(shù)據(jù)。2.3 索引取舍按日期范圍查詢?yōu)橹鱠ay_type 不要單獨(dú)建索引索引設(shè)計上主鍵索引已經(jīng)覆蓋了精確日期查詢。idx_year_month是為了“某年某月”的分組統(tǒng)計比如查 6 月的應(yīng)出勤天數(shù)。踩過的坑是最開始我給day_type單獨(dú)建了索引想著按狀態(tài)過濾會快實際完全沒用。day_type只有三四個取值區(qū)分度極低優(yōu)化器全表掃比走索引回表更快這個索引純屬浪費(fèi)寫放大。如果非要做“按工作日查”的統(tǒng)計正確做法是利用year month組合索引把日期范圍先縮小到一個月再過濾狀態(tài)。還有個細(xì)節(jié)日期務(wù)必用DATE類型不要圖導(dǎo)入方便用VARCHAR。用字符串存日期等值查詢還能靠主鍵索引碰巧命中一旦做BETWEEN或者DATE_SUB運(yùn)算排序規(guī)則和隱式轉(zhuǎn)換會讓索引徹底失效后面第 5 章會把這個坑單獨(dú)展開。3. 用 Java 批量生成全年日歷并寫庫生成器核心代碼與 JDBC 批量參數(shù)3.1 生成器核心流程LocalDate 遍歷 規(guī)則覆蓋生成全年的邏輯并不復(fù)雜第一步從t_rule_override讀出所有覆蓋規(guī)則第二步從 1 月 1 日遍歷到 12 月 31 日第三步對每個日期先按“工作日/周末”默認(rèn)規(guī)則打分再看有沒有覆蓋規(guī)則有就按覆蓋結(jié)果重寫。這里我直接用 JDBC 做示例換成MyBatis或Spring Data思路完全一致public void generateYear(Connection conn, int year) throws SQLException { MapLocalDate, Integer overrideMap loadOverrideMap(conn, year); LocalDate start LocalDate.of(year, 1, 1); LocalDate end LocalDate.of(year, 12, 31); ListCalendarDetail rows new ArrayList(); for (LocalDate date start; !date.isAfter(end); date date.plusDays(1)) { DayOfWeek dow date.getDayOfWeek(); // 1. 默認(rèn)規(guī)則周一至周五為工作日周六周日為周末 int dayType (dow DayOfWeek.SATURDAY || dow DayOfWeek.SUNDAY) ? 2 : 1; String holidayName null; String remark null; // 2. 覆蓋規(guī)則優(yōu)先調(diào)休補(bǔ)班或法定節(jié)假日以規(guī)則表為準(zhǔn) if (overrideMap.containsKey(date)) { Integer override overrideMap.get(date); dayType override; remark 規(guī)則覆蓋; } // 3. 拼裝實體year/month/day 直接從 LocalDate 取不用字符串格式化 CalendarDetail detail new CalendarDetail(); detail.setCalendarDate(date); detail.setYear(date.getYear()); detail.setMonth(date.getMonthValue()); detail.setDay(date.getDayOfMonth()); detail.setWeekday(dow.getValue()); detail.setDayType(dayType); detail.setHolidayName(holidayName); detail.setRemark(remark); rows.add(detail); } writeBatch(conn, year, rows); }這段代碼有幾個值得注意的參數(shù)與選型點(diǎn)。LocalDate遍歷完全沒有SimpleDateFormat的線程安全問題生成 365 個日期對象對 JVM 來說開銷可以忽略。getMonthValue()返回 1-12 的int與表結(jié)構(gòu)里TINYINT對應(yīng)不要用Month枚舉直接存否則下一步寫庫還要做一次轉(zhuǎn)換。overDayType存的是最終類型所以規(guī)則覆蓋時直接賦值即可。3.2 JDBC 批量寫入關(guān)閉自動提交與 rewriteBatchedStatementsgenerateYear里只組裝數(shù)據(jù)真正的性能差異在寫庫這一段。批量插入全年 365/366 條數(shù)據(jù)逐條INSERT和批量addBatch在數(shù)據(jù)量小時看不出差別但如果你要一次性生成未來五年甚至十年差距就是秒級和分鐘級的區(qū)別。我在接入層一般這樣寫private void writeBatch(Connection conn, int year, ListCalendarDetail rows) throws SQLException { // 先清理舊數(shù)據(jù)保證任務(wù)可重復(fù)執(zhí)行不會主鍵沖突 try (PreparedStatement ps conn.prepareStatement(DELETE FROM t_calendar_detail WHERE year ?)) { ps.setInt(1, year); ps.executeUpdate(); } String sql INSERT INTO t_calendar_detail (calendar_date, year, month, day, weekday, day_type, holiday_name, remark) VALUES (?, ?, ?, ?, ?, ?, ?, ?); conn.setAutoCommit(false); // 關(guān)閉自動提交攢一批再提交 try (PreparedStatement ps conn.prepareStatement(sql)) { int batchSize 2000; int count 0; for (CalendarDetail d : rows) { ps.setObject(1, d.getCalendarDate()); ps.setInt(2, d.getYear()); ps.setInt(3, d.getMonth()); ps.setInt(4, d.getDay()); ps.setInt(5, d.getWeekday()); ps.setInt(6, d.getDayType()); ps.setString(7, d.getHolidayName()); ps.setString(8, d.getRemark()); ps.addBatch(); count; // 每 2000 條執(zhí)行一次避免 single batch 過大 if (count % batchSize 0) { ps.executeBatch(); } } ps.executeBatch(); // 收尾再刷一次 conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); } }這里有一個隱藏很深的性能參數(shù)如果你連的是 MySQL 5.1 以上的版本JDBC 連接串一定要加上rewriteBatchedStatementstrue。不加這個參數(shù)驅(qū)動拿到addBatch后依然是一條一條發(fā)給服務(wù)端批處理名存實亡加上之后驅(qū)動才會把多條INSERT重寫為一條多值的INSERT語句。jdbc:mysql://127.0.0.1:3306/yourdb?useUnicodetruecharacterEncodingutf8rewriteBatchedStatementstruesetObject傳LocalDate給DATE列在 JDBC 4.2 之后有標(biāo)準(zhǔn)映射比java.sql.Date.valueOf(date)少一層轉(zhuǎn)換包裝代碼也更干凈。batchSize不建議一次頂滿全年數(shù)據(jù)MySQL 的max_allowed_packet會限制單次包大小2000 一批在絕大多數(shù)默認(rèn)配置下都能安全通過。3.3 全量重建與增量補(bǔ)刷兩種調(diào)用方式怎么選生成器寫好后調(diào)用策略有兩種常見方式。全量重建適合每年年初一次性執(zhí)行先刪掉整年數(shù)據(jù)再插入整年數(shù)據(jù)邏輯干凈表里永遠(yuǎn)只有一份完整結(jié)果。代價是執(zhí)行期間該年份數(shù)據(jù)不可用對排班系統(tǒng)來說凌晨執(zhí)行完全沒問題。增量補(bǔ)刷適合“節(jié)假日安排出了補(bǔ)充通知只改后面三個月”的場景。比如年中突然宣布某一天放假這時全量刪掉整年再重建明顯浪費(fèi)只刷受影響區(qū)間更合理。我的做法是給生成器加一個窗口參數(shù)public void rebuildWindow(Connection conn, LocalDate start, LocalDate end) throws SQLException { // 刪除窗口內(nèi)數(shù)據(jù)重新生成該區(qū)間規(guī)則表全量參與覆蓋判斷 generateRange(conn, start, end); }兩種方式的取舍用一個簡單標(biāo)準(zhǔn)判斷數(shù)據(jù)量不到 5 萬行無腦全量重建超過 5 萬行或業(yè)務(wù)要求“生成期間仍可讀”走增量窗口。實際項目里一般用不到這個閾值全量重建足夠。4. 調(diào)休與補(bǔ)班把節(jié)假日“覆蓋”邏輯做成可回滾的規(guī)則映射4.1 為什么單純“周末等于休息日”會翻車默認(rèn)規(guī)則“周六周日為周末休息”在絕大多數(shù)日子里是成立的但每年總有那么十幾天被官方公布的放假安排打破。最典型的例子五一、國慶前后往往會有“前一個周日補(bǔ)班”或“周六調(diào)休放假”就是說默認(rèn)規(guī)則里的周六周日可能被改成工作日默認(rèn)規(guī)則里周一到周五也可能被改成節(jié)假日。如果不做覆蓋機(jī)制只靠“周末自動休息”的代碼考勤系統(tǒng)在補(bǔ)班那天會漏判大量缺勤。所以生成環(huán)節(jié)里我把覆蓋邏輯設(shè)計成“先按默認(rèn)規(guī)則定基調(diào)再用規(guī)則表整體覆蓋”而不是寫一堆if (date.equals(某節(jié)日))。因為節(jié)日日期每年都變硬編碼經(jīng)不起時間推敲。4.2 規(guī)則合并與去重同一天多條規(guī)則取最新一條覆蓋規(guī)則表維護(hù)的人可能不止一個每年元旦前 A 同學(xué)錄入一批元旦后 B 同學(xué)又改了幾個日期。如果規(guī)則表出現(xiàn)target_date重復(fù)主鍵約束會直接阻止但歷史數(shù)據(jù)替換時可能出現(xiàn)“同一日期先被標(biāo)記調(diào)休補(bǔ)班又被標(biāo)記節(jié)假日”的修改。這種場景我用一句話處理target_date上的記錄永遠(yuǎn)只有最新一條INSERT ... ON DUPLICATE KEY UPDATE保證冪等。INSERT INTO t_rule_override (target_date, override_type, holiday_name, source_year, effective_time) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE override_type VALUES(override_type), holiday_name VALUES(holiday_name), source_year VALUES(source_year), effective_time CURRENT_TIMESTAMP;加載規(guī)則映射時也要注意邏輯方向默認(rèn)工作日被覆蓋成節(jié)假日默認(rèn)周末被覆蓋成補(bǔ)班工作日這兩類都從t_rule_override讀統(tǒng)一按target_date做MapLocalDate, Integer生成器只認(rèn)“日期最終的 day_type 值”不再關(guān)心中間發(fā)生了什么。這樣規(guī)則表與事實表徹底解耦將來想回滾某條規(guī)則直接刪規(guī)則表里的記錄再重新觸發(fā)重建即可。4.3 跨月邊界與節(jié)假日名稱歸檔前后 15 天一起解析每年公布的年放假安排會指定“春節(jié)前一周的周日補(bǔ)班”而這個補(bǔ)班日期很可能落在上一年的 12 月或者下一年的 1 月。所以加載規(guī)則時的查詢條件不能只鎖死當(dāng)前年份而要前后各留出緩沖我把這個窗口叫做“年邊界緩沖”。SELECT target_date, override_type, holiday_name FROM t_rule_override WHERE target_date BETWEEN ? AND ?這里的兩個問號分別是給定年份的1月1日減去15天和12月31日加上15天。生成時日期落入緩沖區(qū)內(nèi)但年份不是目標(biāo)年份的行會直接忽略只有落在目標(biāo)年份內(nèi)的日期才寫入事實表。這個設(shè)計解決了很多團(tuán)隊“春節(jié)補(bǔ)班總漏一天”的毛病。節(jié)假日名稱歸檔也有講究。規(guī)則表里的holiday_name要按真實節(jié)日寫全比如“國慶節(jié)”“中秋節(jié)”不要寫“10月1日放假”這種日期化命名。因為后續(xù)判斷“某節(jié)假日是否調(diào)休”“今年中秋國慶是否重疊”都要靠這個名稱做統(tǒng)計分析命名規(guī)范了報表層就不用再清洗。5. 日歷落庫避坑寫日期字段最容易翻車的 5 個問題5.1 YYYY 與 yyyy 混用年尾日期被排到錯誤年份現(xiàn)象生成 12 月 31 日的數(shù)據(jù)時控制臺打印出來的年份是下一年。查數(shù)據(jù)庫發(fā)現(xiàn)這一行的year字段被寫成了下一年的值。原因SimpleDateFormat或DateTimeFormatter里用了YYYY。這個模式代表“周周年份”不是日歷年。當(dāng)某年的 12 月 31 日恰好屬于下一周的 ISO 周期時YYYY會輸出下周所在的年份。解決生成器里完全不要依賴格式化字符串推導(dǎo)年份直接用LocalDate.getYear()。如果歷史代碼用了DateTimeFormatter.ofPattern(yyyy-MM-dd)一定要明確yyyy才是日歷年。這個坑在年初或者年末跑批時特別容易觸發(fā)因為周周年份與日歷年只在跨年那一兩天不同。5.2 主鍵沖突與重復(fù)生成任務(wù)重跑直接拋異?,F(xiàn)象同樣的生成任務(wù)跑第二遍插入第 366 行時報Duplicate entry 2025-12-31 for key PRIMARY。原因生成器第一次執(zhí)行成功后事實表已經(jīng)存在全年數(shù)據(jù)第二次執(zhí)行沒有做清理直接INSERT主鍵約束攔截。解決writeBatch開頭一定要按年份先DELETE再插入也就是前面代碼里的DELETE FROM t_calendar_detail WHERE year ?。這個操作把重跑任務(wù)從“不可重復(fù)執(zhí)行”變成“冪等”任何時間重放都不會產(chǎn)生臟數(shù)據(jù)。要注意如果刪除之后、插入之前有其他進(jìn)程在查詢這張表會短暫查不到數(shù)據(jù)。低峰期運(yùn)維或者用臨時表替換寫入二選一。5.3 時區(qū)問題日期整體偏移一天現(xiàn)象數(shù)據(jù)庫里存好的日期和本地代碼里L(fēng)ocalDate對象完全對不上總是差一天。比如代碼里是 2025-01-01庫里變成 2024-12-31。原因生成時用了LocalDateTime或者Date在寫入DATE列時經(jīng)過 JDBC 驅(qū)動做了時區(qū)換算。應(yīng)用服務(wù)器時區(qū)與數(shù)據(jù)庫會話時區(qū)不一致時邊界時間會漂移。解決全部改用LocalDate數(shù)據(jù)庫連接串顯式指定時區(qū)參數(shù)useJDBCCompliantTimezoneShifttrueserverTimezoneAsia/Shanghai代碼里ps.setObject(1, localDate)直接傳純?nèi)掌趯ο蟛灰狣ate.valueOf(localDate)之后再轉(zhuǎn)一次。時區(qū)問題屬于典型的“本地沒事、上線翻車”因為本地數(shù)據(jù)庫和線上數(shù)據(jù)庫時區(qū)配置往往不同。5.4 只維護(hù)放假沒維護(hù)補(bǔ)班考勤異常集中在節(jié)前現(xiàn)象某月底考勤報表突然冒出一批缺勤正好都是之前調(diào)休補(bǔ)班的那幾天。系統(tǒng)后臺查“節(jié)假日配置”明明有記錄。原因規(guī)則表只錄入了“法定節(jié)假日”這一類覆蓋也就是只把工作日改成了節(jié)假日卻漏掉了“周末補(bǔ)班”這類逆向覆蓋。默認(rèn)規(guī)則認(rèn)為那天是休息日事實表也就按休息日記錄導(dǎo)致應(yīng)出勤日被算成了休息。解決覆蓋規(guī)則的設(shè)計必須同時支持兩個方向override_type1表示周末改成工作日override_type3表示工作日改成節(jié)假日。生成邏輯不要只處理“放假”那一側(cè)。另外補(bǔ)班日期在規(guī)則表里也要保留holiday_name的原始信息比如備注“國慶前補(bǔ)班”方便報表側(cè)解釋異常出勤記錄。5.5 日期列隱式轉(zhuǎn)換VARCHAR 與 DATE 的混用導(dǎo)致索引失效現(xiàn)象表里calendar_date定義為VARCHAR查詢WHERE calendar_date BETWEEN 2025-01-01 AND 2025-01-31慢到全表掃。原因字符串比較按字典序2025-1-1和2025-01-01排序結(jié)果完全不同邏輯上像一個日期物理上排成兩條序列。一旦數(shù)據(jù)量上來所謂“日期范圍查詢”根本沒有范圍可言。解決表結(jié)構(gòu)里列類型必須是DATE參數(shù)傳LocalDate查詢條件不要用DATE_FORMAT(calendar_date) ...這種寫法因為對列做函數(shù)處理會讓索引失效。正確姿勢是calendar_date BETWEEN ? AND ?讓日期列裸著參與比較。6. 進(jìn)階增量窗口更新、結(jié)果校驗與“只改規(guī)則表”的運(yùn)維習(xí)慣6.1 按未來 90 天窗口增量重建全量重建適合年初但年中若突發(fā)“某天臨時放假”的通知重跑整年代價不大卻也容易把已有人工訂正的數(shù)據(jù)覆蓋掉。我的習(xí)慣是平時只維護(hù)規(guī)則表發(fā)布新規(guī)則后對受影響的未來 90 天窗口做增量重建。窗口選擇 90 天是因為絕大多數(shù)臨時通知都不會影響超過一個季度窗口太大沒必要太小又可能覆蓋不全連續(xù)調(diào)休。這種運(yùn)維方式要求生成器把“刪除與插入”限定在一個窗口內(nèi)public void rebuildWindow(Connection conn, LocalDate start, LocalDate end) { deleteRange(conn, start, end); generateRange(conn, start, end); }刪除和生成需要放在同一個事務(wù)里避免刪除成功、生成失敗造成窗口空窗。6.2 全年結(jié)果校驗對比 SQL 與總數(shù)范圍生成完數(shù)據(jù)別急著上線跑兩組校驗。第一組是總量校驗按day_type分組統(tǒng)計全年三種類型加起來必須等于 366閏年或 365SELECT day_type, COUNT(*) FROM t_calendar_detail WHERE year 2025 GROUP BY day_type;正常年份工作日數(shù)量通常在 240 到 260 之間周末在 100 到 120 之間節(jié)假日約 11 到 20 天。如果統(tǒng)計結(jié)果明顯越界說明規(guī)則覆蓋環(huán)節(jié)出了問題。第二組是規(guī)則匹配校驗把規(guī)則表里所有補(bǔ)班日期與事實表交叉比對SELECT r.target_date FROM t_rule_override r LEFT JOIN t_calendar_detail c ON c.calendar_date r.target_date WHERE r.override_type 1 AND c.day_type ! 1;查出來的每一行都是“規(guī)則表要求這天補(bǔ)班事實表卻沒生效”的漏網(wǎng)之魚。這兩組 SQL 丟進(jìn)定時任務(wù)里每天跑一次能兜住絕大多數(shù)人為配置錯誤。6.3 一個值得養(yǎng)成的習(xí)慣規(guī)則表永遠(yuǎn)比事實表“高級”我接手這個方案半年后最大的體會是事實表只允許被生成器寫入永遠(yuǎn)不要改單行數(shù)據(jù)。哪怕手工修一條“某天應(yīng)該是節(jié)假日”的錯誤也先去改規(guī)則表再觸發(fā)對應(yīng)年份/窗口重建。因為事實表是下游業(yè)務(wù)讀取的唯一來源手工改一條很可能覆蓋掉生成邏輯期望的最終狀態(tài)之后規(guī)則表再變動這一條手工數(shù)據(jù)就被靜默抹掉了。相反“規(guī)則表 → 生成器 → 事實表”的單向數(shù)據(jù)流任何時刻都可以從頭重算等于給日歷系統(tǒng)留了一顆后悔藥。后來遇到“某個日期狀態(tài)不對”的反饋我先查規(guī)則表而不是直接改結(jié)果表十次有九次都是規(guī)則漏配補(bǔ)班。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取