Fragmenty sygnatur czasowych

Jeśli kolekcja zawiera dokumenty z sekwencyjnymi indeksowanymi wartościami: Cloud Firestore ogranicza szybkość zapisu do 500 zapisów na sekundę. Ta strona opisuje, jak podzielić pole dokumentu, aby przekroczyć ten limit. Najpierw zobaczmy, wyjaśnić, co rozumiemy przez „pola indeksowane sekwencyjnie” i sprecyzować, kiedy granica ma zastosowanie.

Pola indeksowane sekwencyjnie

„Pola zindeksowane sekwencyjnie” oznacza dowolny zbiór dokumentów, który zawiera monotonicznie rosnącego lub malejącego indeksowanego pola. W wielu przypadkach oznacza to, pole timestamp, ale każda monotonicznie rosnąca lub malejąca wartość pola może aktywować limit zapisu wynoszący 500 zapisów na sekundę.

Na przykład limit dotyczy zbioru user dokumentów z zindeksowane pole userid, jeśli aplikacja przypisuje wartości userid, na przykład:

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

Z drugiej strony nie wszystkie pola timestamp aktywują ten limit. Jeśli Pole timestamp śledzi losowo rozproszone wartości, limit zapisu nie zastosuj. Nie ma też znaczenia, jaką wartość jest wpisana; wartość pola monotonicznie rosną lub maleją. Przykład: oba poniższe zbiory monotonicznie rosnących wartości pól aktywują limit zapisu:

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

Fragmentowanie pola sygnatury czasowej

Załóżmy, że Twoja aplikacja używa monotonicznie rosnącego pola timestamp. Jeśli aplikacja nie używa pola timestamp w żadnych zapytaniach, możesz usunąć pole Ograniczenie do 500 zapisów na sekundę przy braku indeksowania pola sygnatury czasowej. Jeśli tak, wymagają pola timestamp w zapytaniach, możesz obejść limit przez przy użyciu podzielonych sygnatur czasowych:

  1. Dodaj pole shard obok pola timestamp. Używaj różnych znaków „1..n” w polu shard. Zwiększa to liczbę zapisów dla zbioru do 500*n, ale musisz agregować zapytania n.
  2. Zmień logikę zapisu, by losowo przypisywać wartość shard do każdego z nich dokument.
  3. Zaktualizuj zapytania, aby agregować pofragmentowane zbiory wyników.
  4. Wyłącz indeksy z jednym polem zarówno w polu shard, jak i w timestamp . Usuń istniejące indeksy złożone zawierające timestamp .
  5. Utwórz nowe indeksy złożone, które będą obsługiwać zaktualizowane zapytania. Kolejność pola w indeksie mają znaczenie, a pole shard musi znajdować się przed timestamp. Wszystkie indeksy zawierające parametr Pole timestamp musi też zawierać pole shard.

Sygnatury czasowe podzielone na fragmenty należy wdrażać tylko w przypadkach użycia z długotrwałymi z szybkością zapisu powyżej 500 zapisów na sekundę. W przeciwnym razie jest to przedwczesnej optymalizacji. Fragmentacja pola timestamp powoduje usunięcie 500 zapisów z ograniczeniem na sekundę, ale z ograniczeniem konieczności zapytań po stronie klienta. agregacje.

Z przykładów poniżej dowiesz się, jak posegmentować pole timestamp i jak utworzyć zapytanie podzielony wynik.

Przykładowy model danych i zapytania

Wyobraź sobie na przykład aplikację do analizy finansów w czasie zbliżonym do rzeczywistego. instrumenty takie jak waluty, akcje zwykłe i fundusze ETF. Ta aplikacja zapisuje dokumenty do kolekcji instruments, na przykład:

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

Ta aplikacja uruchamia następujące zapytania i kolejność w polu 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]);
    });

Po zbadaniu sprawy określasz, że aplikacja będzie otrzymywać od 1000 i 1500 aktualizacji instrumentów na sekundę. Przekracza to 500 zapisów na druga dozwolona w przypadku kolekcji zawierających dokumenty ze zindeksowaną sygnaturą czasową . Aby zwiększyć przepustowość zapisu, potrzebujesz 3 wartości fragmentów: MAX_INSTRUMENT_UPDATES/500 = 3 W tym przykładzie użyto wartości fragmentu x, y i z. We fragmencie możesz również użyć cyfr lub innych znaków .

Dodawanie pola fragmentu

Dodaj do dokumentów pole shard. Ustawianie pola shard na wartości x, y lub z, co zwiększa limit zapisu w zbiorze do 1500 zapisów na sekundę.

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

Wysyłanie zapytania dotyczącego sygnatury czasowej podzielonej na fragmenty

Dodanie pola shard wymaga zaktualizowania zapytań w celu agregowania danych Wyniki podzielone na fragmenty:

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]);
    });

Zaktualizuj definicje indeksów

Aby usunąć ograniczenie 500 zapisów na sekundę, usuń istniejące pojedyncze pole i indeksów złożonych korzystających z pola timestamp.

Usuń definicje indeksów złożonych

Konsola Firebase

  1. Otwórz stronę indeksów złożonych Cloud Firestore w konsoli Firebase.

    Przejdź do indeksów złożonych

  2. W przypadku każdego indeksu zawierającego pole timestamp kliknij i kliknij Usuń.

konsola GCP

  1. W konsoli Google Cloud Platform otwórz stronę Bazy danych.

    Otwórz Bazy danych

  2. Wybierz wymaganą bazę danych z listy baz danych.

  3. W menu nawigacyjnym kliknij Indeksy, a następnie wybierz kartę Kompozytowa.

  4. Użyj pola Filtr, aby wyszukać definicje indeksów zawierające timestamp.

  5. W przypadku każdego z tych indeksów kliknij i wybierz Usuń.

