إذا كانت المجموعة تحتوي على مستندات ذات قيم مفهرسة تسلسلية، يحدّ 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 الفهارس المركّبة في وحدة تحكّم Firebase.
بالنسبة إلى كل فهرس يحتوي على الحقل
timestamp
، انقر على الزر ثم على حذف.
وحدة تحكّم Google Cloud Platform
في Google Cloud Console، انتقِل إلى صفحة قواعد البيانات.
اختَر قاعدة البيانات المطلوبة من قائمة قواعد البيانات.
في قائمة التنقّل، انقر على الفهارس، ثمّ على علامة التبويب المركّب.
استخدِم حقل الفلتر للبحث عن تعريفات الفهرس التي تحتوي على الحقل
timestamp
.بالنسبة إلى كل فهرس من هذه الفهارس، انقر على الزر
ثم على حذف.
Firebase CLI
- إذا لم تكن قد أعددت واجهة سطر الأوامر (CLI) في Firebase، اتّبِع هذه التعليمات لتثبيت
واجهة سطر الأوامر وتنفيذ الأمر
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 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, timestamp | التجميع |
آلات موسيقية | جزء، عملية تبادل، طابع زمني | التجميع |
آلات موسيقية | 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