Модуль 2 · Язык фильтров

Справочник по MongoDB · модуль 2 · язык фильтров

2.2

2.2

Сравнение:
$eq $ne $gt $gte $lt $lte

В главе 2.1 значение в фильтре означало точное равенство. Здесь разобраны операторы сравнения: как записать «больше», «меньше» и «не равно», как задать диапазон двумя операторами на одном поле, как сравниваются даты и строки и почему $ne находит документы, в которых поля нет совсем.

Категория
Язык запросов
Уровень
Начальный
Связанные темы
Фильтр и точечная нотация, списки значений $in, наличие поля $exists
Зачем это знать
Отбирать документы по диапазону чисел и дат

Как запускать примеры этой главы

БазыГлава только читает данные — сбрасывать базы не нужно.
Архивmongodb-glava-2.2-python.zip, 62 КБmongodb-glava-2.2-ruby.zip, 61 КБmongodb-glava-2.2-go.zip, 67 КБmongodb-glava-2.2-cpp.zip, 65 КБ — все примеры главы готовыми программами, базы и стенд в Docker. В README.md — запуск в Docker, если Compass нет или он не подключается, и на установленном сервере с Compass, а также что должна напечатать каждая программа.
Файлlessons/ch2_2.py — пример вставляется в конец файлаlessons/ch2_2.rb — пример вставляется в конец файлаlessons/ch2_2.go — пример внутрь скобок в конце main, функции и типы — выше mainlessons/ch2_2.cpp — пример внутрь скобок в конце main, функции — выше main
Запускcd lessons, затем python ch2_2.pycd lessons, затем python3 ch2_2.py · в Docker: docker compose run --rm python lessons/ch2_2.pycd lessons, затем ruby ch2_2.rb · в Docker: docker compose run --rm ruby lessons/ch2_2.rbcd lessons, затем go run ch2_2.go · в Docker: docker compose run --rm go lessons/ch2_2.gocd lessons, затем g++ -std=c++17 ch2_2.cpp -o prog $(pkg-config --cflags --libs libmongocxx1) && ./prog · в Docker: docker compose run --rm cpp lessons/ch2_2.cpp
ПорядокКаждый пример вставляется вместо предыдущего и запускается один раз, по порядку, — так получены все выводы главы. Подробно — глава 1.0, §10.

§1Оператор в фильтре

Условие «равно» записывается парой «поле — значение». Для остальных условий значение поля в фильтре заменяют документом, в котором стоит оператор запроса — поле с именем, начинающимся со знака $. Фильтр {"employees": {"$gte": 500}} читается так: у документа есть поле employees, и его значение больше или равно 500.

Сервер отличает оператор от обычного вложенного документа по знаку $. В главе 2.1 фильтр {"salary": {"from": 60000}} сравнивал поле salary с документом {"from": 60000} целиком. Если имя поля во внутреннем документе начинается с $, сервер читает этот документ как условие.

Операторов сравнения шесть. Их имена — сокращения английских слов: equal — равно, not equal — не равно, greater than — больше, less than — меньше.

ОператорУсловиеПримерЧто отбирает
$eqравно{"city": {"$eq": "Казань"}}то же, что {"city": "Казань"}
$neне равно{"city": {"$ne": "Казань"}}все города, кроме Казани
$gtбольше{"salary": {"$gt": 100000}}строго больше 100 000
$gteбольше или равно{"salary": {"$gte": 100000}}100 000 и больше
$ltменьше{"salary": {"$lt": 100000}}строго меньше 100 000
$lteменьше или равно{"salary": {"$lte": 100000}}100 000 и меньше

Первый пример главы работает с коллекцией companies: восемь компаний, у каждой в поле employees записано число сотрудников — от 6 до 25 000. Запрос отбирает компании, где сотрудников 500 и больше.

Python · PyMongoкомпании от 500 сотрудников
for doc in companies.find({"employees": {"$gte": 500}}, {"_id": 0, "name": 1, "employees": 1}):
    print(doc)
C++ · mongocxxкомпании от 500 сотрудников
mongocxx::options::find options;
options.projection(make_document(kvp("_id", 0), kvp("name", 1), kvp("employees", 1)));

for (const auto& doc : companies.find(make_document(kvp("employees", make_document(kvp("$gte", 500)))), options)) {
    std::cout << bsoncxx::to_json(doc, bsoncxx::ExtendedJsonMode::k_relaxed) << std::endl;
}
Go · mongo-driverкомпании от 500 сотрудников
cursor, _ := companies.Find(ctx, bson.D{{Key: "employees", Value: bson.D{{Key: "$gte", Value: 500}}}},
    options.Find().SetProjection(bson.D{{Key: "_id", Value: 0}, {Key: "name", Value: 1}, {Key: "employees", Value: 1}}))
for cursor.Next(ctx) {
    var doc bson.D
    cursor.Decode(&doc)
    out, _ := bson.MarshalExtJSON(doc, false, false)
    fmt.Println(string(out))
}
Ruby · mongoкомпании от 500 сотрудников
companies.find({ "employees" => { "$gte" => 500 } }, projection: { "_id" => 0, "name" => 1, "employees" => 1 })
         .each { |doc| puts doc.inspect }
Вывод в терминалрезультат запуска
{'name': 'Мосэнергосвязь', 'employees': 900}
{'name': 'Яндекс', 'employees': 25000}
{'name': 'Тензор', 'employees': 4200}
{'name': 'Ozon Tech', 'employees': 11000}
Вывод в терминалрезультат запуска
{ "name" : "Мосэнергосвязь", "employees" : 900 }
{ "name" : "Яндекс", "employees" : 25000 }
{ "name" : "Тензор", "employees" : 4200 }
{ "name" : "Ozon Tech", "employees" : 11000 }
Вывод в терминалрезультат запуска
{"name":"Мосэнергосвязь","employees":900}
{"name":"Яндекс","employees":25000}
{"name":"Тензор","employees":4200}
{"name":"Ozon Tech","employees":11000}
Вывод в терминалрезультат запуска
{"name"=>"Мосэнергосвязь", "employees"=>900}
{"name"=>"Яндекс", "employees"=>25000}
{"name"=>"Тензор", "employees"=>4200}
{"name"=>"Ozon Tech", "employees"=>11000}

