Carimbos de data/hora fragmentados
Se uma coleção contiver documentos com valores indexados sequenciais, o Firestore limitará a taxa de gravação a 500 gravações por segundo. Esta página descreve como fragmentar um campo de documento para ultrapassar esse limite. Primeiro, vamos definir o significado de "campos indexados sequenciais" e esclarecer quando esse limite é aplicado.
Campos indexados sequenciais
"Campos indexados sequenciais" são qualquer coleção de documentos que contém um campo indexado monotonicamente crescente ou decrescente. Em muitos casos, isso significa um campo de timestamp
, mas qualquer valor de campo que aumenta ou diminui monotonicamente pode acionar o limite de 500 gravações por segundo.
Por exemplo, o limite será aplicado a uma coleção de documentos user
com campo indexado userid
se o aplicativo atribuir valores de userid
como:
1281, 1282, 1283, 1284, 1285, ...
Por outro lado, nem todos os campos de timestamp
acionam esse limite. Se um campo de timestamp
rastreia valores distribuídos aleatoriamente, o limite de gravação não é aplicado. Da mesma forma, o valor real do campo também não importa, sendo relevante apenas se ele está aumentando ou diminuindo monotonicamente. Por exemplo, os dois conjuntos a seguir de valores de campo que aumentam monotonicamente acionam o limite de gravação:
100000, 100001, 100002, 100003, ...
0, 1, 2, 3, ...
Como fragmentar um campo de carimbo de data/hora
Suponha que o aplicativo use um campo de timestamp
que aumenta monotonicamente.
Se o aplicativo não usar o campo de timestamp
em uma consulta, será possível remover o limite de 500 gravações por segundo não indexando o campo de carimbo de data/hora. Se for necessário um campo de timestamp
para consultas, será possível contornar o limite usando carimbos de data/hora fragmentados:
- Adicione um campo de
shard
ao lado do campo detimestamp
. Use valores distintos de1..n
para o camposhard
. Isso aumenta o limite de gravação da coleção para500*n
, mas você deve agregarn
consultas. - Atualize a lógica de gravação para atribuir aleatoriamente um valor de
shard
para cada documento. - Atualize as consultas para agregar os conjuntos de resultados fragmentados.
- Desative os índices de campo único para o campo de
shard
e o campo detimestamp
. Exclua os índices compostos atuais que contêm o campo detimestamp
. - Crie novos índices compostos para suportar as consultas atualizadas. A ordem dos campos em um índice é importante, e o campo
shard
deve vir antes do campotimestamp
. Qualquer índice que inclua o campotimestamp
também deve incluir o camposhard
.
Carimbos de data/hora fragmentados devem ser implementados apenas em casos de uso com taxas de gravação sustentadas acima de 500 gravações por segundo. Caso contrário, essa é uma otimização prematura. A fragmentação de um campo de timestamp
remove a restrição de 500 gravações por segundo, mas requer agregações de consulta do lado do cliente.
Os exemplos a seguir mostram como fragmentar um campo de timestamp
e como consultar um conjunto de resultados fragmentados.
Exemplos de consultas e modelos de dados
Como exemplo, imagine um aplicativo para análise quase em tempo real de instrumentos financeiros como moedas, ações ordinárias e ETFs. Este aplicativo grava documentos em uma coleção de instruments
tal como:
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(); }
Este aplicativo executa as seguintes consultas e pedidos pelo campo de 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]); });
Após algumas pesquisas, você determina que o aplicativo receberá entre 1.000 e 1.500 atualizações de instrumento por segundo. Isso ultrapassa as 500 gravações por segundo permitidas para coleções contendo documentos com campos de carimbo de data/hora indexados. Para aumentar a capacidade de gravação, você precisa de três valores de fragmento, MAX_INSTRUMENT_UPDATES/500 = 3
. Este exemplo usa os valores de fragmento x
, y
e z
. Também é possível usar números ou outros caracteres para valores de fragmento.
Como adicionar um campo de fragmento
Adicione um campo de shard
aos documentos. Defina o campo shard
para os valores de x
, y
ou z
, o que aumenta o limite de gravação na coleção para 1.500 gravações 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(); }
Como consultar o carimbo de data/hora fragmentado
A adição de um campo de shard
exige que as consultas sejam atualizadas para agregar 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]); });
Atualizar definições de índice
Para remover a restrição de 500 gravações por segundo, exclua os índices compostos e de campo único existentes que usam o campo de timestamp
.
Excluir definições de índice composto
Console do Firebase
Abra a página Índices compostos do Firestore no Console do Firebase.
Para cada índice que contém o campo de
timestamp
, clique no botão e clique em Excluir.
Console do GCP
No Console do Google Cloud, acesse a página Bancos de Dados.
Selecione o banco de dados necessário na lista de bancos de dados.
No menu de navegação, clique em Índices e na guia Composto.
Use o campo Filtro para procurar definições de índice que contenham o campo
timestamp
.Para cada um desses índices, clique no botão
e em Excluir.
CLI do Firebase
- Se a CLI do Firebase não foi configurada, siga estas instruções para instalar a CLI e executar o comando
firebase init
. Durante o comandoinit
, certifique-se de selecionarFirestore: Deploy rules and create indexes for Firestore
. - Durante a configuração, a CLI do Firebase faz o download das suas definições de índice atuais para um arquivo nomeado, por padrão,
firestore.indexes.json
. Remova as definições de índice que contiverem o campo de
timestamp
, por exemplo:{ "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" } ] }, ] }
Implante as definições de índice atualizadas:
firebase deploy --only firestore:indexes
Atualizar definições de índice de campo único
Console do Firebase
Abra a página Índices de campo único do Firestore no Console do Firebase.
Clique em Adicionar isenção.
Para ID da coleção, insira
instruments
. Para Caminho do campo, insiratimestamp
.Em Escopo da consulta, selecione Grupos de coleções e Coleção.
Clique em Próximo.
Alterne todas as configurações de índice para Desativado. Clique em Salvar.
Repita as mesmas etapas para o campo de
shard
.
Console do GCP
No Console do Google Cloud, acesse a página Bancos de Dados.
Selecione o banco de dados necessário na lista de bancos de dados.
No menu de navegação, clique em Índices e na guia Campo único.
Clique na guia Campo único.
Clique em Adicionar isenção.
Para ID da coleção, insira
instruments
. Para Caminho do campo, insiratimestamp
.Em Escopo da consulta, selecione Grupos de coleções e Coleção.
Clique em Próximo.
Alterne todas as configurações de índice para Desativado. Clique em Salvar.
Repita as mesmas etapas para o campo de
shard
.
CLI do Firebase
Adicione o seguinte à seção
fieldOverrides
do arquivo de definições de índice:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }
Implante as definições de índice atualizadas:
firebase deploy --only firestore:indexes
Criar novos índices compostos
Após remover todos os índices anteriores que contêm o timestamp
, defina os novos índices que o aplicativo requer. Qualquer índice que contenha o campo de timestamp
também precisará conter o campo de shard
. Por exemplo, para dar suporte às consultas acima, adicione os seguintes índices:
Coleção | Campos indexados | Escopo da consulta |
---|---|---|
instruments | shard, price.currency, timestamp | Coleção |
instruments | shard, exchange, timestamp | Coleção |
instruments | shard, instrumentType, timestamp | Coleção |
Mensagens de erro
É possível criar esses índices executando as consultas atualizadas.
Cada consulta retorna uma mensagem de erro com um link para criar o índice necessário no Console do Firebase.
CLI do Firebase
Adicione os seguintes índices ao arquivo de definição 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" } ] }, ] }
Implante as definições de índice atualizadas:
firebase deploy --only firestore:indexes
Como compreender a gravação para limitar os campos indexados sequenciais
O limite da taxa de gravação para os campos indexados sequenciais tem origem na forma como o Firestore armazena os valores e dimensiona as gravações do índice. Para cada gravação de índice, o Firestore define uma entrada de valor-chave que concatena o nome do documento e o valor de cada campo indexado. O Firestore organiza essas entradas de índice em grupos de dados chamados blocos. Cada servidor do Firestore tem um ou mais blocos. Quando a carga de gravação em um bloco específico se torna muito alta, o Firestore é dimensionado horizontalmente, dividindo o bloco em blocos menores que são espalhados pelos diferentes servidores do Firestore.
O Firestore coloca entradas de índice lexicograficamente próximas no mesmo bloco. Se os valores de índice em um bloco estiverem muito próximos, como nos campos de carimbo de data/hora, o Firestore não poderá dividir o bloco com eficiência em blocos menores. Isso cria um hot spot onde um único bloco recebe muito tráfego e as operações de leitura e gravação ficam mais lentas.
Compartilhar um campo de carimbo de data/hora possibilita que o Firestore divida eficientemente as cargas de trabalho em vários blocos. Embora os valores do campo de carimbo de data/hora possam permanecer próximos, o valor concatenado do fragmento e do índice fornecem ao Firestore espaço suficiente entre as entradas do índice para dividi-las em vários blocos.
A seguir
- Leia as práticas recomendadas para projetar em escala
- Para casos com altas taxas de gravação em um único documento, consulte Contadores desorganizados
- Veja os limites padrão do Firestore