Timestamps fragmentados

Se uma coleção contiver documentos com valores indexados sequenciais, o Cloud Firestore limitará a taxa de gravação a 500 gravações por segundo. Esta página descreve como fragmentar um campo de documento para superar esse limite. Primeiro, vamos definir o que queremos dizer com “campos indexados sequenciais” e esclarecer quando esse limite se aplica.

Campos indexados sequenciais

"Campos indexados sequenciais" significa qualquer coleção de documentos que contenha um campo indexado monotonicamente crescente ou decrescente. Em muitos casos, isso significa um campo timestamp , mas qualquer valor de campo crescente ou decrescente monotonicamente pode acionar o limite de gravação de 500 gravações por segundo.

Por exemplo, o limite se aplica a uma coleção de documentos user com campo indexado userid se o aplicativo atribuir valores userid como este:

  • 1281, 1282, 1283, 1284, 1285, ...

Por outro lado, nem todos os campos de timestamp acionam esse limite. Se um campo de carimbo timestamp rastrear 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 está aumentando ou diminuindo monotonicamente. Por exemplo, ambos os seguintes conjuntos de valores de campo crescentes monotonicamente acionam o limite de gravação:

  • 100000, 100001, 100002, 100003, ...
  • 0, 1, 2, 3, ...

Fragmentando um campo de carimbo de data/hora

Suponha que seu aplicativo use um campo timestamp que aumenta monotonicamente. Se seu aplicativo não usar o campo timestamp em nenhuma consulta, você poderá remover o limite de 500 gravações por segundo não indexando o campo de carimbo de data/hora. Se você precisar de um campo timestamp para suas consultas, poderá contornar o limite usando carimbos de data/hora fragmentados :

  1. Adicione um campo shard ao lado do campo timestamp . Use valores distintos 1..n para o campo shard . Isso aumenta o limite de gravação da coleção para 500*n , mas você deve agregar n consultas.
  2. Atualize sua lógica de gravação para atribuir aleatoriamente um valor shard a cada documento.
  3. Atualize suas consultas para agregar os conjuntos de resultados fragmentados.
  4. Desative os índices de campo único para o campo shard e para o campo timestamp . Exclua os índices compostos existentes que contêm o campo timestamp .
  5. Crie novos índices compostos para dar suporte às suas consultas atualizadas. A ordem dos campos em um índice é importante e o campo shard deve vir antes do campo de timestamp . Quaisquer índices que incluam o campo timestamp também deverão incluir o campo shard .

Você deve implementar carimbos de data/hora fragmentados somente em casos de uso com taxas de gravação sustentadas acima de 500 gravações por segundo. Caso contrário, esta é uma otimização prematura. A fragmentação de um campo timestamp remove a restrição de 500 gravações por segundo, mas com a desvantagem de precisar de agregações de consulta do lado do cliente.

Os exemplos a seguir mostram como fragmentar um campo timestamp e como consultar um conjunto de resultados fragmentado.

Exemplo de modelo de dados e consultas

Por 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 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();
}

Este aplicativo executa as seguintes consultas e pedidos 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, você determina que o aplicativo receberá entre 1.000 e 1.500 atualizações de instrumentos por segundo. Isso supera as 500 gravações por segundo permitidas para coleções contendo documentos com campos de carimbo de data/hora indexados. Para aumentar a taxa de transferência de gravação, você precisa de 3 valores de fragmento, MAX_INSTRUMENT_UPDATES/500 = 3 . Este exemplo usa os valores de fragmento x , y e z . Você também pode usar números ou outros caracteres para os valores dos fragmentos.

Adicionando um campo de fragmento

Adicione um campo shard aos seus documentos. Defina o campo shard com valores 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();
}

Consultando o carimbo de data/hora fragmentado

Adicionar um campo shard requer que você atualize suas consultas 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

  1. Abra a página Índices compostos do Cloud Firestore no console do Firebase.

    Vá para índices compostos

  2. Para cada índice que contém o campo timestamp , clique no botão e clique em Excluir .

Console do GCP

  1. No console do Google Cloud Platform, acesse a página Bancos de dados .

    Vá para bancos de dados

  2. Selecione o banco de dados necessário na lista de bancos de dados.

  3. No menu de navegação, clique em Índices e, em seguida, clique na guia Composto .

  4. Use o campo Filtro para procurar definições de índice que contenham o campo timestamp .

  5. Para cada um desses índices, clique no botão e clique em Excluir .

