Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 13 из 25 ~18 мин +50 XP

Проекции, сортировка, лимиты

FindOptions в golang — это три самых частых настроителя Find: проекции отдают только нужные поля, сортировка задаёт порядок, лимиты и skip режут выборку. Разберём SetSort с bson.D, SetProjection, SetLimit и SetSkip — и почему мультиключевую карту в сортировке драйвер отвергает намеренно.

Содержание урока · 10 разделов

Каталог товаров живёт списками: лента на главной, страница категории, результаты поиска. Документ товара при этом тяжёлый — описания на трёх языках, характеристики, галерея из двадцати ссылок. Но карточке в списке нужны имя, цена и ссылка на картинку: тянуть полные документы на каждую строку ленты — значит гонять по сети в десять раз больше данных, чем нужно, и распаковывать поля, которые тут же выбросятся. Вторая задача списков — порядок: «сначала дешёвые», «новые сверху», «по рейтингу». Третья — объём страницы: двадцать позиций за раз, вторая страница — ещё двадцать. Все три задачи решаются на стороне базы опциями Find, и в этом уроке разберём их через options.Find() — единственный билдер, который понадобится: SetProjection для полей, SetSort для порядка, SetLimit и SetSkip для объёма. Отдельная развязка урока — сортировка: она принимает упорядоченные документы, и драйвер проверяет это ещё до отправки запроса. К концу урока соберёте страницу каталога, которая не гоняет лишние байты и не ломается на третьей странице.

FindOptions: один билдер на всё#

coll.Find(ctx, filter, opts...) принимает вариадик опций, а собираются они конструктором options.Find(): каждый Set-метод возвращает тот же билдер, поэтому настройки пишутся цепочкой в один вызов. Поля билдера — прямое отражение серверных опций команды find, и типы в них честные: SetLimit и SetSkip принимают int64, SetBatchSize — int32, SetProjection и SetSort — any, потому что там уезжают документы. Рядом живут и настройки реже: SetCollation задаёт правила сравнения строк — с ней сортировка по имени идёт по правилам языка, а не по кодам символов; SetComment помечает запрос в логах и профайлере сервера — та же метка, что у delete-вызовов одиннадцатого урока. Пустой билдер равен отсутствию опций: сервер вернёт все совпавшие документы целиком в естественном порядке. Опции работают поверх фильтра из восьмого и четырнадцатого уроков: сначала база отбирает совпавшие документы, потом сортирует их, отсекает skip, отдаёт limit и проецирует поля — читать эти этапы полезно именно в таком порядке, потому что так их исполняет сервер. Опции Find и FindOne — разные семейства (FindOptions и FindOneOptions), но базовые настройки у них общие: у FindOne есть свои SetSort, SetProjection и SetSkip, поэтому «самый дорогой товар» берётся FindOne с SetSort по цене убывания. Чего у FindOne нет — SetLimit и SetBatchSize: одиночному документу лимит не нужен, сервер и так вернёт максимум один.

Страница каталога одним вызовом

GO
coll := client.Database("shop").Collection("products")

cursor, err := coll.Find(
        ctx,
        bson.M{"category": "клавиатуры"},
        options.Find().
                SetSort(bson.D{{Key: "price", Value: -1}}).
                SetSkip(20).
                SetLimit(10),
)
if err != nil {
        return fmt.Errorf("page of products: %w", err)
}

var page []Product
if err := cursor.All(ctx, &page); err != nil {
        return fmt.Errorf("decode page: %w", err)
}

Опции собираются цепочкой билдера: SetSort выстраивает цены по убыванию, SetSkip пропускает две страницы, SetLimit отдаёт третью. Сортировка — bson.D: мультиключевую карту драйвер отвергает намеренно, потому что порядок полей в сортировке значим. Cursor.All из седьмого урока декодирует страницу в срез структур и закрывает курсор сам.

Сортировка: bson.D обязателен#

