Datas/horas divididas
Se uma coleção contiver documentos com valores indexados sequenciais, o Firestore limita a taxa de escrita a 500 escritas por segundo. Esta página descreve como dividir um campo de documento para ultrapassar este limite. Primeiro, vamos definir o que entendemos por "campos indexados sequenciais" e esclarecer quando este limite se aplica.
Campos indexados sequenciais
"Campos indexados sequenciais" refere-se a qualquer coleção de documentos que contenha um campo indexado que aumenta ou diminui monotonicamente. Em muitos casos, isto significa
um campo timestamp
, mas qualquer valor de campo que aumente ou diminua monotonicamente
pode acionar o limite de gravação de 500 gravações por segundo.
Por exemplo, o limite aplica-se a uma coleção de user
documentos com
o campo indexado userid
se a app atribuir valores userid
da seguinte forma:
1281, 1282, 1283, 1284, 1285, ...
Por outro lado, nem todos os campos timestamp
acionam este limite. Se um campo timestamp
acompanhar valores distribuídos aleatoriamente, o limite de gravação não se aplica. O valor real do campo também não importa, apenas que o campo esteja a aumentar ou diminuir monotonicamente. Por exemplo, os dois conjuntos seguintes de valores de campos que aumentam monotonicamente acionam o limite de gravação:
100000, 100001, 100002, 100003, ...
0, 1, 2, 3, ...
Dividir um campo de data/hora
Suponha que a sua app usa um campo timestamp
que aumenta monotonicamente.
Se a sua app não usar o campo timestamp
em nenhuma consulta, pode remover o limite de 500 gravações por segundo não indexando o campo de data/hora. Se precisar de um campo timestamp
para as suas consultas, pode contornar o limite usando datas/horas fragmentadas:
- Adicione um campo
shard
junto ao campotimestamp
. Use1..n
valores distintos para o camposhard
. Isto aumenta o limite de gravação da coleção para500*n
, mas tem de agregarn
consultas. - Atualize a lógica de escrita para atribuir aleatoriamente um valor
shard
a cada documento. - Atualize as consultas para agregar os conjuntos de resultados divididos.
- Desative os índices de campo único para o campo
shard
e o campotimestamp
. Elimine os índices compostos existentes que contenham o campotimestamp
. - Crie novos índices compostos para suportar as suas consultas atualizadas. A ordem dos campos num índice é importante, e o campo
shard
tem de vir antes do campotimestamp
. Todos os índices que incluam o campotimestamp
também têm de incluir o camposhard
.
Deve implementar as datas/horas divididas apenas em exemplos de utilização com taxas de gravação sustentadas superiores a 500 gravações por segundo. Caso contrário, trata-se de uma otimização prematura. A divisão de um campo timestamp
remove a restrição de 500 escritas por segundo, mas com a desvantagem de precisar de agregações de consultas do lado do cliente.
Os exemplos seguintes mostram como dividir um campo timestamp
e como consultar um conjunto de resultados dividido.
Exemplo de modelo de dados e consultas
Por exemplo, imagine uma app para análise quase em tempo real de instrumentos financeiros, como moedas, ações comuns e ETFs. Esta app escreve documentos numa coleção instruments
da seguinte forma:
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 executa as seguintes consultas e ordena-as pelo 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]); });
Após alguma pesquisa, determina que a app vai receber entre 1000 e 1500 atualizações de instrumentos por segundo. Isto excede as 500 gravações por segundo permitidas para coleções que contêm documentos com campos de data/hora indexados. Para aumentar o débito de gravação, precisa de 3 valores de fragmentos,
MAX_INSTRUMENT_UPDATES/500 = 3
. Este exemplo usa os valores de fragmento x
,
y
e z
. Também pode usar números ou outros carateres para os valores de fragmentos.
Adicionar um campo de fragmentação
Adicione um campo shard
aos seus documentos. Defina o campo shard
para os valores x
, y
ou z
, o que aumenta o limite de gravação na recolha para 1500 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(); }
Consultar a data/hora dividida
A adição de um campo shard
requer que atualize as suas consultas para agregar
resultados divididos:
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]); });
Atualize as definições do índice
Para remover a restrição de 500 gravações por segundo, elimine os índices de campo único e compostos existentes que usam o campo timestamp
.
Elimine definições de índice composto
Consola do Firebase
Abra a página Índices compostos do Firestore na consola do Firebase.
Para cada índice que contenha o campo
timestamp
, clique no botão e clique em Eliminar.
GCP Console
Na Google Cloud consola, aceda à página Bases de dados.
Selecione a base de dados necessária na lista de bases de dados.
No menu de navegação, clique em Índices e, de seguida, clique no separador Composto.
Use o campo Filtro para pesquisar definições de índice que contenham o campo
timestamp
.Para cada um destes índices, clique no botão
e em Eliminar.
Firebase CLI
- Se não configurou a Firebase CLI, siga estas instruções para instalar
a CLI e executar o comando
firebase init
. Durante o comandoinit
, certifique-se de que selecionaFirestore: Deploy rules and create indexes for Firestore
. - Durante a configuração, a Firebase CLI transfere as definições de índice existentes para um ficheiro denominado, por predefinição,
firestore.indexes.json
. Remova todas as definições de índice que contenham o campo
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" } ] }, ] }
Implemente as definições de índice atualizadas:
firebase deploy --only firestore:indexes
Atualize as definições de índice de campo único
Consola do Firebase
Abra a página Índices de campo único do Firestore na consola do Firebase.
Clique em Adicionar exceção.
Para ID da coleção, introduza
instruments
. Para Caminho do campo, introduzatimestamp
.Em Âmbito da consulta, selecione Coleção e Grupo de coleções.
Clique em Seguinte
Alterne todas as definições de índice para Desativado. Clique em Guardar.
Repita os mesmos passos para o campo
shard
.
GCP Console
Na Google Cloud consola, aceda à página Bases de dados.
Selecione a base de dados necessária na lista de bases de dados.
No menu de navegação, clique em Índices e, de seguida, clique no separador Campo único.
Clique no separador Campo único.
Clique em Adicionar exceção.
Para ID da coleção, introduza
instruments
. Para Caminho do campo, introduzatimestamp
.Em Âmbito da consulta, selecione Coleção e Grupo de coleções.
Clique em Seguinte
Alterne todas as definições de índice para Desativado. Clique em Guardar.
Repita os mesmos passos para o campo
shard
.
Firebase CLI
Adicione o seguinte à secção
fieldOverrides
do ficheiro de definições de índice:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }
Implemente as definições de índice atualizadas:
firebase deploy --only firestore:indexes
Crie novos índices compostos
Depois de remover todos os índices anteriores que contenham timestamp
, defina os novos índices de que a sua app precisa. Qualquer índice que contenha o campo timestamp
também tem de conter o campo shard
. Por exemplo, para suportar as consultas acima, adicione os seguintes índices:
Coleção | Campos indexados | Âmbito da consulta |
---|---|---|
instrumentos | shard, price.currency, timestamp | Coleção |
instrumentos | fragmento, troca, indicação de tempo | Coleção |
instrumentos | shard, instrumentType, timestamp | Coleção |
Mensagens de erro
Pode criar estes índices executando as consultas atualizadas.
Cada consulta devolve uma mensagem de erro com um link para criar o índice necessário na consola do Firebase.
Firebase CLI
Adicione os seguintes índices ao ficheiro de definição do í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 as definições de índice atualizadas:
firebase deploy --only firestore:indexes
Compreender o limite de gravação para campos indexados sequenciais
O limite da taxa de gravação para campos indexados sequenciais resulta da forma como o Firestore armazena os valores de índice e dimensiona as gravações de índice. Para cada gravação de índice, o Firestore define uma entrada de chave-valor que concatena o nome do documento e o valor de cada campo indexado. O Firestore organiza estas entradas de índice em grupos de dados denominados tablets. Cada servidor do Firestore contém um ou mais tablets. Quando a carga de escrita para um tablet específico se torna demasiado elevada, o Firestore é dimensionado horizontalmente dividindo o tablet em tablets mais pequenos e distribuindo os novos tablets por diferentes servidores do Firestore.
O Firestore coloca entradas de índice lexicograficamente próximas no mesmo tablet. Se os valores de índice numa tabela forem demasiado próximos, como para campos de data/hora, o Firestore não consegue dividir eficientemente a tabela em tabelas mais pequenas. Isto cria um ponto crítico onde um único tablet recebe demasiado tráfego e as operações de leitura e escrita no ponto crítico tornam-se mais lentas.
Ao dividir um campo de data/hora, torna possível que o Firestore divida eficientemente as cargas de trabalho em vários tablets. Embora os valores do campo de data/hora possam permanecer próximos, o valor de fragmento e índice concatenado dá ao Firestore espaço suficiente entre as entradas de índice para dividir as entradas entre vários tablets.
O que se segue?
- Leia as práticas recomendadas para criar designs escaláveis
- Para casos com taxas de gravação elevadas num único documento, consulte Contadores distribuídos
- Consulte os limites padrão do Firestore