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