Четыре компании из восьми. Документы идут в том порядке, в каком лежат в коллекции: сортировку оператор сравнения не задаёт. Если нужен порядок по числу сотрудников, к запросу добавляют sort из главы 1.6.

Compass, коллекция hh.companies: фильтр employees больше или равно 500, счётчик 1 – 4 of 4
Compass · hh.companies, фильтр { employees: { $gte: 500 } } — те же четыре компании, что напечатала программа

Вкладка Documents подписана числом 8 — столько компаний в коллекции. Счётчик над списком показывает 1 – 4 of 4: под условие подошли четыре. В каждом документе видно поле employees, и во всех четырёх оно не меньше 500.

§2Диапазон на одном поле

Чтобы задать диапазон, в документ условия для одного поля ставят два оператора: {"salary": {"$gte": 70000, "$lte": 100000}}. Операторы внутри одного документа соединяются по «и», как пары в самом фильтре: зарплата должна быть не меньше 70 000 и не больше 100 000 одновременно.

Граница входит в диапазон или нет в зависимости от оператора. $gte и $lte включают границу, $gt и $lt — исключают. Пример считает резюме в диапазоне двумя способами и затем выводит документы первого.

Python · PyMongoзарплата от 70 до 100 тысяч
print("$gte 70000 и $lte 100000:", resumes.count_documents({"salary": {"$gte": 70000, "$lte": 100000}}))
print("$gt 70000 и $lte 100000: ", resumes.count_documents({"salary": {"$gt": 70000, "$lte": 100000}}))

for doc in resumes.find({"salary": {"$gte": 70000, "$lte": 100000}}, {"_id": 0, "fio": 1, "salary": 1}).sort("salary", 1):
    print(doc)
C++ · mongocxxзарплата от 70 до 100 тысяч
std::cout << "$gte 70000 и $lte 100000: " << resumes.count_documents(make_document(kvp("salary", make_document(kvp("$gte", 70000), kvp("$lte", 100000))))) << std::endl;
std::cout << "$gt 70000 и $lte 100000:  " << resumes.count_documents(make_document(kvp("salary", make_document(kvp("$gt", 70000), kvp("$lte", 100000))))) << std::endl;

mongocxx::options::find options;
options.projection(make_document(kvp("_id", 0), kvp("fio", 1), kvp("salary", 1)));
options.sort(make_document(kvp("salary", 1)));

for (const auto& doc : resumes.find(make_document(kvp("salary", make_document(kvp("$gte", 70000), kvp("$lte", 100000)))), options)) {
    std::cout << bsoncxx::to_json(doc, bsoncxx::ExtendedJsonMode::k_relaxed) << std::endl;
}
Go · mongo-driverзарплата от 70 до 100 тысяч
vklyuchaya, _ := resumes.CountDocuments(ctx, bson.D{{Key: "salary", Value: bson.D{{Key: "$gte", Value: 70000}, {Key: "$lte", Value: 100000}}}})
bez, _ := resumes.CountDocuments(ctx, bson.D{{Key: "salary", Value: bson.D{{Key: "$gt", Value: 70000}, {Key: "$lte", Value: 100000}}}})

fmt.Println("$gte 70000 и $lte 100000:", vklyuchaya)
fmt.Println("$gt 70000 и $lte 100000: ", bez)

cursor, _ := resumes.Find(ctx, bson.D{{Key: "salary", Value: bson.D{{Key: "$gte", Value: 70000}, {Key: "$lte", Value: 100000}}}},
    options.Find().
        SetProjection(bson.D{{Key: "_id", Value: 0}, {Key: "fio", Value: 1}, {Key: "salary", Value: 1}}).
        SetSort(bson.D{{Key: "salary", Value: 1}}))
for cursor.Next(ctx) {
    var doc bson.D
    cursor.Decode(&doc)
    out, _ := bson.MarshalExtJSON(doc, false, false)
    fmt.Println(string(out))
}
Ruby · mongoзарплата от 70 до 100 тысяч
puts "$gte 70000 и $lte 100000: " + resumes.count_documents({ "salary" => { "$gte" => 70000, "$lte" => 100000 } }).to_s
puts "$gt 70000 и $lte 100000:  " + resumes.count_documents({ "salary" => { "$gt" => 70000, "$lte" => 100000 } }).to_s

resumes.find({ "salary" => { "$gte" => 70000, "$lte" => 100000 } }, projection: { "_id" => 0, "fio" => 1, "salary" => 1 })
       .sort({ "salary" => 1 })
       .each { |doc| puts doc.inspect }
Вывод в терминалрезультат запуска
$gte 70000 и $lte 100000: 4
$gt 70000 и $lte 100000:  3
{'fio': 'Пётр Ковалёв', 'salary': 70000}
{'fio': 'Марк Ефремов', 'salary': 75000}
{'fio': 'Дарья Нечаева', 'salary': 80000}
{'fio': 'Ксения Лапина', 'salary': 90000}
Вывод в терминалрезультат запуска
$gte 70000 и $lte 100000: 4
$gt 70000 и $lte 100000:  3
{ "fio" : "Пётр Ковалёв", "salary" : 70000 }
{ "fio" : "Марк Ефремов", "salary" : 75000 }
{ "fio" : "Дарья Нечаева", "salary" : 80000 }
{ "fio" : "Ксения Лапина", "salary" : 90000 }
Вывод в терминалрезультат запуска
$gte 70000 и $lte 100000: 4
$gt 70000 и $lte 100000:  3
{"fio":"Пётр Ковалёв","salary":70000}
{"fio":"Марк Ефремов","salary":75000}
{"fio":"Дарья Нечаева","salary":80000}
{"fio":"Ксения Лапина","salary":90000}
Вывод в терминалрезультат запуска
$gte 70000 и $lte 100000: 4
$gt 70000 и $lte 100000:  3
{"fio"=>"Пётр Ковалёв", "salary"=>70000}
{"fio"=>"Марк Ефремов", "salary"=>75000}
{"fio"=>"Дарья Нечаева", "salary"=>80000}
{"fio"=>"Ксения Лапина", "salary"=>90000}

