Parçalanmış zaman damgaları

Bir koleksiyon sıralı indekslenmiş değerlere sahip belgeler içeriyorsa Cloud Firestore, yazma hızını saniyede 500 yazma ile sınırlar. Bu sayfada, bu sınırı aşmak için bir belge alanının nasıl parçalanacağı açıklanmaktadır. Öncelikle "sıralı indeksli alanlar" ile ne kastettiğimizi tanımlayalım ve bu sınırın ne zaman geçerli olduğunu açıklayalım.

Sıralı indekslenmiş alanlar

"Sıralı indekslenmiş alanlar", monoton olarak artan veya azalan indekslenmiş alan içeren herhangi bir belge koleksiyonu anlamına gelir. Çoğu durumda bu, bir timestamp alanı anlamına gelir, ancak monoton olarak artan veya azalan herhangi bir alan değeri, saniyede 500 yazma yazma sınırını tetikleyebilir.

Örneğin, uygulamanın aşağıdaki gibi userid değerleri ataması durumunda sınır, dizine alınmış kullanıcı userid alanına sahip bir user belgeleri koleksiyonu için geçerli olur:

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

Öte yandan, timestamp alanlarının tümü bu sınırı tetiklemez. timestamp alanı rastgele dağıtılmış değerleri izliyorsa yazma sınırı uygulanmaz. Alanın gerçek değeri de önemli değildir, yalnızca alanın monoton olarak artması veya azalması önemlidir. Örneğin, aşağıdaki monoton olarak artan alan değerleri kümelerinin her ikisi de yazma sınırını tetikler:

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

Zaman damgası alanını parçalama

Uygulamanızın monoton olarak artan bir timestamp alanı kullandığını varsayalım. Uygulamanız herhangi bir sorguda timestamp alanını kullanmıyorsa zaman damgası alanını dizine eklemeyerek saniyede 500 yazma sınırını kaldırabilirsiniz. Sorgularınız için bir timestamp alanına ihtiyacınız varsa, parçalanmış zaman damgalarını kullanarak sınırı aşabilirsiniz:

  1. timestamp alanının yanına bir shard alanı ekleyin. shard alanı için 1..n farklı değerler kullanın. Bu, koleksiyonun yazma sınırını 500*n yükseltir, ancak n sorguyu toplamanız gerekir.
  2. Her belgeye rastgele bir shard değeri atamak için yazma mantığınızı güncelleyin.
  3. Parçalanmış sonuç kümelerini toplamak için sorgularınızı güncelleyin.
  4. Hem shard alanı hem de timestamp alanı için tek alanlı dizinleri devre dışı bırakın. timestamp alanını içeren mevcut bileşik dizinleri silin.
  5. Güncellenmiş sorgularınızı desteklemek için yeni bileşik dizinler oluşturun. Dizindeki alanların sırası önemlidir ve shard alanı timestamp alanından önce gelmelidir. timestamp alanını içeren tüm dizinler aynı zamanda shard alanını da içermelidir.

Parçalanmış zaman damgalarını yalnızca sürekli yazma hızının saniyede 500 yazmanın üzerinde olduğu kullanım durumlarında uygulamanız gerekir. Aksi takdirde bu, olgunlaşmamış bir optimizasyondur. Bir timestamp alanının parçalanması, saniyede 500 yazma sınırlamasını ortadan kaldırır, ancak bu, istemci tarafı sorgu toplama ihtiyacını da ortadan kaldırır.

Aşağıdaki örnekler, timestamp alanının nasıl parçalanacağını ve parçalanmış bir sonuç kümesinin nasıl sorgulanacağını gösterir.

Örnek veri modeli ve sorgular

Örnek olarak, para birimleri, hisse senetleri ve ETF'ler gibi finansal araçların neredeyse gerçek zamanlı analizi için bir uygulama hayal edin. Bu uygulama belgeleri şu şekilde bir instruments koleksiyonuna yazar:

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

