การใช้ RDBMS เป็นที่เก็บข้อมูลการจัดหาเหตุการณ์


119

ถ้าฉันใช้ RDBMS (เช่น SQL Server) เพื่อเก็บข้อมูลการจัดหาเหตุการณ์สคีมาจะมีลักษณะอย่างไร

ฉันเคยเห็นรูปแบบบางอย่างที่พูดถึงในแง่นามธรรม แต่ไม่มีอะไรเป็นรูปธรรม

ตัวอย่างเช่นสมมติว่ามีเอนทิตี "ผลิตภัณฑ์" และการเปลี่ยนแปลงของผลิตภัณฑ์นั้นอาจอยู่ในรูปแบบของ: ราคาต้นทุนและคำอธิบาย ฉันสับสนว่าฉันจะ:

  1. มีตาราง "ProductEvent" ซึ่งมีช่องทั้งหมดสำหรับผลิตภัณฑ์โดยที่การเปลี่ยนแปลงแต่ละครั้งหมายถึงระเบียนใหม่ในตารางนั้นรวมทั้ง "ใครทำอะไรที่ไหนทำไมเมื่อใดและอย่างไร" (WWWWWH) ตามความเหมาะสม เมื่อต้นทุนราคาหรือคำอธิบายมีการเปลี่ยนแปลงแถวใหม่ทั้งหมดที่เพิ่มเพื่อแสดงถึงผลิตภัณฑ์
  2. จัดเก็บต้นทุนราคาและคำอธิบายของผลิตภัณฑ์ในตารางแยกกันที่เชื่อมต่อกับตารางผลิตภัณฑ์ด้วยความสัมพันธ์ของคีย์ต่างประเทศ เมื่อเกิดการเปลี่ยนแปลงคุณสมบัติเหล่านั้นให้เขียนแถวใหม่ด้วย WWWWWH ตามความเหมาะสม
  3. จัดเก็บ WWWWWH รวมทั้งอ็อบเจ็กต์ที่ทำให้เป็นอนุกรมซึ่งเป็นตัวแทนของเหตุการณ์ในตาราง "ProductEvent" ซึ่งหมายความว่าเหตุการณ์นั้นจะต้องถูกโหลดยกเลิกการต่ออนุกรมและเล่นซ้ำในรหัสแอปพลิเคชันของฉันเพื่อสร้างสถานะแอปพลิเคชันใหม่สำหรับผลิตภัณฑ์ที่กำหนด .

โดยเฉพาะอย่างยิ่งฉันกังวลเกี่ยวกับตัวเลือก 2 ข้างต้น ตารางผลิตภัณฑ์จะแทบจะเป็นตารางเดียวต่อคุณสมบัติโดยที่การโหลดสถานะแอปพลิเคชันสำหรับผลิตภัณฑ์ที่กำหนดจะต้องมีการโหลดเหตุการณ์ทั้งหมดสำหรับผลิตภัณฑ์นั้นจากตารางเหตุการณ์ของผลิตภัณฑ์แต่ละรายการ ระเบิดโต๊ะนี้มีกลิ่นผิดสำหรับฉัน

ฉันแน่ใจว่า "ขึ้นอยู่กับ" และแม้ว่าจะไม่มี "คำตอบที่ถูกต้อง" สักคำเดียว แต่ฉันก็พยายามทำความเข้าใจกับสิ่งที่ยอมรับได้และสิ่งใดที่ยอมรับไม่ได้โดยสิ้นเชิง ฉันทราบด้วยว่า NoSQL สามารถช่วยได้ที่นี่ซึ่งเหตุการณ์สามารถจัดเก็บไว้กับรูทรวมซึ่งหมายถึงการร้องขอเพียงครั้งเดียวไปยังฐานข้อมูลเพื่อรับเหตุการณ์เพื่อสร้างวัตถุขึ้นใหม่ แต่เราไม่ได้ใช้ฐานข้อมูล NoSQL ที่ ช่วงเวลาที่ฉันรู้สึกถึงทางเลือกอื่น