Разница — одно резюме: у Петра Ковалёва зарплата ровно 70 000, и $gt его не пропустил. Сортировка по salary здесь нужна только для того, чтобы вывод читался по возрастанию.

Compass, коллекция hh.resumes: фильтр salary от 70000 до 100000, проекция fio и salary, сортировка по salary, счётчик 1 – 4 of 4
Compass · hh.resumes, фильтр { salary: { $gte: 70000, $lte: 100000 } } с проекцией и сортировкой

Панель Options раскрыта: в поле Project стоит та же проекция, что в коде, в поле Sort — сортировка { salary: 1 }. Счётчик 1 – 4 of 4, первым идёт Пётр Ковалёв с зарплатой ровно 70 000 — граница вошла в диапазон.

Оба условия пишутся внутри одного документа поля. Если записать их двумя парами с одинаковым ключом, {"salary": {"$gte": 70000}, "salary": {"$lte": 100000}}, результат зависит от языка. Словарь Python и хеш Ruby хранят ключ один раз и оставляют последнюю пару, поэтому от диапазона остаётся «не больше 100 000». Документы Go и C++ сохраняют обе пары, и сервер применяет обе. Запись с одним документом поля работает одинаково во всех четырёх языках.

§3Сравнение вложенных полей

Оператор ставится после любого имени поля, в том числе записанного через точку. Вилка зарплаты в вакансиях лежит во вложенном документе salary с полями from и to. Задача — найти вакансии, в вилку которых попадает ожидание кандидата в 120 000: нижняя граница вилки не выше 120 000, верхняя — не ниже.

Python · PyMongoвилка, в которую попадает 120 000
filtr = {"salary.from": {"$lte": 120000},
         "salary.to": {"$gte": 120000}}

for doc in vacancies.find(filtr, {"_id": 0, "title": 1, "salary": 1}):
    print(doc)
C++ · mongocxxвилка, в которую попадает 120 000
auto filtr = make_document(
    kvp("salary.from", make_document(kvp("$lte", 120000))),
    kvp("salary.to", make_document(kvp("$gte", 120000))));

mongocxx::options::find options;
options.projection(make_document(kvp("_id", 0), kvp("title", 1), kvp("salary", 1)));

for (const auto& doc : vacancies.find(filtr.view(), options)) {
    std::cout << bsoncxx::to_json(doc, bsoncxx::ExtendedJsonMode::k_relaxed) << std::endl;
}
Go · mongo-driverвилка, в которую попадает 120 000
filtr := bson.D{
    {Key: "salary.from", Value: bson.D{{Key: "$lte", Value: 120000}}},
    {Key: "salary.to", Value: bson.D{{Key: "$gte", Value: 120000}}}}

cursor, _ := vacancies.Find(ctx, filtr,
    options.Find().SetProjection(bson.D{{Key: "_id", Value: 0}, {Key: "title", Value: 1}, {Key: "salary", Value: 1}}))
for cursor.Next(ctx) {
    var doc bson.D
    cursor.Decode(&doc)
    out, _ := bson.MarshalExtJSON(doc, false, false)
    fmt.Println(string(out))
}
Ruby · mongoвилка, в которую попадает 120 000
filtr = { "salary.from" => { "$lte" => 120000 },
          "salary.to" => { "$gte" => 120000 } }

vacancies.find(filtr, projection: { "_id" => 0, "title" => 1, "salary" => 1 })
         .each { |doc| puts doc.inspect }
Вывод в терминалрезультат запуска
{'title': 'Backend-разработчик .NET', 'salary': {'from': 100000, 'to': 150000}}
{'title': 'Frontend-разработчик', 'salary': {'from': 90000, 'to': 140000}}
{'title': 'Инженер сопровождения БД', 'salary': {'from': 80000, 'to': 120000}}
{'title': 'Администратор баз данных', 'salary': {'from': 110000, 'to': 160000}}
{'title': 'Аналитик данных', 'salary': {'from': 120000, 'to': 180000}}
Вывод в терминалрезультат запуска
{ "title" : "Backend-разработчик .NET", "salary" : { "from" : 100000, "to" : 150000 } }
{ "title" : "Frontend-разработчик", "salary" : { "from" : 90000, "to" : 140000 } }
{ "title" : "Инженер сопровождения БД", "salary" : { "from" : 80000, "to" : 120000 } }
{ "title" : "Администратор баз данных", "salary" : { "from" : 110000, "to" : 160000 } }
{ "title" : "Аналитик данных", "salary" : { "from" : 120000, "to" : 180000 } }
Вывод в терминалрезультат запуска
{"title":"Backend-разработчик .NET","salary":{"from":100000,"to":150000}}
{"title":"Frontend-разработчик","salary":{"from":90000,"to":140000}}
{"title":"Инженер сопровождения БД","salary":{"from":80000,"to":120000}}
{"title":"Администратор баз данных","salary":{"from":110000,"to":160000}}
{"title":"Аналитик данных","salary":{"from":120000,"to":180000}}
Вывод в терминалрезультат запуска
{"title"=>"Backend-разработчик .NET", "salary"=>{"from"=>100000, "to"=>150000}}
{"title"=>"Frontend-разработчик", "salary"=>{"from"=>90000, "to"=>140000}}
{"title"=>"Инженер сопровождения БД", "salary"=>{"from"=>80000, "to"=>120000}}
{"title"=>"Администратор баз данных", "salary"=>{"from"=>110000, "to"=>160000}}
{"title"=>"Аналитик данных", "salary"=>{"from"=>120000, "to"=>180000}}

Пять вакансий из восьми. Фильтр вынесен в переменную filtr, потому что в одну строку он не помещается. Условия стоят на двух разных полях, salary.from и salary.to, и соединяются по «и», как любые пары фильтра. Вакансия «Инженер сопровождения БД» с вилкой 80 000 – 120 000 подошла: граница 120 000 входит в условие $gte.

Compass, коллекция hh.vacancies: фильтр по salary.from и salary.to, счётчик 1 – 5 of 5, у каждой вакансии раскрыт документ salary
Compass · hh.vacancies, вилки, в которые попадает 120 000; вложенный документ salary раскрыт

В Compass фильтр записывается одной строкой, имена с точкой берутся в кавычки. У каждой вакансии раскрыт salary: в третьей, «Инженер сопровождения БД», to равно 120 000 — ровно на границе. Проекция оставляет название и вилку, панель Options на кадре свёрнута.

