mongodb 7.5.0Query a Date Range in MongoDB
Build correct MongoDB date-range queries with inclusive and exclusive boundaries, UTC values, ObjectId timestamps, indexes, aggregation, and verification.
Store dates as BSON dates
Range queries work predictably when the field contains BSON Date values. A string that looks like an ISO timestamp is still a string and follows string comparison rules. Mixed types make both filtering and indexing harder to reason about.
Create scoped sample data:
const events = db.collection("date_range_events");
events.deleteMany({ tutorial: "date-range" });
events.insertMany([
{ name: "deploy-start", occurredAt: ISODate("2026-09-01T00:00:00.000Z"), tutorial: "date-range" },
{ name: "deploy-finish", occurredAt: ISODate("2026-09-01T00:18:00.000Z"), tutorial: "date-range" },
{ name: "morning-check", occurredAt: ISODate("2026-09-02T08:30:00.000Z"), tutorial: "date-range" },
{ name: "month-end", occurredAt: ISODate("2026-09-30T23:59:59.500Z"), tutorial: "date-range" },
{ name: "next-month", occurredAt: ISODate("2026-10-01T00:00:00.000Z"), tutorial: "date-range" }
]);
MongoG exposes ISODate() in its BSON-aware script context, so the examples create real dates rather than ordinary strings.
Prefer a half-open interval
A half-open interval includes the start and excludes the end. To query September 2026 in UTC:
const start = ISODate("2026-09-01T00:00:00.000Z");
const end = ISODate("2026-10-01T00:00:00.000Z");
events.find({
tutorial: "date-range",
occurredAt: { $gte: start, $lt: end }
}).sort({ occurredAt: 1 });
This includes every timestamp from the first instant of September and excludes the first instant of October. It avoids guessing the final representable fraction of a second. The same pattern works for a day, hour, billing period, or cursor window as long as both boundaries are calculated in the intended time zone.
Use $gt instead of $gte when the start itself must be excluded. Use $lte only when the business rule truly has an inclusive upper boundary with a known exact value.
Treat time zones as an application decision
BSON dates represent an instant. Calendar boundaries such as “today” or “September” depend on a time zone. The UTC month above is not the same interval as September in Istanbul, New York, or a user's browser zone.
Calculate the local boundary in application code with a reliable time-zone library, convert the resulting instants to UTC, and send those dates to MongoDB. Do not append Z to a local wall-clock value merely to make it look like UTC.
For example, an application asking for a user's local day should derive two instants:
const startUtc = ISODate("2026-09-16T21:00:00.000Z");
const endUtc = ISODate("2026-09-17T21:00:00.000Z");
events.find({
tutorial: "date-range",
occurredAt: { $gte: startUtc, $lt: endUtc }
});
The example represents a particular 24-hour UTC interval; it is not a universal conversion recipe. Daylight-saving transitions can create local days that are not exactly 24 hours.
Combine the range with equality filters
Production queries usually include another boundary such as tenant, account, device, or status:
events.find({
tenantId: ObjectId("64f000000000000000000001"),
occurredAt: { $gte: start, $lt: end }
}).sort({ occurredAt: -1 }).limit(100);
An index should reflect the important access pattern. Equality fields commonly come before the range and sort field:
events.createIndex(
{ tenantId: 1, occurredAt: -1 },
{ name: "tenant_recent_events" }
);
This is a starting point, not a rule to apply without measurement. Run the representative query with explain("executionStats") and inspect the winning plan, keys examined, documents examined, returned count, and whether a blocking sort occurred.
Query by the timestamp inside an ObjectId carefully
A standard ObjectId contains a creation-time component. It can support a rough creation range when no explicit field exists:
const startId = ObjectId.createFromTime(start.getTime() / 1000);
const endId = ObjectId.createFromTime(end.getTime() / 1000);
events.find({
_id: { $gte: startId, $lt: endId }
});
Use this only when _id is a normal ObjectId generated at creation time. Custom identifiers, imported records, client clock behavior, and business-event time can make _id inappropriate. An explicit occurredAt or createdAt field communicates the domain meaning and can be indexed directly.
Group results into calendar buckets
Aggregation can summarize events after the range filter:
events.aggregate([
{
$match: {
tutorial: "date-range",
occurredAt: { $gte: start, $lt: end }
}
},
{
$group: {
_id: { $dateTrunc: { date: "$occurredAt", unit: "day", timezone: "UTC" } },
count: { $sum: 1 }
}
},
{ $sort: { _id: 1 } }
]);
Keep the selective $match early so an appropriate index can reduce the input. Specify the intended timezone in date expressions when calendar interpretation matters.
Verify types, boundaries, and cleanup
In MongoG, run events.findOne() and confirm occurredAt is displayed as a date. Test records exactly at the start and end; the start should be included and the end excluded. Add a malformed string date and confirm that it does not behave like a BSON date, then remove it.
Clean up the tutorial data:
events.deleteMany({ tutorial: "date-range" });
Continue with query filters and projection, MongoDB aggregation examples, and index explain plans.