CLI do Firebase

  1. Se você não configurou a CLI do Firebase, siga estas instruções para instalar a CLI e executar o comando firebase init . Durante o comando init , certifique-se de selecionar Firestore: Deploy rules and create indexes for Firestore .
  2. Durante a configuração, a Firebase CLI faz download das definições de índice existentes para um arquivo chamado, por padrão, firestore.indexes.json .
  3. Remova quaisquer 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"
          }
        ]
      },
     ]
    }
    
  4. Implante suas definições de índice atualizadas:

    firebase deploy --only firestore:indexes
    

Atualizar definições de índice de campo único

Console do Firebase

  1. Abra a página Índices de campo único do Cloud Firestore no Console do Firebase.

    Vá para índices de campo único

  2. Clique em Adicionar isenção .

  3. Para ID da coleção , insira instruments . Para Caminho do campo , insira timestamp .

  4. Em Escopo da consulta , selecione Coleção e Grupo de coleções .

  5. Clique em Próximo

  6. Alterne todas as configurações de índice para Disabled . Clique em Salvar .

  7. Repita as mesmas etapas para o campo shard .

Console do GCP

  1. No console do Google Cloud Platform, acesse a página Bancos de dados .

    Vá para bancos de dados

  2. Selecione o banco de dados necessário na lista de bancos de dados.

  3. No menu de navegação, clique em Índices e, em seguida, clique na guia Campo Único .

  4. Clique na guia Campo Único .

  5. Clique em Adicionar isenção .

  6. Para ID da coleção , insira instruments . Para Caminho do campo , insira timestamp .

  7. Em Escopo da consulta , selecione Coleção e Grupo de coleções .

  8. Clique em Próximo

  9. Alterne todas as configurações de índice para Disabled . Clique em Salvar .

  10. Repita as mesmas etapas para o campo shard .

CLI do Firebase

  1. Adicione o seguinte à seção fieldOverrides do seu arquivo de definições de índice:

    {
     "fieldOverrides": [
       // Disable single-field indexing for the timestamp field
       {
         "collectionGroup": "instruments",
         "fieldPath": "timestamp",
         "indexes": []
       },
     ]
    }
    
  2. Implante suas definições de índice atualizadas:

    firebase deploy --only firestore:indexes
    

Crie novos índices compostos

Depois de remover todos os índices anteriores que contêm o timestamp , defina os novos índices que seu aplicativo exige. Qualquer índice que contenha o campo timestamp também deverá conter o campo shard . Por exemplo, para dar suporte às consultas acima, adicione os seguintes índices:

Coleção Campos indexados Escopo da consulta
instrumentos fragmento , price.currency, carimbo de data/hora Coleção
instrumentos fragmento , troca , carimbo de data/hora Coleção
instrumentos fragmento , tipo de instrumento , carimbo de data/hora Coleção

Mensagens de erro

Você pode construir esses índices executando as consultas atualizadas.

Cada consulta retorna uma mensagem de erro com um link para criar o índice necessário no Firebase Console.

CLI do Firebase

  1. Adicione os seguintes índices ao seu 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"
             }
           ]
         },
       ]
     }
    
  2. Implante suas definições de índice atualizadas:

    firebase deploy --only firestore:indexes
    

Compreendendo a gravação para limites de campos indexados sequenciais

O limite na taxa de gravação para campos indexados sequenciais vem de como o Cloud Firestore armazena valores de índice e dimensiona as gravações de índice. Para cada gravação de índice, o Cloud Firestore define uma entrada de valor-chave que concatena o nome do documento e o valor de cada campo indexado. O Cloud Firestore organiza essas entradas de índice em grupos de dados chamados tablets . Cada servidor Cloud Firestore contém um ou mais tablets. Quando a carga de gravação em um tablet específico fica muito alta, o Cloud Firestore é dimensionado horizontalmente, dividindo o tablet em tablets menores e espalhando os novos tablets por diferentes servidores do Cloud Firestore.

O Cloud Firestore coloca entradas de índice lexicograficamente próximas no mesmo tablet. Se os valores de índice em um tablet estiverem muito próximos, como para campos de carimbo de data/hora, o Cloud Firestore não poderá dividir o tablet com eficiência em tablets menores. Isso cria um ponto de acesso onde um único tablet recebe muito tráfego e as operações de leitura e gravação no ponto de acesso ficam mais lentas.

Ao fragmentar um campo de carimbo de data/hora, você possibilita que o Cloud Firestore divida com eficiência as cargas de trabalho em vários tablets. Embora os valores do campo de carimbo de data/hora possam permanecer próximos, o fragmento concatenado e o valor do índice fornecem ao Cloud Firestore espaço suficiente entre as entradas do índice para dividir as entradas entre vários tablets.

Qual é o próximo