2
ในรูปแบบที่เรียบง่ายที่สุด: [Event] {AggregateId, AggregateVersion, EventPayload} ไม่จำเป็นต้องใช้ประเภทการรวม แต่คุณสามารถเลือกที่จะจัดเก็บได้ ไม่จำเป็นต้องมีประเภทเหตุการณ์ แต่คุณสามารถเลือกจัดเก็บได้ เป็นรายการยาว ๆ ที่เกิดขึ้นสิ่งอื่นเป็นเพียงการเพิ่มประสิทธิภาพ
Yves Reynhout

7
อยู่ห่างจากอันดับ 1 และ # 2 อย่างแน่นอน ทำให้ทุกอย่างต่อเนื่องเป็นหยดและจัดเก็บในลักษณะนั้น
Jonathan Oliver

คำตอบ:


109

ที่เก็บเหตุการณ์ไม่จำเป็นต้องรู้เกี่ยวกับฟิลด์หรือคุณสมบัติเฉพาะของเหตุการณ์ มิฉะนั้นการปรับเปลี่ยนโมเดลของคุณทุกครั้งจะส่งผลให้ต้องย้ายฐานข้อมูลของคุณ (เช่นเดียวกับการคงอยู่ตามสถานะที่ล้าสมัย) ดังนั้นฉันจะไม่แนะนำตัวเลือกที่ 1 และ 2 เลย

ด้านล่างเป็นสคีมาที่ใช้ในNcqrs ดังที่คุณเห็นตาราง "เหตุการณ์" จัดเก็บข้อมูลที่เกี่ยวข้องเป็น CLOB (เช่น JSON หรือ XML) สิ่งนี้สอดคล้องกับตัวเลือก 3 ของคุณ (เฉพาะที่ไม่มีตาราง "ProductEvents" เนื่องจากคุณต้องการเพียงตาราง "เหตุการณ์" ทั่วไปเพียงตารางเดียวใน Ncqrs การแมปกับ Aggregate Roots ของคุณจะเกิดขึ้นผ่านตาราง "แหล่งที่มาของเหตุการณ์" โดยแต่ละแหล่งที่มาของเหตุการณ์จะสอดคล้องกับข้อมูลจริง รูทรวม)

Table Events:
    Id [uniqueidentifier] NOT NULL,
    TimeStamp [datetime] NOT NULL,

    Name [varchar](max) NOT NULL,
    Version [varchar](max) NOT NULL,

    EventSourceId [uniqueidentifier] NOT NULL,
    Sequence [bigint], 

    Data [nvarchar](max) NOT NULL

Table EventSources:
    Id [uniqueidentifier] NOT NULL, 
    Type [nvarchar](255) NOT NULL, 
    Version [int] NOT NULL

กลไกการคงอยู่ของ SQL ของการใช้งาน Event StoreของJonathan Oliverประกอบด้วยตารางหนึ่งตารางที่เรียกว่า "Commits" โดยมีช่อง BLOB "Payload" สิ่งนี้ค่อนข้างเหมือนกับใน Ncqrs เพียงแต่ว่ามันทำให้คุณสมบัติของเหตุการณ์เป็นอนุกรมในรูปแบบไบนารี (ซึ่งเช่นเพิ่มการรองรับการเข้ารหัส)

เกร็กหนุ่มแนะนำวิธีการที่คล้ายกันเป็นเอกสารอย่างกว้างขวางบนเว็บไซต์ของเกร็ก

สคีมาของตาราง "เหตุการณ์" ต้นแบบของเขาอ่านว่า:

Table Events
    AggregateId [Guid],
    Data [Blob],
    SequenceNumber [Long],
    Version [Int]

9
คำตอบที่ดี! หนึ่งในข้อโต้แย้งหลักที่ฉันอ่านเกี่ยวกับการใช้ EventSourcing คือความสามารถในการสืบค้นประวัติ ฉันจะสร้างเครื่องมือรายงานที่มีประสิทธิภาพในการสืบค้นได้อย่างไรเมื่อข้อมูลที่น่าสนใจทั้งหมดถูกจัดลำดับเป็น XML หรือ JSON มีบทความที่น่าสนใจที่กำลังมองหาวิธีแก้ปัญหาแบบตารางหรือไม่?
Marijn Huizendveld

11
@MarijnHuizendveld คุณอาจไม่ต้องการสอบถามเกี่ยวกับการจัดเก็บเหตุการณ์เอง วิธีแก้ปัญหาที่พบบ่อยที่สุดคือการเชื่อมต่อตัวจัดการเหตุการณ์สองตัวที่คาดการณ์เหตุการณ์ลงในฐานข้อมูลการรายงานหรือ BI เล่นซ้ำประวัติเหตุการณ์กับตัวจัดการเหล่านี้
Dennis Traub

