Гид по технологиям

Как писать сложные запросы Firebase в Angular и создавать составные индексы

9 min read Web‑разработка Обновлено 02 Jan 2026
Firebase + Angular: составные индексы и сложные запросы
Firebase + Angular: составные индексы и сложные запросы

Большая библиотека с полками, заполненными книгами

Введение

Firebase предоставляет облачную NoSQL-базу данных Cloud Firestore, которую удобно интегрировать с Angular-приложениями. Firestore отлично подходит для быстрых CRUD-операций, но у него есть важная особенность: при выполнении запросов, использующих комбинацию полей (например, where + orderBy по разным полям), часто требуется составной индекс (composite index). Если индекс отсутствует, браузер вернёт ошибку с ссылкой на создание индекса.

Определения в одну строку

  • Cloud Firestore — документно-ориентированная NoSQL-база в Firebase.
  • Составной индекс — заранее построенная структура, которая позволяет быстро выполнять запросы по нескольким полям.

Что вы получите из этой статьи

  • Пошаговая настройка Angular + Firebase.
  • Пример сервиса и компонента для запроса «товар с минимальным запасом у поставщика».
  • Инструкция по созданию составного индекса в консоли Firebase и через файл индексов.
  • Методика отладки и проверка статуса индекса.
  • Альтернативы и рекомендации по архитектуре данных.

Important: перед публикацией на продакшн убедитесь, что правила безопасности Firestore (Firestore Security Rules) не позволяют неавторизованному доступу к данным.

Подготовка: Angular-приложение и база данных Firebase

Перед написанием запросов выполните базовую настройку:

  1. Если у вас нет Angular-проекта, создайте его командой:
ng new new-angular-app
  1. Создайте проект в Firebase и включите Cloud Firestore.

  2. В Cloud Firestore создайте две коллекции: Product и Supplier. Поставщик может иметь несколько товаров; у товара есть поле supplierId, связывающее его с поставщиком.

  3. Добавьте примеры документов в коллекцию Product. Поля name, productId и supplierId — строки; price и inStock — числа.

Document IDFields

| product1 | - name: “Ribbons”

  • price: 12.99
  • inStock: 10
  • productId: “P1”
  • supplierId: “S1” | | product2 | - name: “Balloons”
  • price: 1.5
  • inStock: 2
  • productId: “P2”
  • supplierId: “S1” | | product3 | - name: “Paper”
  • price: 2.99
  • inStock: 20
  • productId: “P3”
  • supplierId: “S1” | | product4 | - name: “Table”
  • price: 199
  • inStock: 1
  • productId: “P4”
  • supplierId: “S2” |

Вот пример того, как должна выглядеть таблица Product: Таблица Product в базе Cloud Firestore с документами и полями

  1. Добавьте документы в коллекцию Supplier (все поля — строки).
Document IDFields

| supplier1 | - name: “Arts and Crafts Supplier”

  • location: “California, USA”
  • supplierId: “S1” | | supplier2 | - name: “Amazing Tables”
  • location: “Sydney, Australia”
  • supplierId: “S2” |

Пример записи supplier1: Таблица Supplier в базе Cloud Firestore, запись supplier1

  1. Установите angular/fire в проект:
npm i @angular/fire
  1. В Firebase Console откройте Настройки проекта и добавьте приложение (иконка «угловые скобки»). Настройки проекта Firebase: иконка угловых скобок для добавления конфигурации

  2. Firebase даст настройки подключения (firebaseConfig). Сохраните их для окружения. Конфигурация Firebase для подключения приложения

  3. В файле environments/environment.ts замените содержимое на объект с firebaseConfig (подставьте свои значения):

export const environment = {  
  production: false,  
  firebaseConfig: {  
    apiKey: "AIzaSyBzVyXBhDlvXQEltNkE9xKq-37UBIanDlM",  
    authDomain: "muo-firebase-queries.firebaseapp.com",  
    projectId: "muo-firebase-queries",  
    storageBucket: "muo-firebase-queries.appspot.com",  
    messagingSenderId: "569911365044",  
    appId: "1:569911365044:web:9557bfef800caa5cdaf6e1"  
  }  
};

