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

หากคอลเล็กชันมีเอกสารที่มีค่าที่จัดทำดัชนีตามลำดับ 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 ดังนี้

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

คุณควรใช้การประทับเวลาแบบ 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 ครั้งต่อวินาทีที่อนุญาตสำหรับคอลเล็กชันที่มีเอกสารที่มีฟิลด์การประทับเวลาที่จัดทำดัชนี หากต้องการเพิ่มอัตราการส่งข้อมูลการเขียน คุณต้องมีค่า Shard 3 ค่า MAX_INSTRUMENT_UPDATES/500 = 3 ตัวอย่างนี้ใช้ค่า Shard x, y และ z นอกจากนี้ คุณยังใช้ตัวเลขหรืออักขระอื่นๆ สำหรับค่าของ Shard ได้ด้วย

การเพิ่มฟิลด์ Shard

เพิ่ม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 การรวบรวม
เครื่องดนตรี ชาร์ด, การแลกเปลี่ยน, การประทับเวลา การรวบรวม
เครื่องดนตรี ชาร์ด, instrumentType, การประทับเวลา การรวบรวม

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

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

แต่ละคำค้นหาจะแสดงข้อความแสดงข้อผิดพลาดพร้อมลิงก์เพื่อสร้างดัชนีที่จำเป็นในคอนโซล 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 จัดระเบียบรายการดัชนีเหล่านี้เป็นกลุ่มข้อมูลที่เรียกว่าแท็บเล็ต เซิร์ฟเวอร์แต่ละเครื่องCloud Firestoreจะมีแท็บเล็ตอย่างน้อย 1 เครื่อง เมื่อภาระการเขียนไปยัง แท็บเล็ตหนึ่งๆ สูงเกินไป Cloud Firestore จะปรับขนาดในแนวนอน โดยการแยกแท็บเล็ตออกเป็นแท็บเล็ตขนาดเล็กและกระจายแท็บเล็ตใหม่ ไปยังเซิร์ฟเวอร์ Cloud Firestore ต่างๆ

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

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

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