אם אוסף מכיל מסמכים עם ערכי אינדקס עוקבים, 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
עבור השאילתות שלך, תוכל לעקוף את המגבלה על ידי שימוש בחותמות זמן מרוסקות :
- הוסף שדה
shard
לצד שדהtimestamp
. השתמש1..n
ערכים ברורים עבור שדהshard
. זה מעלה את מגבלת הכתיבה של האוסף ל500*n
, אך עליך לצבורn
שאילתות. - עדכן את היגיון הכתיבה שלך כדי להקצות באופן אקראי ערך
shard
לכל מסמך. - עדכן את השאילתות שלך כדי לצבור את קבוצות התוצאות המרוסקות.
- השבת אינדקסים של שדה בודד הן עבור שדה
shard
והן עבור שדהtimestamp
. מחק אינדקסים מורכבים קיימים המכילים את שדה חותמתtimestamp
. - צור אינדקסים מורכבים חדשים כדי לתמוך בשאילתות המעודכנות שלך. יש חשיבות לסדר השדות באינדקס, ושדה
shard
חייב לבוא לפני שדהtimestamp
. כל האינדקסים הכוללים את שדהtimestamp
חייבים לכלול גם את שדהshard
.
עליך ליישם חותמות זמן מרוסקות רק במקרים של שימוש עם קצבי כתיבה מתמשכים מעל 500 כתיבה בשנייה. אחרת, מדובר באופטימיזציה מוקדמת. פיצול שדה timestamp
מסיר את ההגבלה של 500 כתיבה לשנייה, אך עם הפשרה של צורך בצבירה של שאילתות בצד הלקוח.
הדוגמאות הבאות מראות כיצד לגזור שדה timestamp
וכיצד לבצע שאילתה על ערכת תוצאות מפוצלת.
מודל נתונים ושאילתות לדוגמה
כדוגמה, דמיינו אפליקציה לניתוח כמעט בזמן אמת של מכשירים פיננסיים כמו מטבעות, מניות רגילות ותעודות סל. אפליקציה זו כותבת מסמכים לאוסף 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
פתח את הדף של Cloud Firestore Composite Indexs במסוף Firebase.
עבור כל אינדקס המכיל את שדה
timestamp
, לחץ על הלחצן ולחץ על מחק .
מסוף GCP
ב-Google Cloud Platform Console, עבור לדף מסדי נתונים .
בחר את מסד הנתונים הנדרש מרשימת מסדי הנתונים.
בתפריט הניווט, לחץ על אינדקסים ולאחר מכן לחץ על הכרטיסייה Composite .
השתמש בשדה מסנן כדי לחפש הגדרות אינדקס המכילות את שדה
timestamp
.עבור כל אחד מהאינדקסים הללו, לחץ על הלחצן
ולחץ על מחק .
Firebase CLI
- אם לא הגדרת את Firebase CLI, עקוב אחר ההנחיות הבאות כדי להתקין את ה-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 Single Field Indexs במסוף Firebase.
לחץ על הוסף פטור .
עבור מזהה אוסף , הזן
instruments
. עבור נתיב שדה , הזןtimestamp
.תחת היקף שאילתה , בחר גם אוסף וגם קבוצת אוסף .
הקש "הבא
החלף את כל הגדרות האינדקס למצב מושבת . לחץ על שמור .
חזור על אותם שלבים עבור שדה
shard
.
מסוף GCP
ב-Google Cloud Platform Console, עבור לדף מסדי נתונים .
בחר את מסד הנתונים הנדרש מרשימת מסדי הנתונים.
בתפריט הניווט, לחץ על אינדקסים ולאחר מכן לחץ על הכרטיסייה שדה יחיד .
לחץ על הכרטיסייה שדה יחיד .
לחץ על הוסף פטור .
עבור מזהה אוסף , הזן
instruments
. עבור נתיב שדה , הזןtimestamp
.תחת היקף שאילתה , בחר גם אוסף וגם קבוצת אוסף .
הקש "הבא
החלף את כל הגדרות האינדקס למצב מושבת . לחץ על שמור .
חזור על אותם שלבים עבור שדה
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
. לדוגמה, כדי לתמוך בשאילתות למעלה, הוסף את האינדקסים הבאים:
אוסף | שדות הוספו לאינדקס | היקף שאילתה |
---|---|---|
כלים | shard, price.currency, חותמת זמן | אוסף |
כלים | shard, exchange, חותמת זמן | אוסף |
כלים | shard, instrumentType, חותמת זמן | אוסף |
הודעות שגיאה
אתה יכול לבנות את האינדקסים האלה על ידי הפעלת השאילתות המעודכנות.
כל שאילתה מחזירה הודעת שגיאה עם קישור ליצירת האינדקס הנדרש ב-Firebase Console.
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
הבנת שדות ה-write for limit ברצף באינדקס
המגבלה על קצב הכתיבה של שדות עוקבים שנוספו לאינדקס נובעת מהאופן שבו Cloud Firestore מאחסנת ערכי אינדקס ומקנה קנה מידה של כתיבת אינדקס. עבור כל כתיבת אינדקס, Cloud Firestore מגדירה ערך מפתח-ערך שמשרשר את שם המסמך והערך של כל שדה שנוסף לאינדקס. Cloud Firestore מארגנת את כניסות האינדקס הללו לקבוצות נתונים הנקראות טאבלטים . כל שרת Cloud Firestore מכיל טאבלט אחד או יותר. כאשר עומס הכתיבה לטאבלט מסוים הופך גבוה מדי, Cloud Firestore משתנה לרוחב על ידי פיצול הטאבלט לטאבלטים קטנים יותר והפצת הטאבלטים החדשים על פני שרתי Cloud Firestore שונים.
Cloud Firestore מציבה ערכי אינדקס קרובים מבחינה לקסיקוגרפית באותו טאבלט. אם ערכי האינדקס בטאבלט קרובים מדי זה לזה, כגון עבור שדות חותמת זמן, Cloud Firestore לא יכולה לפצל ביעילות את הטאבלט לטאבלטים קטנים יותר. זה יוצר נקודה חמה שבה טאבלט בודד מקבל יותר מדי תעבורה, ופעולות הקריאה והכתיבה לנקודה החמה נעשות איטיות יותר.
על ידי פיצול שדה חותמת זמן, אתה מאפשר ל-Cloud Firestore לפצל ביעילות עומסי עבודה על פני מספר טאבלטים. למרות שהערכים של שדה חותמת הזמן עשויים להישאר קרובים זה לזה, הרסיס המשורשר וערך האינדקס מעניקים ל-Cloud Firestore מספיק מקום בין ערכי האינדקס כדי לפצל את הערכים בין מספר טאבלטים.
מה הלאה
- קרא את השיטות המומלצות לעיצוב בקנה מידה
- למקרים עם שיעורי כתיבה גבוהים למסמך בודד, ראה מונים מופרעים
- עיין במגבלות הסטנדרטיות עבור Cloud Firestore