(Замените все поля на свои значения из консоли.)

  1. Импортируйте environment и модули AngularFire в src/app/app.module.ts:
import { environment } from "../environments/environment";  
import { AngularFireModule } from '@angular/fire/compat';  
import { AngularFirestoreModule } from "@angular/fire/compat/firestore";
  1. Добавьте в imports:
AngularFirestoreModule,  
AngularFireModule.initializeApp(environment.firebaseConfig)

Теперь приложение готово к выполнению запросов в Firestore.

Как написать сложный запрос в сервисе Angular

Идея: выполнить два запроса — сначала найти поставщика по имени, затем найти продукт у этого поставщика с минимальным значением inStock.

  1. Создайте папку services и файл service.ts (или services.service.ts) в ней. Создание файла service.ts в папке services Angular

  2. Пример сервиса с импортами и конструктором:

import { Injectable } from '@angular/core';  
import { AngularFirestore } from '@angular/fire/compat/firestore';  
@Injectable({  
  providedIn: 'root'  
})  
export class Service {  
  constructor(private db: AngularFirestore) { }  
}
  1. Логика: Firestore не поддерживает прямой SQL-подобный JOIN, поэтому требуется два запроса: один к Supplier, другой к Product.

  2. Функция getSupplier() — находит поставщика по имени:

getSupplier(name: string) {  
    return new Promise((resolve)=> {  
      this.db.collection('Supplier', ref => ref.where('name', '==', name)).valueChanges().subscribe(supplier => resolve(supplier))  
    })  
  }
  1. Функция getProductsFromSupplier() — возвращает продукт с наименьшим inStock у поставщика:
getProductsFromSupplier(supplierId: string) {  
    return new Promise((resolve)=> {  
      this.db.collection('Product', ref => ref.where('supplierId', '==', supplierId).orderBy('inStock').startAt(0).limit(1)).valueChanges().subscribe(product => resolve(product))  
    })  
  }

Обратите внимание: сочетание where(‘supplierId’, ‘==’, …) и orderBy(‘inStock’) — это пример запроса, который может требовать составного индекса.

  1. В src/app/app.component.ts импортируйте сервис:
import { Service } from 'src/app/services/service';
  1. Добавьте сервис в конструктор компонента:
constructor(private service: Service) { }
  1. Добавьте переменную для результата и вызов в ngOnInit:
products: any;  
ngOnInit(): void {  
    this.getProductStock();  
}  
async getProductStock() {  
    
}
  1. Внутри getProductStock используйте сервисные функции последовательно:
let supplier = await this.service.getSupplier('Arts and Crafts Supplier');   
this.products = await this.service.getProductsFromSupplier(supplier[0].supplierId);
  1. Замените содержимое src/app/app.component.html на вывод результатов:

Products with lowest stock from "Arts and Crafts Supplier"

Name: {{item.name}}

Number in stock: {{item.inStock}}

Price: ${{item.price}}

  1. Запустите приложение:
ng serve
  1. Откройте http://localhost:4200

  2. Если данные не отображаются, откройте Инструменты разработчика (Inspect) в браузере. Контекстное меню Chrome с опцией Инструменты разработчика (Inspect)

  3. Вкладка Console покажет ошибку с предложением создать индекс. Сообщение об ошибке в консоли браузера: требуется индекс для запроса

Почему возникает ошибка и как её исправить

Firestore требует индекс для эффективного выполнения запросов, комбинирующих фильтры и порядок сортировки по разным полям. Ошибка содержит ссылку, по которой можно перейти в консоль и автоматически создать нужный индекс.

Создание составного индекса через консоль

  1. Нажмите по ссылке из сообщения об ошибке или откройте Firebase Console → Firestore → Индексы.
  2. Войдите в аккаунт Firebase, если требуется.
  3. Нажмите Create index (Создать индекс). Окно создания составного индекса в консоли Firebase
  4. Подтвердите поля и порядок (обычно supplierId ASC, inStock ASC для примера выше).
  5. Дождитесь статуса «Enabled» (сборка индекса может занять несколько минут). Список составных индексов в консоли Firebase со статусом Building/Enabled
  6. Обновите страницу приложения — запрос выполнится, и ошибка в консоли исчезнет. Страница приложения в Chrome, отображающая товар с наименьшим запасом