§4Даты

Даты в BSON хранятся отдельным типом — числом миллисекунд от 1 января 1970 года по UTC (глава 1.2). Операторы сравнения работают с датами так же, как с числами: более поздняя дата больше. Значение в фильтре должно быть датой своего языка: datetime в Python, Time в Ruby, time.Time в Go, b_date в C++. Драйвер переводит его в дату BSON.

Операторы сравнения сравнивают значения только внутри одного типа: число с числом, строку со строкой, дату с датой. Документ, у которого значение поля другого типа, под условие не подходит. Пример ищет резюме, обновлённые с 1 сентября 2026 года, — один раз датой, другой раз строкой.

Python · PyMongoдата и строка с датой
from datetime import datetime

sep01 = datetime(2026, 9, 1)

print("updated $gte дата:  ", resumes.count_documents({"updated": {"$gte": sep01}}))
print("updated $gte строка:", resumes.count_documents({"updated": {"$gte": "2026-09-01"}}))
C++ · mongocxxдата и строка с датой
auto sep01 = bsoncxx::types::b_date{std::chrono::milliseconds{1788220800000}};   // 01.09.2026 00:00 UTC

std::cout << "updated $gte дата:   " << resumes.count_documents(make_document(kvp("updated", make_document(kvp("$gte", sep01))))) << std::endl;
std::cout << "updated $gte строка: " << resumes.count_documents(make_document(kvp("updated", make_document(kvp("$gte", "2026-09-01"))))) << std::endl;
Go · mongo-driverдата и строка с датой
sep01 := time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)

datoy, _ := resumes.CountDocuments(ctx, bson.D{{Key: "updated", Value: bson.D{{Key: "$gte", Value: sep01}}}})
strokoy, _ := resumes.CountDocuments(ctx, bson.D{{Key: "updated", Value: bson.D{{Key: "$gte", Value: "2026-09-01"}}}})

fmt.Println("updated $gte дата:  ", datoy)
fmt.Println("updated $gte строка:", strokoy)
Ruby · mongoдата и строка с датой
sep01 = Time.utc(2026, 9, 1)

puts "updated $gte дата:   " + resumes.count_documents({ "updated" => { "$gte" => sep01 } }).to_s
puts "updated $gte строка: " + resumes.count_documents({ "updated" => { "$gte" => "2026-09-01" } }).to_s
Вывод в терминалрезультат запуска
updated $gte дата:   5
updated $gte строка: 0
Вывод в терминалрезультат запуска
updated $gte дата:   5
updated $gte строка: 0
Вывод в терминалрезультат запуска
updated $gte дата:   5
updated $gte строка: 0
Вывод в терминалрезультат запуска
updated $gte дата:   5
updated $gte строка: 0

С датой нашлось пять резюме, со строкой — ни одного. В поле updated лежат даты, а строка "2026-09-01" с ними не сравнивается. Ошибки при этом нет: запрос выполнен, под условие не подошёл ни один документ. Анна Белова не попала ни в один из результатов, потому что поля updated в её резюме нет.

Compass, коллекция hh.resumes: фильтр updated не раньше ISODate 2026-09-01, счётчик 1 – 5 of 5, поле updated раскрыто
Compass · hh.resumes, фильтр { updated: { $gte: ISODate("2026-09-01") } }

В поле фильтра Compass дата записывается функцией ISODate("…"): без неё строка осталась бы строкой, как во второй строке вывода выше. В документах поле updated раскрыто и показано как $date со временем по UTC; все пять дат — с 1 по 5 сентября.

Дата без времени означает полночь по UTC. Отсюда правило для периодов: конец периода задают оператором $lt с началом следующего дня. Пример считает собеседования за неделю с 21 по 27 сентября двумя способами.

Python · PyMongoсобеседования за неделю
from datetime import datetime

sep21 = datetime(2026, 9, 21)
sep27 = datetime(2026, 9, 27)
sep28 = datetime(2026, 9, 28)

print("с 21.09 до 28.09 ($lt): ", interviews.count_documents({"when": {"$gte": sep21, "$lt": sep28}}))
print("с 21.09 по 27.09 ($lte):", interviews.count_documents({"when": {"$gte": sep21, "$lte": sep27}}))
C++ · mongocxxсобеседования за неделю
auto sep21 = bsoncxx::types::b_date{std::chrono::milliseconds{1789948800000}};   // 21.09.2026 00:00 UTC
auto sep27 = bsoncxx::types::b_date{std::chrono::milliseconds{1790467200000}};   // 27.09.2026 00:00 UTC
auto sep28 = bsoncxx::types::b_date{std::chrono::milliseconds{1790553600000}};   // 28.09.2026 00:00 UTC

std::cout << "с 21.09 до 28.09 ($lt):  " << interviews.count_documents(make_document(kvp("when", make_document(kvp("$gte", sep21), kvp("$lt", sep28))))) << std::endl;
std::cout << "с 21.09 по 27.09 ($lte): " << interviews.count_documents(make_document(kvp("when", make_document(kvp("$gte", sep21), kvp("$lte", sep27))))) << std::endl;
Go · mongo-driverсобеседования за неделю
sep21 := time.Date(2026, 9, 21, 0, 0, 0, 0, time.UTC)
sep27 := time.Date(2026, 9, 27, 0, 0, 0, 0, time.UTC)
sep28 := time.Date(2026, 9, 28, 0, 0, 0, 0, time.UTC)

poluinterval, _ := interviews.CountDocuments(ctx, bson.D{{Key: "when", Value: bson.D{{Key: "$gte", Value: sep21}, {Key: "$lt", Value: sep28}}}})
otrezok, _ := interviews.CountDocuments(ctx, bson.D{{Key: "when", Value: bson.D{{Key: "$gte", Value: sep21}, {Key: "$lte", Value: sep27}}}})

fmt.Println("с 21.09 до 28.09 ($lt): ", poluinterval)
fmt.Println("с 21.09 по 27.09 ($lte):", otrezok)
Ruby · mongoсобеседования за неделю
sep21 = Time.utc(2026, 9, 21)
sep27 = Time.utc(2026, 9, 27)
sep28 = Time.utc(2026, 9, 28)

