หากคอลเล็กชันมีเอกสารที่มีค่าที่จัดทำดัชนีตามลำดับ Cloud Firestore จะจำกัดอัตราการเขียนไว้ที่ 500 ครั้งต่อวินาที หน้านี้ อธิบายวิธีชาร์ดฟิลด์เอกสารเพื่อเอาชนะขีดจำกัดนี้ ก่อนอื่น เรามาทำความเข้าใจกันก่อนว่า "ฟิลด์ที่จัดทำดัชนีตามลำดับ" หมายถึงอะไร และชี้แจงว่าขีดจำกัดนี้มีผลเมื่อใด
ช่องที่จัดทำดัชนีตามลำดับ
"ฟิลด์ที่จัดทำดัชนีตามลำดับ" หมายถึงคอลเล็กชันเอกสารใดๆ ที่มีฟิลด์ที่จัดทำดัชนีซึ่งเพิ่มขึ้นหรือลดลงอย่างสม่ำเสมอ ในหลายกรณี หมายถึงช่อง timestamp แต่ค่าของช่องที่เพิ่มขึ้นหรือลดลงแบบโมโนโทนิกอาจทำให้เกิดขีดจำกัดการเขียน 500 ครั้งต่อวินาที
ตัวอย่างเช่น ขีดจำกัดจะมีผลกับคอลเล็กชันเอกสาร user รายการที่มีช่องที่จัดทำดัชนี userid หากแอปกำหนดค่า userid ดังนี้
1281, 1282, 1283, 1284, 1285, ...
ในทางกลับกัน ฟิลด์ timestamp บางฟิลด์ก็ไม่ได้ทริกเกอร์ขีดจำกัดนี้ หากช่อง timestamp ติดตามค่าที่กระจายแบบสุ่ม ขีดจำกัดการเขียนจะไม่มีผล ค่าจริงของช่องก็ไม่สำคัญเช่นกัน มีเพียงช่องที่เพิ่มขึ้นหรือลดลงแบบโมโนโทนเท่านั้น ตัวอย่างเช่น ชุดค่าในช่องที่เพิ่มขึ้นแบบโมโนโทนิกทั้ง 2 ชุดต่อไปนี้จะทริกเกอร์ขีดจำกัดการเขียน
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 และวิธีค้นหาชุดผลลัพธ์ที่ชาร์ด
ตัวอย่างโมเดลข้อมูลและการค้นหา
ตัวอย่างเช่น ลองนึกถึงแอปสำหรับการวิเคราะห์ตราสารทางการเงินแบบเกือบเรียลไทม์ เช่น สกุลเงิน หุ้นสามัญ และ 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 ให้ไปที่แท็บฐานข้อมูลและพื้นที่เก็บข้อมูล > Firestore > ดัชนีแบบผสม
สำหรับแต่ละดัชนีที่มีช่อง
timestampให้คลิกปุ่ม แล้วคลิกลบ
คอนโซล GCP
ในคอนโซล 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
ในคอนโซล Firebase ให้ไปที่แท็บฐานข้อมูลและพื้นที่เก็บข้อมูล > Firestore > ดัชนีช่องเดียว
คลิกเพิ่มการยกเว้น
ในส่วนรหัสคอลเล็กชัน ให้ป้อน
instrumentsในส่วนเส้นทางของช่อง ให้ป้อนtimestampในส่วนขอบเขตการค้นหา ให้เลือกทั้งคอลเล็กชันและกลุ่มคอลเล็กชัน
คลิกถัดไป
สลับการตั้งค่าดัชนีทั้งหมดเป็นปิดใช้ คลิกบันทึก
ทำซ้ำขั้นตอนเดียวกันสำหรับช่อง
shard
คอนโซล GCP
ในคอนโซล 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 ด้วย ตัวอย่างเช่น หากต้องการรองรับการค้นหาข้างต้น ให้เพิ่มดัชนีต่อไปนี้
| การรวบรวม | ช่องที่จัดทำดัชนี | ขอบเขตการค้นหา |
|---|---|---|
| เครื่องดนตรี | ชาร์ด , price.currency , การประทับเวลา | การรวบรวม |
| เครื่องดนตรี | ชาร์ด รายการ, การแลกเปลี่ยน รายการ, การประทับเวลา รายการ | การรวบรวม |
| เครื่องดนตรี | ชาร์ด , ประเภทเครื่องมือ , การประทับเวลา | การรวบรวม |
ข้อความแสดงข้อผิดพลาด
คุณสร้างดัชนีเหล่านี้ได้โดยเรียกใช้การค้นหาที่อัปเดตแล้ว
การค้นหาแต่ละรายการจะแสดงข้อความแสดงข้อผิดพลาดพร้อมลิงก์เพื่อสร้างดัชนีที่จำเป็นในคอนโซล 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 แต่ละเครื่องมีแท็บเล็ตอย่างน้อย 1 เครื่อง เมื่อภาระงานเขียนไปยังแท็บเล็ตหนึ่งๆ สูงเกินไป Cloud Firestore จะปรับขนาดในแนวนอนโดยแบ่งแท็บเล็ตออกเป็นแท็บเล็ตขนาดเล็กลงและกระจายแท็บเล็ตใหม่ไปยังเซิร์ฟเวอร์ Cloud Firestore ต่างๆ
Cloud Firestore จะวางรายการดัชนีที่ใกล้เคียงกันตามพจนานุกรมไว้ในแท็บเล็ตเดียวกัน หากค่าดัชนีในแท็บเล็ตอยู่ใกล้กันเกินไป เช่น สำหรับช่องการประทับเวลา Cloud Firestore จะแยกแท็บเล็ตออกเป็นแท็บเล็ตขนาดเล็กลงอย่างมีประสิทธิภาพไม่ได้ ซึ่งจะทำให้เกิดฮอตสปอตที่แท็บเล็ตเครื่องเดียวได้รับการเข้าชมมากเกินไป และการดำเนินการอ่านและเขียนไปยังฮอตสปอตจะช้าลง
การชาร์ดฟิลด์การประทับเวลาจะช่วยให้ Cloud Firestore สามารถแยกภาระงานในแท็บเล็ตหลายเครื่องได้อย่างมีประสิทธิภาพ แม้ว่าค่าของช่องการประทับเวลาอาจยังคงใกล้เคียงกัน แต่ชาร์ดที่เชื่อมโยงและค่าดัชนีจะทำให้ Cloud Firestore มีพื้นที่เพียงพอระหว่างรายการดัชนีเพื่อแยกรายการต่างๆ ออกเป็นแท็บเล็ตหลายเครื่อง
ขั้นตอนถัดไป
- อ่านแนวทางปฏิบัติแนะนำสำหรับการออกแบบเพื่อรองรับการปรับขนาด
- สำหรับกรณีที่มีอัตราการเขียนสูงในเอกสารเดียว โปรดดูตัวนับแบบกระจาย
- ดูขีดจำกัดมาตรฐานสำหรับ Cloud Firestore