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

ถ้าคอลเล็กชันมีเอกสารที่มีค่าการจัดทำดัชนีตามลำดับ 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 Platform ให้ไปที่หน้าฐานข้อมูล

    ไปที่ Databases

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

  3. ในเมนูการนำทาง ให้คลิกดัชนี จากนั้นคลิกแท็บองค์ประกอบ

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

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

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 Platform ให้ไปที่หน้าฐานข้อมูล

    ไปที่ Databases

  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 ด้วย เช่น เพื่อสนับสนุน ข้อความค้นหาด้านบน ให้เพิ่มดัชนีต่อไปนี้

การรวบรวม ช่องที่จัดทำดัชนี ขอบเขตการค้นหา
อุปกรณ์ ชาร์ด รายการ, price.currency , การประทับเวลา การรวบรวม
อุปกรณ์ ชาร์ด รายการ การแลกเปลี่ยน การประทับเวลา การรวบรวม
อุปกรณ์ ชาร์ด รายการ, 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 ไม่สามารถแยกได้อย่างมีประสิทธิภาพ แท็บเล็ตให้เป็นแท็บเล็ตขนาดเล็กลง จึงสร้างฮอตสปอตที่แท็บเล็ต 1 เครื่อง ได้รับปริมาณการรับส่งข้อมูลมากเกินไป และอ่านและเขียนการดำเนินการไปยัง สปอตจะช้าลง

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

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