Jeśli kolekcja zawiera dokumenty z sekwencyjnie posortowanymi wartościami indeksowanymi, Cloud Firestore ogranicza szybkość zapisu do 500 zapiszów na sekundę. Na tej stronie opisano, jak podzielić pole dokumentu, aby przekroczyć ten limit. Najpierw wyjaśnijmy, co rozumiemy przez „sekwencyjne indeksowanie pól” i kiedy ten limit ma zastosowanie.
Sekwencyjne indeksowane pola
„Uporządkowane indeksowane pola” to dowolna kolekcja dokumentów zawierająca monotonicznie rosnące lub malejące indeksowane pole. W wielu przypadkach oznacza to pole timestamp
, ale dowolna wartość monotonicznie rosnąca lub malejąca może spowodować przekroczenie limitu zapisu wynoszącego 500 zapiszów na sekundę.
Na przykład limit dotyczy kolekcji dokumentów user
z indekowanym 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 limitu. Jeśli pole timestamp
śledzi losowo rozłożone wartości, limit zapisu nie ma zastosowania. Również rzeczywista wartość pola nie ma znaczenia, ważne jest tylko to, czy rośnie ona monotonicznie czy maleje. 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 na fragmenty pola sygnatury czasowej
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 wpisów na sekundę, nie indeksując pola z datą i godziną. Jeśli w przypadku zapytań potrzebujesz pola timestamp
, możesz obejść ten limit, używając shardowanych sygnatur czasowych:
- Obok pola
timestamp
dodaj poleshard
. W polushard
użyj1..n
niepowtarzalnych wartości. Spowoduje to zwiększenie limitu zapisu dla kolekcji do500*n
, ale musisz zagregować zapytanian
. - Zaktualizuj logikę zapisu, aby losowo przypisywać wartość
shard
do każdego dokumentu. - Zaktualizuj swoje zapytania, aby zsumować podzielone zbiory wyników.
- Wyłącz indeksy pojedynczych pól zarówno w przypadku pola
shard
, jak i polatimestamp
. 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. Pole
shard
musi znajdować się przed polemtimestamp
. Wszystkie indeksy, które zawierają poletimestamp
, muszą zawierać też poleshard
.
Spartycjonowane sygnatury czasowe należy stosować tylko w przypadkach, gdy utrzymywane są szybkości zapisu powyżej 500 napisu na sekundę. W przeciwnym razie jest to przedwczesna optymalizacja. Dzielenie na fragmenty pola timestamp
powoduje usunięcie ograniczenia dotyczącego 500 zapisów na sekundę, ale wymaga agregacji zapytań po stronie klienta.
W poniższych przykładach pokazujemy, jak podzielić pole timestamp
i jak wysłać zapytanie dotyczące podzielonego zbioru wyników.
Przykładowy model danych i zapytania
Wyobraź sobie na przykład aplikację do analizy instrumentów finansowych (np. walut, akcji i funduszy ETF) w czasie zbliżonym do rzeczywistego. Ta aplikacja zapisuje dokumenty do 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 wykonuje 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 badań okazuje się, że aplikacja będzie otrzymywać od 1000 do 1500 aktualizacji instrumentu na sekundę. Liczba ta przekracza 500 zapisów na sekundę dozwolonych w przypadku kolekcji zawierających dokumenty z indeksowanymi polami z datą i godziną. Aby zwiększyć przepustowość zapisu, musisz użyć 3 wartości fragmentów:
MAX_INSTRUMENT_UPDATES/500 = 3
. W tym przykładzie użyto wartości fragmentów x
, y
i z
. Do wartości fragmentów możesz też użyć liczb lub innych znaków.
Dodawanie pola fragmentu
Dodaj pole shard
do dokumentów. Ustaw pole shard
na wartości x
, y
lub z
, aby zwiększyć limit zapisu w kolekcji do 1500 zapiszó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 zapytań dotyczących podzielonego na fragmenty sygnatury czasowej
Dodanie pola shard
wymaga zaktualizowania zapytań w celu zgrupowania wyników w podziale na segmenty:
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 500 wpisów na sekundę, usuń istniejące indeksy jednopolowe i kompleksowe, które używają pola timestamp
.
Usuwanie definicji indeksów złożonych
Konsola Firebase
Otwórz w konsoli Firebase stronę Cloud Firestore Indeksy złożone.
W przypadku każdego indeksu zawierającego pole
timestamp
kliknij kolejno przyciski i 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 kartę Kompozycja.
Użyj pola Filtr, aby wyszukać definicje indeksu, które zawierają pole
timestamp
.W przypadku każdego z tych indeksów kliknij przycisk
, a następnie Usuń.
wiersz poleceń Firebase
- Jeśli nie masz skonfigurowanego wiersza poleceń Firebase, postępuj zgodnie z tymi instrukcjami, aby zainstalować wiersz poleceń i uruchomić polecenie
firebase init
. Podczas wykonywania poleceniainit
pamiętaj, aby wybraćFirestore: Deploy rules and create indexes for Firestore
. - Podczas konfiguracji wiersz poleceń Firebase pobiera istniejące definicje indeksów do pliku o nazwie
firestore.indexes.json
. Usuń definicje indeksów, które zawierają 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 indeksu pojedynczego pola
Konsola Firebase
Otwórz w konsoli Firebase stronę Cloud Firestore Indeksy pojedynczych pól.
Kliknij Dodaj wyjątek.
W polu Identyfikator kolekcji wpisz
instruments
. W polu Ścieżka pola wpisztimestamp
.W sekcji Zakres zapytania zaznacz Kolekcja i Grupa kolekcji.
Kliknij Dalej.
Przełącz 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 kolejno Indeksy i 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 zaznacz Kolekcja i Grupa kolekcji.
Kliknij Dalej.
Przełącz wszystkie ustawienia indeksu na Wyłączone. Kliknij Zapisz.
Powtórz te same czynności w przypadku pola
shard
.
wiersz poleceń Firebase
Dodaj do sekcji
fieldOverrides
w pliku definicji indeksów następujące 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 wymagane przez aplikację. Każdy indeks zawierający pole timestamp
musi też zawierać pole shard
. Na przykład aby obsługiwać powyższe zapytania, dodaj te indeksy:
Kolekcja | Zindeksowane pola | Zakres zapytania |
---|---|---|
instrumenty | shard, price.currency, timestamp | Kolekcja |
instrumenty | fragment, wymiana, sygnatura czasowa | Kolekcja |
instrumenty | shard, instrumentType, timestamp | 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
Limit zapisu w przypadku sekwencyjnie zindeksowanych pól
Limit szybkości zapisu w przypadku sekwencyjnie indeksowanych pól 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 z indeksowanych pól. Cloud Firestore porządkuje te wpisy indeksu w grupy danych zwane tabletami. Każdy serwer Cloud Firestore zawiera co najmniej 1 tablet. Gdy obciążenie związane z zapisywaniem na konkretnym tablecie staje się zbyt duże, Cloud Firestore skaluje go poziomo, dzieląc go na mniejsze tablety i rozmieszczając je na różnych serwerach Cloud Firestore.
Cloud Firestore miejsca na podręcznej liście Jeśli wartości indeksu w tablecie są zbyt zbliżone do siebie, np. w przypadku pól sygnatury czasowej, Cloud Firestore nie może skutecznie podzielić tabletu na mniejsze tablety. Powoduje to, że jeden tablet otrzymuje zbyt dużo ruchu, a operacje odczytu i zapisu w tym punkcie stają się wolniejsze.
Dzięki dzieleniu na fragmenty pola z datą i godziną Cloud Firestore może efektywnie dzielić zbiory zadań na wiele tabletek. Chociaż wartości pola sygnatury czasowej mogą być zbliżone, złączanie wartości fragmentu i indeksów daje Cloud Firestorewystarczająco dużo miejsca pomiędzy wpisami indeksu, aby można było podzielić je na kilka tabletów.
Co dalej?
- Zapoznaj się ze sprawdzonymi metodami projektowania na potrzeby skalowania.
- W przypadku wysokiej częstotliwości zapisywania w jednym dokumencie zapoznaj się z artykułem Zdezorientowane liczniki.
- Limity standardowe dla Cloud Firestore