wiersz poleceń Firebase

  1. Jeśli nie masz skonfigurowanego interfejsu wiersza poleceń Firebase, wykonaj te instrukcje, aby je zainstalować w interfejsie wiersza poleceń i uruchomić polecenie firebase init. W ramach polecenia init skonfiguruj wybierz Firestore: Deploy rules and create indexes for Firestore.
  2. Podczas konfiguracji interfejs wiersza poleceń Firebase pobiera dotychczasowe definicje indeksów, aby plik o nazwie firestore.indexes.json (domyślnie).
  3. Usuń wszystkie definicje indeksów, które zawierają pole timestamp, w przypadku: przykład:

    {
    "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. Wdróż zaktualizowane definicje indeksów:

    firebase deploy --only firestore:indexes
    

Zaktualizuj definicje indeksu pojedynczego pola

Konsola Firebase

  1. Otwórz stronę Indeksy pojedynczych pól Cloud Firestore w konsoli Firebase.

    Przejdź do indeksów pojedynczych pól

  2. Kliknij Dodaj wyjątek.

  3. W polu Identyfikator kolekcji wpisz instruments. W polu Ścieżka pola: wpisz timestamp.

  4. W sekcji Zakres zapytania wybierz Kolekcja i Grupa kolekcji.

  5. Kliknij Dalej.

  6. Przełącz wszystkie ustawienia indeksu na Wyłączony. Kliknij Zapisz.

  7. Powtórz te same czynności w przypadku pola shard.

konsola GCP

  1. W konsoli Google Cloud Platform otwórz stronę Bazy danych.

    Otwórz Bazy danych

  2. Wybierz wymaganą bazę danych z listy baz danych.

  3. W menu nawigacyjnym kliknij Indeksy, a następnie wybierz kartę Pojedyncze pole.

  4. Kliknij kartę Pojedyncze pole.

  5. Kliknij Dodaj wyjątek.

  6. W polu Identyfikator kolekcji wpisz instruments. W polu Ścieżka pola: wpisz timestamp.

  7. W sekcji Zakres zapytania wybierz Kolekcja i Grupa kolekcji.

  8. Kliknij Dalej.

  9. Przełącz wszystkie ustawienia indeksu na Wyłączony. Kliknij Zapisz.

  10. Powtórz te same czynności w przypadku pola shard.

wiersz poleceń Firebase

  1. Dodaj ten kod do sekcji fieldOverrides definicji indeksów plik:

    {
     "fieldOverrides": [
       // Disable single-field indexing for the timestamp field
       {
         "collectionGroup": "instruments",
         "fieldPath": "timestamp",
         "indexes": []
       },
     ]
    }
    
  2. Wdróż zaktualizowane definicje indeksów:

    firebase deploy --only firestore:indexes
    

Utwórz nowe indeksy złożone

Po usunięciu wszystkich poprzednich indeksów zawierających tabelę timestamp zdefiniować nowe indeksy wymagane przez aplikację. Dowolny indeks zawierający Pole timestamp musi też zawierać pole shard. Na przykład, aby obsługiwać w zapytaniach wymienionych powyżej dodaj te indeksy:

Kolekcja Zindeksowane pola Zakres zapytania
instrumenty Fragment , price.currency, sygnatura czasowa Kolekcja
instrumenty Fragment , wymiana, sygnatura czasowa Kolekcja
instrumenty Fragment , typ instrumentu: , sygnatura czasowa Kolekcja

Komunikaty o błędach

Indeksy te możesz tworzyć, uruchamiając zaktualizowane zapytania.

Każde zapytanie zwraca komunikat o błędzie z linkiem do utworzenia wymaganych danych w konsoli Firebase.

wiersz poleceń Firebase

  1. Dodaj do pliku definicji indeksu te indeksy:

     {
       "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. Wdróż zaktualizowane definicje indeksów:

    firebase deploy --only firestore:indexes
    

Informacje o zapisie pól z indeksowaniem sekwencyjnym limitów

Ograniczenie szybkości zapisu dla pól zindeksowanych sekwencyjnie zależy od tego, Cloud Firestore przechowuje wartości indeksów i skaluje zapisy indeksów. Dla każdej wartości zapis w indeksie, Cloud Firestore definiuje wpis w postaci pary klucz-wartość, który łączy nazwę dokumentu i wartość każdego zindeksowanego pola. Cloud Firestore porządkuje te wpisy indeksu w grupy danych nazywane tabletami. Każdy Serwer Cloud Firestore przechowuje na urządzeniu co najmniej 1 tablet. Gdy zapis wczytuje się do określony tablet stanie się zbyt wysoko, Cloud Firestore skaluje się w poziomie dzieląc tablet na mniejsze i rozkładając nowe na różnych serwerach Cloud Firestore.

Cloud Firestore umieszcza leksykograficznie zamykające wpisy indeksu na tym samym tablecie. Jeśli wartości indeksu w tablecie są zbyt blisko siebie, np. pól sygnatur czasowych, Cloud Firestore nie może efektywnie dzielić na mniejsze tablety. W ten sposób powstaje obszar, w którym jeden tablet odbiera za duży ruch i może wykonywać operacje odczytu i zapisu w pamięci „gorącej” spowalniają.

Fragmentując pole sygnatury czasowej, umożliwiasz aby Cloud Firestore mógł wydajnie dzielić zadania między wiele na tabletach. Chociaż wartości w polu sygnatury czasowej mogą być blisko siebie, połączony fragment i wartość indeksu dają Cloud Firestore wystarczającą ilość miejsca między wpisami indeksu, by podzielić je na kilka tabletów.

Co dalej?