SetSort принимает документ вида {поле: направление}, где 1 — по возрастанию, -1 — по убыванию. Составная сортировка пишется списком пар: цена по возрастанию, внутри равных цен — новизна сверху: bson.D{{Key: "price", Value: 1}, {Key: "created_at", Value: -1}}. Здесь и живёт главное правило урока: Sort — упорядоченный аргумент, и мультиключевую bson.M в него передать нельзя. Драйвер проверяет это до отправки: SetSort(bson.M{"price": 1, "name": -1}) вернёт ErrMapForOrderedArgument с текстом multi-key map passed in for ordered parameter sort. Причина в природе карт: обход map в Go не упорядочен, и «price до name» или наоборот драйвер выбрать не может — а от порядка полей зависит результат сортировки равных значений. Проверка стоит в collection.go рядом с marshal аргумента, и это одна из тех клиентских ошибок, которые экономят час отладки. Всё, что упорядочено по смыслу — сортировки, подсказки индексов, конвейеры, — пишите через bson.D; карты остаются для фильтров, где порядок не играет роли. Проверка стоит не только в Find: FindOne с картой-сортировкой вернёт *SingleResult с той же готовой ошибкой до похода в сеть — добраться до неё можно уже методом Err, без Decode. Зато карта с одним ключом легальна: ограничение срабатывает только на две и более пары — драйвер не может упорядочить то, что сам не упорядочивает.

Главные настройки FindOptions#

МетодТип аргументаЧто делает
SetSortany — обычно bson.Dпорядок выдачи: 1 по возрастанию, -1 по убыванию
SetLimitint64максимум документов в результате
SetSkipint64сколько совпавших пропустить перед выдачей
SetProjectionany — bson.M или bson.Dкакие поля отдавать: включение или исключение
SetBatchSizeint32документов в одном сетевом батче курсора

Проекции: отдаём только нужные поля#

Проекция — документ, который говорит серверу, какие поля включить в ответ: bson.M{"name": 1, "price": 1} вернёт только эти два поля. Проекции в mongo из Go живут в SetProjection, и у них два режима. Режим включения: перечисляете поля с 1 — сервер отдаст их и _id, который добавляется по умолчанию; отключить его можно отдельно — bson.M{"name": 1, "_id": 0}. Режим исключения: перечисляете поля с 0 — вернутся все остальные; так отрезают тяжёлое описание от карточки: bson.M{"description": 0}. Смешивать включения и исключения нельзя — сервер вернёт ошибку, единственное исключение из правила и есть _id. Путь заходит и во вложенные документы: {"specs.color": 1} достанет цвет из specs, не трогая остальные характеристики. Для массивов есть отдельный оператор $slice: {"comments": {"$slice": 3}} отдаст максимум три первых элемента массива в каждом документе — проекция умеет не только брать и бросать, но и резать объём. Практическая выгода двойная: меньше сетевого трафика и меньше работы декодера — непроектированные поля даже не распаковываются. Но есть и цена: структура при декодировании получит нули там, где поля нет, — и это следующий коллаут.

Лимиты и skip: пагинация страницами#

SetLimit(10) обрезает выдачу до десяти документов, SetSkip(20) пропускает первые двадцать совпавших — вместе это привычная пагинация: страница N начинается с skip = (N-1) * размер, размер задаёт лимит. Пара тонкостей делает пару честной. Первая: без SetSort лимит отдаст «какие-то десять» — сервер не обещает порядок без сортировки, и на разных запросах состав страницы может отличаться; лимит всегда ходит в паре с сортировкой. Типы аргументов напоминают о себе на переменных: SetLimit и SetSkip принимают int64 — литерал 20 компилятор приведёт сам, а переменную int придётся конвертировать явным int64(page); нормальная плата за честные типы в опциях. Вторая: по докам драйвера отрицательный лимит означает «вернуть документы в одном батче» — экзотика для отладки, в обычном коде не встречается. Третья — цена skip: серверу нужно пройтись и отбросить все пропускаемые документы, поэтому страница 500 по 20 позиций стоит заметно дороже первой; для глубоких листаний лучше диапазон по _id из двенадцатого урока. А ещё лимит — инструмент стоимости: «топ-10 по цене» — это сортировка плюс SetLimit(10), и десять документов сервер и не будет отправлять.