Важно: индексы влияют на время записи (write latency) и хранилище. Чем больше индексов — тем больше накладных расходов на запись и хранение.

Как определить, нужен ли составной индекс

Правило простое: если вы комбинируете фильтры и одновременно используете orderBy по полю, которого нет в базовом предикате, Firestore может попросить индекс. Если запрос использует только одно поле (или простые equality/== по единственному полю), индекс обычно не требуется.

Примеры запросов и поведение:

  • ref.where(‘supplierId’, ‘==’, id).orderBy(‘inStock’) — часто требует составного индекса.
  • ref.where(‘supplierId’, ‘==’, id).where(‘category’, ‘==’, c) — может потребовать индекс, если вы используете оба поля в фильтре и их сочетание не переопределено автоматически.
  • ref.orderBy(‘createdAt’).limit(10) — обычно не требует составного индекса.

Альтернативы и архитектурные варианты

Если создание индекса нежелательно (например, слишком много индексов или частые записи), рассмотрите альтернативы:

  1. Денормализация данных
  • Преобразуйте модель: храните у поставщика поле с массивом id товаров или предвычисленный документ с минимальным запасом.
  • Плюсы: быстрые чтения, меньше индексов.
  • Минусы: сложнее поддерживать целостность, обновления становятся дороже.
  1. Материализованные представления (precomputed views)
  • Отдельная коллекция с агрегированными данными (например, supplierMinStock), обновляемая Cloud Functions при изменении Product.
  1. Использование Cloud Functions или серверного слоя
  • Выполняйте сложную логику на сервере (или в Cloud Functions) и возвращайте готовый результат клиенту.
  1. Переосмысление UX
  • Загружайте больше данных на клиент (например, все продукты поставщика) и выполняйте фильтрацию/сортировку локально, если объём данных мал.

Когда эти подходы не подходят

  • Если у вас требуются свежие данные в реальном времени для тысяч поставщиков, материализованные представления и индексы остаются предпочтительными.

Пример файла индексов firestore.indexes.json

Если вы используете CI/CD, можно хранить индексы в репозитории и деплоить их через Firebase CLI. Пример формата:

{
  "indexes": [
    {
      "collectionGroup": "Product",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "supplierId", "order": "ASCENDING" },
        { "fieldPath": "inStock", "order": "ASCENDING" }
      ]
    }
  ],
  "fieldOverrides": []
}

Команда деплоя:

firebase deploy --only firestore:indexes

Не забудьте настроить Firebase CLI и авторизацию перед деплоем.

Методика отладки индексов (короткая)

  1. Запустите запрос в приложении.
  2. Если в консоли браузера есть ошибка — откройте ссылку на создание индекса.
  3. Создайте индекс в консоли или через firestore.indexes.json.
  4. Подождите статуса Enabled.
  5. Обновите страницу и проверьте результат.
  6. Если ошибка повторяется — проверьте порядок полей и типы данных (строка vs число).

Критерии приёмки

  • Приложение успешно выполняет запрос без ошибок в консоли.
  • Статус индекса в консоли — Enabled.
  • Результат соответствует ожидаемому (товар с минимальным inStock у выбранного поставщика).

Чек-листы (роль-based)

Разработчик (Frontend):

  • Подключил @angular/fire и AngularFirestoreModule.
  • Правильно настроил environment.firebaseConfig.
  • Реализовал сервисы с обработкой ошибок и подписями на valueChanges().
  • Проверил типы полей (числа/строки).
  • Открыл консоль браузера и проверил сообщения об индексах.

Администратор Firebase:

  • Создал составной индекс в консоли или выгрузил firestore.indexes.json в репозиторий.
  • Проверил статус индекса до Enabled.
  • Оценил влияние индекса на стоимость и время записи.
  • Убедился, что правила безопасности Firestore корректны.

