Jeśli kolekcja zawiera dokumenty z sekwencyjnymi indeksowanymi wartościami: Cloud Firestore ogranicza szybkość zapisu do 500 zapisów na sekundę. Ta strona opisuje, jak podzielić pole dokumentu, aby przekroczyć ten limit. Najpierw zobaczmy, wyjaśnić, co rozumiemy przez „pola indeksowane sekwencyjnie” i sprecyzować, kiedy granica ma zastosowanie.
Pola indeksowane sekwencyjnie
„Pola zindeksowane sekwencyjnie” oznacza dowolny zbiór dokumentów, który zawiera
monotonicznie rosnącego lub malejącego indeksowanego pola. W wielu przypadkach oznacza to,
pole timestamp
, ale każda monotonicznie rosnąca lub malejąca wartość pola
może aktywować limit zapisu wynoszący 500 zapisów na sekundę.
Na przykład limit dotyczy zbioru user
dokumentów z
zindeksowane pole userid
, jeśli aplikacja przypisuje wartości userid
, na przykład:
1281, 1282, 1283, 1284, 1285, ...
Z drugiej strony nie wszystkie pola timestamp
aktywują ten limit. Jeśli
Pole timestamp
śledzi losowo rozproszone wartości, limit zapisu nie
zastosuj. Nie ma też znaczenia, jaką wartość jest wpisana; wartość pola
monotonicznie rosną lub maleją. Przykład:
oba poniższe zbiory monotonicznie rosnących wartości pól aktywują
limit zapisu:
100000, 100001, 100002, 100003, ...
0, 1, 2, 3, ...
Fragmentowanie pola sygnatury czasowej
Załóżmy, że Twoja aplikacja używa monotonicznie rosnącego pola timestamp
.
Jeśli aplikacja nie używa pola timestamp
w żadnych zapytaniach, możesz usunąć pole
Ograniczenie do 500 zapisów na sekundę przy braku indeksowania pola sygnatury czasowej. Jeśli tak,
wymagają pola timestamp
w zapytaniach, możesz obejść limit przez
przy użyciu podzielonych sygnatur czasowych:
- Dodaj pole
shard
obok polatimestamp
. Używaj różnych znaków „1..n
” w polushard
. Zwiększa to liczbę zapisów dla zbioru do500*n
, ale musisz agregować zapytanian
. - Zmień logikę zapisu, by losowo przypisywać wartość
shard
do każdego z nich dokument. - Zaktualizuj zapytania, aby agregować pofragmentowane zbiory wyników.
- Wyłącz indeksy z jednym polem zarówno w polu
shard
, jak i wtimestamp
. Usuń istniejące indeksy złożone zawierającetimestamp
. - Utwórz nowe indeksy złożone, które będą obsługiwać zaktualizowane zapytania. Kolejność
pola w indeksie mają znaczenie, a pole
shard
musi znajdować się przedtimestamp
. Wszystkie indeksy zawierające parametr Poletimestamp
musi też zawierać poleshard
.
Sygnatury czasowe podzielone na fragmenty należy wdrażać tylko w przypadkach użycia z długotrwałymi
z szybkością zapisu powyżej 500 zapisów na sekundę. W przeciwnym razie jest to
przedwczesnej optymalizacji. Fragmentacja pola timestamp
powoduje usunięcie 500 zapisów
z ograniczeniem na sekundę, ale z ograniczeniem konieczności zapytań po stronie klienta.
agregacje.
Z przykładów poniżej dowiesz się, jak posegmentować pole timestamp
i jak utworzyć zapytanie
podzielony wynik.
Przykładowy model danych i zapytania
Wyobraź sobie na przykład aplikację do analizy finansów w czasie zbliżonym do rzeczywistego.
instrumenty takie jak waluty, akcje zwykłe i fundusze ETF. Ta aplikacja zapisuje
dokumenty do kolekcji instruments
, na przykład:
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 następujące zapytania i kolejność w polu 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 zbadaniu sprawy określasz, że aplikacja będzie otrzymywać od
1000 i 1500 aktualizacji instrumentów na sekundę. Przekracza to 500 zapisów na
druga dozwolona w przypadku kolekcji zawierających dokumenty ze zindeksowaną sygnaturą czasową
. Aby zwiększyć przepustowość zapisu, potrzebujesz 3 wartości fragmentów:
MAX_INSTRUMENT_UPDATES/500 = 3
W tym przykładzie użyto wartości fragmentu x
,
y
i z
. We fragmencie możesz również użyć cyfr lub innych znaków
.
Dodawanie pola fragmentu
Dodaj do dokumentów pole shard
. Ustawianie pola shard
na wartości x
, y
lub z
, co zwiększa limit zapisu w zbiorze
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(); }
Wysyłanie zapytania dotyczącego sygnatury czasowej podzielonej na fragmenty
Dodanie pola shard
wymaga zaktualizowania zapytań w celu agregowania danych
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]); });
Zaktualizuj definicje indeksów
Aby usunąć ograniczenie 500 zapisów na sekundę, usuń istniejące pojedyncze pole
i indeksów złożonych korzystających z pola timestamp
.
Usuń definicje indeksów złożonych
Konsola Firebase
Otwórz stronę indeksów złożonych Cloud Firestore w konsoli Firebase.
W przypadku każdego indeksu zawierającego pole
timestamp
kliknij i kliknij Usuń.
konsola GCP
W konsoli Google Cloud Platform otwórz stronę Bazy danych.
Wybierz wymaganą bazę danych z listy baz danych.
W menu nawigacyjnym kliknij Indeksy, a następnie wybierz kartę Kompozytowa.
Użyj pola Filtr, aby wyszukać definicje indeksów zawierające
timestamp
.W przypadku każdego z tych indeksów kliknij
i wybierz Usuń.
wiersz poleceń Firebase
- Jeśli nie masz skonfigurowanego interfejsu wiersza poleceń Firebase, wykonaj te instrukcje, aby je zainstalować
w interfejsie wiersza poleceń i uruchomić polecenie
firebase init
. W ramach poleceniainit
skonfiguruj wybierzFirestore: Deploy rules and create indexes for Firestore
. - Podczas konfiguracji interfejs wiersza poleceń Firebase pobiera dotychczasowe definicje indeksów, aby
plik o nazwie
firestore.indexes.json
(domyślnie). Usuń wszystkie definicje indeksów, które zawierają pole
timestamp
, w przypadku: 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
Zaktualizuj definicje indeksu pojedynczego pola
Konsola Firebase
Otwórz stronę Indeksy pojedynczych pól Cloud Firestore w konsoli Firebase.
Kliknij Dodaj wyjątek.
W polu Identyfikator kolekcji wpisz
instruments
. W polu Ścieżka pola: wpisztimestamp
.W sekcji Zakres zapytania wybierz Kolekcja i Grupa kolekcji.
Kliknij Dalej.
Przełącz wszystkie ustawienia indeksu na Wyłączony. Kliknij Zapisz.
Powtórz te same czynności w przypadku pola
shard
.
konsola GCP
W konsoli Google Cloud Platform otwórz stronę Bazy danych.
Wybierz wymaganą bazę danych z listy baz danych.
W menu nawigacyjnym kliknij Indeksy, a następnie wybierz kartę Pojedyncze pole.
Kliknij kartę Pojedyncze pole.
Kliknij Dodaj wyjątek.
W polu Identyfikator kolekcji wpisz
instruments
. W polu Ścieżka pola: wpisztimestamp
.W sekcji Zakres zapytania wybierz Kolekcja i Grupa kolekcji.
Kliknij Dalej.
Przełącz wszystkie ustawienia indeksu na Wyłączony. Kliknij Zapisz.
Powtórz te same czynności w przypadku pola
shard
.
wiersz poleceń Firebase
Dodaj ten kod do sekcji
fieldOverrides
definicji indeksów plik:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }
Wdróż zaktualizowane definicje indeksów:
firebase deploy --only firestore:indexes
Utwórz nowe indeksy złożone
Po usunięciu wszystkich poprzednich indeksów zawierających tabelę timestamp
zdefiniować nowe indeksy wymagane przez aplikację. Dowolny indeks zawierający
Pole timestamp
musi też zawierać pole shard
. Na przykład, aby obsługiwać
w zapytaniach wymienionych powyżej dodaj te indeksy:
Kolekcja | Zindeksowane pola | Zakres zapytania |
---|---|---|
instrumenty | Fragment | , price.currency, sygnatura czasowaKolekcja |
instrumenty | Fragment | , wymiana, sygnatura czasowaKolekcja |
instrumenty | Fragment | , typ instrumentu: , sygnatura czasowaKolekcja |
Komunikaty o błędach
Indeksy te możesz tworzyć, uruchamiając zaktualizowane zapytania.
Każde zapytanie zwraca komunikat o błędzie z linkiem do utworzenia wymaganych danych 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
Informacje o zapisie pól z indeksowaniem sekwencyjnym limitów
Ograniczenie szybkości zapisu dla pól zindeksowanych sekwencyjnie zależy od tego, Cloud Firestore przechowuje wartości indeksów i skaluje zapisy indeksów. Dla każdej wartości zapis w indeksie, Cloud Firestore definiuje wpis w postaci pary klucz-wartość, który łączy nazwę dokumentu i wartość każdego zindeksowanego pola. Cloud Firestore porządkuje te wpisy indeksu w grupy danych nazywane tabletami. Każdy Serwer Cloud Firestore przechowuje na urządzeniu co najmniej 1 tablet. Gdy zapis wczytuje się do określony tablet stanie się zbyt wysoko, Cloud Firestore skaluje się w poziomie dzieląc tablet na mniejsze i rozkładając nowe na różnych serwerach Cloud Firestore.
Cloud Firestore umieszcza leksykograficznie zamykające wpisy indeksu na tym samym tablecie. Jeśli wartości indeksu w tablecie są zbyt blisko siebie, np. pól sygnatur czasowych, Cloud Firestore nie może efektywnie dzielić na mniejsze tablety. W ten sposób powstaje obszar, w którym jeden tablet odbiera za duży ruch i może wykonywać operacje odczytu i zapisu w pamięci „gorącej” spowalniają.
Fragmentując pole sygnatury czasowej, umożliwiasz aby Cloud Firestore mógł wydajnie dzielić zadania między wiele na tabletach. Chociaż wartości w polu sygnatury czasowej mogą być blisko siebie, połączony fragment i wartość indeksu dają Cloud Firestore wystarczającą ilość miejsca między wpisami indeksu, by podzielić je na kilka tabletów.
Co dalej?
- Przeczytaj sprawdzone metody projektowania pod kątem skali
- Informacje o przypadkach z dużą częstotliwością zapisu do 1 dokumentu znajdziesz w sekcji Zakłócanie działania liczników.
- Zapoznaj się ze standardowymi limitami dla Cloud Firestore.