Пагинация с подсчётом страниц

GO
total, err := coll.CountDocuments(ctx, bson.M{"category": "клавиатуры"})
if err != nil {
        return fmt.Errorf("count: %w", err)
}

cursor, err := coll.Find(
        ctx,
        bson.M{"category": "клавиатуры"},
        options.Find().
                SetProjection(bson.M{"name": 1, "price": 1, "_id": 0}).
                SetSort(bson.D{{Key: "price", Value: 1}}).
                SetLimit(20),
)
if err != nil {
        return fmt.Errorf("page: %w", err)
}

CountDocuments возвращает число совпадений для того же фильтра — из него считают количество страниц, а Find с проекцией отдаёт карточки: только name и price, причём _id отключён явно. Лёгкие документы экономят трафик и время декодирования, а тяжёлые описания останутся в базе до момента, когда пользователь откроет конкретную карточку. Итого два запроса на страницу — честная плата за знание общего числа.

Правила проекций одним списком#

  • Включение: перечисляете нужные поля с 1 — остальное не уезжает с сервера.
  • Исключение: перечисляете лишние с 0 — удобно отрезать тяжёлые поля.
  • Смешивать 1 и 0 нельзя, единственное исключение — отключение _id.
  • _id приходит по умолчанию с любым включением — отключайте явно, если не нужен.
  • Непроектированное поле декодируется нулём — проекции меняются вместе со структурой.
  • Для тяжёлых документов выгода измеряется трафиком и временем Decode, а не «скоростью базы».

Проверка: карта в сортировке

options.Find().SetSort(bson.M{"price": 1, "name": -1}) — что произойдёт?

Проверка: включение и _id

Проекция bson.M{"name": 1, "_id": 0} — что вернёт сервер?

Что вернёт сортировка с лимитом?

Прочитайте код и выберите, что он выведет:

// в коллекции товары с ценами: 100, 300, 200
cursor, _ := coll.Find(ctx, bson.M{},
        options.Find().
                SetSort(bson.D{{Key: "price", Value: -1}}).
                SetLimit(2))
var top []bson.M
cursor.All(ctx, &top)
fmt.Println(len(top), top[0]["price"])

Проекция или узкая структура: кто фильтрует#

Декодирование в структуру уже фильтрует результат: поля документа, которых нет в структуре, просто игнорируются — эта механика была в пятом уроке. Возникает законный вопрос: зачем проекция, если структура и так отрежет лишнее. Ответ — в том, где происходит работа. Без проекции сервер отправляет полные документы по сети, и только декодер выбрасывает поля: трафик потрачен, батчи курсора распухли, а тяжёлое описание распаковалось, чтобы умереть в мусоре. С проекцией отбрасывание происходит до отправки: меньше байт, быстрее батчи, ниже нагрузка на декодер. Узкая структура — зеркало того же приёма в обратную сторону: отдельный тип с пятью полями для ленты и полный тип для карточки делают код честнее, чем один жирный Product на все случаи. В паре они дают максимум: узкая структура описывает форму выдачи, проекция обещает серверу, что остальное можно не отправлять. Узкая структура ещё и тестируется дешевле: функция ленты принимает и возвращает пять полей, и тестовый литерал не тащит двадцать. Что выбирать при bson.M-результатах — проекция единственная защита от перекачки данных.

Соберите запрос с опциями

Восстановите цепочку: сортировка по убыванию цены, лимит и исключение _id из проекции.

cursor, err := coll.Find( ctx, bson.M{"category": cat}, options.Find(). Set(bson.D{{Key: "price", Value: -1}}). Set(10). SetProjection(bson.M{"name": 1, "": 0}), )

Соберите метод пагинации

Расставьте строки метода по порядку: вычисление skip, запрос с опциями, обработка ошибки, декодирование страницы.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: опции Find

Термин

FindOptions

нажмите, чтобы перевернуть
Определение

Билдер options.Find() с цепочкой Set-методов: SetSort, SetLimit, SetSkip, SetProjection, SetBatchSize. Пустой билдер = дефолтное поведение Find.

