avatar
Published on

MongoDB 用 $and / $or 查「年、月分開存」的日期區間

Authors
  • avatar
    Name
    Harry
    Twitter

最近在實作薪資與工時資料的篩選功能。使用者可以依「參考時間」、「工作經歷」和「性別」縮小結果;其中參考時間是單選,選擇「所有參考時間」時不加日期條件,選擇「過去一個月/一年/兩年」時,前端則會換算成一組起訖年月交給後端。

這次實作想練習的不只是把篩選功能做出來,而是理解 MongoDB 查詢條件的結構:什麼情況使用 $or、不同種類的條件如何用 $and 組合,以及條件陣列的順序是否會影響結果。

當日期存成 data_time.year 和 data_time.month 兩個欄位時,沒辦法直接寫 >= 2025/10。要拆成「年份比較」加上「年份相同時比月份」,再用 $or 與 $and 組合。

會用到的運算子

  • $gt / $gte / $lt / $lte:大於、大於等於、小於、小於等於。
  • $or:陣列中任一個條件成立就符合。
  • $and:陣列中每一個條件都要成立。
  • 同一個物件裡寫多個 key:隱含 AND。例如 { "data_time.year": 2025, "data_time.month": { $gte: 10 } } 表示年份是 2025 而且月份 ≥ 10。

查詢怎麼拆

目標:找出 2025/10 到 2026/3 之間的資料。

{
  $and: [
    // 起點:2025/10 或之後
    { $or: [
      { "data_time.year": { $gt: 2025 } },
      { "data_time.year": 2025, "data_time.month": { $gte: 10 } },
    ]},
    // 終點:2026/3 或之前
    { $or: [
      { "data_time.year": { $lt: 2026 } },
      { "data_time.year": 2026, "data_time.month": { $lte: 3 } },
    ]},
  ],
}
  • 起點:年份晚於 2025 的全部算,或年份剛好是 2025 且月份 ≥ 10 的也算。
  • 終點:年份早於 2026 的全部算,或年份剛好是 2026 且月份 ≤ 3 的也算。
  • 兩個條件(起點、終點)要同時成立,所以用 $and 包起來。

為什麼起點不能只寫一條

直覺上可能會想把起點簡化成:

{ "data_time.year": 2025, "data_time.month": { $gte: 10 } }   // ❌

但這個條件的意思是「year 剛好等於 2025,而且 month ≥ 10」,2026 年以後的資料會全部被排除:

資料只寫一條$or 兩條
2025/09❌❌
2025/10✅✅(第 2 條)
2025/12✅✅(第 2 條)
2026/01❌ year ≠ 2025✅(第 1 條)
2026/05❌ year ≠ 2025✅(第 1 條)

「2025/10 或之後」其實包含兩種情況:

  1. 年份比 2025 大:月份是多少都可以 → { "data_time.year": { $gt: 2025 } }
  2. 年份剛好是 2025:這時才需要看月份 → { "data_time.year": 2025, "data_time.month": { $gte: 10 } }

這就像比較 (年, 月) >= (2025, 10):先比年,年相同時才比月。年和月分成兩個欄位存,MongoDB 沒辦法直接比較這組數值,所以要用 $or 把兩種情況拆開。終點的邏輯也一樣,只是方向相反。

只有在查詢範圍保證不跨年時(例如起訖都在 2025 年內),才可以只寫第 2 條。

如果能把日期存成單一的 Date 欄位(或 202510 這種數字),就能直接寫 { date: { $gte: ISODate("2025-10-01") } },不必拆成兩種情況。

為什麼不直接寫兩個 $or

{ $or: [...起點], $or: [...終點] }   // ❌

同一層物件不能有兩個同名的 key。MongoDB 文件指出,多數語言與 driver(含 mongosh)不允許建出重複 key 的物件,建議的做法是改用明確的 $and。在 JavaScript 物件字面值裡,後面的 key 會蓋掉前面的,所以起點條件會悄悄消失,而且不會報錯(這點是 JS 語言行為,不是文件內容)。

常見錯誤

  • 把兩個 $or 放同一層:其中一個被蓋掉。
  • 用純函式合併條件物件時:{ ...a, ...b } 如果 a、b 都有 $and,後者會蓋掉前者。要合併,需要把兩個 $and 陣列接起來。

把日期、年資和性別條件組起來

實際功能不只有日期。日期、年資和性別屬於不同種類的篩選,因此需要全部成立;沒有填寫的篩選則不應放進查詢。

const conditions = [
  dataTimeRange ? buildDataTimeRangeCondition(dataTimeRange) : null,
  buildGroupCondition(experienceInYearRanges, buildExperienceInYearRangeCondition),
  genders?.length > 0 ? { gender: { $in: genders } } : null,
].filter(Boolean)

if (conditions.length === 0) return {}
if (conditions.length === 1) return conditions[0]
return { $and: conditions }

這裡先用 null 表示沒有套用的篩選,再透過 .filter(Boolean) 移除。沒有條件時回傳 {};只有一個條件時直接使用它;兩個以上才用 $and 包起來。

$and 陣列中的順序會影響結果嗎?

不會。$and 表示陣列中的每個條件都要成立,所以 A AND B 和 B AND A 的查詢結果相同。$or 也一樣,A OR B 和 B OR A 不會因為交換位置而改變結果。

{
  $and: [{ gender: 'female' }, { experience_in_year: { $gte: 3 } }]
}

// 和下面的查詢語意相同
{
  $and: [{ experience_in_year: { $gte: 3 } }, { gender: 'female' }]
}

程式中的排列順序主要是為了閱讀,例如按照畫面上的「參考時間 → 工作經歷 → 性別」排列。效能上也不應依賴陣列順序;MongoDB query planner 會根據條件與可用的 index 規劃查詢。

不過,如果程式會用 JSON.stringify(conditions) 產生 cache key,或在測試中直接比較整個查詢物件,順序不同就會得到不同字串或資料結構。這不影響 MongoDB 的查詢語意,但可能影響應用程式自己的快取與測試。

參考資料