Частые ошибки и как их исправлять

  • Ошибка: “The query requires an index.” — следовать ссылке в сообщении и создать индекс.
  • Ошибка: пустой результат после создания индекса — проверьте, что порядок полей в индексе совпадает с порядком в запросе (orderBy и where combinations).
  • Неправильные типы данных (строка vs число) — убедитесь, что в документах типы полей соответствуют ожиданиям.

Безопасность и приватность

  • Правила безопасности Firestore должны ограничивать чтение/запись данных только авторизованным пользователям или по бизнес-логике.
  • Избегайте публикации API-ключей в публичном репозитории; firebaseConfig содержит apiKey, который по сути идентификатор приложения, но не секретный ключ. Тем не менее, правила безопасности обязаны защищать данные.

Ментальные модели и эвристики

  • «Индекс — это карта»: если запрос использует два или более поля, Firestore просит иметь карту (индекс), чтобы не сканировать все документы.
  • «Денормализация ради чтения»: в мобильных и web-приложениях часто стоит оптимизировать модель под быстрые чтения, даже если это удорожает записи.

Пример короткого runbook при проблеме с индексом

  1. Пользователь сообщает, что данные не отображаются.
  2. Откройте консоль браузера → снимок ошибки.
  3. Перейдите по ссылке для создания индекса.
  4. Если индекс уже создан, проверьте его статус в Firebase Console → Firestore → Indices.
  5. Если индекс «Building» — дождитесь; если «Error» — проверьте поля и попробуйте пересоздать.
  6. Сообщите пользователю, когда индекс Enabled.

Краткая галерея крайних случаев

  • Много индексов + частые записи → рост затрат и замедление записи. Решение: материализованные представления, denormalization, серверные вычисления.
  • Непредсказуемая схема данных → используйте строгие соглашения об именах полей и типах, либо схему на сервере.

Сравнение подходов (кратко)

  • Денормализация vs индексы: денормализация даёт быстрые чтения, но усложняет обновления; индексы сохраняют нормализованную модель, но увеличивают расходы на запись.

Схема принятия решения (Mermaid)

flowchart TD
  A[Нужно получить данные по нескольким полям?] -->|Нет| B[Обычный запрос, без индекса]
  A -->|Да| C[Пробуем выполнить запрос]
  C --> D{Ошибка 'requires index'?}
  D -->|Нет| E[Результат готов]
  D -->|Да| F[Создать индекс]
  F --> G{Статус}
  G -->|Building| H[Подождать]
  G -->|Enabled| E
  G -->|Error| I[Проверить поля/типы и пересоздать]

Глоссарий (1 строка на термин)

  • Composite index — индекс по комбинации полей для ускорения сложных запросов.
  • Denormalization — хранение дублирующихся данных для оптимизации чтения.
  • Cloud Functions — серверная логика в платформе Firebase/Google Cloud.

Заключение

Firestore делает сложные клиентские запросы быстрыми, но иногда требует составных индексов. В большинстве случаев решение простое: создать индекс по указанным полям и дождаться его включения. Если индексов слишком много или запись данных критична, рассмотрите денормализацию, материализованные представления или серверную агрегацию.

Summary: создание индекса через консоль занимает несколько минут, но решает проблему. Продумайте архитектуру данных заранее, чтобы минимизировать количество составных индексов.

Примечание: альтернативные NoSQL-решения (например MongoDB) предлагают отличные паттерны для агрегации и индексации — изучите их, если у вас сложные требования к аналитике.

Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Изменить скорость видео в TikTok — быстро и просто
Руководство

Изменить скорость видео в TikTok — быстро и просто

Как пригласить и привлечь друзей в BeReal
Социальные сети

Как пригласить и привлечь друзей в BeReal

Как удалить аккаунт BeReal — полное руководство
Социальные сети

Как удалить аккаунт BeReal — полное руководство

Исправить лаги и плохое качество стримов в Discord
Техподдержка

Исправить лаги и плохое качество стримов в Discord

Изменить фото профиля и видео в TikTok
Социальные сети

Изменить фото профиля и видео в TikTok

Удалить или деактивировать аккаунт AskFm
Социальные сети

Удалить или деактивировать аккаунт AskFm