puts "с 21.09 до 28.09 ($lt):  " + interviews.count_documents({ "when" => { "$gte" => sep21, "$lt" => sep28 } }).to_s
puts "с 21.09 по 27.09 ($lte): " + interviews.count_documents({ "when" => { "$gte" => sep21, "$lte" => sep27 } }).to_s
Вывод в терминалрезультат запуска
с 21.09 до 28.09 ($lt):  22
с 21.09 по 27.09 ($lte): 18
Вывод в терминалрезультат запуска
с 21.09 до 28.09 ($lt):  22
с 21.09 по 27.09 ($lte): 18
Вывод в терминалрезультат запуска
с 21.09 до 28.09 ($lt):  22
с 21.09 по 27.09 ($lte): 18
Вывод в терминалрезультат запуска
с 21.09 до 28.09 ($lt):  22
с 21.09 по 27.09 ($lte): 18

Разница — четыре собеседования. Все они назначены на 27 сентября, с 10:00 до 16:00 по UTC, а условие $lte с датой 27.09.2026 пропускает события не позже 00:00 этого дня. Запись «от начала первого дня до начала следующего за последним» называется полуинтервалом: левая граница входит, правая нет. Так периоды стыкуются друг с другом без пропусков и без двойного счёта.

§5Строки

Строки операторы сравнения сравнивают посимвольно, по кодам символов: сначала первые символы, при равенстве — вторые, и так далее. Порядок задаёт таблица кодов, алфавит языка на него не влияет. Латинские буквы в таблице кодов идут раньше кириллических, а в пределах одного алфавита заглавные идут раньше строчных.

Python · PyMongoназвания раньше кириллической «А»
for doc in companies.find({"name": {"$lt": "А"}}, {"_id": 0, "name": 1}):
    print(doc)
C++ · mongocxxназвания раньше кириллической «А»
mongocxx::options::find options;
options.projection(make_document(kvp("_id", 0), kvp("name", 1)));

for (const auto& doc : companies.find(make_document(kvp("name", make_document(kvp("$lt", "А")))), options)) {
    std::cout << bsoncxx::to_json(doc, bsoncxx::ExtendedJsonMode::k_relaxed) << std::endl;
}
Go · mongo-driverназвания раньше кириллической «А»
cursor, _ := companies.Find(ctx, bson.D{{Key: "name", Value: bson.D{{Key: "$lt", Value: "А"}}}},
    options.Find().SetProjection(bson.D{{Key: "_id", Value: 0}, {Key: "name", Value: 1}}))
for cursor.Next(ctx) {
    var doc bson.D
    cursor.Decode(&doc)
    out, _ := bson.MarshalExtJSON(doc, false, false)
    fmt.Println(string(out))
}
Ruby · mongoназвания раньше кириллической «А»
companies.find({ "name" => { "$lt" => "А" } }, projection: { "_id" => 0, "name" => 1 })
         .each { |doc| puts doc.inspect }
Вывод в терминалрезультат запуска
{'name': 'Retail Lab'}
{'name': 'Ozon Tech'}
Вывод в терминалрезультат запуска
{ "name" : "Retail Lab" }
{ "name" : "Ozon Tech" }
Вывод в терминалрезультат запуска
{"name":"Retail Lab"}
{"name":"Ozon Tech"}
Вывод в терминалрезультат запуска
{"name"=>"Retail Lab"}
{"name"=>"Ozon Tech"}

Условие «меньше кириллической буквы А» отобрало две компании, названия которых начинаются с латинской буквы: коды латиницы меньше любого кода кириллицы. По русскому алфавиту такое сравнение смысла не имеет: латинских букв в нём нет, и «раньше А» они стоят только в таблице кодов.

Compass, коллекция hh.companies: фильтр name меньше кириллической А, счётчик 1 – 2 of 2, Retail Lab и Ozon Tech
Compass · hh.companies, фильтр { name: { $lt: "А" } } — две компании с латинскими названиями

Счётчик 1 – 2 of 2: из восьми компаний под условие подошли Retail Lab и Ozon Tech. Буква А в фильтре — кириллическая; латинская A дала бы другой результат, потому что её код меньше.

По той же причине числа, записанные строками, сравниваются не как числа: "100000" меньше "70000", потому что символ 1 меньше символа 7. Сравнение строк по правилам конкретного языка задаётся отдельным параметром запроса — collation.

§6$ne и отсутствующее поле

Оператор $ne отбирает документы, у которых условие «равно» не выполняется. Документ, где поля нет совсем, тоже не равен заданному значению — поэтому $ne его возвращает. Операторы $gt, $gte, $lt, $lte ведут себя иначе: если поля нет, сравнивать не с чем, и документ не подходит.

В коллекции резюме поле ready_to_move — готовность к переезду — есть у восьми документов из девяти. В резюме Анны Беловой его нет.

Python · PyMongoне равно и отсутствующее поле
print("ready_to_move $ne true:", resumes.count_documents({"ready_to_move": {"$ne": True}}))
print("ready_to_move false:   ", resumes.count_documents({"ready_to_move": False}))
print("ready_to_move $lt true:", resumes.count_documents({"ready_to_move": {"$lt": True}}))

for doc in resumes.find({"ready_to_move": {"$ne": True}}, {"_id": 0, "fio": 1, "ready_to_move": 1}):
    print(doc)
C++ · mongocxxне равно и отсутствующее поле
std::cout << "ready_to_move $ne true: " << resumes.count_documents(make_document(kvp("ready_to_move", make_document(kvp("$ne", true))))) << std::endl;
std::cout << "ready_to_move false:    " << resumes.count_documents(make_document(kvp("ready_to_move", false))) << std::endl;
std::cout << "ready_to_move $lt true: " << resumes.count_documents(make_document(kvp("ready_to_move", make_document(kvp("$lt", true))))) << std::endl;

mongocxx::options::find options;
options.projection(make_document(kvp("_id", 0), kvp("fio", 1), kvp("ready_to_move", 1)));