1
@Denis Traub ขอบคุณสำหรับคำตอบของคุณ ทำไมไม่สอบถามกับร้านค้าเหตุการณ์เอง? ฉันกลัวว่ามันจะยุ่งเหยิง / เข้มข้นมากถ้าเราต้องเล่นประวัติแบบเต็มทุกครั้งที่เราสร้างเคส BI ใหม่?
Marijn Huizendveld

1
ฉันคิดว่าเมื่อถึงจุดหนึ่งคุณควรจะมีตารางนอกเหนือจากที่เก็บเหตุการณ์เพื่อเก็บข้อมูลจากโมเดลในสถานะล่าสุด? และคุณแบ่งโมเดลออกเป็นโมเดลการอ่านและโมเดลการเขียน รูปแบบการเขียนขัดกับที่เก็บกิจกรรมและการจัดเก็บเหตุการณ์การต่อสู้จะอัปเดตโมเดลการอ่าน แบบจำลองการอ่านประกอบด้วยตารางที่แสดงถึงเอนทิตีในระบบของคุณดังนั้นคุณจึงสามารถใช้โมเดลการอ่านเพื่อทำการรายงานและดูได้ ฉันต้องเข้าใจผิดบางอย่าง
theBoringCoder

10
@theBoringCoder ดูเหมือนว่าคุณมี Event Sourcing และ CQRS สับสนหรืออย่างน้อยก็สับสนในหัวของคุณ พบได้บ่อย แต่ไม่ใช่สิ่งเดียวกัน CQRS ให้คุณแยกโมเดลการอ่านและเขียนของคุณในขณะที่ Event Sourcing ให้คุณใช้สตรีมเหตุการณ์เป็นแหล่งความจริงเดียวในแอปพลิเคชันของคุณ
Bryan Anderson

7

โครงการ GitHub CQRS.NETมีตัวอย่างที่เป็นรูปธรรมบางประการเกี่ยวกับวิธีที่คุณสามารถทำ EventStores ในเทคโนโลยีต่างๆ ในขณะที่เขียนมีการนำไปใช้งานในSQL โดยใช้ Linq2SQLและสคีมา SQL ที่จะไปด้วยมีหนึ่งสำหรับMongoDBหนึ่งสำหรับDocumentDB (CosmosDB ถ้าคุณอยู่ใน Azure) และอีกอันที่ใช้EventStore (ตามที่กล่าวไว้ข้างต้น) Azure ยังมีอีกมากเช่น Table Storage และ Blob storage ซึ่งคล้ายกับที่เก็บไฟล์แบบแบน

ฉันเดาว่าประเด็นหลักที่นี่คือทั้งหมดเป็นไปตามเงินต้น / สัญญาเดียวกัน พวกเขาทั้งหมดจัดเก็บข้อมูลไว้ในที่เดียว / คอนเทนเนอร์ / ตารางโดยใช้ข้อมูลเมตาเพื่อระบุเหตุการณ์หนึ่งจากอีกเหตุการณ์หนึ่งและ 'เพียง' จัดเก็บเหตุการณ์ทั้งหมดเหมือนเดิม - ในบางกรณีต่อเนื่องกันในเทคโนโลยีที่สนับสนุนเหมือนเดิม ดังนั้นขึ้นอยู่กับว่าคุณเลือกฐานข้อมูลเอกสารฐานข้อมูลเชิงสัมพันธ์หรือแม้แต่ไฟล์แบบแบนมีหลายวิธีในการบรรลุจุดประสงค์เดียวกันของที่เก็บเหตุการณ์ (จะมีประโยชน์หากคุณเปลี่ยนใจ ณ จุดใดก็ได้และพบว่าคุณจำเป็นต้องย้ายหรือสนับสนุน เทคโนโลยีการจัดเก็บข้อมูลมากกว่าหนึ่งรายการ)

ในฐานะนักพัฒนาของโครงการฉันสามารถแบ่งปันข้อมูลเชิงลึกเกี่ยวกับตัวเลือกบางอย่างที่เราทำ

