Marcas de tiempo fragmentadas
Si una colección contiene documentos con valores indexados secuenciales, Firestore limita la tasa de escritura a 500 operaciones por segundo. En esta página, se describe cómo fragmentar un campo de documento para superar este límite. Primero, definamos qué significa “campos indexados secuenciales” y aclaremos cuándo se aplica este límite.
Campos indexados secuenciales
“Campos indexados secuenciales” se refiere a cualquier colección de documentos que contiene un
campo indexado que aumenta o disminuye monótonamente. En muchos casos, se trata de un
campo timestamp
, pero cualquier valor de campo que aumente o disminuya de forma monótona
puede activar el límite de 500 operaciones de escritura por segundo.
Por ejemplo, el límite se aplica a una colección de documentos de user
con un campo userid
indexado si la app asigna valores userid
de la siguiente manera:
1281, 1282, 1283, 1284, 1285, ...
Sin embargo, no todos los campos timestamp
activan este límite. Si un campo timestamp
rastrea valores distribuidos de forma aleatoria, no se aplica el límite de escritura. El valor real del campo tampoco importa, solo importa que el campo
aumente o disminuya monótonamente. Por ejemplo,
los siguientes conjuntos de valores de campo con aumento monótono activan
el límite de escritura:
100000, 100001, 100002, 100003, ...
0, 1, 2, 3, ...
Fragmenta un campo de marca de tiempo
Supongamos que tu app utiliza un campo timestamp
que aumenta de forma monótona.
Si en tu app no se utiliza el campo timestamp
en ninguna consulta, puedes quitar el límite de 500 operaciones de escritura por segundo. Para ello, no indexes el campo de marca de tiempo. Sin embargo, si necesitas un campo timestamp
para tus consultas, puedes utilizar marcas de tiempo fragmentadas a fin de evitar el límite de la siguiente manera:
- Agrega un campo
shard
junto al campotimestamp
. Utiliza valores1..n
distintos en el camposhard
. Esto aumenta el límite de escritura de la colección a500*n
, pero debes agregarn
consultas. - Actualiza la lógica de escritura para asignar aleatoriamente un valor
shard
a cada documento. - Actualiza tus consultas para que agreguen los conjuntos de resultados fragmentados.
- Inhabilita los índices de campo único para los campos
shard
ytimestamp
. Borra los índices compuestos existentes que contienen el campotimestamp
. - Crea índices compuestos nuevos que respalden tus consultas actualizadas. El orden de
los campos de un índice es importante, por lo que el campo
shard
debe anteceder al campotimestamp
. Si un índice incluye el campotimestamp
, también debe incluir el camposhard
.
Te recomendamos que solo implementes marcas de tiempo fragmentadas en los casos de uso que tengan tasas de escritura sostenidas
superiores a 500 operaciones por segundo. De lo contrario, se tratará de una optimización prematura. Cuando se fragmenta un campo timestamp
se quita la restricción de 500 operaciones de escritura por segundo, pero, en su lugar, se necesitan agregaciones de consultas en el cliente.
En los siguientes ejemplos se muestra cómo fragmentar un campo timestamp
y hacer consultas a un conjunto fragmentado de resultados.
Ejemplo de modelo de datos y consultas
Como ejemplo, supongamos que tienes una app para el análisis casi en tiempo real de instrumentos financieros como monedas, acciones comunes y fondos cotizados (ETF). Esta app escribe documentos en una colección de instruments
de la siguiente manera:
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(); }
Esta app ejecuta las siguientes consultas y ordena los datos por el campo 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]); });
Después de investigar un poco, determinas que la app recibirá entre 1,000 y 1,500 actualizaciones de instrumentos por segundo. Esto supera las 500 operaciones de escritura por segundo permitidas para colecciones que contienen documentos con campos de marca de tiempo indexados. Para aumentar la capacidad de procesamiento de escritura, necesita 3 valores de fragmento, (es decir, MAX_INSTRUMENT_UPDATES/500 = 3
). En este ejemplo, se utilizan los valores de fragmento x
, y
y z
. También puedes usar números y otros caracteres para tus valores de fragmento.
Agrega un campo de fragmento
Agrega un campo shard
a tus documentos. Establece los valores x
, y
o z
para el campo shard
, esto eleva el límite de escritura de la colección a 1,500 operaciones por segundo.
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(); }
Consulta la marca de tiempo fragmentada
Para agregar un campo shard
, debes actualizar tus consultas a fin de que agreguen los resultados fragmentados:
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]); });
Actualiza las definiciones de índices
Para quitar el límite de 500 operaciones de escritura por segundo, borra los índices existentes de campo único y compuestos que usan el campo timestamp
.
Borra las definiciones de índice compuesto
Firebase console
En Firebase console, abre la página Índices compuestos de Firestore.
Para cada índice con el campo
timestamp
, haz clic en el botón y, luego, en Borrar.
GCP Console
En la consola de Google Cloud, ve a la página Bases de datos.
Selecciona la base de datos requerida de la lista.
En el menú de navegación, haz clic en Índices y, luego, en la pestaña Compuestos.
Usa el campo Filtrar para buscar definiciones de índice que contengan el campo
timestamp
.Para cada uno de estos índices, haz clic en el botón
y, luego, en Borrar.
Firebase CLI
- Si aún no configuraste Firebase CLI, sigue estas instrucciones para instalarlo y ejecutar el comando
firebase init
. Durante la ejecución del comandoinit
, asegúrate de seleccionarFirestore: Deploy rules and create indexes for Firestore
. - Durante la configuración, Firebase CLI descarga tus definiciones de índice existentes a un archivo llamado
firestore.indexes.json
de forma predeterminada. Quita las definiciones de índices que contengan 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" } ] }, ] }
Implementa tus definiciones de índice actualizadas con este comando:
firebase deploy --only firestore:indexes
Actualiza las definiciones de índice de campo único
Firebase console
En Firebase console, abre la página Índices de campo único de Firestore.
Haz clic en Agregar exención.
En ID de la colección, ingresa
instruments
. En Ruta del campo , ingresatimestamp
.En Alcance de la consulta, selecciona Colección y Grupo de colecciones.
Haz clic en Siguiente.
Cambia la configuración de todos los índices a Inhabilitado. Haz clic en Guardar.
Repite los mismos pasos para el campo
shard
.
GCP Console
En la consola de Google Cloud, ve a la página Bases de datos.
Selecciona la base de datos requerida de la lista.
En el menú de navegación, haz clic en Índices y, luego, en la pestaña De campo único.
Haz clic en la pestaña De campo único.
Haz clic en Agregar exención.
En ID de la colección, ingresa
instruments
. En Ruta del campo , ingresatimestamp
.En Alcance de la consulta, selecciona Colección y Grupo de colecciones.
Haz clic en Siguiente.
Cambia la configuración de todos los índices a Inhabilitado. Haz clic en Guardar.
Repite los mismos pasos para el campo
shard
.
Firebase CLI
Agrega el siguiente código a la sección
fieldOverrides
de tu archivo de definiciones de índice:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }
Implementa tus definiciones de índice actualizadas con este comando:
firebase deploy --only firestore:indexes
Crea índices compuestos nuevos
Después de quitar todos los índices anteriores que contienen timestamp
, define los índices nuevos que requiere tu app. Si un índice incluye el campo timestamp
, también debe incluir el campo shard
. Por ejemplo, agrega los siguientes índices para admitir las consultas anteriores:
Colección | Campos indexados | Alcance de la consulta |
---|---|---|
instruments | shard, price.currency, timestamp | Colección |
instruments | shard, exchange, timestamp | Colección |
instruments | shard, instrumentType, timestamp | Colección |
Mensajes de error
Para crear estos índices, ejecuta las consultas actualizadas.
Cada consulta muestra un mensaje de error con un vínculo para crear el índice requerido en Firebase console.
Firebase CLI
Agrega los siguientes índices a tu archivo de definición de índices:
{ "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" } ] }, ] }
Implementa tus definiciones de índice actualizadas con este comando:
firebase deploy --only firestore:indexes
Información sobre el límite de escritura de los campos indexados secuenciales
El límite de la tasa de escritura de los campos indexados secuenciales proviene del método que usa Firestore para almacenar los valores de índices y escalar las escrituras de índices. En cada operación de escritura de índice, Firestore define una entrada clave-valor que concatena el nombre del documento con el valor de cada campo indexado. Firestore organiza estas entradas de índice en grupos de datos llamados tablets. Cada servidor de Firestore almacena una o más tablets. Cuando la carga de escritura en una tablet en particular es demasiado alta, Firestore ajusta la escala de forma horizontal. Para ello, divide la tablet en unidades más pequeñas y las distribuye en diferentes servidores de Firestore.
Firestore coloca las entradas de índice con similitud lexicográfica en la misma tablet. Si los valores de índice de una tablet son demasiado cercanos, como en el caso de los campos de marca de tiempo, Firestore no puede dividir la tablet de manera eficiente. Esto crea hotspots en los que una sola tablet recibe demasiado tráfico y las operaciones de lectura y escritura se vuelven más lentas.
Si fragmentas un campo de marca de tiempo, permites que Firestore divida de forma eficiente las cargas de trabajo en varias tablets. Aunque los valores del campo de marca de tiempo pueden permanecer juntos, el fragmento concatenado y el valor de índice le dan a Firestore suficiente espacio entre las entradas de índice para dividirlas en varias tablets.
¿Qué sigue?
- Lee las prácticas recomendadas para diseñar en consideración a la escala.
- En las situaciones que incluyen tasas de escritura altas en un solo documento, consulta el artículo Contadores distribuidos.
- Consulta los límites estándar para Firestore.