for (const auto& doc : resumes.find(make_document(kvp("ready_to_move", make_document(kvp("$ne", true)))), options)) {
    std::cout << bsoncxx::to_json(doc, bsoncxx::ExtendedJsonMode::k_relaxed) << std::endl;
}
Go · mongo-driverне равно и отсутствующее поле
neRavno, _ := resumes.CountDocuments(ctx, bson.D{{Key: "ready_to_move", Value: bson.D{{Key: "$ne", Value: true}}}})
lozh, _ := resumes.CountDocuments(ctx, bson.D{{Key: "ready_to_move", Value: false}})
menshe, _ := resumes.CountDocuments(ctx, bson.D{{Key: "ready_to_move", Value: bson.D{{Key: "$lt", Value: true}}}})

fmt.Println("ready_to_move $ne true:", neRavno)
fmt.Println("ready_to_move false:   ", lozh)
fmt.Println("ready_to_move $lt true:", menshe)

cursor, _ := resumes.Find(ctx, bson.D{{Key: "ready_to_move", Value: bson.D{{Key: "$ne", Value: true}}}},
    options.Find().SetProjection(bson.D{{Key: "_id", Value: 0}, {Key: "fio", Value: 1}, {Key: "ready_to_move", Value: 1}}))
for cursor.Next(ctx) {
    var doc bson.D
    cursor.Decode(&doc)
    out, _ := bson.MarshalExtJSON(doc, false, false)
    fmt.Println(string(out))
}
Ruby · mongoне равно и отсутствующее поле
puts "ready_to_move $ne true: " + resumes.count_documents({ "ready_to_move" => { "$ne" => true } }).to_s
puts "ready_to_move false:    " + resumes.count_documents({ "ready_to_move" => false }).to_s
puts "ready_to_move $lt true: " + resumes.count_documents({ "ready_to_move" => { "$lt" => true } }).to_s

resumes.find({ "ready_to_move" => { "$ne" => true } }, projection: { "_id" => 0, "fio" => 1, "ready_to_move" => 1 })
       .each { |doc| puts doc.inspect }
Вывод в терминалрезультат запуска
ready_to_move $ne true: 5
ready_to_move false:    4
ready_to_move $lt true: 4
{'fio': 'Анна Белова'}
{'fio': 'Пётр Ковалёв', 'ready_to_move': False}
{'fio': 'Игорь Самойлов', 'ready_to_move': False}
{'fio': 'Тимур Валиев', 'ready_to_move': False}
{'fio': 'Ольга Пирогова', 'ready_to_move': False}
Вывод в терминалрезультат запуска
ready_to_move $ne true: 5
ready_to_move false:    4
ready_to_move $lt true: 4
{ "fio" : "Анна Белова" }
{ "fio" : "Пётр Ковалёв", "ready_to_move" : false }
{ "fio" : "Игорь Самойлов", "ready_to_move" : false }
{ "fio" : "Тимур Валиев", "ready_to_move" : false }
{ "fio" : "Ольга Пирогова", "ready_to_move" : false }
Вывод в терминалрезультат запуска
ready_to_move $ne true: 5
ready_to_move false:    4
ready_to_move $lt true: 4
{"fio":"Анна Белова"}
{"fio":"Пётр Ковалёв","ready_to_move":false}
{"fio":"Игорь Самойлов","ready_to_move":false}
{"fio":"Тимур Валиев","ready_to_move":false}
{"fio":"Ольга Пирогова","ready_to_move":false}
Вывод в терминалрезультат запуска
ready_to_move $ne true: 5
ready_to_move false:    4
ready_to_move $lt true: 4
{"fio"=>"Анна Белова"}
{"fio"=>"Пётр Ковалёв", "ready_to_move"=>false}
{"fio"=>"Игорь Самойлов", "ready_to_move"=>false}
{"fio"=>"Тимур Валиев", "ready_to_move"=>false}
{"fio"=>"Ольга Пирогова", "ready_to_move"=>false}

Условие «не равно true» дало пять резюме, условие «равно false» — четыре. Пятое — резюме Анны Беловой: в выводе у него нет поля ready_to_move. Условие $lt: true тоже дало четыре: false меньше true, а документ без поля в сравнение не попал.

Compass, коллекция hh.resumes: фильтр ready_to_move не равно true, счётчик 1 – 5 of 5, у первого документа поля ready_to_move нет
Compass · hh.resumes, фильтр { ready_to_move: { $ne: true } } — пять резюме, включая документ без поля

Первый документ в списке — резюме Анны Беловой. В нём шесть полей, и ready_to_move среди них нет. У остальных документов это поле есть и равно false. Счётчик 1 – 5 of 5 совпадает с первой строкой вывода выше.

Какой результат верный, зависит от задачи. «Не готов к переезду» — это четыре резюме, где соискатель ответил «нет». «Не отметил готовность» — пять, включая резюме без ответа. Отделить документы без поля позволяет оператор $exists — он разобран в главе 2.5.

§7$eq: равенство, записанное явно

У «не равно» есть пара — оператор $eq, «равно». Он проверяет то же, что пара «поле — значение»: {"city": "Москва"} и {"city": {"$eq": "Москва"}} — один и тот же запрос. Разница появляется, когда значение для фильтра приходит извне программы — например, из формы поиска на сайте.

Допустим, программа ищет резюме по ФИО и подставляет в фильтр то, что прислал браузер. Обычно это строка. Но если вместо строки прислать JSON-объект {"$ne": null}, фильтр {"fio": значение} превратится в {"fio": {"$ne": null}} — условие «ФИО не пустое», под которое подходят все резюме. Такая подмена называется внедрением операторов. Оператор $eq от неё защищает: всё, что стоит после него, сервер сравнивает как значение, даже если это документ со знаком $.