ประการแรกเราพบ (แม้จะมี UUID / GUID ที่ไม่ซ้ำกันแทนที่จะเป็นจำนวนเต็ม) ด้วยเหตุผลหลายประการที่ ID ลำดับเกิดขึ้นด้วยเหตุผลเชิงกลยุทธ์ดังนั้นการมี ID ไม่ซ้ำกันเพียงพอสำหรับคีย์ดังนั้นเราจึงรวมคอลัมน์คีย์ ID หลักของเราเข้ากับข้อมูล / ประเภทออบเจ็กต์เพื่อสร้างสิ่งที่ควรเป็นคีย์เฉพาะอย่างแท้จริง (ในแง่ของแอปพลิเคชันของคุณ) ฉันรู้ว่าบางคนบอกว่าคุณไม่จำเป็นต้องจัดเก็บ แต่ขึ้นอยู่กับว่าคุณเป็นกรีนฟิลด์หรือต้องอยู่ร่วมกับระบบที่มีอยู่

เราติดอยู่กับคอนเทนเนอร์ / ตาราง / คอลเลกชันเดียวด้วยเหตุผลด้านการบำรุงรักษา แต่เราเล่นกับตารางแยกต่างหากต่อเอนทิตี / อ็อบเจ็กต์ เราพบว่าในทางปฏิบัตินั่นหมายความว่าแอปพลิเคชันต้องการสิทธิ์ "CREATE" (ซึ่งโดยทั่วไปแล้วไม่ใช่ความคิดที่ดี ... โดยทั่วไปมักจะมีข้อยกเว้น / การยกเว้นอยู่เสมอ) หรือทุกครั้งที่มีเอนทิตี / อ็อบเจ็กต์ใหม่หรือถูกทำให้ใช้งานใหม่ ต้องทำตู้คอนเทนเนอร์ / ตาราง / คอลเลกชัน เราพบว่าสิ่งนี้ช้าอย่างเจ็บปวดสำหรับการพัฒนาในพื้นที่และเป็นปัญหาสำหรับการปรับใช้งานจริง คุณอาจจะไม่ได้ แต่นั่นคือประสบการณ์ในโลกแห่งความเป็นจริงของเรา

สิ่งที่ควรจำอีกประการหนึ่งคือการขอให้การกระทำ X เกิดขึ้นอาจส่งผลให้มีเหตุการณ์ต่างๆเกิดขึ้นดังนั้นการรู้เหตุการณ์ทั้งหมดที่สร้างขึ้นโดยคำสั่ง / เหตุการณ์ / สิ่งที่เคยเป็นประโยชน์ นอกจากนี้ยังอาจอยู่ในวัตถุประเภทต่างๆเช่นการกด "ซื้อ" ในตะกร้าสินค้าอาจทำให้บัญชีและคลังสินค้าเริ่มทำงานได้ แอปพลิเคชันที่ใช้งานอาจต้องการทราบข้อมูลทั้งหมดนี้ดังนั้นเราจึงเพิ่ม CorrelationId ซึ่งหมายความว่าผู้บริโภคสามารถขอเหตุการณ์ทั้งหมดที่เกิดขึ้นอันเป็นผลมาจากคำขอของพวกเขา คุณจะเห็นว่าในสคี

โดยเฉพาะกับ SQL เราพบว่าประสิทธิภาพกลายเป็นคอขวดหากดัชนีและพาร์ติชันไม่ได้ใช้อย่างเพียงพอ จำไว้ว่ากิจกรรมจะต้องสตรีมในลำดับย้อนกลับหากคุณใช้สแนปชอต เราลองใช้ดัชนีที่แตกต่างกันสองสามรายการและพบว่าในทางปฏิบัติแล้วจำเป็นต้องมีดัชนีเพิ่มเติมบางอย่างสำหรับการแก้ไขข้อบกพร่องในการใช้งานจริงในการผลิต อีกครั้งคุณจะเห็นว่าในสคี

