Si une collection contient des documents avec des valeurs indexées séquentiellement, Cloud Firestore limite le taux d'écriture à 500 écritures par seconde. Cette page décrit comment partitionner un champ de document pour surmonter cette limite. Tout d'abord, définissons ce que nous entendons par « champs indexés séquentiellement » et clarifions quand cette limite s'applique.
Champs indexés séquentiellement
« Champs indexés séquentiellement » désigne toute collection de documents contenant un champ indexé croissant ou décroissant de manière monotone. Dans de nombreux cas, cela signifie un champ timestamp
, mais toute valeur de champ augmentant ou diminuant de manière monotone peut déclencher la limite d'écriture de 500 écritures par seconde.
Par exemple, la limite s'applique à une collection de documents user
avec userid
de champ indexé si l'application attribue des valeurs userid
comme suit :
-
1281, 1282, 1283, 1284, 1285, ...
En revanche, tous les champs timestamp
ne déclenchent pas cette limite. Si un champ timestamp
suit des valeurs distribuées de manière aléatoire, la limite d'écriture ne s'applique pas. La valeur réelle du champ n'a pas d'importance non plus, seulement le fait que le champ augmente ou diminue de manière monotone. Par exemple, les deux ensembles suivants de valeurs de champ croissantes de manière monotone déclenchent la limite d'écriture :
-
100000, 100001, 100002, 100003, ...
-
0, 1, 2, 3, ...
Partager un champ d'horodatage
Supposons que votre application utilise un champ timestamp
augmentant de manière monotone. Si votre application n'utilise pas le champ timestamp
dans aucune requête, vous pouvez supprimer la limite de 500 écritures par seconde en n'indexant pas le champ d'horodatage. Si vous avez besoin d'un champ timestamp
pour vos requêtes, vous pouvez contourner la limite en utilisant des horodatages fragmentés :
- Ajoutez un champ
shard
à côté du champtimestamp
. Utilisez1..n
valeurs distinctes pour le champshard
. Cela augmente la limite d'écriture de la collection à500*n
, mais vous devez regroupern
requêtes. - Mettez à jour votre logique d'écriture pour attribuer de manière aléatoire une valeur
shard
à chaque document. - Mettez à jour vos requêtes pour agréger les jeux de résultats fragmentés.
- Désactivez les index à champ unique pour le champ
shard
et le champtimestamp
. Supprimez les index composites existants qui contiennent le champtimestamp
. - Créez de nouveaux index composites pour prendre en charge vos requêtes mises à jour. L'ordre des champs dans un index est important et le champ
shard
doit précéder le champtimestamp
. Tous les index qui incluent le champtimestamp
doivent également inclure le champshard
.
Vous devez implémenter des horodatages fragmentés uniquement dans les cas d'utilisation avec des taux d'écriture soutenus supérieurs à 500 écritures par seconde. Sinon, il s’agit d’une optimisation prématurée. Le partage d'un champ timestamp
supprime la restriction de 500 écritures par seconde, mais avec le compromis de nécessiter des agrégations de requêtes côté client.
Les exemples suivants montrent comment partitionner un champ timestamp
et comment interroger un jeu de résultats fragmenté.
Exemple de modèle de données et de requêtes
À titre d'exemple, imaginez une application permettant d'analyser en temps quasi réel des instruments financiers tels que les devises, les actions ordinaires et les ETF. Cette application écrit des documents dans une collection instruments
comme ceci :
Noeud.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(); }
Cette application exécute les requêtes et commandes suivantes par le champ timestamp
:
Noeud.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]); });
Après quelques recherches, vous déterminez que l'application recevra entre 1 000 et 1 500 mises à jour d'instruments par seconde. Cela dépasse les 500 écritures par seconde autorisées pour les collections contenant des documents avec des champs d'horodatage indexés. Pour augmenter le débit d'écriture, vous avez besoin de 3 valeurs de partition, MAX_INSTRUMENT_UPDATES/500 = 3
. Cet exemple utilise les valeurs de partition x
, y
et z
. Vous pouvez également utiliser des nombres ou d'autres caractères pour vos valeurs de partition.
Ajout d'un champ de partition
Ajoutez un champ shard
à vos documents. Définissez le champ shard
sur les valeurs x
, y
ou z
, ce qui augmente la limite d'écriture de la collection à 1 500 écritures par seconde.
Noeud.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(); }
Interroger l'horodatage fragmenté
L'ajout d'un champ shard
nécessite que vous mettiez à jour vos requêtes pour regrouper les résultats fragmentés :
Noeud.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]); });
Mettre à jour les définitions d'index
Pour supprimer la contrainte de 500 écritures par seconde, supprimez les index à champ unique et composites existants qui utilisent le champ timestamp
.
Supprimer les définitions d'index composites
Console Firebase
Ouvrez la page Index composites Cloud Firestore dans la console Firebase.
Pour chaque index contenant le champ
timestamp
, cliquez sur le bouton et cliquez sur Supprimer .
Console GCP
Dans la console Google Cloud Platform, accédez à la page Bases de données .
Sélectionnez la base de données requise dans la liste des bases de données.
Dans le menu de navigation, cliquez sur Index , puis cliquez sur l'onglet Composite .
Utilisez le champ Filtre pour rechercher des définitions d'index contenant le champ
timestamp
.Pour chacun de ces index, cliquez sur le bouton
et cliquez sur Supprimer .
CLI Firebase
- Si vous n'avez pas configuré la CLI Firebase, suivez ces instructions pour installer la CLI et exécutez la commande
firebase init
. Pendant la commandeinit
, assurez-vous de sélectionnerFirestore: Deploy rules and create indexes for Firestore
. - Lors de l'installation, la CLI Firebase télécharge vos définitions d'index existantes dans un fichier nommé, par défaut,
firestore.indexes.json
. Supprimez toutes les définitions d'index contenant le champ
timestamp
, par exemple :{ "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" } ] }, ] }
Déployez vos définitions d'index mises à jour :
firebase deploy --only firestore:indexes
Mettre à jour les définitions d'index à champ unique
Console Firebase
Ouvrez la page Index à champ unique Cloud Firestore dans la console Firebase.
Cliquez sur Ajouter une exemption .
Pour ID de collection , saisissez
instruments
. Pour Field path , saisisseztimestamp
.Sous Portée de la requête , sélectionnez à la fois Collection et Groupe de collections .
Cliquez sur Suivant
Basculez tous les paramètres d’index sur Disabled . Cliquez sur Enregistrer .
Répétez les mêmes étapes pour le champ
shard
.
Console GCP
Dans la console Google Cloud Platform, accédez à la page Bases de données .
Sélectionnez la base de données requise dans la liste des bases de données.
Dans le menu de navigation, cliquez sur Index , puis cliquez sur l'onglet Champ unique .
Cliquez sur l' onglet Champ unique .
Cliquez sur Ajouter une exemption .
Pour ID de collection , saisissez
instruments
. Pour Field path , saisisseztimestamp
.Sous Portée de la requête , sélectionnez à la fois Collection et Groupe de collections .
Cliquez sur Suivant
Basculez tous les paramètres d’index sur Disabled . Cliquez sur Enregistrer .
Répétez les mêmes étapes pour le champ
shard
.
CLI Firebase
Ajoutez ce qui suit à la section
fieldOverrides
de votre fichier de définitions d'index :{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }
Déployez vos définitions d'index mises à jour :
firebase deploy --only firestore:indexes
Créer de nouveaux index composites
Après avoir supprimé tous les index précédents contenant le timestamp
, définissez les nouveaux index requis par votre application. Tout index contenant le champ timestamp
doit également contenir le champ shard
. Par exemple, pour prendre en charge les requêtes ci-dessus, ajoutez les index suivants :
Collection | Champs indexés | Portée de la requête |
---|---|---|
instruments | Fragment | , price.currency, horodatageCollection |
instruments | Fragment | , échange , horodatageCollection |
instruments | Fragment | , type d'instrument , horodatageCollection |
Messages d'erreur
Vous pouvez créer ces index en exécutant les requêtes mises à jour.
Chaque requête renvoie un message d'erreur avec un lien pour créer l'index requis dans la console Firebase.
CLI Firebase
Ajoutez les index suivants à votre fichier de définition d'index :
{ "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" } ] }, ] }
Déployez vos définitions d'index mises à jour :
firebase deploy --only firestore:indexes
Comprendre l'écriture pour les champs indexés séquentiels limités
La limite du taux d'écriture pour les champs indexés séquentiellement provient de la façon dont Cloud Firestore stocke les valeurs d'index et met à l'échelle les écritures d'index. Pour chaque écriture d'index, Cloud Firestore définit une entrée clé-valeur qui concatène le nom du document et la valeur de chaque champ indexé. Cloud Firestore organise ces entrées d'index en groupes de données appelés tablettes . Chaque serveur Cloud Firestore contient une ou plusieurs tablettes. Lorsque la charge d'écriture sur une tablette particulière devient trop élevée, Cloud Firestore évolue horizontalement en divisant la tablette en tablettes plus petites et en répartissant les nouvelles tablettes sur différents serveurs Cloud Firestore.
Cloud Firestore place les entrées d'index lexicographiquement proches sur la même tablette. Si les valeurs d'index dans une tablette sont trop rapprochées, comme pour les champs d'horodatage, Cloud Firestore ne peut pas diviser efficacement la tablette en tablettes plus petites. Cela crée un point chaud où une seule tablette reçoit trop de trafic et les opérations de lecture et d'écriture sur le point chaud deviennent plus lentes.
En partitionnant un champ d'horodatage, vous permettez à Cloud Firestore de répartir efficacement les charges de travail sur plusieurs tablettes. Bien que les valeurs du champ d'horodatage puissent rester proches les unes des autres, la partition concaténée et la valeur d'index donnent à Cloud Firestore suffisamment d'espace entre les entrées d'index pour diviser les entrées entre plusieurs tablettes.
Et après
- Lisez les meilleures pratiques pour concevoir à grande échelle
- Pour les cas avec des taux d'écriture élevés sur un seul document, voir Compteurs perturbés
- Consultez les limites standard pour Cloud Firestore