การประทับเวลาแบบชาร์ด

หากคอลเล็กชันมีเอกสารที่มีค่าที่จัดทำดัชนีตามลำดับ 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 สําหรับการค้นหา คุณสามารถหลีกเลี่ยงขีดจํากัดนี้ได้โดยใช้การประทับเวลาที่มีการแบ่งกลุ่ม ดังนี้

  1. เพิ่มช่อง shard ข้างช่อง timestamp ใช้ค่า 1..n ที่ต่างกันสําหรับช่อง shard ซึ่งจะเพิ่มขีดจํากัดการเขียนสําหรับคอลเล็กชันเป็น 500*n แต่คุณต้องรวบรวมการค้นหา n
  2. อัปเดตตรรกะการเขียนเพื่อกำหนดค่า shard ให้กับเอกสารแต่ละรายการแบบสุ่ม
  3. อัปเดตการค้นหาเพื่อรวบรวมชุดผลลัพธ์ที่แบ่งกลุ่ม
  4. ปิดใช้ดัชนีช่องเดี่ยวสําหรับทั้งฟิลด์ shard และฟิลด์ timestamp ลบดัชนีคอมโพสิตที่มีอยู่ซึ่งมีtimestamp ฟิลด์
  5. สร้างดัชนีผสมใหม่เพื่อรองรับการค้นหาที่อัปเดต ลําดับของช่องในดัชนีมีความสําคัญ และช่อง shard ต้องอยู่ก่อนช่อง timestamp ดัชนีที่มีช่อง timestamp ต้องมีช่อง shard ด้วย

คุณควรใช้การประทับเวลาแบบแยกกลุ่มเฉพาะใน Use Case ที่มีอัตราการเขียนอย่างต่อเนื่องมากกว่า 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

  1. เปิดหน้าCloud Firestore ดัชนีคอมโพสิตในคอนโซล Firebase

    ไปที่ดัชนีผสม

  2. สําหรับดัชนีแต่ละรายการที่มีช่อง timestamp ให้คลิกปุ่ม แล้วคลิกลบ

คอนโซล GCP

  1. ในคอนโซล Google Cloud ให้ไปที่หน้าฐานข้อมูล

    ไปที่ฐานข้อมูล

  2. เลือกฐานข้อมูลที่จําเป็นจากรายการฐานข้อมูล

  3. ในเมนูการนำทาง ให้คลิกดัชนี แล้วคลิกแท็บคอมโพสิต

  4. ใช้ช่องตัวกรองเพื่อค้นหาคําจํากัดความดัชนีที่มีช่อง timestamp

  5. สําหรับดัชนีแต่ละรายการเหล่านี้ ให้คลิกปุ่ม แล้วคลิกลบ

Firebase CLI

  1. หากยังไม่ได้ตั้งค่า Firebase CLI ให้ทําตามวิธีการต่อไปนี้เพื่อติดตั้ง CLI และเรียกใช้คําสั่ง firebase init ในระหว่างใช้คำสั่ง init โปรดตรวจสอบว่าได้เลือก Firestore: Deploy rules and create indexes for Firestore
  2. ในระหว่างการตั้งค่า Firebase CLI จะดาวน์โหลดคําจํากัดความดัชนีที่มีอยู่ไปยังไฟล์ชื่อ firestore.indexes.json โดยค่าเริ่มต้น
  3. นําคําจํากัดความดัชนีที่มีฟิลด์ 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"
          }
        ]
      },
     ]
    }
    
  4. ติดตั้งใช้งานคําจํากัดความดัชนีที่อัปเดตแล้ว โดยทําดังนี้

    firebase deploy --only firestore:indexes
    

อัปเดตคําจํากัดความของดัชนีแบบช่องเดียว

คอนโซล Firebase

  1. เปิดหน้าCloud Firestore ดัชนีช่องเดียวในคอนโซล Firebase

    ไปที่ดัชนีช่องเดียว

  2. คลิกเพิ่มข้อยกเว้น

  3. ป้อน instruments ในส่วนรหัสคอลเล็กชัน ป้อน timestamp สำหรับเส้นทางฟิลด์

  4. ในส่วนขอบเขตการค้นหา ให้เลือกทั้งคอลเล็กชันและกลุ่มคอลเล็กชัน

  5. คลิกถัดไป

  6. สลับการตั้งค่าดัชนีทั้งหมดเป็นปิดใช้ คลิกบันทึก

  7. ทำขั้นตอนเดียวกันซ้ำสำหรับช่อง shard

คอนโซล GCP

  1. ในคอนโซล Google Cloud ให้ไปที่หน้าฐานข้อมูล

    ไปที่ฐานข้อมูล

  2. เลือกฐานข้อมูลที่จําเป็นจากรายการฐานข้อมูล

  3. ในเมนูการนำทาง ให้คลิกดัชนี แล้วคลิกแท็บช่องเดียว

  4. คลิกแท็บช่องเดียว

  5. คลิกเพิ่มข้อยกเว้น

  6. ป้อน instruments ในส่วนรหัสคอลเล็กชัน ป้อน timestamp สำหรับเส้นทางฟิลด์

  7. ในส่วนขอบเขตการค้นหา ให้เลือกทั้งคอลเล็กชันและกลุ่มคอลเล็กชัน

  8. คลิกถัดไป

  9. สลับการตั้งค่าดัชนีทั้งหมดเป็นปิดใช้ คลิกบันทึก

  10. ทำขั้นตอนเดียวกันซ้ำสำหรับช่อง shard