ข้อมูลเมตาในการผลิตอื่น ๆ มีประโยชน์ในระหว่างการตรวจสอบตามการผลิตการประทับเวลาทำให้เราเข้าใจถึงลำดับเหตุการณ์ที่ยังคงอยู่และเพิ่มขึ้น นั่นทำให้เราได้รับความช่วยเหลือบางอย่างเกี่ยวกับระบบที่ขับเคลื่อนด้วยเหตุการณ์ที่หนักหน่วงโดยเฉพาะซึ่งทำให้เกิดเหตุการณ์จำนวนมากทำให้เรามีข้อมูลเกี่ยวกับประสิทธิภาพของสิ่งต่างๆเช่นเครือข่ายและการกระจายระบบทั่วทั้งเครือข่าย


เยี่ยมมากขอบคุณ เมื่อมันเกิดขึ้นมานานตั้งแต่เขียนคำถามนี้ฉันได้สร้างตัวเองขึ้นมาสองสามตัวโดยเป็นส่วนหนึ่งของห้องสมุด Inforigami.Regalo บน github การใช้งาน RavenDB, SQL Server และ EventStore สงสัยเกี่ยวกับการทำแบบไฟล์เพื่อความสนุกสนาน :)
Neil Barnwell

1
ไชโย ฉันได้เพิ่มคำตอบสำหรับคนอื่น ๆ ที่เจอคำตอบนี้เป็นหลักและแบ่งปันบทเรียนบางส่วนที่ได้เรียนรู้แทนที่จะเป็นเพียงผลลัพธ์
cdmdotnet

3

คุณอาจจะลองดู Datomic

Datomic คือฐานข้อมูลของข้อเท็จจริงที่ยืดหยุ่นตามเวลารองรับการสืบค้นและการรวมที่มีความยืดหยุ่นในการปรับขนาดและการทำธุรกรรม ACID

ฉันเขียนคำตอบโดยละเอียดที่นี่

คุณสามารถรับชมการบรรยายจาก Stuart Halloway ที่อธิบายการออกแบบ Datomic ได้ที่นี่

เนื่องจาก Datomic จัดเก็บข้อมูลได้ทันเวลาคุณจึงสามารถใช้เพื่อจัดกิจกรรมกรณีการใช้งานและอื่น ๆ อีกมากมาย


2

ฉันคิดว่าวิธีแก้ปัญหา (1 & 2) อาจกลายเป็นปัญหาได้อย่างรวดเร็วเมื่อโมเดลโดเมนของคุณพัฒนาขึ้น มีการสร้างช่องใหม่ความหมายบางอย่างเปลี่ยนไปและบางช่องไม่สามารถใช้งานได้อีกต่อไป ในที่สุดตารางของคุณจะมีช่องว่างหลายสิบช่องและการโหลดเหตุการณ์จะยุ่งเหยิง

นอกจากนี้อย่าลืมว่าที่เก็บเหตุการณ์ควรใช้สำหรับการเขียนเท่านั้นคุณจะสอบถามเพื่อโหลดเหตุการณ์เท่านั้นไม่ใช่คุณสมบัติของการรวม เป็นสิ่งที่แยกจากกัน (นั่นคือสาระสำคัญของ CQRS)

แนวทางที่ 3 สิ่งที่ผู้คนมักทำมีหลายวิธีที่จะทำให้สำเร็จ

ตัวอย่างเช่นEventFlow CQRSเมื่อใช้กับ SQL Server จะสร้างตารางที่มี schema นี้:

CREATE TABLE [dbo].[EventFlow](
    [GlobalSequenceNumber] [bigint] IDENTITY(1,1) NOT NULL,
    [BatchId] [uniqueidentifier] NOT NULL,
    [AggregateId] [nvarchar](255) NOT NULL,
    [AggregateName] [nvarchar](255) NOT NULL,
    [Data] [nvarchar](max) NOT NULL,
    [Metadata] [nvarchar](max) NOT NULL,
    [AggregateSequenceNumber] [int] NOT NULL,
 CONSTRAINT [PK_EventFlow] PRIMARY KEY CLUSTERED 
(
    [GlobalSequenceNumber] ASC
)

ที่อยู่:

  • GlobalSequenceNumber : การระบุส่วนกลางอย่างง่ายอาจใช้สำหรับการสั่งซื้อหรือระบุเหตุการณ์ที่ขาดหายไปเมื่อคุณสร้างการฉายภาพของคุณ (readmodel)
  • BatchId : การระบุกลุ่มของเหตุการณ์ที่แทรกด้วยอะตอม (TBH ไม่รู้ว่าเหตุใดจึงเป็นประโยชน์)
  • AggregateId : การระบุการรวม
  • ข้อมูล : เหตุการณ์ต่อเนื่อง
  • ข้อมูลเมตา : ข้อมูลที่เป็นประโยชน์อื่น ๆ จากเหตุการณ์ (เช่นประเภทเหตุการณ์ที่ใช้สำหรับ deserialize การประทับเวลารหัสผู้เริ่มต้นจากคำสั่ง ฯลฯ )
  • AggregateSequenceNumber : หมายเลขลำดับภายในผลรวมเดียวกัน (นี่เป็นประโยชน์อย่างยิ่งหากคุณไม่สามารถเขียนผิดพลาดได้ดังนั้นคุณจึงใช้ฟิลด์นี้เพื่อหาค่าพร้อมกันในแง่ดี)

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


ฉันจะเถียงว่า BatchId อาจเกี่ยวข้องกับ CorrelationId และ CausationId ใช้เพื่อค้นหาว่าอะไรเป็นสาเหตุของเหตุการณ์และรวมเข้าด้วยกันหากจำเป็น
Daniel Park

มันอาจจะเป็น. อย่างไรก็ตามมันเป็นเช่นนั้นมันก็สมเหตุสมผลที่จะให้วิธีปรับแต่งมัน (เช่นการตั้งค่าเป็น id ของคำขอ) แต่เฟรมเวิร์กไม่ทำเช่นนั้น
Fabio Marreco

1

คำใบ้ที่เป็นไปได้คือการออกแบบตามด้วย "การเปลี่ยนมิติอย่างช้าๆ" (type = 2) จะช่วยให้คุณครอบคลุม:

  • ลำดับเหตุการณ์ที่เกิดขึ้น (ผ่านคีย์ตัวแทน)
  • ความทนทานของแต่ละสถานะ (ใช้ได้ตั้งแต่ - ใช้ได้ถึง)

ฟังก์ชันพับซ้ายก็ใช้ได้เช่นกัน แต่คุณต้องคิดถึงความซับซ้อนของการสืบค้นในอนาคต


1

ฉันคิดว่านี่เป็นคำตอบที่ล่าช้า แต่ฉันอยากจะชี้ให้เห็นว่าการใช้ RDBMS เป็นที่เก็บข้อมูลการจัดหาเหตุการณ์นั้นเป็นไปได้โดยสิ้นเชิงหากความต้องการปริมาณงานของคุณไม่สูง ฉันจะแสดงตัวอย่างของบัญชีแยกประเภทที่จัดหาเหตุการณ์ที่ฉันสร้างขึ้นเพื่อเป็นตัวอย่าง

https://github.com/andrewkkchan/client-ledger-service ข้างต้นเป็นบริการเว็บบัญชีแยกประเภทที่จัดหาเหตุการณ์ https://github.com/andrewkkchan/client-ledger-core-db และข้างต้นฉันใช้ RDBMS เพื่อคำนวณสถานะเพื่อให้คุณสามารถเพลิดเพลินกับข้อดีทั้งหมดที่มาพร้อมกับ RDBMS เช่นการรองรับธุรกรรม https://github.com/andrewkkchan/client-ledger-core-memory และฉันมีผู้บริโภคอีกรายที่ต้องประมวลผลในหน่วยความจำเพื่อจัดการกับระเบิด

อาจมีคนโต้แย้งว่าที่เก็บเหตุการณ์จริงด้านบนยังคงมีชีวิตอยู่ในคาฟคาเนื่องจาก RDBMS ช้าสำหรับการแทรกโดยเฉพาะอย่างยิ่งเมื่อการแทรกนั้นต่อท้ายเสมอ

ฉันหวังว่ารหัสนี้จะช่วยให้คุณมีภาพประกอบนอกเหนือจากคำตอบทางทฤษฎีที่ดีมากสำหรับคำถามนี้แล้ว


ขอบคุณ ฉันสร้างการใช้งานบน SQL มานานแล้ว ฉันไม่แน่ใจว่าทำไม RDBMS จึงช้าสำหรับการแทรกเว้นแต่คุณจะเลือกคีย์คลัสเตอร์ที่ไม่มีประสิทธิภาพ ภาคผนวกเท่านั้นน่าจะใช้ได้
Neil Barnwell
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.