Python · PyMongoявное и неявное равенство
print("город Москва:       ", resumes.count_documents({"city": "Москва"}))
print("город $eq Москва:   ", resumes.count_documents({"city": {"$eq": "Москва"}}))
print("fio {$ne: null}:    ", resumes.count_documents({"fio": {"$ne": None}}))
print("fio $eq {$ne: null}:", resumes.count_documents({"fio": {"$eq": {"$ne": None}}}))
C++ · mongocxxявное и неявное равенство
std::cout << "город Москва:        " << resumes.count_documents(make_document(kvp("city", "Москва"))) << std::endl;
std::cout << "город $eq Москва:    " << resumes.count_documents(make_document(kvp("city", make_document(kvp("$eq", "Москва"))))) << std::endl;
std::cout << "fio {$ne: null}:     " << resumes.count_documents(make_document(kvp("fio", make_document(kvp("$ne", bsoncxx::types::b_null{}))))) << std::endl;
std::cout << "fio $eq {$ne: null}: " << resumes.count_documents(make_document(kvp("fio", make_document(kvp("$eq", make_document(kvp("$ne", bsoncxx::types::b_null{}))))))) << std::endl;
Go · mongo-driverявное и неявное равенство
neyavno, _ := resumes.CountDocuments(ctx, bson.D{{Key: "city", Value: "Москва"}})
yavno, _ := resumes.CountDocuments(ctx, bson.D{{Key: "city", Value: bson.D{{Key: "$eq", Value: "Москва"}}}})
operator, _ := resumes.CountDocuments(ctx, bson.D{{Key: "fio", Value: bson.D{{Key: "$ne", Value: nil}}}})
bukvalno, _ := resumes.CountDocuments(ctx, bson.D{{Key: "fio", Value: bson.D{{Key: "$eq", Value: bson.D{{Key: "$ne", Value: nil}}}}}})

fmt.Println("город Москва:       ", neyavno)
fmt.Println("город $eq Москва:   ", yavno)
fmt.Println("fio {$ne: null}:    ", operator)
fmt.Println("fio $eq {$ne: null}:", bukvalno)
Ruby · mongoявное и неявное равенство
puts "город Москва:        " + resumes.count_documents({ "city" => "Москва" }).to_s
puts "город $eq Москва:    " + resumes.count_documents({ "city" => { "$eq" => "Москва" } }).to_s
puts "fio {$ne: null}:     " + resumes.count_documents({ "fio" => { "$ne" => nil } }).to_s
puts "fio $eq {$ne: null}: " + resumes.count_documents({ "fio" => { "$eq" => { "$ne" => nil } } }).to_s
Вывод в терминалрезультат запуска
город Москва:        1
город $eq Москва:    1
fio {$ne: null}:     9
fio $eq {$ne: null}: 0
Вывод в терминалрезультат запуска
город Москва:        1
город $eq Москва:    1
fio {$ne: null}:     9
fio $eq {$ne: null}: 0
Вывод в терминалрезультат запуска
город Москва:        1
город $eq Москва:    1
fio {$ne: null}:     9
fio $eq {$ne: null}: 0
Вывод в терминалрезультат запуска
город Москва:        1
город $eq Москва:    1
fio {$ne: null}:     9
fio $eq {$ne: null}: 0

Первые две строки совпадают: неявное и явное равенство находят одно резюме из Москвы. Третья строка — подменённый фильтр, он вернул все девять резюме. Четвёртая — та же подмена внутри $eq: сервер искал ФИО, равное документу {"$ne": null}, и не нашёл ни одного. В программе, которая подставляет в фильтр данные пользователя, равенство записывают через $eq или заранее проверяют, что пришла строка.

Compass, коллекция hh.resumes: фильтр fio $eq документ с $ne null, счётчик 0 – 0 of 0, No results
Compass · hh.resumes, подменённое значение внутри $eq — ни одного документа

Внутри $eq документ { $ne: null } остался значением: сервер искал резюме, у которого ФИО равно этому документу, и счётчик показал 0 – 0 of 0. Без $eq тот же ввод вернул бы все девять резюме.

§8Сравнение в Compass

Кадры в разделах выше сняты в Compass на тех же запросах, что и примеры. Операторы в поле фильтра пишутся так же, как в коде, с двумя отличиями в записи. Кавычки вокруг имени поля и оператора можно не ставить, если в имени нет точки: { employees: { $gte: 500 } }; имя с точкой берут в кавычки: { "salary.from": { $lte: 120000 } }. Дата записывается функцией ISODate("2026-09-01") — в коде её роль играет тип даты языка.

§9Частые ошибки

Что написаноЧто происходитКак правильно
{"salary": {"$gt": "100000"}}Пусто: строка сравнивается только со строками, а в базе числа{"$gt": 100000}
{"updated": {"$gte": "2026-09-01"}}Пусто: в базе дата, а в фильтре строкаdatetime(2026, 9, 1) вместо строки
{"salary": {"gte": 70000}}Пусто: без знака $ это вложенный документ, и сравнивается он целиком{"$gte": 70000}
{"salary": {"$gte": 70000}, "salary": {"$lte": 100000}}Работает только второе условие: в словаре остаётся последняя пара{"$gte": 70000, "$lte": 100000}
Конец периода задан $lte с датой без времениТеряются события последнего дня: дата без времени — это полночь$lt с началом следующего дня
{"ready_to_move": {"$ne": True}}Кроме false находит документы, где поля нетЕсли нужны только false — равенство false; отсутствующее поле — глава 2.5
Что написаноЧто происходитКак правильно
make_document(kvp("salary", make_document(kvp("$gt", "100000"))))Пусто: строка сравнивается только со строками, а в базе числаmake_document(kvp("$gt", 100000))
make_document(kvp("updated", make_document(kvp("$gte", "2026-09-01"))))Пусто: в базе дата, а в фильтре строкаbsoncxx::types::b_date вместо строки
make_document(kvp("salary", make_document(kvp("gte", 70000))))Пусто: без знака $ это вложенный документ, и сравнивается он целикомmake_document(kvp("$gte", 70000))
Конец периода задан $lte с датой без времениТеряются события последнего дня: дата без времени — это полночь$lt с началом следующего дня
make_document(kvp("ready_to_move", make_document(kvp("$ne", true))))Кроме false находит документы, где поля нетЕсли нужны только false — равенство false; отсутствующее поле — глава 2.5
Что написаноЧто происходитКак правильно
bson.D{{Key: "salary", Value: bson.D{{Key: "$gt", Value: "100000"}}}}Пусто: строка сравнивается только со строками, а в базе числаbson.D{{Key: "$gt", Value: 100000}}
bson.D{{Key: "updated", Value: bson.D{{Key: "$gte", Value: "2026-09-01"}}}}Пусто: в базе дата, а в фильтре строкаtime.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC) вместо строки
bson.D{{Key: "salary", Value: bson.D{{Key: "gte", Value: 70000}}}}Пусто: без знака $ это вложенный документ, и сравнивается он целикомbson.D{{Key: "$gte", Value: 70000}}
Конец периода задан $lte с датой без времениТеряются события последнего дня: дата без времени — это полночь$lt с началом следующего дня
bson.D{{Key: "ready_to_move", Value: bson.D{{Key: "$ne", Value: true}}}}Кроме false находит документы, где поля нетЕсли нужны только false — равенство false; отсутствующее поле — глава 2.5
Что написаноЧто происходитКак правильно
{ "salary" => { "$gt" => "100000" } }Пусто: строка сравнивается только со строками, а в базе числа{ "$gt" => 100000 }
{ "updated" => { "$gte" => "2026-09-01" } }Пусто: в базе дата, а в фильтре строкаTime.utc(2026, 9, 1) вместо строки
{ "salary" => { "gte" => 70000 } }Пусто: без знака $ это вложенный документ, и сравнивается он целиком{ "$gte" => 70000 }
{ "salary" => { "$gte" => 70000 }, "salary" => { "$lte" => 100000 } }Работает только второе условие: в хеше остаётся последняя пара, Ruby предупреждает о повторе{ "$gte" => 70000, "$lte" => 100000 }
Конец периода задан $lte с датой без времениТеряются события последнего дня: дата без времени — это полночь$lt с началом следующего дня
{ "ready_to_move" => { "$ne" => true } }Кроме false находит документы, где поля нетЕсли нужны только false — равенство false; отсутствующее поле — глава 2.5

