Jeśli kolekcja zawiera dokumenty z sekwencyjnymi wartościami indeksowanymi, Cloud Firestore ogranicza szybkość zapisu do 500 zapisów na sekundę. Na tej stronie opisano, jak podzielić pole dokumentu na fragmenty, aby obejść ten limit. Najpierw zdefiniujmy, co rozumiemy przez „sekwencyjne pola indeksowane”, i wyjaśnijmy, kiedy obowiązuje ten limit.
Sekwencyjne pola indeksowane
„Sekwencyjne pola indeksowane” oznaczają dowolną kolekcję dokumentów, która zawiera monotonicznie rosnące lub malejące pole indeksowane. W wielu przypadkach oznacza to pole timestamp, ale każda monotonicznie rosnąca lub malejąca wartość pola może spowodować przekroczenie limitu 500 zapisów na sekundę.
Limit dotyczy na przykład kolekcji user dokumentów z indeksowanym polem userid, jeśli aplikacja przypisuje wartości userid w ten sposób:
1281, 1282, 1283, 1284, 1285, ...
Z drugiej strony nie wszystkie pola timestamp powodują przekroczenie tego limitu. Jeśli pole timestamp śledzi losowo rozłożone wartości, limit zapisu nie obowiązuje. Rzeczywista wartość pola również nie ma znaczenia, liczy się tylko to, czy pole rośnie lub maleje monotonicznie. Na przykład oba te zestawy monotonicznie rosnących wartości pól powodują przekroczenie limitu zapisu:
100000, 100001, 100002, 100003, ...0, 1, 2, 3, ...
Dzielenie pola sygnatury czasowej na fragmenty
Załóżmy, że Twoja aplikacja używa monotonicznie rosnącego pola timestamp.
Jeśli Twoja aplikacja nie używa pola timestamp w żadnych zapytaniach, możesz usunąć limit 500 zapisów na sekundę, nie indeksując pola sygnatury czasowej. Jeśli w zapytaniach potrzebujesz pola timestamp, możesz obejść ten limit, używając sygnatur czasowych z podziałem na fragmenty:
- Dodaj pole
shardobok polatimestamp. Użyj1..nróżnych wartości w polushard. Zwiększy to limit zapisu dla kolekcji do500*n, ale musisz zagregować zapytanian. - Zaktualizuj logikę zapisu, aby losowo przypisywać wartość
sharddo każdego dokumentu. - Zmień swoje zapytania, aby agregować podzielone na fragmenty zbiory wyników.
- Wyłącz indeksy pojedynczego pola dla pól
sharditimestamp. Usuń istniejące indeksy złożone, które zawierają poletimestamp. - Utwórz nowe indeksy złożone, aby obsługiwać zaktualizowane zapytania. Kolejność pól w indeksie ma znaczenie, a pole
shardmusi znajdować się przed polemtimestamp. Wszystkie indeksy zawierające poletimestampmuszą też zawierać poleshard.
Sygnatury czasowe z podziałem na fragmenty należy wdrażać tylko w przypadkach, gdy liczba zapisów na sekundę stale przekracza 500. W przeciwnym razie będzie to przedwczesna optymalizacja. Fragmentowanie pola timestamp usuwa ograniczenie 500 zapisów na sekundę, ale wymaga agregacji zapytań po stronie klienta.
Poniższe przykłady pokazują, jak podzielić pole timestamp na fragmenty i jak wysłać zapytanie do podzielonego na fragmenty zbioru wyników.
Przykładowy model danych i zapytania
Wyobraź sobie na przykład aplikację do analizy instrumentów finansowych, takich jak waluty, akcje zwykłe i ETF-y, w czasie zbliżonym do rzeczywistego. Ta aplikacja zapisuje dokumenty w kolekcji instruments w ten sposób:
Node.js
async function insertData() { const instruments = [ { symbol: 'AAA', price: { currency: 'USD', micros: 34790000 }, exchange: 'EXCHG1', instrumentType: 'commonstock', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.010Z')) }, { symbol: 'BBB', price: { currency: 'JPY', micros: 64272000000 }, exchange: 'EXCHG2', instrumentType: 'commonstock', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.101Z')) }, { symbol: 'Index1 ETF', price: { currency: 'USD', micros: 473000000 }, exchange: 'EXCHG1', instrumentType: 'etf', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.001Z')) } ]; const batch = fs.batch(); for (const inst of instruments) { const ref = fs.collection('instruments').doc(); batch.set(ref, inst); } await batch.commit(); }
Ta aplikacja uruchamia te zapytania i porządkuje wyniki według pola timestamp:
Node.js
function createQuery(fieldName, fieldOperator, fieldValue, limit = 5) { return fs.collection('instruments') .where(fieldName, fieldOperator, fieldValue) .orderBy('timestamp', 'desc') .limit(limit) .get(); } function queryCommonStock() { return createQuery('instrumentType', '==', 'commonstock'); } function queryExchange1Instruments() { return createQuery('exchange', '==', 'EXCHG1'); } function queryUSDInstruments() { return createQuery('price.currency', '==', 'USD'); }
insertData() .then(() => { const commonStock = queryCommonStock() .then( (docs) => { console.log('--- queryCommonStock: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); const exchange1Instruments = queryExchange1Instruments() .then( (docs) => { console.log('--- queryExchange1Instruments: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); const usdInstruments = queryUSDInstruments() .then( (docs) => { console.log('--- queryUSDInstruments: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); return Promise.all([commonStock, exchange1Instruments, usdInstruments]); });
Po przeprowadzeniu analizy ustalasz, że aplikacja będzie otrzymywać od 1000 do 1500 aktualizacji instrumentów na sekundę. Przekracza to limit 500 zapisów na sekundę dozwolonych w przypadku kolekcji zawierających dokumenty z indeksowanymi polami sygnatury czasowej. Aby zwiększyć przepustowość zapisu, potrzebujesz 3 wartości fragmentu: MAX_INSTRUMENT_UPDATES/500 = 3. W tym przykładzie użyto wartości fragmentów x, y i z. Możesz też używać liczb lub innych znaków jako wartości fragmentów.
Dodawanie pola fragmentu
Dodaj do dokumentów pole shard. Ustaw pole shard na wartości x, y lub z, co zwiększy limit zapisu w kolekcji do 1500 zapisów na sekundę.
Node.js
// Define our 'K' shard values const shards = ['x', 'y', 'z']; // Define a function to help 'chunk' our shards for use in queries. // When using the 'in' query filter there is a max number of values that can be // included in the value. If our number of shards is higher than that limit // break down the shards into the fewest possible number of chunks. function shardChunks() { const chunks = []; let start = 0; while (start < shards.length) { const elements = Math.min(MAX_IN_VALUES, shards.length - start); const end = start + elements; chunks.push(shards.slice(start, end)); start = end; } return chunks; } // Add a convenience function to select a random shard function randomShard() { return shards[Math.floor(Math.random() * Math.floor(shards.length))]; }
async function insertData() { const instruments = [ { shard: randomShard(), // add the new shard field to the document symbol: 'AAA', price: { currency: 'USD', micros: 34790000 }, exchange: 'EXCHG1', instrumentType: 'commonstock', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.010Z')) }, { shard: randomShard(), // add the new shard field to the document symbol: 'BBB', price: { currency: 'JPY', micros: 64272000000 }, exchange: 'EXCHG2', instrumentType: 'commonstock', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.101Z')) }, { shard: randomShard(), // add the new shard field to the document symbol: 'Index1 ETF', price: { currency: 'USD', micros: 473000000 }, exchange: 'EXCHG1', instrumentType: 'etf', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.001Z')) } ]; const batch = fs.batch(); for (const inst of instruments) { const ref = fs.collection('instruments').doc(); batch.set(ref, inst); } await batch.commit(); }
Wykonywanie zapytania dotyczącego sygnatury czasowej we fragmentach
Dodanie pola shard wymaga zaktualizowania zapytań, aby agregować wyniki podzielone na fragmenty:
Node.js
function createQuery(fieldName, fieldOperator, fieldValue, limit = 5) { // For each shard value, map it to a new query which adds an additional // where clause specifying the shard value. return Promise.all(shardChunks().map(shardChunk => { return fs.collection('instruments') .where('shard', 'in', shardChunk) // new shard condition .where(fieldName, fieldOperator, fieldValue) .orderBy('timestamp', 'desc') .limit(limit) .get(); })) // Now that we have a promise of multiple possible query results, we need // to merge the results from all of the queries into a single result set. .then((snapshots) => { // Create a new container for 'all' results const docs = []; snapshots.forEach((querySnapshot) => { querySnapshot.forEach((doc) => { // append each document to the new all container docs.push(doc); }); }); if (snapshots.length === 1) { // if only a single query was returned skip manual sorting as it is // taken care of by the backend. return docs; } else { // When multiple query results are returned we need to sort the // results after they have been concatenated. // // since we're wanting the `limit` newest values, sort the array // descending and take the first `limit` values. By returning negated // values we can easily get a descending value. docs.sort((a, b) => { const aT = a.data().timestamp; const bT = b.data().timestamp; const secondsDiff = aT.seconds - bT.seconds; if (secondsDiff === 0) { return -(aT.nanoseconds - bT.nanoseconds); } else { return -secondsDiff; } }); return docs.slice(0, limit); } }); } function queryCommonStock() { return createQuery('instrumentType', '==', 'commonstock'); } function queryExchange1Instruments() { return createQuery('exchange', '==', 'EXCHG1'); } function queryUSDInstruments() { return createQuery('price.currency', '==', 'USD'); }
insertData() .then(() => { const commonStock = queryCommonStock() .then( (docs) => { console.log('--- queryCommonStock: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); const exchange1Instruments = queryExchange1Instruments() .then( (docs) => { console.log('--- queryExchange1Instruments: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); const usdInstruments = queryUSDInstruments() .then( (docs) => { console.log('--- queryUSDInstruments: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); return Promise.all([commonStock, exchange1Instruments, usdInstruments]); });
Aktualizowanie definicji indeksów
Aby usunąć ograniczenie do 500 zapisów na sekundę, usuń istniejące indeksy jednopolowe i złożone, które używają pola timestamp.
Usuwanie definicji indeksów złożonych
Konsola Firebase
W konsoli Firebase otwórz kartę Databases & Storage (Bazy danych i miejsce na dane) > Firestore > Composite Indexes (Indeksy wieloskładnikowe).
W przypadku każdego indeksu zawierającego pole
timestampkliknij przycisk , a następnie Usuń.
konsola GCP
W konsoli Google Cloud otwórz stronę Bazy danych.
Wybierz wymaganą bazę danych z listy baz danych.
W menu nawigacyjnym kliknij Indeksy, a następnie kliknij kartę Złożone.
Użyj pola Filtr, aby wyszukać definicje indeksów zawierające pole
timestamp.Przy każdym z tych indeksów kliknij przycisk , a następnie Usuń.
wiersz poleceń Firebase
- Jeśli nie masz skonfigurowanego interfejsu wiersza poleceń Firebase, wykonaj te instrukcje, aby go zainstalować i uruchomić polecenie
firebase init. Podczas wykonywania poleceniainitwybierzFirestore: Deploy rules and create indexes for Firestore. - Podczas konfiguracji wiersz poleceń Firebase pobiera istniejące definicje indeksów do pliku o domyślnej nazwie
firestore.indexes.json. Usuń wszystkie definicje indeksów zawierające pole
timestamp, na przykład:{ "indexes": [ // Delete composite index definition that contain the timestamp field { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "exchange", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "instrumentType", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "price.currency", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, ] }Wdróż zaktualizowane definicje indeksów:
firebase deploy --only firestore:indexes
Aktualizowanie definicji indeksów pojedynczego pola
Konsola Firebase
W konsoli Firebase otwórz kartę Bazy danych i miejsce na dane > Firestore > Indeksy jednopolowe.
Kliknij Dodaj zwolnienie.
W polu Identyfikator kolekcji wpisz
instruments. W polu Ścieżka pola wpisztimestamp.W sekcji Zakres zapytania wybierz Kolekcja i Grupa kolekcji.
Kliknij Dalej.
Ustaw wszystkie ustawienia indeksu na Wyłączone. Kliknij Zapisz.
Powtórz te same czynności w przypadku pola
shard.
konsola GCP
W konsoli Google Cloud otwórz stronę Bazy danych.
Wybierz wymaganą bazę danych z listy baz danych.
W menu nawigacyjnym kliknij Indeksy, a następnie kartę Pojedyncze pole.
Kliknij kartę Pojedyncze pole.
Kliknij Dodaj zwolnienie.
W polu Identyfikator kolekcji wpisz
instruments. W polu Ścieżka pola wpisztimestamp.W sekcji Zakres zapytania wybierz Kolekcja i Grupa kolekcji.
Kliknij Dalej.
Ustaw wszystkie ustawienia indeksu na Wyłączone. Kliknij Zapisz.
Powtórz te same czynności w przypadku pola
shard.
wiersz poleceń Firebase
Dodaj do sekcji
fieldOverridespliku definicji indeksu te informacje:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }Wdróż zaktualizowane definicje indeksów:
firebase deploy --only firestore:indexes
Tworzenie nowych indeksów złożonych
Po usunięciu wszystkich poprzednich indeksów zawierających timestamp zdefiniuj nowe indeksy, których wymaga aplikacja. Każdy indeks zawierający pole timestamp musi też zawierać pole shard. Aby na przykład obsługiwać powyższe zapytania, dodaj te indeksy:
| Kolekcja | Zindeksowane pola | Zakres zapytania |
|---|---|---|
| instrumenty | fragment, price.currency, sygnatura czasowa | Kolekcja |
| instrumenty | fragment, wymiana wiadomości, sygnatura czasowa | Kolekcja |
| instrumenty | fragment, instrumentType, sygnatura czasowa | Kolekcja |
Komunikaty o błędach
Możesz utworzyć te indeksy, uruchamiając zaktualizowane zapytania.
Każde zapytanie zwraca komunikat o błędzie z linkiem do utworzenia wymaganego indeksu w konsoli Firebase.
wiersz poleceń Firebase
Dodaj do pliku definicji indeksu te indeksy:
{ "indexes": [ // New indexes for sharded timestamps { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "shard", "order": "DESCENDING" }, { "fieldPath": "exchange", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "shard", "order": "DESCENDING" }, { "fieldPath": "instrumentType", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "shard", "order": "DESCENDING" }, { "fieldPath": "price.currency", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, ] }Wdróż zaktualizowane definicje indeksów:
firebase deploy --only firestore:indexes
Omówienie limitu zapisu dla sekwencyjnych pól indeksowanych
Limit szybkości zapisu w przypadku pól indeksowanych sekwencyjnie wynika ze sposobu, w jaki Cloud Firestore przechowuje wartości indeksu i skaluje zapisy indeksu. W przypadku każdego zapisu indeksu Cloud Firestore definiuje wpis klucz-wartość, który łączy nazwę dokumentu i wartość każdego indeksowanego pola. Cloud Firestore porządkuje te wpisy indeksu w grupy danych nazywane tabletami. Każdy serwer Cloud Firestore zawiera co najmniej 1 tablet. Gdy obciążenie zapisem na określonym tablecie staje się zbyt duże, Cloud Firestore skaluje się poziomo, dzieląc tablet na mniejsze tablety i rozprzestrzeniając nowe tablety na różne serwery Cloud Firestore.
Cloud Firestore umieszcza wpisy indeksu, które są blisko siebie pod względem leksykograficznym, na tym samym tablecie. Jeśli wartości indeksu w tablecie są zbyt blisko siebie, np. w przypadku pól sygnatury czasowej, Cloud Firestore nie może skutecznie podzielić tabletu na mniejsze tablety. Powstaje wtedy punkt krytyczny, w którym pojedyncza tabela otrzymuje zbyt duży ruch, a operacje odczytu i zapisu w tym punkcie stają się wolniejsze.
Dzięki fragmentowaniu pola sygnatury czasowej Cloud Firestore może efektywnie rozdzielać zadania między wiele tabletów. Chociaż wartości pola sygnatury czasowej mogą być zbliżone, połączone wartości fragmentu i indeksu dają usłudze Cloud Firestore wystarczająco dużo miejsca między wpisami indeksu, aby podzielić je między wiele tabletów.
Co dalej?
- Przeczytaj sprawdzone metody projektowania pod kątem skalowania.
- W przypadku wysokich częstotliwości zapisu w pojedynczym dokumencie zapoznaj się z sekcją Liczniki rozproszone.
- Zobacz standardowe limity dla: Cloud Firestore