Bu uygulama, timestamp alanına göre aşağıdaki sorguları ve siparişleri çalıştırır:

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

Biraz araştırma yaptıktan sonra uygulamanın saniyede 1.000 ile 1.500 arasında cihaz güncellemesi alacağını belirlersiniz. Bu, indekslenmiş zaman damgası alanlarına sahip belgeleri içeren koleksiyonlar için izin verilen saniyede 500 yazma sınırını aşıyor. Yazma verimini artırmak için 3 parça değerine ihtiyacınız vardır: MAX_INSTRUMENT_UPDATES/500 = 3 . Bu örnekte x , y ve z parça değerleri kullanılır. Parça değerleriniz için sayıları veya diğer karakterleri de kullanabilirsiniz.

Parça alanı ekleme

Belgelerinize bir shard alanı ekleyin. shard alanını x , y veya z değerlerine ayarlayın; bu, koleksiyondaki yazma sınırını saniyede 1.500 yazma sayısına yükseltir.

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

Parçalanmış zaman damgasını sorgulama

shard alanı eklemek, sorgularınızı parçalı sonuçları toplayacak şekilde güncellemenizi gerektirir:

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

Dizin tanımlarını güncelleyin

Saniyede 500 yazma sınırlamasını kaldırmak için, timestamp alanını kullanan mevcut tek alanlı ve bileşik dizinleri silin.

Bileşik dizin tanımlarını silin

Firebase Konsolu

  1. Firebase konsolunda Cloud Firestore Bileşik Dizinler sayfasını açın.

    Bileşik Dizinlere Git

  2. timestamp damgası alanını içeren her dizin için düğmesini ve ardından Sil'i tıklayın.

GCP Konsolu

  1. Google Cloud Platform Console'da Veritabanları sayfasına gidin.

    Veritabanlarına Git

  2. Veritabanları listesinden gerekli veritabanını seçin.

  3. Gezinme menüsünde Dizinler'e ve ardından Bileşik sekmesine tıklayın.

  4. timestamp alanını içeren dizin tanımlarını aramak için Filtre alanını kullanın.

  5. Bu dizinlerin her biri için düğmesini ve ardından Sil'i tıklayın.

