資料分割的時間戳記

如果集合含有索引值依序的文件, Cloud Firestore 將寫入速率限制為每秒 500 次寫入。這個頁面 說明如何分割文件欄位以超過這項限制。首先 定義「依序建立索引的欄位」並清楚說明

依序建立索引的欄位

「依序建立索引的欄位」表示所有包含 增加或減少已建立索引的欄位。在許多情況下 timestamp 欄位,但任何單調增加或減少的欄位值 可觸發每秒 500 次寫入作業的限制

例如,這項限制適用於含有以下屬性的 user 份文件: 已編入索引的 userid 欄位 (如果應用程式指派以下方式指派 userid 值):

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

另一方面,並非所有 timestamp 欄位都會觸發這項限制。如果 timestamp 欄位會追蹤隨機分配的值,寫入限制不會 或欄位的實際值並不會產生影響,只會影響該欄位值 訂閱次數就不會增加或減少例如: 由一連串單調遞增欄位值觸發 寫入限制:

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

分割時間戳記欄位

假設應用程式使用單調增加的 timestamp 欄位。 如果應用程式未在任何查詢中使用「timestamp」欄位,您可以移除 未建立索引時間戳記欄位的每秒 500 次寫入限制。如果使用 請為查詢加入 timestamp 欄位,您可以嘗試 透過資料分割時間戳記

  1. timestamp 欄位旁邊新增 shard 欄位。使用 1..n 個不重複選項 shard欄位中的值。這會引發寫入 集合範圍限定為 500*n,但您必須匯總 n 查詢。
  2. 更新寫入邏輯,以隨機指派 shard 值給各個 文件。
  3. 更新查詢,匯總資料分割結果集。
  4. 停用 shard 欄位和 timestamp 的單一欄位索引 ] 欄位。刪除包含 timestamp 的現有複合式索引 ] 欄位。
  5. 建立新的複合式索引以支援更新後的查詢。請注意, 索引中的欄位很重要,而且 shard 欄位必須出現在 「timestamp」欄位中的值。包含 timestamp 欄位也必須包含 shard 欄位。

分割時間戳記僅適用於 且寫入速率超過每秒 500 次否則,這是 進行早期最佳化的最佳化分割 timestamp 欄位會移除 500 次寫入作業 但需要在用戶端進行查詢時 進行匯總。

以下範例說明如何分割 timestamp 欄位,以及如何查詢 分割結果集。

資料模型與查詢範例

舉例來說,假設某個應用程式可提供近乎即時的財務分析 貨幣、常見股票和 ETF 等樂器。這個應用程式寫入 匯出至 instruments 集合,如下所示:

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

這個應用程式會依照 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]);
    });

經過一番研究後,您判斷在 每秒 1,000 次和 1,500 次樂器更新。這會超過每秒 500 次寫入 每秒允許的集合,其中含有索引時間戳記的文件 只要使用來自這些領域的 小型資料集訓練即可如要提高寫入處理量,您需要 3 個資料分割值, MAX_INSTRUMENT_UPDATES/500 = 3。這個範例使用資料分割值 xyz。您也可以使用數字或其他字元做為資料分割 輕鬆分配獎金

新增資料分割欄位

在文件中新增 shard 欄位。設定 shard 欄位 設為 xyz 值,這會提高集合的寫入限制 每秒可執行 1,500 次寫入

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

查詢資料分割時間戳記

如要新增「shard」欄位,你必須更新查詢來匯總 分割結果:

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

更新索引定義

如要移除每秒 500 次寫入的限制,請刪除現有的單一欄位 以及使用 timestamp 欄位的複合式索引。

刪除複合式索引定義

Firebase 主控台

  1. 在 Firebase 控制台中開啟「Cloud Firestore 複合式索引」頁面。

    前往複合式索引

  2. 針對包含 timestamp 欄位的每個索引,按一下 按鈕,然後按一下「刪除

GCP 控制台

  1. 在 Google Cloud Platform Console 中,前往「Databases」頁面。

    前往「資料庫」頁面

  2. 從資料庫清單中選取所需的資料庫。

  3. 在導覽選單中,依序按一下「索引」和「複合」分頁標籤。

  4. 使用「篩選器」欄位搜尋包含 timestamp 欄位。

  5. 針對每個索引按一下 按鈕,然後按一下「Delete」(刪除)

