Si una colección contiene documentos con valores indexados secuenciales, Cloud Firestore limita la velocidad de escritura a 500 escrituras por segundo. Esta página describe cómo fragmentar un campo de documento para superar este límite. Primero, definamos qué queremos decir con "campos indexados secuenciales" y aclaremos cuándo se aplica este límite.
Campos indexados secuenciales
"Campos indexados secuenciales" significa cualquier colección de documentos que contiene un campo indexado que aumenta o disminuye monótonamente. En muchos casos, esto significa un campo timestamp
, pero cualquier valor de campo que aumente o disminuya monótonamente puede activar el límite de escritura de 500 escrituras por segundo.
Por ejemplo, el límite se aplica a una colección de documentos user
con un campo indexado userid
si la aplicación asigna valores userid
como este:
-
1281, 1282, 1283, 1284, 1285, ...
Por otro lado, no todos los campos de timestamp
activan este límite. Si un campo timestamp
rastrea valores distribuidos aleatoriamente, no se aplica el límite de escritura. El valor real del campo tampoco importa, sólo que el campo aumente o disminuya monótonamente. Por ejemplo, los dos conjuntos siguientes de valores de campo que aumentan monótonamente activan el límite de escritura:
-
100000, 100001, 100002, 100003, ...
-
0, 1, 2, 3, ...
Fragmentar un campo de marca de tiempo
Supongamos que su aplicación utiliza un campo timestamp
que aumenta monótonamente. Si su aplicación no utiliza el campo timestamp
en ninguna consulta, puede eliminar el límite de 500 escrituras por segundo al no indexar el campo de marca de tiempo. Si necesita un campo timestamp
para sus consultas, puede evitar el límite utilizando marcas de tiempo fragmentadas :
- Agregue un campo
shard
junto al campotimestamp
. Utilice1..n
valores distintos para el camposhard
. Esto aumenta el límite de escritura para la colección a500*n
, pero debe agregarn
consultas. - Actualice su lógica de escritura para asignar aleatoriamente un valor
shard
a cada documento. - Actualice sus consultas para agregar los conjuntos de resultados fragmentados.
- Deshabilite los índices de campo único tanto para el campo
shard
como para el campotimestamp
. Elimine los índices compuestos existentes que contengan el campotimestamp
. - Cree nuevos índices compuestos para respaldar sus consultas actualizadas. El orden de los campos en un índice es importante y el campo
shard
debe aparecer antes del campotimestamp
. Cualquier índice que incluya el campotimestamp
también debe incluir el camposhard
.
Debe implementar marcas de tiempo fragmentadas solo en casos de uso con velocidades de escritura sostenidas superiores a 500 escrituras por segundo. De lo contrario, se trata de una optimización prematura. La fragmentación de un campo timestamp
elimina la restricción de 500 escrituras por segundo, pero con la desventaja de necesitar agregaciones de consultas del lado del cliente.
Los siguientes ejemplos muestran cómo fragmentar un campo timestamp
y cómo consultar un conjunto de resultados fragmentado.
Modelo de datos de ejemplo y consultas.
Como ejemplo, imagine una aplicación para el análisis casi en tiempo real de instrumentos financieros como divisas, acciones ordinarias y ETF. Esta aplicación escribe documentos en una colección instruments
de esta manera:
Nodo.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(); }
Esta aplicación ejecuta las siguientes consultas y pedidos según el campo de timestamp
:
Nodo.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]); });
Después de investigar un poco, determina que la aplicación recibirá entre 1000 y 1500 actualizaciones de instrumentos por segundo. Esto supera las 500 escrituras por segundo permitidas para colecciones que contienen documentos con campos de marca de tiempo indexados. Para aumentar el rendimiento de escritura, necesita 3 valores de fragmento, MAX_INSTRUMENT_UPDATES/500 = 3
. Este ejemplo utiliza los valores de fragmento x
, y
y z
. También puedes usar números u otros caracteres para los valores de tus fragmentos.
Agregar un campo de fragmento
Agregue un campo shard
a sus documentos. Establezca el campo shard
en los valores x
, y
o z
, lo que aumenta el límite de escritura en la colección a 1500 escrituras por segundo.
Nodo.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(); }
Consultando la marca de tiempo fragmentada
Agregar un campo shard
requiere que actualice sus consultas para agregar resultados fragmentados:
Nodo.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]); });
Actualizar definiciones de índice
Para eliminar la restricción de 500 escrituras por segundo, elimine los índices compuestos y de campo único existentes que utilizan el campo timestamp
.
Eliminar definiciones de índice compuesto
Consola de base de fuego
Abra la página Índices compuestos de Cloud Firestore en Firebase console.
Para cada índice que contenga el campo
timestamp
, haga clic en el botón y haga clic en Eliminar .
Consola GCP
En la consola de Google Cloud Platform, vaya a la página Bases de datos .
Seleccione la base de datos requerida de la lista de bases de datos.
En el menú de navegación, haga clic en Índices y luego haga clic en la pestaña Compuesto .
Utilice el campo Filtro para buscar definiciones de índice que contengan el campo
timestamp
.Para cada uno de estos índices, haga clic en el botón
y haga clic en Eliminar .
CLI de base de fuego
- Si no ha configurado Firebase CLI, siga estas instrucciones para instalar la CLI y ejecutar el comando
firebase init
. Durante el comandoinit
, asegúrese de seleccionarFirestore: Deploy rules and create indexes for Firestore
. - Durante la instalación, Firebase CLI descarga las definiciones de índice existentes en un archivo denominado, de forma predeterminada,
firestore.indexes.json
. Elimine cualquier definición de índice que contenga el campo
timestamp
, por ejemplo:{ "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" } ] }, ] }
Implemente sus definiciones de índice actualizadas:
firebase deploy --only firestore:indexes
Actualizar definiciones de índice de campo único
Consola de base de fuego
Abra la página Índices de campo único de Cloud Firestore en Firebase console.
Haga clic en Agregar exención .
Para ID de colección , ingrese
instruments
. En Ruta de campo , introduzcatimestamp
.En Alcance de la consulta , seleccione Colección y Grupo de colección .
Haga clic en Siguiente
Cambie todas las configuraciones del índice a Desactivado . Clic en Guardar .
Repita los mismos pasos para el campo
shard
.
Consola GCP
En la consola de Google Cloud Platform, vaya a la página Bases de datos .
Seleccione la base de datos requerida de la lista de bases de datos.
En el menú de navegación, haga clic en Índices y luego haga clic en la pestaña Campo único .
Haga clic en la pestaña Campo único .
Haga clic en Agregar exención .
Para ID de colección , ingrese
instruments
. En Ruta de campo , introduzcatimestamp
.En Alcance de la consulta , seleccione Colección y Grupo de colección .
Haga clic en Siguiente
Cambie todas las configuraciones del índice a Desactivado . Clic en Guardar .
Repita los mismos pasos para el campo
shard
.
CLI de base de fuego
Agregue lo siguiente a la sección
fieldOverrides
de su archivo de definiciones de índice:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }
Implemente sus definiciones de índice actualizadas:
firebase deploy --only firestore:indexes
Crear nuevos índices compuestos
Después de eliminar todos los índices anteriores que contienen la timestamp
, defina los nuevos índices que requiere su aplicación. Cualquier índice que contenga el campo timestamp
también debe contener el campo shard
. Por ejemplo, para admitir las consultas anteriores, agregue los siguientes índices:
Recopilación | Campos indexados | Alcance de la consulta |
---|---|---|
instrumentos | fragmento | , precio.moneda , marca de tiempoRecopilación |
instrumentos | fragmento | , intercambio , marca de tiempoRecopilación |
instrumentos | fragmento | , tipo de instrumento , marca de tiempoRecopilación |
Error de mensajes
Puede crear estos índices ejecutando las consultas actualizadas.
Cada consulta devuelve un mensaje de error con un enlace para crear el índice requerido en Firebase Console.
CLI de base de fuego
Agregue los siguientes índices a su archivo de definición de índice:
{ "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" } ] }, ] }
Implemente sus definiciones de índice actualizadas:
firebase deploy --only firestore:indexes
Comprender la escritura para limitar campos indexados secuenciales
El límite en la velocidad de escritura para campos indexados secuenciales proviene de cómo Cloud Firestore almacena los valores de índice y escala las escrituras de índice. Para cada escritura de índice, Cloud Firestore define una entrada clave-valor que concatena el nombre del documento y el valor de cada campo indexado. Cloud Firestore organiza estas entradas de índice en grupos de datos llamados tabletas . Cada servidor de Cloud Firestore tiene capacidad para una o más tabletas. Cuando la carga de escritura en una tableta en particular se vuelve demasiado alta, Cloud Firestore escala horizontalmente dividiendo la tableta en tabletas más pequeñas y distribuyendo las nuevas tabletas en diferentes servidores de Cloud Firestore.
Cloud Firestore coloca entradas de índice lexicográficamente cercanas en la misma tableta. Si los valores de índice en una tableta están demasiado juntos, como en el caso de los campos de marca de tiempo, Cloud Firestore no puede dividir la tableta de manera eficiente en tabletas más pequeñas. Esto crea un punto de acceso donde una sola tableta recibe demasiado tráfico y las operaciones de lectura y escritura en el punto de acceso se vuelven más lentas.
Al fragmentar un campo de marca de tiempo, hace posible que Cloud Firestore divida cargas de trabajo de manera eficiente en varias tabletas. Aunque los valores del campo de marca de tiempo pueden permanecer muy juntos, el fragmento concatenado y el valor del índice le dan a Cloud Firestore suficiente espacio entre las entradas del índice para dividir las entradas entre varias tabletas.
Que sigue
- Lea las mejores prácticas para diseñar a escala
- Para casos con altas tasas de escritura en un solo documento, consulte Contadores perturbados
- Ver los límites estándar para Cloud Firestore