Firebase CLI

  1. Firebase CLI'yi kurmadıysanız CLI'yi yüklemek ve firebase init komutunu çalıştırmak için bu talimatları izleyin . init komutu sırasında Firestore: Deploy rules and create indexes for Firestore seçtiğinizden emin olun.
  2. Kurulum sırasında Firebase CLI, mevcut dizin tanımlarınızı varsayılan olarak firestore.indexes.json adlı bir dosyaya indirir.
  3. timestamp alanını içeren tüm dizin tanımlarını kaldırın, örneğin:

    {
    "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. Güncellenmiş dizin tanımlarınızı dağıtın:

    firebase deploy --only firestore:indexes
    

Tek alanlı dizin tanımlarını güncelleyin

Firebase Konsolu

  1. Firebase konsolunda Cloud Firestore Tek Alan Dizinleri sayfasını açın.

    Tek Alan Dizinlerine Git

  2. Muafiyet Ekle'yi tıklayın.

  3. Koleksiyon Kimliği için instruments girin. Alan yolu için timestamp girin.

  4. Sorgu kapsamı altında hem Koleksiyon hem de Koleksiyon grubunu seçin.

  5. Sonrakine tıkla

  6. Tüm dizin ayarlarını Devre Dışı olarak değiştirin. Kaydet'i tıklayın.

  7. shard alanı için aynı adımları tekrarlayın.

GCP Konsolu

  1. Google Cloud Platform Console'da Veritabanları sayfasına gidin.

    Veritabanlarına Git

  2. Veritabanları listesinden gerekli veritabanını seçin.

  3. Gezinme menüsünde Dizinler'e ve ardından Tek Alan sekmesine tıklayın.

  4. Tek Alan sekmesine tıklayın.

  5. Muafiyet Ekle'yi tıklayın.

  6. Koleksiyon Kimliği için instruments girin. Alan yolu için timestamp girin.

  7. Sorgu kapsamı altında hem Koleksiyon hem de Koleksiyon grubunu seçin.

  8. Sonrakine tıkla

  9. Tüm dizin ayarlarını Devre Dışı olarak değiştirin. Kaydet'i tıklayın.

  10. shard alanı için aynı adımları tekrarlayın.

Firebase CLI

  1. Dizin tanımları dosyanızın fieldOverrides bölümüne aşağıdakini ekleyin:

    {
     "fieldOverrides": [
       // Disable single-field indexing for the timestamp field
       {
         "collectionGroup": "instruments",
         "fieldPath": "timestamp",
         "indexes": []
       },
     ]
    }
    
  2. Güncellenmiş dizin tanımlarınızı dağıtın:

    firebase deploy --only firestore:indexes
    

Yeni bileşik dizinler oluşturun

timestamp içeren önceki tüm dizinleri kaldırdıktan sonra, uygulamanızın gerektirdiği yeni dizinleri tanımlayın. timestamp alanını içeren herhangi bir dizin aynı zamanda shard alanını da içermelidir. Örneğin yukarıdaki sorguları desteklemek için aşağıdaki dizinleri ekleyin:

Toplamak Dizine eklenen alanlar Sorgu kapsamı
aletleri parça, fiyatı.para birimi, zaman damgası Toplamak
aletleri parça, değişimi, zaman damgası Toplamak
aletleri Shard, instrumentsType, timestamp Toplamak

Hata mesajları

Güncellenen sorguları çalıştırarak bu dizinleri oluşturabilirsiniz.

Her sorgu, Firebase Konsolunda gerekli dizini oluşturmaya yönelik bağlantıyı içeren bir hata mesajı döndürür.

Firebase CLI

  1. Aşağıdaki dizinleri dizin tanımlama dosyanıza ekleyin:

     {
       "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. Güncellenmiş dizin tanımlarınızı dağıtın:

    firebase deploy --only firestore:indexes
    

Limit sıralı indeksli alanlar için yazmayı anlama

Sıralı dizine eklenen alanlar için yazma hızı sınırı, Cloud Firestore'un dizin değerlerini nasıl sakladığına ve dizin yazma işlemlerini nasıl ölçeklendirdiğine bağlıdır. Her dizin yazma işlemi için Cloud Firestore, belge adını ve dizine eklenen her alanın değerini birleştiren bir anahtar/değer girişi tanımlar. Cloud Firestore, bu dizin girişlerini tablet adı verilen veri grupları halinde düzenler. Her Cloud Firestore sunucusu bir veya daha fazla tableti barındırır. Belirli bir tabletin yazma yükü çok yükseldiğinde Cloud Firestore, tableti daha küçük tabletlere bölerek ve yeni tabletleri farklı Cloud Firestore sunucularına yayarak yatay olarak ölçeklenir.

Cloud Firestore sözlüksel olarak yakın dizin girişlerini aynı tablete yerleştirir. Bir tabletteki dizin değerleri (örneğin, zaman damgası alanları) birbirine çok yakınsa Cloud Firestore, tableti verimli bir şekilde daha küçük tabletlere bölemez. Bu, tek bir tabletin çok fazla trafik aldığı ve etkin noktaya yönelik okuma ve yazma işlemlerinin yavaşladığı bir etkin nokta oluşturur.

Bir zaman damgası alanını parçalayarak Cloud Firestore'un iş yüklerini birden fazla tablete verimli bir şekilde bölmesini mümkün kılarsınız. Zaman damgası alanının değerleri birbirine yakın kalabilse de, birleştirilmiş parça ve dizin değeri, Cloud Firestore'a dizin girişleri arasında girişleri birden fazla tablete bölmek için yeterli alan sağlar.

Sıradaki ne