Firebase CLI

  1. 如果尚未設定 Firebase CLI,請按照這裡的操作說明安裝 然後執行 firebase init 指令。在 init 指令期間,進行以下操作: 請務必選取「Firestore: Deploy rules and create indexes for Firestore」。
  2. 在設定期間,Firebase CLI 會將您現有的索引定義下載至 名為 firestore.indexes.json 的檔案。
  3. 移除任何包含 timestamp 欄位的索引定義, 範例:

    {
    "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. 部署更新過的索引定義:

    firebase deploy --only firestore:indexes
    

更新單一欄位索引定義

Firebase 主控台

  1. 開啟 Cloud Firestore 單一欄位索引頁面, Firebase 控制台。

    前往單一欄位索引

  2. 按一下「Add Exemption」(新增豁免)

  3. 「Collection ID」(集合 ID) 中輸入 instruments。在 Field path 部分 輸入 timestamp

  4. 「Query range」(查詢範圍) 下方,選取 [Collection] (集合)集合群組

  5. 按一下「下一步」。

  6. 將所有索引設定切換為「停用」。按一下「Save」(儲存)

  7. 針對 shard 欄位重複上述步驟。

GCP 控制台

  1. 在 Google Cloud Platform Console 中,前往「Databases」頁面。

    前往「資料庫」頁面

  2. 從資料庫清單中選取所需的資料庫。

  3. 在導覽選單中,依序按一下「Indexes」和「Single Field」分頁標籤。

  4. 按一下「Single Field」(單一欄位) 分頁標籤。

  5. 按一下「Add Exemption」(新增豁免)

  6. 「Collection ID」(集合 ID) 中輸入 instruments。在 Field path 部分 輸入 timestamp

  7. 「Query range」(查詢範圍) 下方,選取 [Collection] (集合)集合群組

  8. 按一下「下一步」。

  9. 將所有索引設定切換為「停用」。按一下「Save」(儲存)

  10. 針對 shard 欄位重複上述步驟。

Firebase CLI

  1. 請將以下內容新增至索引定義的 fieldOverrides 區段 檔案:

    {
     "fieldOverrides": [
       // Disable single-field indexing for the timestamp field
       {
         "collectionGroup": "instruments",
         "fieldPath": "timestamp",
         "indexes": []
       },
     ]
    }
    
  2. 部署更新過的索引定義:

    firebase deploy --only firestore:indexes
    

建立新的複合式索引

移除所有先前包含 timestamp 的索引後, 定義應用程式需要的新索引。任何包含 timestamp 欄位也必須包含 shard 欄位。例如支援 上述查詢,新增下列索引:

集合 已建立索引的欄位 查詢範圍
instruments 個資料分割, 價格.currency,時間戳記 集合
instruments 個資料分割, 個交換, 個時間戳記 集合
instruments 資料分割, instrumentType, 時間戳記 集合

錯誤訊息

您可以執行更新的查詢來建立這些索引。

每項查詢都傳回錯誤訊息,其中包含用來建立必要查詢的連結 也就在 Firebase 控制台中

Firebase CLI

  1. 請在索引定義檔案中加入下列索引:

     {
       "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. 部署更新過的索引定義:

    firebase deploy --only firestore:indexes
    

瞭解限制依序建立索引的欄位寫入作業

已建立索引序列欄位的寫入頻率限制取決於 Cloud Firestore 會儲存索引值,並調度索引寫入資源。對於每項 索引寫入,Cloud Firestore 定義了 文件名稱以及每個已建立索引欄位的值。Cloud Firestore 將這些索引項目分成稱為「子表」的資料群組。每項 Cloud Firestore 伺服器包含一���多個平板電腦。寫入載入 而 Cloud Firestore 是水平擴充 做法是將平板電腦分割成較小的平板電腦 在不同 Cloud Firestore 伺服器之間傳輸資料

Cloud Firestore 會將���字�����序排列的索引項目���置在相同位置 平板電腦。如果平板電腦上的索引值太接近,例如 時間戳記欄位,Cloud Firestore 無法有效率地分割 隨著平板電腦進入小型平板電腦這會造成單一平板電腦的熱點 接收過多流量 以及讀取和寫入作業 變得比較慢

分割時間戳記欄位後, 可讓 Cloud Firestore 有效率地將工作負載拆分至 平板電腦。雖然時間戳記欄位的值可能彼此相近 串連的資料分割和索引值能為 Cloud Firestore 提供足夠的空間 即可在多個子表之間分割項目。

後續步驟