1 / 6

Опции Find работают и в паре с другими глаголами: сортировка из этого урока — обязательная пара к лимиту, а проекция встретится в агрегациях пятнадцатого урока в виде этапа $project. Дальше список усложняется по смыслу: в уроке про $and, $or и $regex соберём фильтры поиска, которые уходят в тот же Find вместе с опциями, а итоговый проект курса соберёт каталог целиком: фильтры, сортировка, пагинация и счётчики на одной странице. До этого стоит закрепить механику на живой базе: песочница сайта — тематическая программа на чистом Go, поэтому запросы с опциями гоняйте локально в mongosh с explain — там видно, как сервер отрезал лимит и взял ли индекс под сортировку.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • options.Find() — единый билдер: SetProjection для полей, SetSort для порядка, SetLimit и SetSkip для объёма.
  • Сортировка принимает bson.D: мультиключевая bson.M отвергается клиентской ошибкой ErrMapForOrderedArgument.
  • Включающая проекция отдаёт перечисленные поля плюс _id; исключение _id — единственное смешивание, которое разрешено.
  • Непроектированные поля декодируются нулями: проекция меняется вместе со структурой, иначе отчёты видят нули.
  • Лимит без сортировки отдаёт произвольный набор документов — пара SetSort плюс SetLimit обязательна.
  • Skip растёт линейно по глубине страницы: для глубоких листаний дешевле диапазон по _id.
  • CountDocuments даёт число совпадений для пагинации, SetBatchSize режет транспорт, а не результат.
  • У FindOneOptions те же SetSort и SetProjection, что у Find; нет только SetLimit — одиночному документу он не нужен.

Домашнее задание

Соберите страницу каталога: фильтр по категории, сортировка по цене с переключением направления, лимит 20 и skip по номеру страницы, проекция карточки из трёх полей. Затем измерьте разницу: запрос с проекцией и без неё на коллекции с тяжёлым полем-описанием — локальная база и mongosh с explain покажут разницу в пересылаемых байтах. Добавьте в страницу переключение направления сортировки и проверьте, что CountDocuments и Find получают один и тот же собранный фильтр — рассинхрон двух фильтров чаще всего ломает номера страниц.

Личные заметкиПусто
Хранятся только в вашем браузере (MongoDB · Проекции, сортировка, лимиты)

Вопросы и ответы по уроку

Почему драйвер не принимает bson.M в SetSort?

Потому что обход map в Go не упорядочен, а порядок полей сортировки определяет результат для равных значений: драйвер не может выбрать, «price до name» или наоборот. Мультиключевая карта в Sort даёт клиентскую ошибку ErrMapForOrderedArgument — bson.D со срезом пар сохраняет порядок явно.

Проекция или узкая структура — что выбрать для списков?

Вместе: узкая структура описывает форму выдачи ленты, проекция обещает серверу не отправлять остальное. Одна структура без проекции гоняет полные документы по сети, одна проекция без структуры оставляет bson.M без типов. Для карточки товара — отдельный полный тип.

Почему на глубокой странице запрос тормозит?

SetSkip(10000) заставляет сервер пройтись и отбросить десять тысяч совпавших документов перед выдачей: цена растёт линейно с глубиной. Для «дальше и дальше» используют диапазон по _id: {_id: {$gt: последний}} читает следующую страницу из индекса без отбрасывания. Если страницы листают вслед за новыми записями — лента событий, журнал — диапазон дешевле с первого же листа.

Термины урока

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

Следующая ступень учебного пути

zerolog25 уроков~12 чпрактика25 уроков уже доступны

Zerolog в Go: быстрое JSON-логирование

Курс zerolog для Go: JSON-события, уровни, поля, ConsoleWriter, контекстные поля, ротация, sampling, HTTP-логи и тесты. 25 уроков с примерами.

  • Писать структурированные JSON-логи с нуля аллокаций
  • Выбирать уровни и поля события осознанно
  • Настраивать ConsoleWriter для dev и JSON для прода
  • Строить сублоггеры и контекстные поля на запрос