§10Проверьте себя

Чем {"salary": {"$gte": 100000}} отличается от {"salary": {"gte": 100000}}?

Первый фильтр — условие «зарплата не меньше 100 000». Во втором у внутреннего документа нет знака $, поэтому сервер сравнивает поле salary с документом {"gte": 100000} целиком. Числовое поле с документом не совпадает, результат пустой.

Сколько документов отберёт {"salary": {"$gt": 70000, "$lt": 70000}}?

Ни одного. Два оператора в одном документе поля соединяются по «и», а число не может быть одновременно больше и меньше 70 000. Сервер такой фильтр выполнит без ошибки.

Почему фильтр по дате со строкой "2026-09-01" вернул пусто, хотя резюме обновлялись в сентябре?

Операторы сравнения сравнивают значения одного типа. В поле updated лежат даты BSON, а в фильтре — строка, поэтому ни один документ под условие не подходит. Значение в фильтре должно быть датой: datetime, Time, time.Time или b_date.

Как отобрать собеседования за 27 сентября целиком?

Полуинтервалом: {"when": {"$gte": 27.09.2026, "$lt": 28.09.2026}}, где даты записаны датами языка. Условие $lte с датой 27.09.2026 из событий этого дня пропустит только назначенные ровно на 00:00, потому что дата без времени — это полночь.

Почему {"ready_to_move": {"$ne": true}} нашёл на одно резюме больше, чем {"ready_to_move": false}?

В одном резюме поля ready_to_move нет. Для $ne отсутствующее поле не равно true, и документ подходит. Равенство false требует, чтобы поле было и содержало false.

§11Шпаргалка

Больше или равно {"employees": {"$gte": 500}}
Диапазон {"salary": {"$gte": 70000, "$lte": 100000}}
Вложенное поле {"salary.from": {"$gte": 100000}}
Период дат {"when": {"$gte": datetime(2026, 9, 21), "$lt": datetime(2026, 9, 28)}}
Не равно {"ready_to_move": {"$ne": True}}
Значение как есть {"fio": {"$eq": "Анна Белова"}}
Больше или равно make_document(kvp("employees", make_document(kvp("$gte", 500))))
Диапазон make_document(kvp("salary", make_document(kvp("$gte", 70000), kvp("$lte", 100000))))
Вложенное поле make_document(kvp("salary.from", make_document(kvp("$gte", 100000))))
Период дат kvp("when", make_document( kvp("$gte", sep21), kvp("$lt", sep28)))
Не равно make_document(kvp("ready_to_move", make_document(kvp("$ne", true))))
Значение как есть make_document(kvp("fio", make_document(kvp("$eq", "Анна Белова"))))
Больше или равно bson.D{{Key: "employees", Value: bson.D{{Key: "$gte", Value: 500}}}}
Диапазон bson.D{{Key: "salary", Value: bson.D{{Key: "$gte", Value: 70000}, {Key: "$lte", Value: 100000}}}}
Вложенное поле bson.D{{Key: "salary.from", Value: bson.D{{Key: "$gte", Value: 100000}}}}
Период дат {Key: "when", Value: bson.D{ {Key: "$gte", Value: sep21}, {Key: "$lt", Value: sep28}}}
Не равно bson.D{{Key: "ready_to_move", Value: bson.D{{Key: "$ne", Value: true}}}}
Значение как есть bson.D{{Key: "fio", Value: bson.D{{Key: "$eq", Value: "Анна Белова"}}}}
Больше или равно { "employees" => { "$gte" => 500 } }
Диапазон { "salary" => { "$gte" => 70000, "$lte" => 100000 } }
Вложенное поле { "salary.from" => { "$gte" => 100000 } }
Период дат { "when" => { "$gte" => Time.utc(2026, 9, 21), "$lt" => Time.utc(2026, 9, 28) } }
Не равно { "ready_to_move" => { "$ne" => true } }
Значение как есть { "fio" => { "$eq" => "Анна Белова" } }

Итог главы

Условие, отличное от равенства, записывается документом с оператором: {"поле": {"$gte": значение}}. Шесть операторов сравнения — $eq, $ne, $gt, $gte, $lt, $lte. Несколько операторов на одном поле соединяются по «и» и задают диапазон; оператор ставится и после имени через точку. Сравнение идёт только между значениями одного типа, поэтому дата в фильтре записывается датой языка, а период задаётся полуинтервалом. Строки сравниваются по кодам символов. $eq защищает фильтр от подстановки операторов, $ne отбирает и документы без поля. Следующая глава — о проверке по списку значений: $in и $nin.

Глава 2.2 · Справочник по MongoDB Автор: Макаров Максим Николаевич