如果集合含有索引值依序的文件, 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
欄位,您可以嘗試
透過資料分割時間戳記:
- 在
timestamp
欄位旁邊新增shard
欄位。使用1..n
個不重複選項shard
欄位中的值。這會引發寫入 集合範圍限定為500*n
,但您必須匯總n
查詢。 - 更新寫入邏輯,以隨機指派
shard
值給各個 文件。 - 更新查詢,匯總資料分割結果集。
- 停用
shard
欄位和timestamp
的單一欄位索引 ] 欄位。刪除包含timestamp
的現有複合式索引 ] 欄位。 - 建立新的複合式索引以支援更新後的查詢。請注意,
索引中的欄位很重要,而且
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
。這個範例使用資料分割值 x
,
y
和z
。您也可以使用數字或其他字元做為資料分割
輕鬆分配獎金
新增資料分割欄位
在文件中新增 shard
欄位。設定 shard
欄位
設為 x
、y
或 z
值,這會提高集合的寫入限制
每秒可執行 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 主控台
在 Firebase 控制台中開啟「Cloud Firestore 複合式索引」頁面。
針對包含
timestamp
欄位的每個索引,按一下 按鈕,然後按一下「刪除」。
GCP 控制台
在 Google Cloud Platform Console 中,前往「Databases」頁面。
從資料庫清單中選取所需的資料庫。
在導覽選單中,依序按一下「索引」和「複合」分頁標籤。
使用「篩選器」欄位搜尋包含
timestamp
欄位。針對每個索引按一下
按鈕,然後按一下「Delete」(刪除)。
Firebase CLI
- 如果尚未設定 Firebase CLI,請按照這裡的操作說明安裝
然後執行
firebase init
指令。在init
指令期間,進行以下操作: 請務必選取「Firestore: Deploy rules and create indexes for Firestore
」。 - 在設定期間,Firebase CLI 會將您現有的索引定義下載至
名為
firestore.indexes.json
的檔案。 移除任何包含
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" } ] }, ] }
部署更新過的索引定義:
firebase deploy --only firestore:indexes
更新單一欄位索引定義
Firebase 主控台
開啟 Cloud Firestore 單一欄位索引頁面, Firebase 控制台。
按一下「Add Exemption」(新增豁免)。
在「Collection ID」(集合 ID) 中輸入
instruments
。在 Field path 部分 輸入timestamp
。在「Query range」(查詢範圍) 下方,選取 [Collection] (集合) 和 集合群組:
按一下「下一步」。
將所有索引設定切換為「停用」。按一下「Save」(儲存)。
針對
shard
欄位重複上述步驟。
GCP 控制台
在 Google Cloud Platform Console 中,前往「Databases」頁面。
從資料庫清單中選取所需的資料庫。
在導覽選單中,依序按一下「Indexes」和「Single Field」分頁標籤。
按一下「Single Field」(單一欄位) 分頁標籤。
按一下「Add Exemption」(新增豁免)。
在「Collection ID」(集合 ID) 中輸入
instruments
。在 Field path 部分 輸入timestamp
。在「Query range」(查詢範圍) 下方,選取 [Collection] (集合) 和 集合群組:
按一下「下一步」。
將所有索引設定切換為「停用」。按一下「Save」(儲存)。
針對
shard
欄位重複上述步驟。
Firebase CLI
請將以下內容新增至索引定義的
fieldOverrides
區段 檔案:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }
部署更新過的索引定義:
firebase deploy --only firestore:indexes
建立新的複合式索引
移除所有先前包含 timestamp
的索引後,
定義應用程式需要的新索引。任何包含
timestamp
欄位也必須包含 shard
欄位。例如支援
上述查詢,新增下列索引:
集合 | 已建立索引的欄位 | 查詢範圍 |
---|---|---|
instruments | 個資料分割, 價格.currency, 時間戳記 | 集合 |
instruments | 個資料分割, 個交換, 個時間戳記 | 集合 |
instruments | 資料分割, instrumentType, 時間戳記 | 集合 |
錯誤訊息
您可以執行更新的查詢來建立這些索引。
每項查詢都傳回錯誤訊息,其中包含用來建立必要查詢的連結 也就在 Firebase 控制台中
Firebase CLI
請在索引定義檔案中加入下列索引:
{ "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" } ] }, ] }
部署更新過的索引定義:
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 提供足夠的空間 即可在多個子表之間分割項目。
後續步驟
- 參閱大規模設計最佳做法
- 如果單一文件的寫入速率偏高,請參閱簡化計數器。
- 查看 Cloud Firestore 的標準限制