إذا كانت المجموعة تحتوي على مستندات تحتوي على قيم مفهرَسة تسلسلية، يقتصر معدل الكتابة في 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 المالية في الوقت الفعلي تقريبًا، مثل العملات والأسهم العادية وصناديق المؤشرات المتداولة. يكتب هذا التطبيق
المستندات في مجموعة 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 الفهارس المركبة في وحدة تحكّم Firebase.
لكل فهرس يحتوي على الحقل
timestamp
، انقر على الزر ثمّ انقر على حذف.
وحدة تحكّم Google Cloud Platform
في وحدة تحكّم Google Cloud، انتقِل إلى صفحة قواعد البيانات.
اختَر قاعدة البيانات المطلوبة من قائمة قواعد البيانات.
في قائمة التنقّل، انقر على الفهارس، ثمّ انقر على علامة التبويب مركب.
استخدِم حقل الفلترة للبحث عن تعريفات الفهرس التي تحتوي على الحقل
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 فهارس الحقول الفردية في وحدة تحكُّم Firebase.
انقر على إضافة إعفاء.
بالنسبة إلى معرّف المجموعة، أدخِل
instruments
. بالنسبة إلى مسار الحقل، أدخِلtimestamp
.ضمن نطاق طلب البحث، اختَر كلّ من المجموعة و مجموعة المجموعات.
انقر على التالي.
فعِّل جميع إعدادات الفهرس على إيقاف. انقر على حفظ.
كرِّر الخطوات نفسها للحقل
shard
.
وحدة تحكّم Google Cloud Platform
في وحدة تحكّم Google Cloud، انتقِل إلى صفحة قواعد البيانات.
اختَر قاعدة البيانات المطلوبة من قائمة قواعد البيانات.
في قائمة التنقّل، انقر على الفهارس، ثم على علامة التبويب حقل واحد.
انقر على علامة التبويب حقل واحد.
انقر على إضافة إعفاء.
بالنسبة إلى معرّف المجموعة، أدخِل
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, instrumentType, timestamp | التجميع |
رسائل الخطأ
يمكنك إنشاء هذه الفهارس من خلال تنفيذ طلبات البحث المعدّلة.
يعرض كلّ طلب بحث رسالة خطأ تتضمّن رابطًا لإنشاء ملف الفهرس المطلوب في "وحدة تحكّم 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 مساحة كافية بين إدخالات الفهرس لتقسيم الإدخالات بين أقسام متعددة.
الخطوات التالية
- اطّلِع على أفضل الممارسات المتعلّقة بالتصميم على نطاق واسع.
- بالنسبة إلى الحالات التي يكون فيها معدّل الكتابة مرتفعًا في مستند واحد، راجِع المعدادات الموزّعة.
- اطّلِع على الحدود العادية لـ Cloud Firestore.