Firebase CLI

  1. เพิ่มข้อมูลต่อไปนี้ลงในส่วน fieldOverrides ของไฟล์คำจำกัดความของดัชนี

    {
     "fieldOverrides": [
       // Disable single-field indexing for the timestamp field
       {
         "collectionGroup": "instruments",
         "fieldPath": "timestamp",
         "indexes": []
       },
     ]
    }
    
  2. ติดตั้งใช้งานคําจํากัดความดัชนีที่อัปเดตแล้ว โดยทําดังนี้

    firebase deploy --only firestore:indexes
    

สร้างดัชนีผสมใหม่

หลังจากนำดัชนีก่อนหน้าทั้งหมดที่มี timestamp ออกแล้ว ให้กำหนดดัชนีใหม่ที่แอปของคุณต้องใช้ ดัชนีที่มีฟิลด์ timestamp ต้องมีฟิลด์ shard ด้วย ตัวอย่างเช่น หากต้องการรองรับการค้นหาข้างต้น ให้เพิ่มดัชนีต่อไปนี้

การรวบรวม ช่องที่จัดทำดัชนี ขอบเขตการค้นหา
เครื่องมือ shard, price.currency, timestamp การรวบรวม
เครื่องดนตรี shard, exchange, timestamp การรวบรวม
เครื่องดนตรี shard, instrumentType, timestamp การรวบรวม

ข้อความแสดงข้อผิดพลาด

คุณสร้างดัชนีเหล่านี้ได้โดยเรียกใช้การค้นหาที่อัปเดตแล้ว

การค้นหาแต่ละรายการจะแสดงข้อความแสดงข้อผิดพลาดพร้อมลิงก์สำหรับสร้างดัชนีที่จําเป็นในคอนโซล Firebase

Firebase CLI

  1. เพิ่มดัชนีต่อไปนี้ลงในไฟล์คําจํากัดความของดัชนี

     {
       "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"
             }
           ]
         },
       ]
     }
    
  2. ติดตั้งใช้งานคําจํากัดความดัชนีที่อัปเดตแล้ว โดยทําดังนี้

    firebase deploy --only firestore:indexes
    

ทำความเข้าใจขีดจํากัดการเขียนสําหรับช่องที่จัดทําดัชนีตามลําดับ

ขีดจํากัดของอัตราการเขียนสําหรับช่องที่จัดทําดัชนีตามลําดับมาจากวิธีที่ Cloud Firestore จัดเก็บค่าดัชนีและปรับขนาดการเขียนดัชนี สำหรับการเขียนดัชนีแต่ละครั้ง Cloud Firestore จะกำหนดรายการคีย์-ค่าซึ่งต่อเชื่อมชื่อเอกสารและค่าของช่องที่จัดทําดัชนีแต่ละช่อง Cloud Firestore จัดระเบียบรายการดัชนีเหล่านี้เป็นกลุ่มข้อมูลที่เรียกว่า tablet เซิร์ฟเวอร์ Cloud Firestore แต่ละเครื่องมีแท็บเล็ตอย่างน้อย 1 เครื่อง เมื่อการเขียนข้อมูลไปยังแท็บเล็ตหนึ่งๆ มีปริมาณมากเกินไป Cloud Firestore จะปรับขนาดในแนวนอนโดยแบ่งแท็บเล็ตออกเป็นแท็บเล็ตขนาดเล็กลงและกระจายแท็บเล็ตใหม่ไปยังเซิร์ฟเวอร์ Cloud Firestore ต่างๆ

Cloud Firestore วางรายการดัชนีที่ใกล้เคียงกันตามลําดับอักขระในแท็บเล็ตเดียวกัน หากค่าดัชนีในแท็บเล็ตอยู่ใกล้กันเกินไป เช่น สำหรับช่องการประทับเวลา Cloud Firestore จะแยกแท็บเล็ตออกเป็นแท็บเล็ตขนาดเล็กๆ ได้อย่างมีประสิทธิภาพ ซึ่งจะทำให้เกิดฮอตสปอตที่แท็บเล็ตเครื่องเดียวรับการรับส่งข้อมูลมากเกินไป และการดำเนินการอ่านและเขียนไปยังฮอตสปอตจะช้าลง

การแยกส่วนช่องการประทับเวลาช่วยให้ Cloud Firestore แบ่งภาระงานในแท็บเล็ตหลายเครื่องได้อย่างมีประสิทธิภาพ แม้ว่าค่าของช่องการประทับเวลาอาจอยู่ใกล้กัน แต่กลุ่มที่ต่อเชื่อมและค่าดัชนีจะให้Cloud Firestoreพื้นที่เพียงพอระหว่างรายการดัชนีเพื่อแยกรายการต่างๆ ไปยังแท็บเล็ตหลายแท็บ

ขั้นตอนถัดไป