ขอบเขตของภาษาของฉันคือขอบเขตของโลกของฉัน

Ludwig Wittgenstein, Tractatus Logico-Philosophicus (1922)

โมเดลข้อมูลน่าจะเป็นส่วนที่สำคัญที่สุดของการพัฒนาซอฟต์แวร์ เพราะมันส่งผลลึกซึ้งต่อทั้งวิธีเขียนซอฟต์แวร์ และวิธีที่เรา คิดเกี่ยวกับปัญหาที่ เรากำลังแก้ไขด้วย

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

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

  2. เมื่อต้องการเก็บโครงสร้างข้อมูลเหล่านั้น คุณจะแสดงมันด้วยโมเดลข้อมูลทั่วไป เช่น เอกสาร JSON หรือ XML ตารางในฐานข้อมูลเชิงสัมพันธ์ หรือ vertex และ edge ในกราฟ โมเดลข้อมูลเหล่านี้คือหัวข้อของบทนี้

  3. วิศวกรผู้สร้างซอฟต์แวร์ฐานข้อมูลเลือกวิธีแทนเอกสาร ข้อมูลเชิงสัมพันธ์ หรือกราฟเป็นไบต์ในหน่วยความจำ บนดิสก์ หรือบนเครือข่าย การแทนค่านี้อาจทำให้สืบค้น ค้นหา จัดการ และประมวลผลข้อมูลได้หลายรูปแบบ เราจะพูดถึงการออกแบบ storage engine เหล่านี้ใน บทที่ 4

  4. ลงไปอีกระดับ วิศวกรฮาร์ดแวร์คิดวิธีแทนไบต์ด้วยกระแสไฟฟ้า คลื่นแสง สนามแม่เหล็ก และอื่นๆ

แอปพลิเคชันที่ซับซ้อนอาจมีชั้นกลางเพิ่มอีก เช่น API ที่สร้างบน API อื่น แต่แนวคิดพื้นฐานยังเหมือนเดิม: แต่ละชั้นซ่อนความซับซ้อนของชั้นล่างไว้หลังโมเดลข้อมูลที่สะอาด abstraction เหล่านี้ช่วยให้คนหลายกลุ่ม—เช่น วิศวกรของผู้ผลิตฐานข้อมูลกับนักพัฒนาแอปพลิเคชันที่ใช้ฐานข้อมูลนั้น—ทำงานร่วมกันได้อย่างมีประสิทธิภาพ

ในทางปฏิบัติมีการใช้โมเดลข้อมูลหลายแบบอย่างแพร่หลาย โดยมักใช้เพื่อจุดประสงค์ต่างกัน ข้อมูลและ query บางแบบแสดงในโมเดลหนึ่งได้ง่าย แต่ในอีกโมเดลหนึ่งกลับยาก ในบทนี้เราจะสำรวจ trade-off เหล่านี้โดยเปรียบเทียบโมเดลเชิงสัมพันธ์ โมเดลเอกสาร โมเดลข้อมูลแบบกราฟ event sourcing และ DataFrame พร้อมดูภาษาสืบค้นที่ใช้กับโมเดลเหล่านี้อย่างคร่าวๆ การเปรียบเทียบนี้จะช่วยให้คุณตัดสินใจได้ว่าเมื่อใดควรใช้โมเดลใด

Terminology: Declarative Query Languages (คำศัพท์: ภาษาในการสืบค้นแบบ Declarative)

ภาษาในการสืบค้นหลายภาษาที่กล่าวถึงในบทนี้ (เช่น SQL, Cypher, SPARQL และ Datalog) เป็นภาษาแบบ declarative ซึ่งหมายความว่าคุณระบุรูปแบบของข้อมูลที่ต้องการ—ว่าเงื่อนไขของผลลัพธ์ต้องเป็นอย่างไร และคุณต้องการให้ข้อมูลถูกแปลงอย่างไร (เช่น เรียงลำดับ จัดกลุ่ม และรวมค่าต่างๆ)—แต่ไม่ได้ระบุ วิธีการ ที่จะทำให้บรรลุเป้าหมายนั้น query optimizer ของระบบฐานข้อมูลตัดสินใจได้ว่าจะใช้ index และ join algorithm ใด และจะรันแต่ละส่วนของ query ตามลำดับไหน

ในทางกลับกัน ด้วยภาษาโปรแกรมส่วนใหญ่ (เช่น Python และ Java) คุณจะต้องเขียน algorithm ที่บอกคอมพิวเตอร์ว่าต้องทำคำสั่งใดตามลำดับไหน ภาษาสืบค้นแบบ declarative น่าสนใจเพราะโดยทั่วไปกระชับและเขียนง่ายกว่าการเขียน algorithm ตรงๆ ที่สำคัญกว่านั้น มันซ่อนรายละเอียดการทำงานของ query engine ทำให้ฐานข้อมูลปรับปรุงประสิทธิภาพได้โดยไม่ต้องแก้ query [ 1 , 2 ]

ตัวอย่างเช่น ฐานข้อมูลอาจรัน declarative query แบบขนานบนหลาย CPU core และหลายเครื่องได้ โดยคุณไม่ต้องกังวลว่าจะ implement การทำงานแบบขนานนั้นอย่างไร [ 3 ] ถ้าเขียน algorithm เอง การ implement การทำงานแบบขนานแบบนี้ด้วยตัวเองเป็นงานที่หนักมาก

Relational Versus Document Models (โมเดลเชิงสัมพันธ์เทียบกับโมเดลเอกสาร)

โมเดลข้อมูลที่คนรู้จักมากที่สุดในปัจจุบันน่าจะเป็นของ SQL ซึ่งตั้งอยู่บนโมเดลเชิงสัมพันธ์ที่ Edgar Codd เสนอไว้ในปี 1970 [ 4 ] ในโมเดลนี้ ข้อมูลถูกจัดระเบียบเป็น relation (เรียกว่า table ใน SQL) โดยแต่ละ relation คือคอลเลกชันของ tuple ( row ใน SQL) ที่ไม่เรียงลำดับ

ตอนแรกโมเดลเชิงสัมพันธ์เป็นเพียงข้อเสนอเชิงทฤษฎี และหลายคนสงสัยว่าจะ implement ให้มีประสิทธิภาพได้หรือไม่ แต่พอถึงกลางทศวรรษ 1980 ระบบจัดการฐานข้อมูลเชิงสัมพันธ์ (RDBMS) และ SQL ก็กลายเป็นตัวเลือกหลักของคนที่ต้องการเก็บและสืบค้นข้อมูลที่มีโครงสร้างปกติ งานจัดการข้อมูลหลายประเภท—เช่น การวิเคราะห์ทางธุรกิจ (ดู "Stars and Snowflakes: Schemas for Analytics" )—ยังคงใช้ข้อมูลเชิงสัมพันธ์เป็นหลักแม้ผ่านมาหลายทศวรรษแล้ว

ตลอดหลายปีที่ผ่านมา มีแนวทางคู่แข่งมากมายสำหรับการเก็บและสืบค้นข้อมูล ในทศวรรษ 1970 และต้นทศวรรษ 1980 network model และ hierarchical model เป็นทางเลือกหลัก แต่สุดท้ายโมเดลเชิงสัมพันธ์ก็ขึ้นมาครองตำแหน่งแทน ฐานข้อมูลเชิงวัตถุ (ซึ่งไม่ใช่ object storage สำหรับไฟล์ขนาดใหญ่แบบบริการคลาวด์ยอดนิยมในปัจจุบัน) โผล่มาแล้วก็หายไปในช่วงปลายทศวรรษ 1980 และต้นทศวรรษ 1990 ส่วนฐานข้อมูล XML ปรากฏขึ้นในช่วงต้นทศวรรษ 2000 แต่มีคนใช้เฉพาะกลุ่มเล็กๆ คู่แข่งแต่ละรายของโมเดลเชิงสัมพันธ์ต่างสร้างกระแสในยุคของตัวเอง แต่ไม่มีรายไหนอยู่รอด [ 5 ] ส่วน SQL เองก็พัฒนาต่อจนรองรับข้อมูลประเภทอื่นได้—เช่น XML, JSON และข้อมูลกราฟ [ 6 ]

ในทศวรรษ 2010 NoSQL เป็นคำฮิตล่าสุดที่พยายามโค่นความโดดเด่นของฐานข้อมูลเชิงสัมพันธ์ NoSQL ไม่ใช่เทคโนโลยีเดียว แต่เป็นชุดแนวคิดหลวมๆ เกี่ยวกับโมเดลข้อมูลใหม่ ความยืดหยุ่นของ schema ความสามารถในการ scale และการเปลี่ยนไปใช้ลิขสิทธิ์แบบ open source ฐานข้อมูลบางตัวโปรโมตตัวเองเป็น NewSQL ซึ่งสะท้อนเป้าหมายที่จะให้ scale ได้แบบระบบ NoSQL ควบคู่ไปกับโมเดลข้อมูลและ transactional guarantee ของฐานข้อมูลเชิงสัมพันธ์แบบดั้งเดิม แนวคิดของ NoSQL และ NewSQL มีอิทธิพลมากต่อการออกแบบระบบข้อมูล แต่พอหลักการเหล่านี้ถูกยอมรับกันอย่างกว้างขวาง คำสองคำนี้ก็ถูกใช้น้อยลง

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

ข้อดีข้อเสียของข้อมูลแบบเอกสารกับแบบเชิงสัมพันธ์ถูกถกเถียงกันมาก มาดูประเด็นสำคัญบางข้อของการถกเถียงนั้นกัน

The Object-Relational Mismatch (ความไม่สอดคล้องระหว่างออบเจ็กต์กับเชิงสัมพันธ์)

แอปพลิเคชันส่วนใหญ่ในปัจจุบันพัฒนาด้วยภาษาโปรแกรมเชิงวัตถุ ซึ่งนำไปสู่ข้อวิจารณ์ที่พบบ่อยต่อโมเดลข้อมูลของ SQL: ถ้าข้อมูลถูกเก็บในตารางเชิงสัมพันธ์ ก็ต้องมีชั้นแปลงที่เทอะทะคั่นระหว่างออบเจ็กต์ในโค้ดแอปพลิเคชันกับโมเดลฐานข้อมูลแบบตาราง แถว และคอลัมน์ ความไม่เข้ากันระหว่างสองโมเดลนี้บางครั้งเรียกว่า impedance mismatch (ความไม่สอดคล้องของอิมพีแดนซ์)

Note

คำว่า impedance mismatch ยืมมาจากอิเล็กทรอนิกส์ วงจรไฟฟ้าทุกวงจรมีอิมพีแดนซ์ (ความต้านทานต่อกระแสสลับ) ที่อินพุตและเอาต์พุต เมื่อต่อเอาต์พุตของวงจรหนึ่งเข้ากับอินพุตของอีกวงจร กำลังไฟฟ้าจะถ่ายโอนผ่านจุดต่อได้สูงสุดถ้าอิมพีแดนซ์ของเอาต์พุตและอินพุตตรงกัน ถ้าอิมพีแดนซ์ไม่ตรงกัน อาจเกิดการสะท้อนของสัญญาณและปัญหาอื่นๆ

Object-relational mapping (การแมปออบเจ็กต์-เชิงสัมพันธ์)

framework แบบ object-relational mapping (ORM) เช่น ActiveRecord และ Hibernate ช่วยลดโค้ด boilerplate ที่ต้องเขียนสำหรับชั้นแปลงนี้ แต่ก็มักถูกวิจารณ์ [ 7 ] ปัญหาที่มักถูกกล่าวถึงมีดังนี้:

  • ORM ซับซ้อนและซ่อนความแตกต่างระหว่างสองโมเดลได้ไม่สมบูรณ์ นักพัฒนาจึงยังต้องคิดถึงทั้งการแทนข้อมูลแบบเชิงสัมพันธ์และแบบเชิงวัตถุ

  • โดยทั่วไป ORM ใช้กับการพัฒนาแอปพลิเคชัน OLTP เท่านั้น (ดู "Characterizing Transaction Processing and Analytics" ); วิศวกรข้อมูลที่เตรียมข้อมูลให้พร้อมสำหรับการวิเคราะห์ยังต้องทำงานกับโครงสร้างเชิงสัมพันธ์ที่อยู่เบื้องหลัง ดังนั้นการออกแบบ schema เชิงสัมพันธ์จึงยังสำคัญแม้ใช้ ORM

  • ORM จำนวนมากใช้ได้กับฐานข้อมูล OLTP เชิงสัมพันธ์เท่านั้น องค์กรที่มีระบบข้อมูลหลากหลาย เช่น search engine, graph database และระบบ NoSQL อาจพบว่า ORM รองรับไม่พอ

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

  • ORM ทำให้เผลอเขียน query ที่ไม่มีประสิทธิภาพได้ง่าย ตัวอย่างคือ N+1 query problem [ 8 ] สมมติว่าคุณต้องการแสดงรายการความคิดเห็นของผู้ใช้บนหน้าเว็บ คุณจึงรัน query หนึ่งครั้งที่คืนค่า N รายการความคิดเห็น โดยแต่ละรายการมี ID ของผู้เขียน เพื่อแสดงชื่อของผู้เขียนแต่ละคน คุณต้องค้นหา ID ในตาราง users ถ้าเขียน SQL เอง คุณอาจ join ใน query เดียวแล้วคืนชื่อผู้เขียนมาพร้อมกับแต่ละความคิดเห็น แต่ด้วย ORM คุณอาจลงเอยด้วยการ query แยกบนตาราง users สำหรับแต่ละ N รายการความคิดเห็นเพื่อค้นหาผู้เขียน ทำให้ต้อง query ฐานข้อมูลรวม N +1 ครั้ง ซึ่งช้ากว่าการ join ในฐานข้อมูล เพื่อเลี่ยงปัญหานี้ คุณอาจต้องสั่งให้ ORM ดึงข้อมูลผู้เขียนมาพร้อมกับความคิดเห็น

อย่างไรก็ตาม ORM ก็มีข้อดีเช่นกัน:

  • สำหรับข้อมูลที่เหมาะกับโมเดลเชิงสัมพันธ์ การแปลงระหว่างโครงสร้างเชิงสัมพันธ์ในฐานข้อมูลกับออบเจ็กต์ในหน่วยความจำเป็นสิ่งที่หลีกเลี่ยงไม่ได้อยู่แล้ว และ ORM ช่วยลดโค้ด boilerplate ที่ต้องเขียนสำหรับการแปลงนี้ query ที่ซับซ้อนอาจยังต้องจัดการนอก ORM แต่ ORM ช่วยงานที่ง่ายและทำซ้ำๆ ได้

  • ORM บางตัวช่วย cache ผลลัพธ์ของ query ฐานข้อมูล ซึ่งช่วยลดภาระของฐานข้อมูลได้

  • ORM ยังช่วยจัดการ schema migration และงานดูแลระบบอื่นๆ

The document data model for one-to-many relationships (โมเดลข้อมูลแบบเอกสารสำหรับความสัมพันธ์แบบหนึ่งต่อกลุ่ม)

ข้อมูลบางอย่างไม่เหมาะกับการแทนด้วยรูปแบบเชิงสัมพันธ์ ลองดูตัวอย่างเพื่อสำรวจข้อจำกัดของโมเดลเชิงสัมพันธ์ รูปที่ 3-1 แสดงให้เห็นว่าประวัติย่อ (โปรไฟล์ LinkedIn) แทนใน schema เชิงสัมพันธ์ได้อย่างไร โปรไฟล์ทั้งหมดระบุได้ด้วยตัวระบุที่ไม่ซ้ำกัน user_id ฟิลด์เช่น first_name และ last_name ปรากฏเพียงครั้งเดียวต่อผู้ใช้หนึ่งคน ดังนั้นจึงสามารถสร้างโมเดลเป็นคอลัมน์ในตาราง users ได้

คนส่วนใหญ่ทำงานมากกว่าหนึ่งที่ตลอดอาชีพ และอาจมีประวัติการศึกษาหลายช่วง รวมถึงมีข้อมูลติดต่อกี่รายการก็ได้ วิธีหนึ่งในการแทน ความสัมพันธ์แบบหนึ่งต่อกลุ่ม แบบนี้คือแยกตำแหน่งงาน การศึกษา และข้อมูลติดต่อไว้คนละตาราง โดยแต่ละตารางมี foreign key ที่อ้างอิงไปยังตาราง users ดังใน รูปที่ 3-1

อีกวิธีหนึ่งซึ่งอาจเป็นธรรมชาติกว่าและใกล้โครงสร้างออบเจ็กต์ในโค้ดแอปพลิเคชันมากกว่า คือแทนข้อมูลเดียวกันเป็นเอกสาร JSON ดังที่แสดงใน ตัวอย่างที่ 3-1

แผนภาพที่แสดง schema ฐานข้อมูลเชิงสัมพันธ์ที่แทนโปรไฟล์ LinkedIn โดยมีตารางสำหรับผู้ใช้ ตำแหน่งงาน การศึกษา ภูมิภาค และข้อมูลติดต่อ แสดงความสัมพันธ์แบบหนึ่งต่อกลุ่ม

รูปที่ 3-1. การใช้ schema เชิงสัมพันธ์เพื่อแทนโปรไฟล์ LinkedIn
ตัวอย่างที่ 3-1. การแสดงโปรไฟล์ LinkedIn เป็นเอกสาร JSON
{
  "user_id":     251,
  "first_name":  "Barack",
  "last_name":   "Obama",
  "headline":    "Former President of the United States of America",
  "region_id":   "us:91",
  "photo_url":   "/p/7/000/253/05b/308dd6e.jpg",
  "positions": [
    {"job_title": "President", "organization": "United States of America"},
    {"job_title": "US Senator (D-IL)", "organization": "United States Senate"}
  ],
  "education": [
    {"school_name": "Harvard University",  "start": 1988, "end": 1991},
    {"school_name": "Columbia University", "start": 1981, "end": 1983}
  ],
  "contact_info": {
    "website": "https://barackobama.com",
    "x": "https://x.com/barackobama"
  }
}

นักพัฒนาบางคนรู้สึกว่าโมเดล JSON ช่วยลด impedance mismatch ระหว่างโค้ดแอปพลิเคชันกับชั้นจัดเก็บข้อมูล การที่ไม่ต้องมี schema ก็มักถูกยกให้เป็นข้อดีเช่นกัน เราจะพูดถึงเรื่องนี้ใน "Schema flexibility in the document model" อย่างไรก็ตาม ดังที่เราจะเห็นใน บทที่ 5 JSON ในฐานะรูปแบบ encoding ข้อมูลก็มีปัญหาเหมือนกัน

การแสดงแบบ JSON มี locality ที่ดีกว่า schema หลายตารางใน รูปที่ 3-1 (ดู "Data locality for reads and writes" ) ถ้าคุณต้องการดึงโปรไฟล์ในตัวอย่างเชิงสัมพันธ์ คุณต้องทำหลาย query (query แต่ละตารางด้วย user_id ) หรือทำ multiway join ที่ยุ่งยากระหว่างตาราง users และตารางรองของมัน [ 9 , 10 ] ในรูปแบบ JSON ข้อมูลที่เกี่ยวข้องทั้งหมดอยู่ในที่เดียว ทำให้ query ทั้งเร็วและง่ายขึ้น

ความสัมพันธ์แบบหนึ่งต่อกลุ่มจากโปรไฟล์ผู้ใช้ไปยังตำแหน่งงาน ประวัติการศึกษา และข้อมูลติดต่อทำให้ข้อมูลมีโครงสร้างแบบต้นไม้ และการแสดงแบบ JSON ทำให้โครงสร้างต้นไม้นี้เห็นชัด (ดู รูปที่ 3-2 )

แผนภาพแสดงโครงสร้างแบบต้นไม้ที่แสดงความสัมพันธ์แบบหนึ่งต่อกลุ่มระหว่างโปรไฟล์ผู้ใช้และแอตทริบิวต์ต่างๆ เช่น ตำแหน่งงานและประวัติการศึกษา

รูปที่ 3-2. ความสัมพันธ์แบบหนึ่งต่อกลุ่มที่สร้างโครงสร้างแบบต้นไม้
Note

ความสัมพันธ์แบบหนึ่งต่อกลุ่มบางครั้งเรียกว่า one-to-few (หนึ่งต่อน้อย) เนื่องจากประวัติย่อโดยทั่วไปมีตำแหน่งงานจำนวนน้อย [ 11 , 12 ] ถ้ามีรายการที่เกี่ยวข้องจำนวนมากจริงๆ—เช่น ความคิดเห็นบนโพสต์โซเชียลมีเดียของคนดัง ซึ่งอาจมีเป็นพันๆ—การฝังทั้งหมดไว้ในเอกสารเดียวอาจยุ่งยากเกินไป วิธีแบบเชิงสัมพันธ์ใน รูปที่ 3-1 จึงดีกว่า

Normalization, Denormalization, and Joins (การทำ normalization และ denormalization กับ join)

ใน ตัวอย่างที่ 3-1 ในหัวข้อก่อนหน้านี้ region_id เก็บเป็น ID ไม่ใช่ข้อความธรรมดา Washington, DC, United States ทำไมถึงเป็นเช่นนั้น?

ถ้า UI เป็นช่องให้พิมพ์ภูมิภาคเองได้อย่างอิสระ การเก็บเป็นข้อความธรรมดาก็สมเหตุสมผล แต่การมีรายการภูมิภาคมาตรฐานให้ผู้ใช้เลือกจาก dropdown หรือ autocompleter ก็มีข้อดี เช่น:

  • รูปแบบและการสะกดที่สอดคล้องกันในทุกโปรไฟล์

  • เลี่ยงความกำกวมเมื่อหลายสถานที่มีชื่อเหมือนกัน (ถ้าข้อความมีแค่ Washington, DC มันจะหมายถึง DC หรือรัฐ?)

  • อัปเดตง่าย—ชื่อถูกเก็บไว้ที่เดียว จึงแก้ทุกที่ได้ง่ายเมื่อต้องเปลี่ยน (เช่น เปลี่ยนชื่อเมืองเพราะเหตุการณ์ทางการเมือง)

  • รองรับการแปลภาษา—เมื่อแปลเว็บไซต์เป็นภาษาอื่น ก็แปลรายการมาตรฐานได้ จึงแสดงภูมิภาคเป็นภาษาของผู้ชมแต่ละคนได้

  • ค้นหาได้ดีขึ้น (เช่น การค้นหาคนบนชายฝั่งตะวันออกของสหรัฐฯ จะเจอโปรไฟล์นี้ เพราะรายการภูมิภาคเก็บข้อเท็จจริงไว้ได้ว่า Washington ตั้งอยู่บนชายฝั่งตะวันออก—ซึ่งไม่ปรากฏชัดจากข้อความ Washington, DC เพียงอย่างเดียว)

การเลือกเก็บ ID หรือข้อความธรรมดาเป็นคำถามเรื่อง normalization เมื่อใช้ ID ข้อมูลของคุณจะเป็น normalized มากขึ้น: ข้อมูลที่มนุษย์อ่านแล้วเข้าใจ (เช่น ข้อความ Washington, DC ) จะถูกเก็บไว้ที่เดียว และทุกอย่างที่อ้างถึงมันจะใช้ ID (ซึ่งมีความหมายเฉพาะภายในฐานข้อมูล) แต่ถ้าเก็บข้อความตรงๆ คุณกำลังทำซ้ำข้อมูลที่มนุษย์อ่านเข้าใจนั้นในทุก record ที่ใช้มัน การแทนแบบนี้เรียกว่า denormalized

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

ข้อเสียของการแทนแบบ normalized คือ ทุกครั้งที่ต้องแสดง record ที่มี ID คุณต้องค้นหาเพิ่มเพื่อแปลง ID เป็นสิ่งที่มนุษย์อ่านได้ ในโมเดลข้อมูลเชิงสัมพันธ์ ทำได้ด้วย join ตัวอย่างเช่น:

SELECT users.*, regions.region_name
FROM users
JOIN regions ON users.region_id = regions.id
WHERE users.id = 251;

ฐานข้อมูลเอกสารเก็บได้ทั้งข้อมูลแบบ normalized และ denormalized แต่มักเกี่ยวข้องกับการทำ denormalization—ส่วนหนึ่งเพราะโมเดล JSON ทำให้เก็บฟิลด์ denormalized เพิ่มได้ง่าย และอีกส่วนเพราะฐานข้อมูลเอกสารหลายตัวรองรับ join ได้ไม่ดี ทำให้การทำ normalization ไม่สะดวก ฐานข้อมูลเอกสารบางตัวไม่รองรับ join เลย คุณจึงต้อง join ในโค้ดแอปพลิเคชัน—คือดึงเอกสารที่มี ID ก่อน แล้วรัน query ที่สองเพื่อแปลง ID นั้นเป็นเอกสารอีกชุด ใน MongoDB ก็ join ได้ด้วย operator $lookup ใน aggregation pipeline:

db.users.aggregate([
  { $match: { _id: 251 } },
  { $lookup: {
      from: "regions",
      localField: "region_id",
      foreignField: "_id",
      as: "region"
  } }
])

Trade-offs of normalization (ข้อแลกเปลี่ยนของ normalization)

ในตัวอย่างประวัติย่อ ฟิลด์ region_id อ้างอิงไปยังชุดภูมิภาคมาตรฐาน organization (บริษัทหรือหน่วยงานที่บุคคลนั้นทำงาน) และ school_name (สถานที่ที่เรียน) เป็นเพียงข้อความ การแทนส่วนนี้จึงเป็นแบบ denormalized: หลายคนอาจทำงานในบริษัทเดียวกัน แต่ไม่มี ID เชื่อมโยงพวกเขาเข้าด้วยกัน

ลองพิจารณาว่าองค์กรและโรงเรียนควรเป็น entity ของตัวเองหรือไม่ โดยให้โปรไฟล์อ้างอิงด้วย ID ของมัน ข้อโต้แย้งเดียวกันกับการอ้างอิง ID ของภูมิภาคใช้ได้ที่นี่เช่นกัน ตัวอย่างเช่น สมมติว่าเราต้องการใส่โลโก้ของโรงเรียนหรือบริษัทเพิ่มจากชื่อ:

  • ในแบบ denormalized เราจะใส่ URL รูปโลโก้ไว้ในโปรไฟล์ของแต่ละคน ทำให้เอกสาร JSON ครบในตัวเอง แต่จะมีปัญหาเมื่อต้องเปลี่ยนโลโก้ เพราะต้องหาทุกที่ที่มี URL เก่าแล้วอัปเดต [ 11 ]

  • ในแบบ normalized เราจะสร้าง entity แทนองค์กรหรือโรงเรียน แล้วเก็บชื่อ URL โลโก้ และอาจมีแอตทริบิวต์อื่นๆ (คำอธิบาย ฟีดข่าว ฯลฯ) ไว้ครั้งเดียวใน entity นั้น ประวัติย่อทุกฉบับที่กล่าวถึงองค์กรนั้นจะอ้างอิงแค่ ID ของมัน และการอัปเดตโลโก้ก็ทำได้ง่าย

โดยหลักการแล้ว ข้อมูลแบบ normalized มักเขียนได้เร็วกว่า (เพราะมีสำเนาเดียว) แต่ query ช้ากว่า (เพราะต้องใช้ join) ส่วนข้อมูลแบบ denormalized มักอ่านได้เร็วกว่า (join น้อยกว่า) แต่เขียนแพงกว่า (มีสำเนาให้อัปเดตมากกว่า และใช้พื้นที่ดิสก์มากกว่า) คุณอาจเห็นว่าการมอง denormalization เป็นรูปแบบหนึ่งของ derived data (ดู "Systems of Record and Derived Data" ) มีประโยชน์ เพราะคุณต้องมีกระบวนการสำหรับอัปเดตสำเนาข้อมูลที่ซ้ำซ้อน

นอกจากต้นทุนของการอัปเดตทั้งหมดนี้แล้ว คุณยังต้องคำนึงถึงความสอดคล้องของฐานข้อมูลถ้า process ล้มเหลวกลางคันระหว่างอัปเดต ฐานข้อมูลที่มี atomic transaction (ดู "Atomicity" ) ช่วยให้รักษาความสอดคล้องได้ง่ายขึ้น แต่ไม่ใช่ทุกฐานข้อมูลที่ให้ atomicity ข้ามหลายเอกสาร อีกทางคือรับประกันความสอดคล้องผ่าน stream processing ซึ่งเราจะพูดถึงใน บทที่ 12

การทำ normalization มักเหมาะกับระบบ OLTP ซึ่งทั้งการอ่านและการอัปเดตต้องเร็ว ส่วนระบบวิเคราะห์มักทำงานได้ดีกว่ากับข้อมูล denormalized เพราะอัปเดตเป็นชุดใหญ่ และสิ่งที่กังวลหลักคือประสิทธิภาพของ query แบบอ่านอย่างเดียว ในระบบขนาดเล็กถึงกลาง โมเดลข้อมูลแบบ normalized มักดีที่สุด เพราะไม่ต้องห่วงเรื่องการรักษาสำเนาข้อมูลหลายชุดให้สอดคล้องกัน และต้นทุนของ join ก็ยอมรับได้ แต่ในระบบขนาดใหญ่มาก ต้นทุนของ join อาจกลายเป็นปัญหา

Denormalization in the social networking case study (denormalization ในกรณีศึกษาเครือข่ายสังคม)

ใน "Case Study: Social Network Home Timelines" เราเปรียบเทียบการแทนแบบ normalized ( รูปที่ 2-1 ) และแบบ denormalized (timeline ที่ precompute หรือ materialized) ในที่นี้ join ระหว่าง posts และ follows แพงเกินไป และ materialized timeline ก็คือ cache ของผลลัพธ์ของ join นั้น กระบวนการ fan-out ที่แทรกโพสต์ใหม่ลงใน timeline ของผู้ติดตามคือวิธีที่เรารักษาความสอดคล้องของการแทนแบบ denormalized

อย่างไรก็ตาม materialized timeline ของ X (เดิมคือ Twitter) ที่ implement จริงไม่ได้เก็บข้อความของแต่ละโพสต์ แต่ละรายการเก็บแค่ ID โพสต์ ID ของผู้ใช้ที่โพสต์ และข้อมูลเพิ่มเติมเล็กน้อยสำหรับระบุ repost และ reply [ 13 ] พูดอีกอย่างคือ มันเป็นผลลัพธ์ที่ precompute ไว้ของ query (โดยประมาณ) ต่อไปนี้:

SELECT posts.id, posts.sender_id FROM posts
  JOIN follows ON posts.sender_id = follows.followee_id
  WHERE follows.follower_id = current_user
  ORDER BY posts.timestamp DESC
  LIMIT 1000

ซึ่งหมายความว่าทุกครั้งที่อ่าน timeline บริการยังต้องทำ join สองอย่าง: ใช้ ID โพสต์ไปดึงเนื้อหาโพสต์จริง (รวมถึงสถิติอย่างจำนวนไลค์และ reply) และใช้ ID ผู้ส่งไปค้นโปรไฟล์ (เพื่อเอาชื่อผู้ใช้ รูปโปรไฟล์ และรายละเอียดอื่นๆ) กระบวนการแปลง ID ให้เป็นข้อมูลที่มนุษย์อ่านได้นี้เรียกว่า hydrating IDs ซึ่งโดยพื้นฐานแล้วคือ join ที่ทำในโค้ดแอปพลิเคชัน [ 13 ]

ที่ timeline ซึ่ง precompute ไว้เก็บแค่ ID ก็เพราะข้อมูลที่ ID เหล่านั้นอ้างถึงเปลี่ยนบ่อย จำนวนไลค์และ reply อาจเปลี่ยนหลายครั้งต่อวินาทีบนโพสต์ยอดนิยม และผู้ใช้บางคนก็เปลี่ยนชื่อผู้ใช้หรือรูปโปรไฟล์อยู่เป็นประจำ เนื่องจาก timeline ควรแสดงจำนวนไลค์และรูปโปรไฟล์ล่าสุด ณ ตอนที่ดู การ denormalize ข้อมูลเหล่านี้ลงใน materialized timeline จึงไม่สมเหตุสมผล ยิ่งไปกว่านั้น การ denormalize แบบนี้จะทำให้ต้นทุนพื้นที่จัดเก็บเพิ่มขึ้นมาก

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

ถ้าคุณต้องตัดสินใจว่าจะ denormalize อะไรในแอปพลิเคชันของคุณหรือไม่ กรณีศึกษาเครือข่ายสังคมนี้แสดงให้เห็นว่าตัวเลือกไม่ได้ชัดเจนในทันที วิธีที่ scale ได้มากที่สุดอาจเป็นการ denormalize บางอย่างและปล่อยให้ส่วนอื่นเป็น normalized คุณต้องพิจารณาให้ดีว่าข้อมูลเปลี่ยนบ่อยแค่ไหน และต้นทุนของการอ่านและการเขียนเป็นอย่างไร (ซึ่งอาจถูกครอบงำโดยค่าผิดปกติ เช่น ผู้ใช้ที่มีผู้ติดตามหรือติดตามคนจำนวนมากในกรณีของเครือข่ายสังคมทั่วไป) normalization และ denormalization ไม่ได้ดีหรือไม่ดีโดยตัวมันเอง—มันเป็นแค่ trade-off ระหว่างประสิทธิภาพการอ่าน ประสิทธิภาพการเขียน และความพยายามในการ implement

Many-to-One and Many-to-Many Relationships (ความสัมพันธ์แบบหลายต่อหนึ่งและหลายต่อหลาย)

ในขณะที่ตาราง positions และ education ใน รูปที่ 3-1 เป็นตัวอย่างของความสัมพันธ์แบบหนึ่งต่อกลุ่มหรือหนึ่งต่อน้อย (ประวัติย่อหนึ่งฉบับมีหลายตำแหน่ง แต่แต่ละตำแหน่งเป็นของประวัติย่อเพียงฉบับเดียว) ฟิลด์ region_id เป็นตัวอย่างของความสัมพันธ์แบบ หลายต่อหนึ่ง (หลายคนอาศัยอยู่ในภูมิภาคเดียวกัน แต่เราสมมติว่าแต่ละคนอาศัยอยู่ในภูมิภาคเดียวเท่านั้นในเวลาใดเวลาหนึ่ง)

ถ้าเราสร้าง entity สำหรับองค์กรและโรงเรียน แล้วอ้างอิงด้วย ID จากประวัติย่อ เราก็จะมี ความสัมพันธ์แบบหลายต่อหลาย (คนหนึ่งคนอาจทำงานให้หลายองค์กร และองค์กรหนึ่งมีพนักงานปัจจุบันหรืออดีตหลายคน) ในโมเดลเชิงสัมพันธ์ ความสัมพันธ์แบบนี้มักแทนด้วย associative table หรือ join table ดังที่แสดงใน รูปที่ 3-3 : แต่ละตำแหน่งเชื่อมโยง ID ผู้ใช้หนึ่งรายกับ ID องค์กรหนึ่งราย

แผนภาพแสดงความสัมพันธ์แบบหลายต่อหลายในฐานข้อมูลเชิงสัมพันธ์ แสดงให้เห็นว่า ID ผู้ใช้ ID องค์กร และตำแหน่งงานเชื่อมโยงกันอย่างไรในตาราง users, positions และ organizations

รูปที่ 3-3. ความสัมพันธ์แบบหลายต่อหลายในโมเดลเชิงสัมพันธ์

ความสัมพันธ์แบบหลายต่อหนึ่งและหลายต่อหลายใส่ในเอกสาร JSON ที่ครบในตัวเองได้ไม่ง่ายนัก มันเหมาะกับการแทนแบบ normalized มากกว่า ในโมเดลเอกสาร วิธีแทนที่เป็นไปได้วิธีหนึ่งคือ ตัวอย่างที่ 3-2 และแสดงใน รูปที่ 3-4 ข้อมูลในสี่เหลี่ยมเส้นประแต่ละกล่องจัดรวมเป็นเอกสารเดียวได้ แต่ลิงก์ไปยังองค์กรและโรงเรียนควรเป็น reference ไปยังเอกสารอื่น

ตัวอย่างที่ 3-2. ประวัติย่อที่อ้างอิงองค์กรด้วย ID
{
  "user_id":    251,
  "first_name": "Barack",
  "last_name":  "Obama",
  "positions": [
    {"start": 2009, "end": 2017, "job_title": "President",         "org_id": 513},
    {"start": 2005, "end": 2008, "job_title": "US Senator (D-IL)", "org_id": 514}
  ],
  ...
}

ความสัมพันธ์แบบหลายต่อหลายมักต้อง query ใน "ทั้งสองทิศทาง"—เช่น หาองค์กรทั้งหมดที่คนคนหนึ่งเคยทำงาน และหาคนทั้งหมดที่เคยทำงานในองค์กรหนึ่ง วิธีหนึ่งที่ทำให้ query แบบนี้เป็นไปได้คือเก็บ ID reference ไว้ทั้งสองฝั่ง โดยให้ประวัติย่อมี ID ของทุกองค์กรที่คนนั้นเคยทำงาน และให้เอกสารองค์กรมี ID ของประวัติย่อที่กล่าวถึงองค์กรนั้น การแทนแบบนี้เป็น denormalized เพราะเก็บความสัมพันธ์ไว้สองที่ ซึ่งอาจทำให้ข้อมูลไม่สอดคล้องกัน

แผนภาพแสดงความสัมพันธ์แบบหลายต่อหลายในโมเดลเอกสาร แสดงการเชื่อมต่อระหว่างผู้ใช้ งาน องค์กร และสถาบันการศึกษา

รูปที่ 3-4. ความสัมพันธ์แบบหลายต่อหลายในโมเดลเอกสาร—ข้อมูลภายในกล่องเส้นประแต่ละกล่องสามารถจัดกลุ่มเป็นเอกสารเดียว

การแทนแบบ normalized เก็บความสัมพันธ์ไว้ที่เดียว และอาศัย secondary index (ซึ่งเราจะพูดถึงใน บทที่ 4 ) เพื่อให้ query ความสัมพันธ์ได้อย่างมีประสิทธิภาพทั้งสองทิศทาง ใน schema เชิงสัมพันธ์ที่แสดงใน รูปที่ 3-3 เราจะบอกให้ฐานข้อมูลสร้าง index บนทั้งคอลัมน์ user_id และ org_id ของตาราง positions

ในโมเดลเอกสารของ ตัวอย่างที่ 3-2 ฐานข้อมูลต้องสร้าง index บนฟิลด์ org_id ของออบเจ็กต์ภายในอาร์เรย์ positions ฐานข้อมูลเอกสารหลายตัว และฐานข้อมูลเชิงสัมพันธ์ที่รองรับ JSON สร้าง index แบบนั้นบนค่าในเอกสารได้

Stars and Snowflakes: Schemas for Analytics (Star และ Snowflake: schema สำหรับการวิเคราะห์)

data warehouse (ดู "Data Warehousing" ) มักเป็นแบบเชิงสัมพันธ์ และมีรูปแบบที่ใช้กันแพร่หลายสำหรับโครงสร้างตารางใน data warehouse เช่น star schema, snowflake schema, dimensional modeling [ 14 ] และ one big table (OBT) โครงสร้างเหล่านี้ปรับมาให้เหมาะกับความต้องการของนักวิเคราะห์ธุรกิจ กระบวนการ ETL จะแปลงข้อมูลจาก operational system (ระบบที่ใช้งานจริง) ไปเป็น schema ที่เลือก

รูปที่ 3-5 แสดงตัวอย่างของ star schema ที่อาจพบใน data warehouse ของร้านขายของชำ ตรงกลางของ schema คือสิ่งที่เรียกว่า fact table (ในตัวอย่างนี้เรียกว่า fact_sales ) แต่ละแถวของ fact table แทนเหตุการณ์ที่เกิดขึ้น ณ เวลาใดเวลาหนึ่ง (ในที่นี้ แต่ละแถวแทนการซื้อสินค้าของลูกค้า) ถ้าเราวิเคราะห์ปริมาณการเข้าชมเว็บไซต์แทนการขายปลีก แต่ละแถวอาจแทนการเปิดดูหน้าเว็บหรือการคลิกของผู้ใช้

แผนภาพแสดง star schema สำหรับ Data Warehouse ของร้านขายของชำ โดยมีตาราง fact_sales ตรงกลางเชื่อมต่อกับตารางมิติต่างๆ เช่น product, store, date, customer และ promotion

รูปที่ 3-5. Star schema สำหรับใช้ใน data warehouse

โดยปกติ fact จะถูกบันทึกเป็นเหตุการณ์แต่ละรายการ เพราะวิธีนี้ให้ความยืดหยุ่นสูงสุดในการวิเคราะห์ภายหลัง แต่นั่นก็หมายความว่า fact table อาจใหญ่มาก องค์กรขนาดใหญ่อาจมีประวัติธุรกรรมหลายเพตาไบต์ใน data warehouse ซึ่งส่วนใหญ่อยู่ใน fact table

บางคอลัมน์ใน fact table เป็นแอตทริบิวต์ เช่น ราคาที่ขายสินค้าและต้นทุนที่ซื้อจากซัพพลายเออร์ (ทำให้คำนวณอัตรากำไรได้) ส่วนคอลัมน์อื่นๆ เป็น foreign key ที่อ้างอิงไปยังตารางอื่นๆ ที่เรียกว่า dimension table เนื่องจากแต่ละแถวใน fact table แทนเหตุการณ์หนึ่งรายการ มิติต่างๆ จึงแทน ใคร , อะไร , ที่ไหน , เมื่อไหร่ , อย่างไร และ ทำไม ของเหตุการณ์นั้น

ตัวอย่างเช่น ใน รูปที่ 3-5 หนึ่งในมิติคือสินค้าที่ขาย แต่ละแถวในตาราง dim_product แทนสินค้าหนึ่งประเภทที่วางขาย รวมถึงรหัส SKU, คำอธิบาย, ชื่อแบรนด์, หมวดหมู่, ปริมาณไขมัน และขนาดบรรจุภัณฑ์ แต่ละแถวในตาราง fact_sales ใช้ foreign key เพื่อระบุว่าสินค้าใดถูกขายในธุรกรรมนั้นๆ query มักต้อง join กับ dimension table หลายตาราง

แม้แต่วันที่และเวลาก็มักแทนด้วย dimension table เพราะช่วยให้เก็บข้อมูลเพิ่มเติมเกี่ยวกับวันที่ได้ (เช่น วันหยุดนักขัตฤกษ์) ทำให้ query แยกยอดขายในวันหยุดกับวันที่ไม่หยุดได้

ชื่อ star schema มาจากการที่เมื่อวาดความสัมพันธ์ของตารางออกมา fact table จะอยู่ตรงกลาง ล้อมรอบด้วย dimension table (ดังที่แสดงใน รูปที่ 3-5 ); เส้นเชื่อมไปยังตารางเหล่านี้ดูเหมือนแฉกของดาว

รูปแบบหนึ่งของแม่แบบนี้คือ snowflake schema ซึ่งมิติต่างๆ ถูกแบ่งย่อยเป็นมิติย่อย ตัวอย่างเช่น อาจมีตารางแยกสำหรับแบรนด์และหมวดหมู่สินค้า และแต่ละแถวในตาราง dim_product อ้างอิงแบรนด์และหมวดหมู่ด้วย foreign key แทนการเก็บเป็นข้อความในตาราง dim_product snowflake schema ผ่านการ normalize มากกว่า star schema แต่ star schema มักถูกเลือกใช้เพราะนักวิเคราะห์ทำงานด้วยได้ง่ายกว่า [ 14 ]

ใน data warehouse ทั่วไป ตารางมักค่อนข้างกว้าง: fact table มักมีมากกว่าร้อยคอลัมน์ บางครั้งเป็นหลายร้อยคอลัมน์ dimension table ก็กว้างได้เช่นกัน เพราะรวม metadata ทั้งหมดที่อาจเกี่ยวข้องกับการวิเคราะห์—ตัวอย่างเช่น ตาราง dim_store อาจมีรายละเอียดของบริการในแต่ละร้าน เช่น มีเบเกอรี่ในร้านหรือไม่ พื้นที่ (ตารางฟุต) วันที่ร้านเปิดครั้งแรก วันที่ปรับปรุงร้านครั้งล่าสุด และระยะทางจากทางหลวงที่ใกล้ที่สุด

star หรือ snowflake schema ส่วนใหญ่ประกอบด้วยความสัมพันธ์แบบหลายต่อหนึ่ง (เช่น มีการขายหลายครั้งสำหรับสินค้าเฉพาะรายการหนึ่ง ในร้านเฉพาะแห่งหนึ่ง) ซึ่งแทนด้วย fact table ที่มี foreign key ไปยัง dimension table หรือจากมิติไปยังมิติย่อย โดยหลักการแล้ว ความสัมพันธ์ประเภทอื่นก็มีได้ แต่มักถูก denormalize เพื่อให้ query ง่ายขึ้น ตัวอย่างเช่น ถ้าลูกค้าซื้อสินค้าหลายอย่างพร้อมกันในครั้งเดียว ธุรกรรมที่มีหลายรายการนั้นจะไม่ได้ถูกแทนไว้อย่างชัดเจน แต่ fact table จะมีแถวแยกสำหรับสินค้าแต่ละรายการที่ซื้อ และ fact เหล่านั้นทั้งหมดก็เพียงบังเอิญมี ID ลูกค้า ID ร้านค้า และ timestamp เดียวกัน

schema ของ data warehouse บางแบบทำ denormalization หนักขึ้นไปอีกจนไม่มี dimension table เลย โดยพับข้อมูลของมิติต่างๆ เข้ามาเป็นคอลัมน์ denormalized ใน fact table แทน (โดยพื้นฐานคือการ precompute join ระหว่าง fact table กับ dimension table) วิธีนี้เรียกว่า one big table (OBT) แม้จะใช้พื้นที่จัดเก็บมากขึ้น แต่บางครั้งก็ทำให้ query เร็วขึ้น [ 15 ]

ในบริบทของการวิเคราะห์ การ denormalize แบบนี้ไม่มีปัญหา เพราะข้อมูลมักเป็น log ของข้อมูลในอดีตที่จะไม่เปลี่ยนอีก (ยกเว้นบางครั้งที่ต้องแก้ข้อผิดพลาด) ปัญหาความสอดคล้องของข้อมูลและ overhead ในการเขียนที่เกิดขึ้นกับการ denormalize ในระบบ OLTP จึงไม่ใช่เรื่องเร่งด่วนในงานวิเคราะห์

When to Use Which Model (เมื่อใดควรใช้โมเดลใด)

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

ถ้าข้อมูลในแอปพลิเคชันของคุณมีโครงสร้างคล้ายเอกสาร (เช่น ต้นไม้ของความสัมพันธ์แบบหนึ่งต่อกลุ่ม ซึ่งมักโหลดทั้งต้นในครั้งเดียว) การใช้โมเดลเอกสารน่าจะเป็นความคิดที่ดี เทคนิคเชิงสัมพันธ์ที่เรียกว่า shredding —การแยกโครงสร้างที่คล้ายเอกสารออกเป็นหลายตาราง (เช่น positions , education และ contact_info ใน รูปที่ 3-1 )—อาจนำไปสู่ schema ที่ยุ่งยากและโค้ดแอปพลิเคชันที่ซับซ้อนโดยไม่จำเป็น

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

บางแอปพลิเคชันให้ผู้ใช้เลือกลำดับของรายการเอง—เช่น to-do list หรือ issue tracker ที่ผู้ใช้ลากและวางงานเพื่อเรียงลำดับใหม่ได้ โมเดลเอกสารรองรับแอปพลิเคชันแบบนี้ได้ดี เพราะเก็บรายการ (หรือ ID ของมัน) ไว้ใน JSON array เพื่อกำหนดลำดับได้ ส่วนในฐานข้อมูลเชิงสัมพันธ์ไม่มีวิธีมาตรฐานในการแทนรายการที่ผู้ใช้เรียงลำดับเองแบบนี้ จึงต้องใช้กลเม็ดต่างๆ เช่น เรียงด้วยคอลัมน์จำนวนเต็ม (ต้องกำหนดหมายเลขใหม่เมื่อแทรกตรงกลาง) รักษา linked list ของ ID หรือใช้ fractional indexing [ 16 , 17 , 18 ]

Schema flexibility in the document model (ความยืดหยุ่นของ Schema ในโมเดลเอกสาร)

ฐานข้อมูลเอกสารส่วนใหญ่ รวมถึงการรองรับ JSON ในฐานข้อมูลเชิงสัมพันธ์ ไม่บังคับ schema ใดๆ กับข้อมูลในเอกสาร ส่วนการรองรับ XML ในฐานข้อมูลเชิงสัมพันธ์มักมาพร้อมการตรวจสอบ schema แบบเลือกใช้ได้ การไม่มี schema หมายความว่าเพิ่มคีย์และค่าอะไรลงในเอกสารก็ได้ และตอนอ่าน client ไม่มีหลักประกันว่าเอกสารจะมีฟิลด์อะไรบ้าง

ฐานข้อมูลเอกสารบางครั้งเรียกว่า schemaless (ไม่มี schema) แต่ชื่อนี้ทำให้เข้าใจผิด เพราะโค้ดที่อ่านข้อมูลมักสมมติโครงสร้างบางอย่าง—กล่าวคือมี schema โดยนัย เพียงแต่ฐานข้อมูลไม่ได้บังคับ [ 19 ] คำที่แม่นยำกว่าคือ schema-on-read (โครงสร้างของข้อมูลเป็นแบบโดยนัยและถูกตีความตอนอ่านเท่านั้น) ซึ่งตรงข้ามกับ schema-on-write (วิธีดั้งเดิมของฐานข้อมูลเชิงสัมพันธ์ ซึ่ง schema ถูกกำหนดไว้ชัดเจน และฐานข้อมูลรับประกันว่าข้อมูลทั้งหมดสอดคล้องกับ schema ตอนเขียน) [ 20 ]

schema-on-read คล้ายกับการตรวจสอบชนิดแบบ dynamic (runtime) ในภาษาโปรแกรม ส่วน schema-on-write คล้ายกับการตรวจสอบชนิดแบบ static (compile-time) และเช่นเดียวกับที่ฝ่ายสนับสนุน static กับ dynamic type checking ถกเถียงกันมากเรื่องข้อดีของแต่ละแบบ [ 21 ] การบังคับ schema ในฐานข้อมูลก็เป็นหัวข้อที่ถกเถียงกัน และโดยทั่วไปไม่มีฝ่ายชนะที่ชัดเจน

ความแตกต่างระหว่างสองวิธีนี้เห็นชัดเป็นพิเศษเมื่อแอปพลิเคชันต้องการเปลี่ยนรูปแบบของข้อมูล ตัวอย่างเช่น สมมติว่าคุณเก็บชื่อเต็มของผู้ใช้แต่ละคนไว้ในฟิลด์เดียว และอยากเปลี่ยนไปเก็บชื่อต้นกับนามสกุลแยกกัน [ 22 ] ในฐานข้อมูลเอกสาร คุณแค่เริ่มเขียนเอกสารใหม่ด้วยฟิลด์ใหม่ และมีโค้ดในแอปพลิเคชันคอยจัดการกรณีที่อ่านเจอเอกสารเก่า ตัวอย่างเช่น:

if (user && user.name && !user.first_name) {
    // Documents written before Dec 8, 2023 don't have first_name
    user.first_name = user.name.split(" ")[0];
}

ข้อเสียของวิธีนี้คือ ทุกส่วนของแอปพลิเคชันที่อ่านจากฐานข้อมูลต้องรับมือกับเอกสารรูปแบบเก่าที่อาจถูกเขียนไว้นานแล้ว ในทางกลับกัน ในฐานข้อมูลแบบ schema-on-write คุณมักทำ migration ดังนี้:

ALTER TABLE users ADD COLUMN first_name text DEFAULT NULL;
UPDATE users SET first_name = split_part(name, ' ', 1);      -- PostgreSQL
UPDATE users SET first_name = substring_index(name, ' ', 1);      -- MySQL

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

มีเครื่องมือหลายตัวที่ช่วยเปลี่ยน schema แบบนี้ในเบื้องหลังโดยไม่ต้องหยุดระบบ [ 23 , 24 , 25 , 26 ] แต่การ migrate แบบนี้บนฐานข้อมูลขนาดใหญ่ก็ยังท้าทายในเชิงปฏิบัติการ เราหลีกเลี่ยง migration ที่ซับซ้อนได้โดยเพิ่มคอลัมน์ first_name ด้วยค่าเริ่มต้นเป็น NULL (ซึ่งเร็ว) แล้วเติมค่าตอนอ่าน เหมือนที่ทำกับฐานข้อมูลเอกสาร

วิธีแบบ schema-on-read มีข้อดีเมื่อรายการในคอลเลกชันไม่ได้มีโครงสร้างเดียวกันทั้งหมด (กล่าวคือ ข้อมูลไม่เป็นเนื้อเดียวกัน) ตัวอย่างเช่น:

  • มีออบเจ็กต์หลายประเภท และไม่สมเหตุสมผลในทางปฏิบัติที่จะให้ออบเจ็กต์แต่ละประเภทมีตารางของตัวเอง

  • โครงสร้างของข้อมูลถูกกำหนดโดยระบบภายนอกที่คุณควบคุมไม่ได้ และอาจเปลี่ยนเมื่อไรก็ได้

ในสถานการณ์แบบนี้ schema อาจสร้างปัญหามากกว่าช่วย และเอกสารแบบ schemaless อาจเป็นโมเดลข้อมูลที่เป็นธรรมชาติกว่ามาก แต่ถ้า record ทั้งหมดควรมีโครงสร้างเดียวกัน schema ก็เป็นกลไกที่มีประโยชน์ทั้งในการเป็นเอกสารอธิบายและการบังคับโครงสร้างนั้น เราจะพูดถึง schema และ schema evolution ในรายละเอียดเพิ่มเติมใน บทที่ 5

Data locality for reads and writes (ความเป็นท้องถิ่นของข้อมูลสำหรับการอ่านและการเขียน)

เอกสารมักถูกเก็บเป็นสตริงต่อเนื่องก้อนเดียว เข้ารหัสเป็น JSON, XML หรือรูปแบบไบนารี (เช่น BSON ของ MongoDB) ถ้าแอปพลิเคชันของคุณมักต้องเข้าถึงเอกสารทั้งฉบับ (เช่น เพื่อแสดงบนหน้าเว็บ) storage locality นี้มีข้อดีด้านประสิทธิภาพ ถ้าข้อมูลถูกแบ่งข้ามหลายตาราง ดังใน รูปที่ 3-1 ต้องค้นหาใน index หลายครั้งเพื่อดึงข้อมูลทั้งหมด ซึ่งอาจต้อง disk seek มากขึ้นและใช้เวลานานขึ้น

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

อย่างไรก็ตาม การเก็บข้อมูลที่เกี่ยวข้องไว้ด้วยกันเพื่อ locality ไม่ได้มีเฉพาะในโมเดลเอกสาร ตัวอย่างเช่น Spanner ของ Google มี locality แบบเดียวกันในโมเดลข้อมูลเชิงสัมพันธ์ โดยให้ schema ประกาศได้ว่าแถวของตารางหนึ่งควรถูก interleave (สอดแทรก) อยู่ภายในตารางแม่ [ 27 ] Oracle ก็ทำได้เหมือนกันด้วยฟีเจอร์ที่เรียกว่า multi-table index cluster tables [ 28 ] โมเดลข้อมูลแบบ wide-column ที่ Bigtable ของ Google ทำให้เป็นที่นิยม และใช้ใน HBase และ Accumulo เป็นต้น มี column family ซึ่งมีจุดประสงค์คล้ายกันคือจัดการ locality [ 29 ]

Query languages for documents (ภาษาในการสืบค้นสำหรับเอกสาร)

อีกข้อแตกต่างระหว่างฐานข้อมูลเชิงสัมพันธ์กับฐานข้อมูลเอกสารคือภาษาหรือ API ที่ใช้สืบค้น ฐานข้อมูลเชิงสัมพันธ์ส่วนใหญ่ใช้ SQL แต่ฐานข้อมูลเอกสารหลากหลายกว่า บางตัวให้เข้าถึงได้แค่แบบ key-value ด้วย primary key บางตัวมี secondary index สำหรับค้นหาค่าภายในเอกสาร และบางตัวมีภาษาสืบค้นที่ครบครัน

ฐานข้อมูล XML มักถูกสืบค้นด้วย XQuery และ XPath ซึ่งออกแบบมาให้รองรับ query ที่ซับซ้อน รวมถึงการ join ข้ามหลายเอกสาร และจัดรูปแบบผลลัพธ์ เป็น XML [ 30 ] JSON Pointer [ 31 ] และ JSONPath [ 32 ] ให้ความสามารถที่เทียบเท่ากับ XPath สำหรับ JSON aggregation pipeline ของ MongoDB ซึ่งมี operator $lookup สำหรับ join ที่เราเห็นใน "Normalization, Denormalization, and Joins" ก็เป็นตัวอย่างของภาษาสืบค้นสำหรับคอลเลกชันของเอกสาร JSON

ลองดูอีกตัวอย่างเพื่อทำความเข้าใจภาษานี้—คราวนี้เป็น aggregation ซึ่งจำเป็นเป็นพิเศษสำหรับการวิเคราะห์ สมมติว่าคุณเป็นนักชีววิทยาทางทะเล และเพิ่ม record การสังเกตลงฐานข้อมูลทุกครั้งที่เห็นสัตว์ในมหาสมุทร ตอนนี้คุณอยากทำรายงานว่าพบฉลามกี่ตัวต่อเดือน ใน PostgreSQL คุณอาจเขียน query นั้นได้ดังนี้:

SELECT date_trunc('month', observation_timestamp) AS observation_month, 
       sum(num_animals) AS total_animals
FROM observations
WHERE family = 'Sharks'
GROUP BY observation_month;

1

ฟังก์ชัน date_trunc('month', observation_timestamp) หาเดือนในปฏิทินที่มี timestamp แล้วคืน timestamp อีกค่าที่เป็นจุดเริ่มต้นของเดือนนั้น พูดอีกอย่างคือ ฟังก์ชันนี้ปัด timestamp ลงเป็นต้นเดือน

query นี้กรองการสังเกตให้เหลือเฉพาะสัตว์ในตระกูล Sharks ก่อน จากนั้นจัดกลุ่มการสังเกตตามเดือนในปฏิทินที่เกิดขึ้น และสุดท้ายรวมจำนวนสัตว์ที่เห็นในทุกการสังเกตของเดือนนั้น query เดียวกันนี้เขียนด้วย MongoDB aggregation pipeline ได้ดังนี้:

db.observations.aggregate([
    { $match: { family: "Sharks" } },
    { $group: {
        _id: {
            year:  { $year:  "$observationTimestamp" },
            month: { $month: "$observationTimestamp" }
        },
        totalAnimals: { $sum: "$numAnimals" }
    } }
]);

ภาษา aggregation pipeline แสดงออกได้ในระดับใกล้เคียงกับ subset หนึ่งของ SQL แต่ใช้ syntax แบบ JSON แทน syntax ที่คล้ายประโยคภาษาอังกฤษของ SQL ความแตกต่างนี้อาจเป็นเรื่องของรสนิยม

Convergence of document and relational databases (การบรรจบกันของฐานข้อมูลเอกสารและเชิงสัมพันธ์)

ฐานข้อมูลเอกสารกับฐานข้อมูลเชิงสัมพันธ์เริ่มต้นจากแนวทางจัดการข้อมูลที่ต่างกันมาก แต่เมื่อเวลาผ่านไปก็คล้ายกันมากขึ้น [ 33 ] ฐานข้อมูลเชิงสัมพันธ์เพิ่มการรองรับชนิดข้อมูล JSON ตัว operator สำหรับ query และความสามารถในการ index คุณสมบัติภายในเอกสาร ส่วนฐานข้อมูลเอกสารบางตัว (เช่น MongoDB, Couchbase และ RethinkDB) ก็เพิ่มการรองรับ join, secondary index และภาษาสืบค้นแบบ declarative

การที่โมเดลทั้งสองมาบรรจบกันเป็นข่าวดีสำหรับนักพัฒนาแอปพลิเคชัน เพราะโมเดลเชิงสัมพันธ์และโมเดลเอกสารทำงานได้ดีที่สุดเมื่อรวมทั้งสองอย่างไว้ในฐานข้อมูลเดียวกัน ฐานข้อมูลเอกสารหลายตัวต้องใช้ reference แบบเชิงสัมพันธ์ไปยังเอกสารอื่น และฐานข้อมูลเชิงสัมพันธ์หลายตัวก็มีบางส่วนที่ schema ยืดหยุ่นจะเป็นประโยชน์ ลูกผสมระหว่าง relational กับ document จึงเป็นการผสมผสานที่ทรงพลัง

Note

คำอธิบายดั้งเดิมของโมเดลเชิงสัมพันธ์ของ Codd [ 4 ] อนุญาตให้มีสิ่งที่คล้าย JSON อยู่ภายใน schema เชิงสัมพันธ์ ซึ่งเขาเรียกว่า nonsimple domains แนวคิดคือค่าในแถวไม่จำเป็นต้องเป็นชนิดข้อมูลพื้นฐานอย่างตัวเลขหรือสตริง แต่เป็น relation (ตาราง) ที่ซ้อนอยู่ข้างในก็ได้ คุณจึงมีโครงสร้างต้นไม้ที่ซ้อนกันลึกแค่ไหนก็ได้เป็นค่า ซึ่งเทียบได้กับการรองรับ JSON และ XML ที่ถูกเพิ่มเข้าไปใน SQL กว่า 30 ปีต่อมา

Graph-Like Data Models (โมเดลข้อมูลแบบกราฟ)

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

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

กราฟประกอบด้วยออบเจ็กต์สองประเภท: vertex (หรือที่เรียกว่า node หรือ entity ) และ edge (หรือที่เรียกว่า relationship หรือ arc ) ข้อมูลหลายชนิดสร้างโมเดลเป็นกราฟได้ ตัวอย่างทั่วไปได้แก่:

Social graph

Vertex คือผู้คน และ edge ระบุว่าใครรู้จักใคร

Web graph

Vertex คือ หน้าเว็บ และ edge แทนลิงก์ HTML ไปยังหน้าอื่น

เครือข่ายถนนหรือรางรถไฟ

Vertex คือทางแยก และ edge แทนถนนหรือ เส้นทางรถไฟระหว่างพวกมัน

มี algorithm ที่รู้จักกันดีที่ทำงานบนกราฟเหล่านี้ได้—ตัวอย่างเช่น แอปแผนที่หาเส้นทางที่สั้นที่สุดระหว่างสองจุดใน เครือข่ายถนน และ PageRank ใช้กับ web graph เพื่อหาความนิยมของหน้าเว็บและกำหนดอันดับในผลการค้นหาได้ [ 34 ]

กราฟแทนได้หลายวิธี ใน โมเดล adjacency list vertex แต่ละตัวจะเก็บ ID ของ vertex ข้างเคียงที่อยู่ห่างออกไปหนึ่ง edge หรือจะใช้ adjacency matrix ซึ่งเป็นอาร์เรย์สองมิติที่แต่ละแถวและแต่ละคอลัมน์ตรงกับ vertex หนึ่งตัว โดยค่าเป็น 0 เมื่อไม่มี edge ระหว่าง vertex ของแถวกับคอลัมน์นั้น และเป็น 1 เมื่อมี edge adjacency list เหมาะกับการ traverse กราฟ ส่วน matrix เหมาะกับ machine learning (ดู "DataFrames, Matrices, and Arrays" )

ในตัวอย่างที่เพิ่งยกมา vertex ทั้งหมดในกราฟแทนสิ่งชนิดเดียวกัน (คน หน้าเว็บ หรือทางแยกถนน ตามลำดับ) อย่างไรก็ตาม กราฟไม่ได้จำกัดอยู่แค่ ข้อมูล homogeneous เช่นนั้น การใช้กราฟที่ทรงพลังไม่แพ้กันคือการให้วิธีที่สอดคล้องกันในการจัดเก็บออบเจ็กต์ประเภทที่แตกต่างกันโดยสิ้นเชิงในฐานข้อมูลเดียว ตัวอย่างเช่น:

  • Facebook รักษากราฟเดียวที่มี vertex และ edge หลายประเภท Vertex แทนผู้คน สถานที่ เหตุการณ์ check-in และความคิดเห็นที่ผู้ใช้ทำ Edge ระบุว่าใครเป็นเพื่อนกับใคร check-in ไหนเกิดขึ้นที่ไหน ใครแสดงความคิดเห็นในโพสต์ไหน ใครเข้าร่วมเหตุการณ์ไหน และอื่นๆ [ 35 ]

  • Search Engine ใช้ knowledge graph เพื่อบันทึกข้อเท็จจริงเกี่ยวกับ entity ที่มักปรากฏใน query ค้นหา เช่น องค์กร ผู้คน และสถานที่ [ 36 ] ข้อมูลนี้ได้มาจากการ crawl และวิเคราะห์ข้อความบนเว็บไซต์ บางเว็บไซต์ เช่น Wikidata ยังเผยแพร่ข้อมูลกราฟในรูปแบบที่มีโครงสร้าง

กราฟมีหลาย วิธีที่แตกต่างแต่เกี่ยวข้องกันในการจัดโครงสร้างและสืบค้นข้อมูล ในส่วนนี้เราจะพูดถึง property graph model (implemented โดย Neo4j, Memgraph, KùzuDB [ 37 ] และอื่นๆ [ 38 ]) และ triple store model (implemented โดย Datomic, AllegroGraph, Blazegraph และอื่นๆ) โมเดลเหล่านี้ค่อนข้างคล้ายคลึงกันในสิ่งที่สามารถแสดงได้ และฐานข้อมูลกราฟบางแห่ง (เช่น Amazon Neptune) รองรับทั้งสองแบบ

เรายังจะดูภาษาสืบค้นสี่ภาษาสำหรับ กราฟ (Cypher, SPARQL, Datalog และ GraphQL) รวมถึงการรองรับ SQL สำหรับการสืบค้นกราฟ ภาษาการสืบค้นกราฟอื่นๆ ก็มีอยู่ เช่น Gremlin [ 39 ] แต่ภาษาที่เลือกมานี้ก็ให้ภาพรวมที่เป็นตัวแทนได้

เพื่ออธิบายภาษาและโมเดลเหล่านี้ ส่วนนี้ใช้ รูปที่ 3-6 เป็นตัวอย่างหลักที่ใช้ต่อเนื่อง ซึ่งอาจมาจากเครือข่ายสังคมหรือฐานข้อมูลลำดับวงศ์ตระกูล มันแสดงคนสองคน Lucy จาก Idaho และ Alain จาก Saint-Lô ประเทศฝรั่งเศส พวกเขาแต่งงานกันและอาศัยอยู่ในลอนดอน แต่ละคนและแต่ละสถานที่แสดงเป็น vertex และความสัมพันธ์ระหว่างพวกเขาแสดงเป็น edge ตัวอย่างนี้จะช่วยสาธิต query ที่ง่ายในฐานข้อมูลกราฟแต่ยากในโมเดลข้อมูลอื่น

แผนภาพของข้อมูลแบบกราฟที่แสดงความสัมพันธ์ระหว่างคนสองคน Lucy และ Alain พร้อมกับสถานที่ต่างๆ เช่น Idaho และ Saint-Lô ที่เชื่อมต่อผ่าน vertex และ edge แสดงการเชื่อมต่อทางภูมิศาสตร์และส่วนบุคคล

รูปที่ 3-6. ข้อมูลแบบกราฟ (กล่องแทน vertex ลูกศรแทน edge)

Property Graphs (กราฟคุณสมบัติ)

ในโมเดล property graph (หรือที่เรียกว่า labeled property graph ) แต่ละ vertex ประกอบด้วย:

  • ตัวระบุที่ไม่ซ้ำกัน

  • label (ข้อความ) เพื่ออธิบายประเภทของออบเจ็กต์ที่ vertex นี้แทน

  • ชุดของ edge ขาออก

  • ชุดของ edge ขาเข้า

  • ชุดของคุณสมบัติ (คู่ key-value)

แต่ละ edge ประกอบด้วย:

  • ตัวระบุที่ไม่ซ้ำกัน

  • vertex ที่ edge เริ่มต้น (เรียกว่า tail vertex )

  • vertex ที่ edge สิ้นสุด (เรียกว่า head vertex )

  • label เพื่ออธิบายชนิดของความสัมพันธ์ระหว่าง vertex ทั้งสอง

  • ชุดของคุณสมบัติ (คู่ key-value)

คุณอาจมอง graph store ว่าประกอบด้วยตารางเชิงสัมพันธ์สองตาราง ตารางหนึ่งสำหรับ vertex และอีกตารางสำหรับ edge ดังที่แสดงใน ตัวอย่างที่ 3-3 (schema นี้ใช้ชนิดข้อมูล jsonb ของ PostgreSQL เพื่อจัดเก็บคุณสมบัติของแต่ละ vertex หรือ edge) head และ tail vertex ถูกจัดเก็บสำหรับแต่ละ edge ถ้าต้องการชุดของ edge ขาเข้าหรือขาออกของ vertex ก็ query ตาราง edges ด้วย head_vertex หรือ tail_vertex ตามลำดับ

ตัวอย่างที่ 3-3. การแทน property graph ด้วย schema เชิงสัมพันธ์
CREATE TABLE vertices (
    vertex_id   integer PRIMARY KEY,
    label       text,
    properties  jsonb
);

CREATE TABLE edges (
    edge_id     integer PRIMARY KEY,
    tail_vertex integer REFERENCES vertices (vertex_id),
    head_vertex integer REFERENCES vertices (vertex_id),
    label       text,
    properties  jsonb
);

CREATE INDEX edges_tails ON edges (tail_vertex);
CREATE INDEX edges_heads ON edges (head_vertex);

บางแง่มุมสำคัญของโมเดลนี้มีดังนี้:

  • vertex ใดๆ ก็สามารถมี edge เชื่อมต่อกับ vertex อื่นใดก็ได้ ไม่มี schema ที่จำกัดว่าสิ่งใดสามารถหรือไม่สามารถเชื่อมโยงกันได้

  • สำหรับ vertex ใดๆ คุณสามารถค้นหาทั้ง edge ขาเข้าและขาออกได้อย่างมีประสิทธิภาพ และจึงสามารถ traverse กราฟ (เช่น เดินตามเส้นทางผ่าน chain ของ vertex) ทั้งไปข้างหน้าและข้างหลัง (นั่นคือสาเหตุที่ ตัวอย่างที่ 3-3 มี index บนทั้งคอลัมน์ tail_vertex และ head_vertex )

  • โดยการใช้ label ที่แตกต่างกันสำหรับ vertex และความสัมพันธ์ประเภทต่างๆ คุณสามารถจัดเก็บข้อมูลหลายชนิดในกราฟเดียว ในขณะที่ยังคงรักษาโมเดลข้อมูลที่สะอาด

ตาราง edges ก็เหมือน associative table หรือ join table แบบหลายต่อหลายที่เราเห็นใน "Many-to-One and Many-to-Many Relationships" แต่ขยายให้เก็บความสัมพันธ์ได้หลายประเภทในตารางเดียวกัน นอกจากนี้ยังอาจมี index บน label และ property ต่างๆ เพื่อให้ค้นหา vertex หรือ edge ที่มี property บางอย่างได้อย่างมีประสิทธิภาพ

Note

ข้อจำกัดของโมเดลกราฟคือ edge เชื่อม vertex ได้เพียงสองตัว ในขณะที่ join table เชิงสัมพันธ์แสดงความสัมพันธ์แบบสามทางหรือมากกว่านั้นได้ โดยมี foreign key หลายตัวในแถวเดียว ความสัมพันธ์แบบนั้นแสดงในกราฟได้โดยสร้าง vertex เพิ่มที่ตรงกับแต่ละแถวของ join table พร้อม edge ไปยังและมาจาก vertex นั้น หรือใช้ hypergraph

คุณสมบัติเหล่านั้นทำให้กราฟยืดหยุ่นมากสำหรับการสร้างโมเดลข้อมูล ดังที่แสดงใน รูปที่ 3-6 รูปนี้แสดงบางอย่างที่น่าจะแสดงใน schema เชิงสัมพันธ์แบบดั้งเดิมได้ยาก เช่น โครงสร้างภูมิภาคที่แตกต่างกันในประเทศต่างๆ (ฝรั่งเศสมี départements และ régions ในขณะที่สหรัฐฯ มี counties และ states ) ความแปลกประหลาดทางประวัติศาสตร์ เช่น ประเทศภายในประเทศ (โดยยังไม่พูดถึงความซับซ้อนของรัฐอธิปไตยและชาติ) และความละเอียดของข้อมูลที่แตกต่างกัน (ที่อยู่ปัจจุบันของ Lucy ระบุเป็นเมือง ในขณะที่สถานที่เกิดของเธอระบุแค่ระดับรัฐ)

คุณอาจจินตนาการถึงการขยายกราฟเพื่อรวมข้อเท็จจริงอื่นๆ มากมายเกี่ยวกับ Lucy และ Alain หรือคนอื่นๆ ตัวอย่างเช่น คุณสามารถใช้กราฟเพื่อระบุอาการแพ้อาหารที่พวกเขามี (โดยการเพิ่ม vertex สำหรับสารก่อภูมิแพ้แต่ละชนิด และ edge ระหว่างบุคคลกับสารก่อภูมิแพ้เพื่อระบุอาการแพ้) และเชื่อมโยงสารก่อภูมิแพ้กับชุดของ vertex ที่แสดงว่าอาหารใดมีสารใดบ้าง จากนั้นคุณสามารถเขียน query เพื่อค้นหาว่าอะไรที่ปลอดภัยสำหรับแต่ละคนในการรับประทาน กราฟเหมาะกับ evolvability (ความสามารถในการปรับเปลี่ยนต่อยอด): เมื่อคุณเพิ่มฟีเจอร์ให้แอปพลิเคชัน ก็ขยายกราฟเพื่อรองรับการเปลี่ยนแปลงในโครงสร้างข้อมูลของแอปพลิเคชันได้ง่าย

The Cypher Query Language (ภาษาในการสืบค้น Cypher)

Cypher เป็นภาษาการสืบค้นสำหรับ property graph ที่เดิมสร้างขึ้นสำหรับฐานข้อมูลกราฟ Neo4j และต่อมาพัฒนาเป็นมาตรฐานเปิด openCypher [ 40 ] นอกจาก Neo4j แล้ว Cypher ยังรองรับโดย Memgraph, KùzuDB [ 37 ], Amazon Neptune, Apache AGE (พร้อมจัดเก็บใน PostgreSQL) และอื่นๆ ภาษานี้ตั้งชื่อตามตัวละครในภาพยนตร์ The Matrix และไม่เกี่ยวข้องกับ cipher ในการเข้ารหัส [ 41 ]

ตัวอย่างที่ 3-4 แสดง query Cypher เพื่อแทรกส่วนซ้ายมือของ รูปที่ 3-6 ลงในฐานข้อมูลกราฟ ส่วนที่เหลือของกราฟสามารถเพิ่มได้ในทำนองเดียวกัน แต่ละ vertex จะได้รับชื่อเชิงสัญลักษณ์ เช่น usa หรือ idaho ชื่อนั้นไม่ได้ถูกจัดเก็บในฐานข้อมูลแต่ใช้ภายใน query เท่านั้นเพื่อสร้าง edge ระหว่าง vertex โดยใช้สัญกรณ์ลูกศร: (idaho) -[:WITHIN]-> (usa) สร้าง edge ที่มี label WITHIN โดยมี idaho เป็น tail node และ usa เป็น head node

ตัวอย่างที่ 3-4. ชุดย่อยของข้อมูลใน รูปที่ 3-6 แสดงเป็น query Cypher
CREATE
  (namerica :Location {name:'North America',  type:'continent'}),
  (usa      :Location {name:'United States',  type:'country'  }),
  (idaho    :Location {name:'Idaho',          type:'state'    }),
  (lucy     :Person   {name:'Lucy' }),
  (idaho) -[:WITHIN ]-> (usa)  -[:WITHIN]-> (namerica),
  (lucy)  -[:BORN_IN]-> (idaho)

เมื่อ vertex และ edge ทั้งหมดของ รูปที่ 3-6 ถูกเพิ่มลงในฐานข้อมูลแล้ว เราสามารถเริ่มถามคำถามที่น่าสนใจได้ ตัวอย่างเช่น สมมติว่าเราต้องการหาชื่อของทุกคนที่อพยพจากสหรัฐอเมริกาไปยุโรป เราสามารถทำได้โดยการค้นหา vertex ทั้งหมดที่มี edge BORN_IN ไปยังสถานที่ภายในสหรัฐฯ และ edge LIVING_IN ไปยังสถานที่ภายในยุโรป และคืนค่า property name ของ vertex เหล่านั้น

ตัวอย่างที่ 3-5 แสดงวิธีเขียน query นั้นใน Cypher สัญกรณ์ลูกศรเดียวกันถูกใช้ใน clause MATCH เพื่อค้นหารูปแบบในกราฟ: (person) -[:BORN_IN]-> () จับคู่ vertex สองตัวใดๆ ที่เชื่อมต่อกันด้วย edge ที่มี label BORN_IN tail vertex ของ edge นั้นถูกผูกกับตัวแปร person และ head vertex ถูกปล่อยให้ไม่มีชื่อ

ตัวอย่างที่ 3-5. Query Cypher เพื่อค้นหาผู้คนที่อพยพจากสหรัฐฯ ไปยุโรป
MATCH
  (person) -[:BORN_IN]->  () -[:WITHIN*0..]-> (:Location {name:'United States'}),
  (person) -[:LIVES_IN]-> () -[:WITHIN*0..]-> (:Location {name:'Europe'})
RETURN person.name

query สามารถอ่านได้ดังนี้:

ค้นหา vertex ใดๆ (เรียกมันว่า person ) ที่ตรงตาม ทั้งสอง เงื่อนไขต่อไปนี้:

  1. person มี edge ขาออก BORN_IN ไปยัง vertex หนึ่ง จาก vertex นั้น คุณสามารถเดินตาม chain ของ edge ขาออก WITHIN จนกระทั่งถึง vertex ชนิด Location ที่มี property name เท่ากับ United States

  2. vertex person เดียวกันนั้นยังมี edge ขาออก LIVES_IN การเดินตาม edge นั้น และตาม chain ของ edge ขาออก WITHIN ในที่สุดคุณก็จะถึง vertex ชนิด Location ที่มี property name เท่ากับ Europe

สำหรับแต่ละ vertex person ดังกล่าว ให้คืนค่า property name

มีหลายวิธีที่เป็นไปได้ในการประมวลผล query คำอธิบายที่ให้ไว้ที่นี่แนะนำว่าคุณเริ่มต้นด้วยการสแกนทุกคนในฐานข้อมูล ตรวจสอบสถานที่เกิดและที่อยู่อาศัยของแต่ละคน และคืนเฉพาะคนที่ตรงตามเกณฑ์

แต่ก็เทียบเท่ากันถ้าคุณจะเริ่มด้วย vertex Location สองแห่งแล้วทำงานย้อนกลับ ถ้ามี index บน property name คุณสามารถค้นหา vertex สองแห่งที่แทนสหรัฐฯ และยุโรปได้อย่างมีประสิทธิภาพ จากนั้นคุณก็หาสถานที่ทั้งหมด (รัฐ ภูมิภาค เมือง ฯลฯ) ในสหรัฐฯ และยุโรปตามลำดับได้ โดยเดินตาม edge ขาเข้า WITHIN ทั้งหมด สุดท้ายก็มองหาคนที่เจอได้ผ่าน edge ขาเข้า BORN_IN หรือ LIVES_IN ที่ vertex ของสถานที่แห่งใดแห่งหนึ่ง

Graph Queries in SQL (การสืบค้นกราฟใน SQL)

ตัวอย่างที่ 3-3 ชี้ให้เห็นว่าแทนข้อมูลกราฟในฐานข้อมูลเชิงสัมพันธ์ได้ แต่ถ้าเราใส่ข้อมูลกราฟในโครงสร้างเชิงสัมพันธ์แล้ว จะ query ด้วย SQL ได้ไหม?

คำตอบคือได้ แต่ด้วยความยากลำบาก ทุก edge ที่ traverse ใน query กราฟก็เท่ากับเป็น join กับตาราง edges ในฐานข้อมูลเชิงสัมพันธ์ คุณมักจะรู้ล่วงหน้าว่าต้องใช้ join ใดใน query ในทางกลับกัน ใน query กราฟ คุณอาจต้อง traverse จำนวน edge ที่แปรผันก่อนที่จะพบ vertex ที่คุณกำลังมองหา—นั่นคือ จำนวน join ไม่ได้ถูกกำหนดล่วงหน้า

ในตัวอย่างของเรา สิ่งนั้นเกิดขึ้นในรูปแบบ () -[:WITHIN*0..]-> () ใน query Cypher edge LIVES_IN ของบุคคลอาจชี้ไปยังสถานที่ประเภทใดก็ได้ เช่น ถนน เมือง เขต ภูมิภาค หรือรัฐ เมืองอาจอยู่ WITHIN ภูมิภาค ภูมิภาค WITHIN รัฐ รัฐ WITHIN ประเทศ และอื่นๆ edge LIVES_IN อาจชี้ไปยัง vertex สถานที่ที่คุณกำลังมองหาโดยตรง หรืออาจห่างออกไปหลายระดับในลำดับชั้นของสถานที่

ใน Cypher :WITHIN*0.. แสดงข้อเท็จจริงนั้นอย่างกระชับมาก: มันหมายถึง "เดินตาม edge WITHIN , ศูนย์ครั้งหรือมากกว่า" มันเหมือนกับ operator * ใน regular expression

แนวคิดของเส้นทางการ traversal ที่มีความยาวแปรผันใน query นี้สามารถแสดงโดยใช้ recursive common table expression (syntax WITH RECURSIVE ) ตัวอย่างที่ 3-6 แสดง query เดียวกัน—การค้นหาชื่อคนที่อพยพจากสหรัฐฯ ไปยุโรป—ใน SQL โดยใช้เทคนิคนี้ (บรรทัดที่ขึ้นต้นด้วย -- คือ comment) อย่างที่เห็น syntax ยุ่งยากมากเมื่อเทียบกับ Cypher

ตัวอย่างที่ 3-6. Query เดียวกับ ตัวอย่างที่ 3-5 เขียนใน SQL โดยใช้ recursive common table expression
WITH RECURSIVE

  -- in_usa is the set of vertex IDs of all locations within the United States
  in_usa(vertex_id) AS (
      SELECT vertex_id FROM vertices
        WHERE label = 'Location' AND properties->>'name' = 'United States' 
    UNION
      SELECT edges.tail_vertex FROM edges 
        JOIN in_usa ON edges.head_vertex = in_usa.vertex_id
        WHERE edges.label = 'within'
  ),

  -- in_europe is the set of vertex IDs of all locations within Europe
  in_europe(vertex_id) AS (
      SELECT vertex_id FROM vertices
        WHERE label = 'location' AND properties->>'name' = 'Europe' 
    UNION
      SELECT edges.tail_vertex FROM edges
        JOIN in_europe ON edges.head_vertex = in_europe.vertex_id
        WHERE edges.label = 'within'
  ),

  -- born_in_usa is the set of vertex IDs of all people born in the US
  born_in_usa(vertex_id) AS ( 
    SELECT edges.tail_vertex FROM edges
      JOIN in_usa ON edges.head_vertex = in_usa.vertex_id
      WHERE edges.label = 'born_in'
  ),

  -- lives_in_europe is the set of vertex IDs of all people living in Europe
  lives_in_europe(vertex_id) AS ( 
    SELECT edges.tail_vertex FROM edges
      JOIN in_europe ON edges.head_vertex = in_europe.vertex_id
      WHERE edges.label = 'lives_in'
  )

SELECT vertices.properties->>'name'
FROM vertices
-- join to find those people who were both born in the US *and* live in Europe
JOIN born_in_usa     ON vertices.vertex_id = born_in_usa.vertex_id 
JOIN lives_in_europe ON vertices.vertex_id = lives_in_europe.vertex_id;

1

ค้นหา vertex ที่มี property name มีค่า United States และทำให้มันเป็น element แรกของเซต vertex in_usa

2

เดินตาม edge ขาเข้า within ทั้งหมดจาก vertex ในเซต in_usa และเพิ่มพวกมันเข้าไปในเซตเดียวกัน จนกว่า edge ขาเข้า within ทั้งหมดถูกเยี่ยมชมแล้ว

3

ทำแบบเดียวกันโดยเริ่มจาก vertex ที่มี property name มีค่า Europe และสร้างเซต vertex in_europe

4

สำหรับแต่ละ vertex ในเซต in_usa ให้เดินตาม edge ขาเข้า born_in เพื่อค้นหาคนที่เกิดในสถานที่ภายในสหรัฐฯ

5

ในทำนองเดียวกัน สำหรับแต่ละ vertex ในเซต in_europe ให้เดินตาม edge ขาเข้า lives_in เพื่อค้นหาคนที่อาศัยอยู่ในยุโรป

6

สุดท้าย ทำ intersection ระหว่างเซตของคนที่เกิดในสหรัฐฯ กับเซตของคนที่อาศัยอยู่ในยุโรปโดยการ join พวกมัน

ข้อเท็จจริงที่ว่า query Cypher 4 บรรทัดต้องใช้ 31 บรรทัดใน SQL แสดงให้เห็นว่าการเลือกโมเดลข้อมูลและภาษาการสืบค้นที่เหมาะสมนั้นสร้างความแตกต่างได้มากเพียงใด และนี่เป็นเพียงจุดเริ่มต้น ยังมีรายละเอียดอื่นๆ ที่ต้องพิจารณา เช่น การจัดการ cycles และการเลือกระหว่างการ traversal แบบ breadth-first หรือ depth-first [ 42 ] Oracle มี SQL extension ที่แตกต่างสำหรับ recursive query ซึ่งเรียกว่า hierarchical [ 43 ] ภาษาการสืบค้นกราฟอื่นๆ รวมถึง GSQL ของ TigerGraph [ 44 ] และ Property Graph Query Language (PGQL) [ 45 ]

มาตรฐาน ISO Graph Query Language (GQL) ซึ่งอิงจาก Cypher ถูกเผยแพร่ในปี 2024 [ 46 , 47 , 48 ] แม้ตอนนี้ยังไม่ถูกนำไปใช้อย่างแพร่หลาย แต่หวังว่าจะช่วยให้ฐานข้อมูลกราฟมีความสอดคล้องกันมากขึ้นในอีกไม่กี่ปีข้างหน้า

Triple Stores and SPARQL

โมเดล triple store ส่วนใหญ่เทียบเท่ากับโมเดล property graph โดยใช้คำเรียกต่างกันเพื่ออธิบายแนวคิดเดียวกัน แต่ก็ยังน่าพูดถึง เพราะเครื่องมือและภาษาสำหรับ triple store อาจเป็นส่วนเสริมที่มีค่าให้ชุดเครื่องมือที่คุณใช้สร้างแอปพลิเคชัน

ใน triple store ข้อมูลทั้งหมดถูกจัดเก็บในรูปของคำสั่งสามส่วนที่ง่ายมาก: ( subject , predicate , object ) ตัวอย่างเช่น ใน triple ( Jim , likes , bananas ), Jim คือ subject, likes คือ predicate (กริยา) และ bananas คือ object

Note

เพื่อความแม่นยำ ฐานข้อมูลที่มีโมเดลข้อมูลแบบ triple มักต้องจัดเก็บ metadata เพิ่มเติมบนแต่ละ tuple ตัวอย่างเช่น AWS Neptune ใช้ quad (4-tuple) โดยการเพิ่ม graph ID ให้กับแต่ละ triple [ 49 ]; Datomic ใช้ 5-tuple โดยขยายแต่ละ triple ด้วย transaction ID และ Boolean เพื่อบ่งชี้การลบ [ 50 ] เนื่องจากฐานข้อมูลเหล่านี้ยังคงรักษาโครงสร้างพื้นฐาน subject-predicate-object ที่อธิบายไว้ที่นี่ หนังสือเล่มนี้จึงยังคงเรียกพวกมันว่า triple store

subject ของ triple เทียบเท่ากับ vertex ในกราฟ object คือหนึ่งในสองสิ่ง:

  • ค่าของชนิดข้อมูลพื้นฐาน เช่น ข้อความหรือตัวเลข ในกรณีนั้น predicate และ object ของ triple จะเทียบเท่ากับ key และ value ของ property บน subject vertex โดยใช้ตัวอย่างจาก รูปที่ 3-6 , ( lucy , birthYear , 1989 ) ก็เหมือน vertex lucy ที่มี properties {"birthYear": 1989}

  • vertex อีกตัวในกราฟ ในกรณีนั้น predicate คือ edge ในกราฟ subject คือ tail vertex และ object คือ head vertex ตัวอย่างเช่น ใน ( lucy , marriedTo , alain ), subject และ object lucy และ alain เป็น vertex ทั้งคู่ และ predicate marriedTo คือ label ของ edge ที่เชื่อมต่อพวกมัน

ตัวอย่างที่ 3-7 แสดงข้อมูลเดียวกันกับ ตัวอย่างที่ 3-4 ที่เขียนเป็น triple ในรูปแบบที่เรียกว่า Turtle ซึ่งเป็น subset ของ Notation3 ( N3 ) [ 51 ]

ตัวอย่างที่ 3-7. ชุดย่อยของข้อมูลใน รูปที่ 3-6 แสดงเป็น Turtle triples
@prefix : <urn:example:>.
_:lucy     a       :Person.
_:lucy     :name   "Lucy".
_:lucy     :bornIn _:idaho.
_:idaho    a       :Location.
_:idaho    :name   "Idaho".
_:idaho    :type   "state".
_:idaho    :within _:usa.
_:usa      a       :Location.
_:usa      :name   "United States".
_:usa      :type   "country".
_:usa      :within _:namerica.
_:namerica a       :Location.
_:namerica :name   "North America".
_:namerica :type   "continent".

ในตัวอย่างนี้ vertex ของกราฟถูกเขียนเป็น _: _someName_ ชื่อไม่มีความหมายอะไรนอกไฟล์นี้ มันมีอยู่เพียงเพราะไม่อย่างนั้นเราจะไม่รู้ว่า triple ใดอ้างถึง vertex เดียวกัน เมื่อ predicate แทน edge object คือ vertex ดังใน _:idaho :within _:usa เมื่อ predicate เป็น property object คือ literal ข้อความ ดังใน _:usa :name "United States"

เพื่อให้กระชับขึ้น คุณใช้เครื่องหมาย semicolon เพื่อบอกหลายอย่างเกี่ยวกับ subject เดียวกันได้ ดังแสดงใน ตัวอย่างที่ 3-8 วิธีนี้ทำให้รูปแบบ Turtle อ่านง่ายพอสมควร

ตัวอย่างที่ 3-8. วิธีที่กระชับยิ่งขึ้นในการเขียนข้อมูลใน ตัวอย่างที่ 3-7
@prefix : <urn:example:>.
_:lucy     a :Person;   :name "Lucy";          :bornIn _:idaho.
_:idaho    a :Location; :name "Idaho";         :type "state";   :within _:usa.
_:usa      a :Location; :name "United States"; :type "country"; :within _:namerica.
_:namerica a :Location; :name "North America"; :type "continent".

The Semantic Web (เว็บเชิงความหมาย)

งานวิจัยและพัฒนาเกี่ยวกับ triple store บางส่วนเกิดจากแรงจูงใจของ Semantic Web ซึ่งเป็นความพยายามในช่วงต้นทศวรรษ 2000 ที่จะทำให้แลกเปลี่ยนข้อมูลข้ามอินเทอร์เน็ตได้ง่ายขึ้น โดยเผยแพร่ข้อมูลไม่เพียงเป็นหน้าเว็บที่มนุษย์อ่านได้ แต่ยังอยู่ในรูปแบบมาตรฐานที่เครื่องอ่านได้ด้วย แม้ว่า Semantic Web ตามที่วาดฝันไว้แต่แรกจะไม่ประสบความสำเร็จ [ 52 , 53 ] แต่มรดกของโครงการยังคงอยู่ในหลายสิ่ง เช่น มาตรฐาน linked data อย่าง JSON-LD [ 54 ], ontology ที่ใช้ในวิทยาศาสตร์ชีวการแพทย์ [ 55 ], Open Graph protocol ของ Facebook [ 56 ] (ซึ่งใช้สำหรับ link unfurling [ 57 ]), knowledge graph เช่น Wikidata และคำศัพท์มาตรฐานสำหรับ structured data ที่ดูแลโดย Schema.org

Triple store เป็นอีกเทคโนโลยี Semantic Web ที่ถูกนำไปใช้นอกเหนือกรณีใช้งานดั้งเดิม แม้คุณจะไม่สนใจ Semantic Web เลย triple ก็ยังอาจเป็นโมเดลข้อมูลภายในที่ดีสำหรับแอปพลิเคชัน

The RDF data model (โมเดลข้อมูล RDF)

ภาษา Turtle ที่เราใช้ใน ตัวอย่างที่ 3-8 จริงๆ แล้วเป็นวิธีเข้ารหัสข้อมูลใน Resource Description Framework (RDF) [ 58 ] ซึ่งเป็นโมเดลข้อมูลที่ออกแบบมาสำหรับ Semantic Web ข้อมูล RDF เข้ารหัสด้วยวิธีอื่นได้ด้วย รวมถึง XML ซึ่ง verbose กว่า ดังที่แสดงใน ตัวอย่างที่ 3-9 เครื่องมืออย่าง Apache Jena แปลงระหว่างรูปแบบการเข้ารหัส RDF ต่างๆ ให้อัตโนมัติได้

ตัวอย่างที่ 3-9. ข้อมูลจาก ตัวอย่างที่ 3-8 ที่แสดงโดยใช้ RDF/XML syntax
<rdf:RDF xmlns="urn:example:"
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

  <Location rdf:nodeID="idaho">
    <name>Idaho</name>
    <type>state</type>
    <within>
      <Location rdf:nodeID="usa">
        <name>United States</name>
        <type>country</type>
        <within>
          <Location rdf:nodeID="namerica">
            <name>North America</name>
            <type>continent</type>
          </Location>
        </within>
      </Location>
    </within>
  </Location>

  <Person rdf:nodeID="lucy">
    <name>Lucy</name>
    <bornIn rdf:nodeID="idaho"/>
  </Person>
</rdf:RDF>

RDF มีจุดแปลกๆ อยู่บ้างเพราะออกแบบมาสำหรับแลกเปลี่ยนข้อมูลทั่วอินเทอร์เน็ต subject, predicate และ object ของ triple มักเป็น URI ตัวอย่างเช่น predicate อาจเป็น URI เช่น <http://my-company.com/namespace#within> หรือ <http://my-company.com/namespace#lives_in> แทนที่จะเป็นแค่ WITHIN หรือ LIVES_IN เหตุผลเบื้องหลังการออกแบบนี้คือ คุณควรรวมข้อมูลของคุณกับข้อมูลของคนอื่นได้ และถ้าเขาให้ความหมายต่างออกไปกับคำว่า within หรือ lives_in ก็จะไม่เกิดความขัดแย้ง เพราะที่จริง predicate ของเขาคือ <http://other.org/foo#within> และ <http://other.org/foo#lives_in>

URL <http://my-company.com/namespace> ไม่จำเป็นต้อง resolve ไปยังอะไร—จากมุมมองของ RDF มันก็แค่ namespace เพื่อหลีกเลี่ยงความสับสนที่อาจเกิดขึ้นกับ URL http:// ตัวอย่างในส่วนนี้ใช้ URI ที่ไม่สามารถ resolve ได้ เช่น urn:example:within โชคดีที่คุณสามารถระบุ prefix นี้ครั้งเดียวที่ด้านบนของไฟล์แล้วลืมมันไปได้

The SPARQL query language (ภาษาในการสืบค้น SPARQL)

SPARQL เป็นภาษาการสืบค้นสำหรับ triple store ที่ใช้โมเดลข้อมูล RDF [ 59 ] (ชื่อเป็น recursive acronym สำหรับ SPARQL Protocol and RDF Query Language , อ่านว่า "sparkle") SPARQL เกิดก่อน Cypher และเนื่องจาก pattern matching ของ Cypher ยืมมาจาก SPARQL สองภาษานี้จึงดูคล้ายกันพอสมควร

query เดียวกับก่อนหน้านี้—การค้นหาคนที่ย้ายจากสหรัฐฯ ไปยุโรป—นั้นกระชับใน SPARQL พอๆ กับใน Cypher (ดู ตัวอย่างที่ 3-10 )

ตัวอย่างที่ 3-10. Query เดียวกับ ตัวอย่างที่ 3-5 ที่แสดงใน SPARQL
PREFIX : <urn:example:>

SELECT ?personName WHERE {
  ?person :name ?personName.
  ?person :bornIn  / :within* / :name "United States".
  ?person :livesIn / :within* / :name "Europe".
}

โครงสร้างคล้ายกันมาก สอง expression ต่อไปนี้เทียบเท่ากัน (ตัวแปรขึ้นต้นด้วยเครื่องหมายคำถามใน SPARQL):

(person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (location)   # Cypher

?person :bornIn / :within* ?location.                   # SPARQL

เนื่องจาก RDF ไม่แยก property กับ edge แต่ใช้ predicate กับทั้งสองอย่าง คุณจึงใช้ syntax เดียวกันในการ match property ได้ ใน expression ต่อไปนี้ ตัวแปร usa ถูกผูกกับ vertex ใดๆ ที่มี property name ที่มีค่าเป็น string United States :

(usa {name:'United States'})   # Cypher

?usa :name "United States".    # SPARQL

SPARQL รองรับโดย Amazon Neptune, AllegroGraph, Blazegraph, OpenLink Virtuoso, Apache Jena และ triple store อื่นๆ อีกมากมาย [ 38 ]

Datalog: Recursive Relational Queries (Datalog: การสืบค้นเชิงสัมพันธ์แบบเรียกซ้ำ)

Datalog , ซึ่งเป็นภาษาที่เก่ากว่า SPARQL หรือ Cypher มาก เกิดจากการวิจัยทางวิชาการในทศวรรษ 1980 [ 60 , 61 , 62 ] มันไม่ค่อยเป็นที่รู้จักในหมู่วิศวกรซอฟต์แวร์ และฐานข้อมูลกระแสหลักก็รองรับไม่แพร่หลาย แต่ควรเป็นที่รู้จักมากกว่านี้ เพราะเป็นภาษาที่แสดงออกได้ดีมาก และทรงพลังเป็นพิเศษสำหรับ query ที่ซับซ้อน ฐานข้อมูลเฉพาะกลุ่มหลายตัว รวมถึง Datomic, LogicBlox, CozoDB และ LIquid ของ LinkedIn [ 63 ] ใช้ Datalog เป็นภาษาการสืบค้น มัน มีพื้นฐานบนโมเดลข้อมูลเชิงสัมพันธ์ ไม่ใช่กราฟ แต่เราพูดถึงมันที่นี่เพราะ recursive query บนกราฟเป็นจุดแข็งเฉพาะตัวของ Datalog

เนื้อหาของฐานข้อมูล Datalog เรียกว่า facts (ข้อเท็จจริง) และแต่ละ fact สอดคล้องกับแถวในตารางเชิงสัมพันธ์ ตัวอย่างเช่น สมมติว่าเรามีตาราง location ที่เก็บสถานที่ และมันมีสามคอลัมน์: ID , name และ type ข้อเท็จจริงที่ว่าสหรัฐฯ เป็นประเทศสามารถเขียนได้เป็น location(2, "United States", "country") โดยที่ 2 คือ ID ของสหรัฐฯ โดยทั่วไป statement table(val1, val2, …​ ) หมายความว่า table มีแถวที่คอลัมน์แรกมีค่า val1 , คอลัมน์ที่สองมีค่า val2 และอื่นๆ

ตัวอย่างที่ 3-11 แสดงวิธีการเขียนข้อมูลจากด้านซ้ายมือของ รูปที่ 3-6 ใน Datalog edge ของกราฟ ( within , born_in และ lives_in ) แสดงเป็น join table สองคอลัมน์ ตัวอย่างเช่น Lucy มี ID 100 และ Idaho มี ID 3 ดังนั้นความสัมพันธ์ "Lucy เกิดใน Idaho" แสดงเป็น born_in(100, 3)

ตัวอย่างที่ 3-11. ชุดย่อยของข้อมูลใน รูปที่ 3-6 ที่แสดงเป็น Datalog facts
location(1, "North America", "continent").
location(2, "United States", "country").
location(3, "Idaho", "state").

within(2, 1).    /* US is in North America */
within(3, 2).    /* Idaho is in the US     */

person(100, "Lucy").
born_in(100, 3). /* Lucy was born in Idaho */

เมื่อเรากำหนดข้อมูลแล้ว เราสามารถเขียน query เดียวกันกับก่อนหน้านี้ได้ดังที่แสดงใน ตัวอย่างที่ 3-12 มันดูแตกต่างจาก Cypher หรือ SPARQL เล็กน้อย แต่อย่าปล่อยให้เรื่องนั้นทำให้คุณท้อ Datalog เป็น subset ของ Prolog ซึ่งเป็นภาษาโปรแกรมที่คุณอาจเคยเห็นมาก่อนถ้าคุณเรียนวิทยาการคอมพิวเตอร์

ตัวอย่างที่ 3-12. Query เดียวกับ ตัวอย่างที่ 3-5 ที่แสดงใน Datalog
within_recursive(LocID, PlaceName) :- location(LocID, PlaceName, _). /* Rule 1 */

within_recursive(LocID, PlaceName) :- within(LocID, ViaID),          /* Rule 2 */
                                      within_recursive(ViaID, PlaceName).

migrated(PName, BornIn, LivingIn)  :- person(PersonID, PName),       /* Rule 3 */
                                      born_in(PersonID, BornID),
                                      within_recursive(BornID, BornIn),
                                      lives_in(PersonID, LivingID),
                                      within_recursive(LivingID, LivingIn).

us_to_europe(Person) :- migrated(Person, "United States", "Europe"). /* Rule 4 */
/* us_to_europe contains the row "Lucy". */

Cypher และ SPARQL เริ่มต้นทันทีด้วย SELECT แต่ Datalog ค่อยๆ ไปทีละขั้น เรากำหนด rules (กฎ) ที่สร้างตารางเสมือนใหม่จาก facts พื้นฐาน derived table เหล่านี้เหมือนกับ SQL view (แบบ virtual): มันไม่ได้ถูกเก็บในฐานข้อมูล แต่คุณ query มันได้แบบเดียวกับตารางที่เก็บ fact

ใน ตัวอย่างที่ 3-12 เรากำหนด derived table สามตาราง: within_recursive , migrated และ us_to_europe ชื่อและคอลัมน์ของตารางเสมือนถูกกำหนดโดยสิ่งที่อยู่ก่อนสัญลักษณ์ :- ในแต่ละกฎ ตัวอย่างเช่น migrated(PName, BornIn, LivingIn) คือตารางเสมือนที่มีสามคอลัมน์: ชื่อของบุคคล, ชื่อของสถานที่ที่พวกเขาเกิด และชื่อของสถานที่ที่พวกเขาอาศัยอยู่

เนื้อหาของตารางเสมือนถูกกำหนดโดยส่วนของกฎหลังสัญลักษณ์ :- ที่เราพยายามหาแถวที่ตรงกับรูปแบบบางอย่างในตาราง ตัวอย่างเช่น person(PersonID, PName) ตรงกับแถว person(100, "Lucy") โดยที่ตัวแปร PersonID ผูกกับค่า 100 และตัวแปร PName ผูกกับค่า "Lucy" กฎจะใช้ได้ถ้าระบบสามารถหาค่าที่ตรงกันสำหรับ ทุกรูปแบบ ทางด้านขวาของ operator :- เมื่อกฎใช้ได้ ก็เหมือนกับว่าด้านซ้ายของ :- ถูกเพิ่มลงในฐานข้อมูล (โดยที่ตัวแปรถูกแทนที่ด้วยค่าที่ตรงกัน)

ดังนั้นวิธีหนึ่งที่เป็นไปได้ในการใช้กฎคือ (ดังที่แสดงใน รูปที่ 3-7 ):

  1. location(1, "North America", "continent") มีอยู่ในฐานข้อมูล ดังนั้นกฎ 1 ใช้ได้ มันสร้าง within_recursive(1, "North America")

  2. within(2, 1) มีอยู่ในฐานข้อมูล และขั้นตอนก่อนหน้านี้สร้าง within_recursive(1, "North America") ดังนั้นกฎ 2 ใช้ได้ มันสร้าง within_recursive(2, "North America")

  3. within(3, 2) มีอยู่ในฐานข้อมูล และขั้นตอนก่อนหน้านี้สร้าง within_recursive(2, "North America") ดังนั้นกฎ 2 ใช้ได้ มันสร้าง within_recursive(3, "North America")

โดยการใช้กฎ 1 และ 2 ซ้ำๆ ตารางเสมือน within_recursive บอกเราได้ว่ามีสถานที่ใดบ้างในอเมริกาเหนือ (หรือสถานที่อื่นใด) ที่อยู่ในฐานข้อมูลของเรา

แผนภาพแสดงการประยุกต์ใช้กฎแบบเรียกซ้ำเพื่อตรวจสอบว่า Idaho อยู่ในอเมริกาเหนือ โดยแสดงการเปลี่ยนแปลงในตาราง `within_recursive` ผ่านขั้นตอนต่างๆ ของการใช้กฎ

รูปที่ 3-7. การตรวจสอบว่า Idaho อยู่ในอเมริกาเหนือ โดยใช้กฎ Datalog จาก ตัวอย่างที่ 3-12

ตอนนี้กฎ 3 สามารถค้นหาคนที่เกิดในสถานที่ BornIn และอาศัยอยู่ในสถานที่ LivingIn กฎ 4 เรียกกฎ 3 ด้วย BornIn = 'United States' และ LivingIn = 'Europe' และคืนเฉพาะชื่อของคนที่ตรงกับเงื่อนไข เมื่อ query เนื้อหาของตารางเสมือน us_to_europe ในที่สุดระบบ Datalog ก็ได้คำตอบเดียวกับ query Cypher และ SPARQL ก่อนหน้านี้

แนวทางของ Datalog ต้องคิดต่างจากภาษาสืบค้นอื่นๆ ที่กล่าวถึงในบทนี้ มันให้คุณค่อยๆ สร้าง query ที่ซับซ้อนขึ้นมาทีละกฎ โดยกฎหนึ่งอ้างถึงกฎอื่นได้ คล้ายกับที่คุณแบ่งโค้ดออกเป็นฟังก์ชันที่เรียกหากัน และเหมือนกับที่ฟังก์ชันเรียกตัวเองซ้ำ (recursive) ได้ กฎ Datalog ก็เรียกตัวเองได้เช่นกัน เช่น กฎ 2 ใน ตัวอย่างที่ 3-12 ซึ่งทำให้ traverse กราฟใน query Datalog ได้

GraphQL

GraphQL เป็นภาษาสืบค้นที่ถูกออกแบบให้มีข้อจำกัดมากกว่าภาษาอื่นๆ ที่เราเห็นในบทนี้มาก มันมีไว้สำหรับ query แบบ OLTP โดยมีจุดประสงค์เพื่อให้ซอฟต์แวร์ client ที่ทำงานบนอุปกรณ์ของผู้ใช้ (เช่น แอปมือถือหรือ frontend ของเว็บแอป JavaScript) ขอเอกสาร JSON ที่มีโครงสร้างเฉพาะ ซึ่งมีฟิลด์ที่จำเป็นสำหรับแสดง UI

GraphQL ช่วยให้นักพัฒนาเปลี่ยน query ในโค้ด client ได้อย่างรวดเร็วโดยไม่ต้องแก้ API ฝั่ง server อย่างไรก็ตาม ความยืดหยุ่นนั้นมีต้นทุน องค์กรที่นำ GraphQL มาใช้มักต้องมีเครื่องมือแปลง query เป็น request ไปยัง service ภายใน ซึ่งมักใช้ REST หรือ gRPC (ดู บทที่ 5 ) เรื่อง authorization, rate limiting และความท้าทายด้านประสิทธิภาพก็เป็นข้อกังวลเพิ่มเติม [ 64 ]

ภาษานี้ถูกจำกัดโดยเจตนา เพราะ query GraphQL มาจากแหล่งที่ไม่น่าเชื่อถือ มันจึงไม่อนุญาตสิ่งใดที่อาจใช้ทรัพยากรมากในการประมวลผล ไม่เช่นนั้นผู้ใช้อาจ (โดยอาจไม่ได้ตั้งใจ) ทำให้เกิดภาวะ denial-of-service บน server ด้วยการรัน query ที่หนักจำนวนมาก โดยเฉพาะอย่างยิ่ง GraphQL ไม่อนุญาต recursive query (ต่างจาก Cypher, SPARQL, SQL หรือ Datalog) และไม่อนุญาตเงื่อนไขการค้นหาตามอำเภอใจ (เช่น "หาคนที่เกิดในสหรัฐฯ และตอนนี้อาศัยอยู่ในยุโรป") เว้นแต่เจ้าของ service จะเลือกเปิดฟังก์ชันค้นหาแบบนั้นให้โดยเฉพาะ

แม้อย่างนั้น GraphQL ก็มีประโยชน์ ตัวอย่างที่ 3-13 แสดงวิธีที่คุณอาจ implement แอปแชทกลุ่มอย่าง Discord หรือ Slack ด้วย GraphQL query นี้ขอทุก channel ที่ผู้ใช้เข้าถึงได้ รวมถึงชื่อ channel และ 50 ข้อความล่าสุดในแต่ละ channel สำหรับแต่ละข้อความ query ขอ timestamp เนื้อหาข้อความ และชื่อกับ URL รูปโปรไฟล์ของผู้ส่ง ถ้าข้อความเป็นการตอบกลับข้อความอื่น query จะขอชื่อผู้ส่งและเนื้อหาของข้อความนั้นด้วย (ซึ่งอาจแสดงด้วยฟอนต์ที่เล็กกว่าเหนือข้อความตอบกลับ เพื่อให้บริบท)

ตัวอย่างที่ 3-13. Query GraphQL สำหรับแอปพลิเคชันแชทกลุ่ม
query ChatApp {
  channels {
    name
    recentMessages(latest: 50) {
      timestamp
      content
      sender {
        fullName
        imageUrl
      }
      replyTo {
        content
        sender {
          fullName
        }
      }
    }
  }
}

ตัวอย่างที่ 3-14 แสดงให้เห็นว่า response ของ query ใน ตัวอย่างที่ 3-13 อาจมีหน้าตาอย่างไร response เป็นเอกสาร JSON ที่สะท้อนโครงสร้างของ query: มีแอตทริบิวต์ที่ถูกร้องขอพอดี ไม่มากไม่น้อย วิธีนี้มีข้อดีคือ server ไม่จำเป็นต้องรู้ว่า client ต้องการแอตทริบิวต์ใดเพื่อ render UI เพราะ client ขอสิ่งที่ต้องการได้เอง ตัวอย่างเช่น query นี้ไม่ได้ขอ URL รูปโปรไฟล์ของผู้ส่งข้อความ replyTo แต่ถ้า UI ถูกเปลี่ยนให้แสดงรูปโปรไฟล์นั้นด้วย client ก็เพิ่มแอตทริบิวต์ imageUrl ที่ต้องใช้ลงใน query ได้ง่ายๆ โดยไม่ต้องแก้อะไรฝั่ง server

ตัวอย่างที่ 3-14. response ที่เป็นไปได้ของ query ใน ตัวอย่างที่ 3-13
{
  "data": {
    "channels": [
      {
        "name": "#general",
        "recentMessages": [
          {
            "timestamp": 1693143014,
            "content": "Hey! How are y'all doing?",
            "sender": {"fullName": "Aaliyah", "imageUrl": "https://..."},
            "replyTo": null
          },
          {
            "timestamp": 1693143024,
            "content": "Great! And you?",
            "sender": {"fullName": "Caleb", "imageUrl": "https://..."},
            "replyTo": {
              "content": "Hey! How are y'all doing?",
              "sender": {"fullName": "Aaliyah"}
            }
          },
          ...

ในตัวอย่างนี้ ชื่อและ URL รูปภาพของผู้ส่งถูกฝังไว้ตรงๆ ใน object ของข้อความ ถ้าผู้ใช้คนเดียวกันส่งหลายข้อความ ข้อมูลนี้ก็จะซ้ำอยู่ในทุกข้อความ โดยหลักการแล้วเราลดการซ้ำนี้ได้ แต่ GraphQL เลือกยอมให้ response ใหญ่ขึ้น เพื่อให้ render UI จากข้อมูลที่ร้องขอได้ง่ายขึ้น

ฟิลด์ replyTo ก็คล้ายกัน: ใน ตัวอย่างที่ 3-14 ข้อความที่สองเป็นการตอบกลับข้อความแรก และเนื้อหา (“Hey…”) กับชื่อผู้ส่ง (Aaliyah) ซ้ำอยู่ใน replyTo เราอาจคืนแค่ ID ของข้อความที่ถูกตอบกลับแทนก็ได้ แต่ถ้า ID นั้นไม่อยู่ใน 50 ข้อความล่าสุดที่คืนมา client ก็ต้องส่ง request เพิ่มไปยัง server การทำซ้ำเนื้อหาทำให้การทำงานกับข้อมูลง่ายขึ้นมาก

ฐานข้อมูลฝั่ง server เก็บข้อมูลในรูปแบบที่ normalized กว่า แล้ว join ตามที่จำเป็นเพื่อประมวลผล query ได้ ตัวอย่างเช่น server อาจเก็บข้อความพร้อม user ID ของผู้ส่งและ ID ของข้อความที่ตอบกลับ เมื่อได้รับ query แบบใน ตัวอย่างที่ 3-13 server ก็จะ resolve ID เหล่านั้นเพื่อหา record ที่อ้างถึง อย่างไรก็ตาม client ร้องขอได้เฉพาะ join ที่ประกาศไว้อย่างชัดเจนใน GraphQL schema เท่านั้น

แม้ response ของ GraphQL query จะดูคล้าย response จากฐานข้อมูลเอกสาร และแม้ชื่อจะมีคำว่า "graph" แต่ GraphQL implement บนฐานข้อมูลประเภทใดก็ได้ ไม่ว่าจะเป็นแบบเชิงสัมพันธ์ แบบเอกสาร หรือแบบกราฟ

Event Sourcing and CQRS

ในโมเดลข้อมูลทั้งหมดที่เราพูดถึงมาจนถึงตอนนี้ ข้อมูลถูก query ในรูปแบบเดียวกับที่เขียน ไม่ว่าจะเป็นเอกสาร JSON แถวในตาราง หรือ vertex และ edge ในกราฟ แต่ในแอปพลิเคชันที่ซับซ้อน บางครั้งอาจยากที่จะหารูปแบบข้อมูลเดียวที่ตอบสนองทุกวิธีที่ต้อง query และนำเสนอข้อมูลได้ ในสถานการณ์แบบนี้ การเขียนข้อมูลในรูปแบบหนึ่ง แล้วสร้าง (derive) รูปแบบข้อมูลที่ optimize สำหรับการอ่านแต่ละแบบขึ้นมาจากมัน อาจเป็นประโยชน์

เราเคยเห็นแนวคิดนี้มาแล้วใน "Systems of Record and Derived Data" และ ETL (ดู "Data Warehousing") ก็เป็นตัวอย่างหนึ่งของกระบวนการ derive แบบนั้น ตอนนี้เราจะต่อยอดแนวคิดนี้ไปอีก ในเมื่อเราจะ derive รูปแบบข้อมูลหนึ่งจากอีกรูปแบบอยู่แล้ว เราก็เลือกรูปแบบที่ต่างกันสำหรับการเขียนและการอ่านได้ ถ้าคุณอยาก optimize ข้อมูลสำหรับการเขียนอย่างเดียว โดยไม่ต้องห่วงเรื่อง query ที่มีประสิทธิภาพ คุณจะออกแบบข้อมูลอย่างไร?

วิธีเขียนข้อมูลที่ง่าย เร็ว และแสดงออกได้มากที่สุดน่าจะเป็น event log: ทุกครั้งที่ต้องการเขียนข้อมูล คุณ encode มันเป็น string ที่ครบในตัวเอง (อาจเป็น JSON) พร้อม timestamp แล้วผนวกต่อท้ายลำดับของ event event ใน log นี้เป็น immutable คุณไม่เคยแก้ไขหรือลบมัน มีแต่ผนวก event ใหม่ต่อท้าย log (ซึ่งอาจมาแทนที่ event ก่อนหน้า) event หนึ่งมี property อะไรก็ได้

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

Diagram illustrating an event log for a conference management system, showing how events like registration and booking generate multiple materialized views, such as customer booking confirmations and conference organizer dashboards.

รูปที่ 3-8. การใช้ log ของ event ที่ immutable เป็น source of truth และสร้าง materialized view จากมัน

ใน รูปที่ 3-8 ทุกการเปลี่ยนแปลงสถานะของการประชุม (เช่น ผู้จัดงานเปิดให้ลงทะเบียน หรือผู้เข้าร่วมทำการจองและยกเลิกการจอง) จะถูกเก็บเป็น event ก่อน ทุกครั้งที่มี event ถูกผนวกเข้าใน log จะมี materialized view หลายตัว (หรือเรียกว่า projections หรือ read models) ถูกอัปเดตเพื่อสะท้อนผลของ event นั้นด้วย ในตัวอย่างการประชุม อาจมี materialized view ตัวหนึ่งที่รวบรวมข้อมูลทั้งหมดเกี่ยวกับสถานะของการจองแต่ละรายการ อีกตัวคำนวณแผนภูมิสำหรับ dashboard ของผู้จัดงาน และตัวที่สามสร้างไฟล์สำหรับเครื่องพิมพ์ที่พิมพ์ป้ายชื่อผู้เข้าร่วม

แนวคิดที่ใช้ event เป็น source of truth และแทนทุกการเปลี่ยนแปลงสถานะด้วย event เรียกว่า event sourcing [65, 66] ส่วนหลักการที่เก็บ representation ที่ optimize สำหรับการอ่านแยกไว้ต่างหาก แล้ว derive มาจาก representation ที่ optimize สำหรับการเขียน เรียกว่า command query responsibility segregation (CQRS) [67] คำเหล่านี้มีต้นกำเนิดในชุมชน DDD แม้ว่าแนวคิดคล้ายกันจะมีมานานแล้ว เช่น ใน state machine replication (ดู "Using shared logs")

เมื่อ request จากผู้ใช้เข้ามา มันจะถูกเรียกว่า command และต้องผ่านการตรวจสอบความถูกต้องก่อน เมื่อ command ถูก execute และพบว่าถูกต้อง (เช่น มีที่นั่งว่างพอสำหรับการจองที่ร้องขอ) มันก็กลายเป็น fact และ event ที่สอดคล้องกันจะถูกเพิ่มลงใน log ดังนั้น event log ควรมีเฉพาะ event ที่ถูกต้อง และ consumer ของ event log ที่สร้าง materialized view ไม่ได้รับอนุญาตให้ปฏิเสธ event

เมื่อสร้างโมเดลข้อมูลแบบ event sourcing แนะนำให้ตั้งชื่อ event เป็นรูปอดีตกาล (เช่น "the seats were booked" (ที่นั่งถูกจองแล้ว)) เพราะ event คือบันทึกของข้อเท็จจริงว่าบางสิ่งได้เกิดขึ้นแล้ว แม้ว่าผู้ใช้จะตัดสินใจเปลี่ยนแปลงหรือยกเลิกการจองในภายหลัง ข้อเท็จจริงที่ว่าพวกเขาเคยมีการจองมาก่อนก็ยังคงเป็นจริง และการเปลี่ยนแปลงหรือยกเลิกนั้นเป็นอีก event หนึ่งที่ถูกเพิ่มเข้าไปในภายหลัง

สิ่งที่ event sourcing คล้ายกับ fact table ใน star schema ซึ่งกล่าวถึงใน "Stars and Snowflakes: Schemas for Analytics" คือทั้งสองเป็นคอลเลกชันของ event ที่เกิดขึ้นในอดีต แต่แถวใน fact table ทั้งหมดมีชุดคอลัมน์เหมือนกัน ส่วนใน event sourcing อาจมี event หลายประเภท แต่ละประเภทมี property ต่างกัน นอกจากนี้ fact table เป็นคอลเลกชันที่ไม่เรียงลำดับ ในขณะที่ใน event sourcing ลำดับของ event สำคัญ: ถ้ามีการจองก่อนแล้วจึงยกเลิก การประมวลผล event เหล่านั้นผิดลำดับจะไม่สมเหตุสมผล

Event sourcing และ CQRS มีข้อดีหลายประการ:

  • สำหรับคนที่พัฒนาระบบ event สื่อสารเจตนาของ เหตุผล ที่บางอย่างเกิดขึ้นได้ดีกว่า ตัวอย่างเช่น event "the booking was canceled" (การจองถูกยกเลิก) เข้าใจง่ายกว่า "คอลัมน์ active ในแถวที่ 4001 ของตาราง bookings ถูกตั้งค่าเป็น false สามแถวที่เกี่ยวข้องกับการจองนั้นถูกลบจากตาราง seat_assignments และแถวที่แสดงการคืนเงินถูกแทรกลงในตาราง payments" การแก้ไขแถวเหล่านี้อาจยังเกิดขึ้นเมื่อ materialized view ประมวลผล event การยกเลิก แต่เมื่อมี event เป็นตัวขับเคลื่อน เหตุผลของการอัปเดตก็ชัดเจนขึ้นมาก

  • หลักการสำคัญของ event sourcing คือ materialized view ถูก derive จาก event log ในแบบที่ทำซ้ำได้ คุณควรลบ materialized view ทิ้งแล้วคำนวณใหม่ได้เสมอ โดยประมวลผล event ชุดเดียวกันตามลำดับเดียวกันด้วยโค้ดเดียวกัน ถ้ามีบั๊กในโค้ดที่ดูแล view คุณก็แค่ลบ view แล้วคำนวณใหม่ด้วยโค้ดใหม่ การหาบั๊กก็ง่ายขึ้นด้วย เพราะคุณรันโค้ดที่ดูแล view ซ้ำกี่ครั้งก็ได้และตรวจสอบพฤติกรรมของมัน

  • คุณมี materialized view ได้หลายตัว โดยแต่ละตัว optimize สำหรับ query เฉพาะที่แอปพลิเคชันต้องการ view เหล่านี้เก็บไว้ใน database เดียวกับ event หรือคนละตัวก็ได้ ขึ้นอยู่กับความต้องการ ใช้โมเดลข้อมูลแบบไหนก็ได้ และ denormalize เพื่อให้อ่านเร็วได้ คุณถึงกับเก็บ view ไว้ในหน่วยความจำอย่างเดียวโดยไม่ persist ก็ได้ ตราบใดที่รับได้ที่จะคำนวณ view ใหม่จาก event log ทุกครั้งที่ service restart

  • ถ้าคุณอยากนำเสนอข้อมูลที่มีอยู่ในรูปแบบใหม่ การสร้าง materialized view ใหม่จาก event log ที่มีอยู่ก็ทำได้ง่าย คุณยังพัฒนาระบบให้รองรับฟีเจอร์ใหม่ได้ด้วยการเพิ่ม event ประเภทใหม่ หรือเพิ่ม property ใหม่ให้ event ประเภทที่มีอยู่ (event เก่าทั้งหมดยังคงไม่ถูกแก้ไข) และยังต่อยอดพฤติกรรมใหม่จาก event ที่มีอยู่ได้ (เช่น เมื่อผู้เข้าร่วมประชุมยกเลิก ที่นั่งของเขาอาจถูกเสนอให้คนถัดไปในรายการรอ)

  • ถ้าเขียน event ผิดพลาดลงไป คุณเขียน event ลบตามมาเพื่อย้อนกลับได้ view ปลายน้ำจะรวมการลบนี้เข้าไปโดยอัตโนมัติ ทำให้ข้อมูลถูกแก้ไข ในทางกลับกัน ใน database ที่อัปเดตและลบข้อมูลโดยตรง transaction ที่ commit ไปแล้วมักย้อนกลับได้ยาก event sourcing จึงช่วยลดจำนวนการกระทำที่ย้อนกลับไม่ได้ในระบบ ทำให้เปลี่ยนแปลงระบบได้ง่ายขึ้น (ดู "Evolvability: Making Change Easy")

  • event log ยังใช้เป็น audit log ของสิ่งที่เกิดขึ้นในระบบได้ด้วย ซึ่งมีค่าในอุตสาหกรรมที่มีการกำกับดูแลและต้องการ auditability แบบนี้

  • โดยทั่วไป event log รองรับ write throughput ได้สูงกว่า database เพราะมีรูปแบบการเข้าถึงแบบเรียงตามลำดับ ถ้ามี event เข้ามาถล่มชั่วคราว log ก็รับไว้ได้ และระบบปลายน้ำที่ดูแล materialized view ก็ค่อยๆ ตามทันตามจังหวะของตัวเองโดยไม่ถูกท่วม

อย่างไรก็ตาม event sourcing และ CQRS ก็มีข้อเสียเช่นกัน:

  • คุณต้องระวังถ้ามีข้อมูลจากภายนอกเกี่ยวข้อง ตัวอย่างเช่น สมมติว่า event มีราคาที่กำหนดในสกุลเงินหนึ่ง และสำหรับ view หนึ่งมันต้องถูกแปลงเป็นอีกสกุลเงินหนึ่ง เนื่องจากอัตราแลกเปลี่ยนอาจผันผวน การดึงอัตราแลกเปลี่ยนจากแหล่งข้อมูลภายนอกเมื่อประมวลผล event จะเป็นปัญหา เพราะคุณจะได้ผลลัพธ์ที่แตกต่างถ้าคุณคำนวณ materialized view ใหม่ในวันอื่น เพื่อให้ logic การประมวลผล event เป็น deterministic คุณต้องรวมอัตราแลกเปลี่ยนไว้ใน event เอง หรือมีวิธี query อัตราแลกเปลี่ยนในอดีต ณ timestamp ที่ระบุใน event โดยที่ query นี้จะคืนผลลัพธ์เดิมเสมอสำหรับ timestamp เดียวกัน

  • ข้อกำหนดที่ว่า event ต้อง immutable ทำให้เกิดปัญหาถ้า event มีข้อมูลส่วนบุคคลของผู้ใช้ เพราะผู้ใช้อาจใช้สิทธิ์ (เช่นภายใต้ GDPR) ขอให้ลบข้อมูลของตน ถ้า event log แยกตามผู้ใช้ คุณก็แค่ลบ log ทั้งหมดของผู้ใช้นั้น แต่ถ้า event log มี event ที่เกี่ยวข้องกับผู้ใช้หลายคน วิธีนี้ก็ใช้ไม่ได้ คุณอาจลองเก็บข้อมูลส่วนบุคคลไว้นอก event จริง หรือเข้ารหัสด้วยคีย์ที่เลือกลบทีหลังได้ (เทคนิคที่เรียกว่า crypto-shredding [68]) แต่นั่นก็ทำให้คำนวณ derived state ใหม่ได้ยากขึ้นเมื่อจำเป็นเช่นกัน

  • การประมวลผล event ซ้ำต้องระวังถ้ามี side effect ที่มองเห็นจากภายนอก—เช่น คุณคงไม่อยากส่งอีเมลยืนยันซ้ำทุกครั้งที่สร้าง materialized view ใหม่

คุณ implement event sourcing บน database ชนิดใดก็ได้ แต่บางระบบถูกออกแบบมาเพื่อรองรับ pattern นี้โดยเฉพาะ เช่น EventStoreDB, MartenDB (ที่ใช้ PostgreSQL) และ Axon Framework คุณยังใช้ message broker เช่น Apache Kafka เก็บ event log และใช้ stream processor ทำให้ materialized view อัปเดตอยู่เสมอได้ เราจะกลับมาที่หัวข้อเหล่านี้ใน บทที่ 12

ข้อกำหนดสำคัญข้อเดียวคือ ระบบที่เก็บ event ต้องรับประกันว่า materialized view ทั้งหมดประมวลผล event ตามลำดับเดียวกับที่ปรากฏใน log เป๊ะๆ อย่างที่เราจะเห็นใน บทที่ 10 เรื่องนี้ไม่ได้ทำให้สำเร็จได้ง่ายเสมอไปในระบบแบบ distributed

DataFrames, Matrices, and Arrays (DataFrames, เมทริกซ์ และอาร์เรย์)

โมเดลข้อมูลที่เราเห็นในบทนี้จนถึงตอนนี้ ส่วนใหญ่ใช้ได้ทั้งกับ transaction processing และ analytics (ดู "Operational Versus Analytical Systems") นอกจากนี้ยังมีโมเดลข้อมูลอีกไม่กี่แบบที่คุณน่าจะพบในบริบทเชิงวิเคราะห์หรือวิทยาศาสตร์ แต่ไม่ค่อยพบในระบบ OLTP ได้แก่ DataFrame และอาร์เรย์หลายมิติของตัวเลขอย่างเมทริกซ์

โมเดลข้อมูลแบบ DataFrame รองรับโดยภาษา R, library Pandas ของ Python, Apache Spark, ArcticDB, Dask และระบบอื่นๆ DataFrame เป็นเครื่องมือยอดนิยมของ data scientist ที่เตรียมข้อมูลเพื่อ train โมเดล ML แต่ก็ใช้กันอย่างแพร่หลายในการสำรวจข้อมูล วิเคราะห์ข้อมูลเชิงสถิติ แสดงผลข้อมูลเป็นภาพ และงานลักษณะคล้ายกัน

เมื่อมองผ่านๆ DataFrame คล้ายกับตารางในฐานข้อมูลเชิงสัมพันธ์หรือ spreadsheet DataFrame รองรับ operator แบบเชิงสัมพันธ์ที่ทำงานกับเนื้อหาทั้งก้อน (bulk) ตัวอย่างเช่น ใช้ฟังก์ชันกับทุกแถว กรองแถวตามเงื่อนไข จัดกลุ่มแถวตามบางคอลัมน์แล้วรวมค่าของคอลัมน์อื่น และ join แถวใน DataFrame หนึ่งกับอีก DataFrame หนึ่งตามคีย์ (สิ่งที่ฐานข้อมูลเชิงสัมพันธ์เรียกว่า join มักเรียกว่า merge บน DataFrame)

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

DataFrame API ยังมี operation หลากหลายที่ไปไกลเกินกว่าที่ฐานข้อมูลเชิงสัมพันธ์มีให้ และโมเดลข้อมูลนี้มักถูกใช้ต่างจากการทำ data modeling แบบเชิงสัมพันธ์ทั่วไปมาก [69] ตัวอย่างเช่น การใช้งาน DataFrame ที่พบบ่อยคือการแปลงข้อมูลจากรูปแบบคล้ายเชิงสัมพันธ์ให้เป็น matrix หรืออาร์เรย์หลายมิติ ซึ่งเป็นรูปแบบที่ ML algorithm จำนวนมากคาดหวังสำหรับ input

ตัวอย่างง่ายๆ ของการแปลงดังกล่าวแสดงใน รูปที่ 3-9 ทางซ้ายเป็นตารางเชิงสัมพันธ์ที่แสดงคะแนนที่ผู้ใช้ให้ภาพยนตร์ต่างๆ (ในระดับ 1 ถึง 5) และทางขวาข้อมูลถูกแปลงเป็น matrix ที่แต่ละคอลัมน์คือภาพยนตร์และแต่ละแถวคือผู้ใช้ (คล้ายกับ pivot table ใน spreadsheet) matrix นี้เป็น sparse ซึ่งหมายความว่าไม่มีข้อมูลสำหรับคู่ผู้ใช้กับภาพยนตร์หลายคู่ แต่ก็ไม่เป็นไร matrix นี้อาจมีหลายพันคอลัมน์ จึงอาจเข้ากับฐานข้อมูลเชิงสัมพันธ์ได้ไม่ดี แต่ DataFrame และ library ที่รองรับ sparse array (เช่น NumPy ของ Python) จัดการข้อมูลแบบนี้ได้ง่าย

Diagram illustrating the transformation of a relational table of user movie ratings into a sparse matrix, with users as rows and movies as columns.

รูปที่ 3-9. การแปลงตารางเชิงสัมพันธ์ของคะแนนภาพยนตร์เป็น matrix

matrix เก็บได้เฉพาะตัวเลข จึงมีเทคนิคต่างๆ สำหรับแปลงข้อมูลที่ไม่ใช่ตัวเลขให้เป็นตัวเลขใน matrix ตัวอย่างเช่น:

  • วันที่ (ซึ่งไม่ได้ใส่ไว้ใน matrix ตัวอย่างใน รูปที่ 3-9) อาจถูก scale ให้เป็นจำนวน floating-point ในช่วงที่เหมาะสม

  • สำหรับคอลัมน์ที่รับค่าได้เพียงค่าเดียวจากชุดค่าที่จำกัดและตายตัว (เช่น ประเภทของภาพยนตร์ในฐานข้อมูลภาพยนตร์) มักใช้ one-hot encoding เราสร้างคอลัมน์สำหรับแต่ละค่าที่เป็นไปได้ ("comedy", "drama", "horror" ฯลฯ) และสำหรับแต่ละแถวที่แทนภาพยนตร์ เราใส่ 1 ในคอลัมน์ที่ตรงกับประเภทของภาพยนตร์นั้น และใส่ 0 ในคอลัมน์อื่นทั้งหมด การแทนแบบนี้ยังขยายไปรองรับภาพยนตร์ที่เข้าได้หลายประเภทได้ง่ายด้วย

เมื่อข้อมูลอยู่ในรูป matrix ของตัวเลขแล้ว ก็เหมาะกับการดำเนินการทาง linear algebra ซึ่งเป็นพื้นฐานของ ML algorithm จำนวนมาก ตัวอย่างเช่น ข้อมูลใน รูปที่ 3-9 อาจเป็นส่วนหนึ่งของระบบแนะนำภาพยนตร์ที่ผู้ใช้น่าจะชอบ DataFrame ยืดหยุ่นพอที่จะให้ข้อมูลค่อยๆ เปลี่ยนจากรูปแบบเชิงสัมพันธ์ไปเป็น matrix ได้ และในขณะเดียวกันก็ให้ data scientist ควบคุมได้ว่าจะใช้รูปแบบใดที่เหมาะที่สุดกับเป้าหมายของกระบวนการวิเคราะห์ข้อมูลหรือการ train โมเดล

ฐานข้อมูลบางแห่ง เช่น TileDB [70] เชี่ยวชาญด้านการเก็บอาร์เรย์หลายมิติขนาดใหญ่ของตัวเลข ซึ่งเรียกว่า array databases และมักใช้กับชุดข้อมูลทางวิทยาศาสตร์ เช่น การวัดข้อมูลภูมิสารสนเทศ (raster data บนกริดที่มีระยะห่างสม่ำเสมอ) ภาพถ่ายทางการแพทย์ หรือข้อมูลที่สังเกตได้จากกล้องโทรทรรศน์ดาราศาสตร์ [71] DataFrame ยังใช้ในอุตสาหกรรมการเงินเพื่อแทน time-series data เช่น ราคาสินทรัพย์และการซื้อขายตามช่วงเวลา [72] เพราะเป็นที่นิยมในหมู่ data scientist DataFrame จึงถูกเพิ่มเข้าไปใน batch processing framework อย่าง Spark และ Flink ด้วย เราจะกลับมาที่หัวข้อนี้ใน บทที่ 11

Summary (สรุป)

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

โมเดลเชิงสัมพันธ์ แม้จะมีอายุมากกว่าครึ่งศตวรรษแล้ว ก็ยังเป็นโมเดลข้อมูลที่สำคัญสำหรับหลายแอปพลิเคชัน โดยเฉพาะใน data warehousing และ business analytics ที่ star schema หรือ snowflake schema แบบเชิงสัมพันธ์ และ SQL query พบได้ทั่วไป อย่างไรก็ตาม ทางเลือกหลายแบบแทนข้อมูลเชิงสัมพันธ์ก็ได้รับความนิยมในโดเมนอื่น:

  • โมเดลเอกสาร มุ่งเป้าไปที่กรณีใช้งานที่ข้อมูลอยู่ในเอกสาร JSON ที่ครบในตัวเอง และความสัมพันธ์ระหว่างเอกสารหนึ่งกับอีกเอกสารหนึ่งมีน้อย

  • โมเดลข้อมูลแบบกราฟ ไปในทิศทางตรงข้าม โดยมุ่งเป้าไปที่กรณีใช้งานที่ทุกอย่างอาจเกี่ยวข้องกับทุกอย่าง และ query อาจต้อง traverse หลาย hop เพื่อหาข้อมูลที่สนใจ (ความต้องการที่ตอบได้ด้วย recursive query ใน Cypher, SPARQL หรือ Datalog)

  • DataFrame ขยายข้อมูลเชิงสัมพันธ์ให้รองรับคอลัมน์จำนวนมาก โดยเป็นสะพานเชื่อมระหว่างฐานข้อมูลกับอาร์เรย์หลายมิติที่เป็นพื้นฐานของ machine learning การวิเคราะห์ข้อมูลเชิงสถิติ และการคำนวณทางวิทยาศาสตร์

ในระดับหนึ่ง โมเดลหนึ่งมักถูกจำลองด้วยอีกโมเดลได้—เช่น แทนข้อมูลกราฟในฐานข้อมูลเชิงสัมพันธ์—แต่ผลลัพธ์อาจดูทุลักทุเล ดังที่เราเห็นกับการรองรับ recursive query ใน SQL

ฐานข้อมูลเฉพาะทางจึงถูกพัฒนาขึ้นสำหรับแต่ละโมเดลข้อมูล โดยมี query language และ storage engine ที่ optimize สำหรับโมเดลนั้นโดยเฉพาะ อย่างไรก็ตาม ก็มีแนวโน้มที่ฐานข้อมูลจะขยายไปยังตลาดเฉพาะกลุ่มข้างเคียงด้วยการเพิ่มการรองรับโมเดลข้อมูลอื่น เช่น ฐานข้อมูลเชิงสัมพันธ์เพิ่มการรองรับข้อมูลเอกสารในรูปแบบคอลัมน์ JSON ฐานข้อมูลเอกสารเพิ่ม join แบบเชิงสัมพันธ์ และการรองรับข้อมูลกราฟภายใน SQL ก็ค่อยๆ ดีขึ้น

อีกโมเดลหนึ่งที่เราพูดถึงคือ event sourcing ซึ่งแทนข้อมูลเป็น append-only log ของ event ที่ immutable และอาจมีข้อดีสำหรับการสร้างโมเดลกิจกรรมในโดเมนธุรกิจที่ซับซ้อน append-only log เหมาะกับการเขียนข้อมูล (อย่างที่เราจะเห็นใน บทที่ 4) และเพื่อรองรับ query ที่มีประสิทธิภาพ event log จะถูกแปลงเป็น materialized view ที่ optimize สำหรับการอ่านผ่าน CQRS

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

แม้เราจะครอบคลุมเนื้อหามากมาย แต่ก็ยังมีโมเดลข้อมูลบางแบบที่ไม่ได้กล่าวถึง ขอยกตัวอย่างสั้นๆ เพียงไม่กี่อย่าง:

  • นักวิจัยที่ทำงานกับข้อมูลจีโนมมักต้องทำ sequence similarity search ซึ่งเอา string ที่ยาวมากหนึ่งตัว (แทนโมเลกุล DNA) ไปจับคู่กับฐานข้อมูลขนาดใหญ่ของ string ที่คล้ายกันแต่ไม่เหมือนกันทุกประการ ไม่มีฐานข้อมูลใดที่อธิบายมาในบทนี้รองรับการใช้งานแบบนี้ได้ ซึ่งเป็นเหตุผลที่นักวิจัยเขียนซอฟต์แวร์ฐานข้อมูลจีโนมเฉพาะทางขึ้นมา เช่น GenBank [73]

  • ระบบการเงินจำนวนมากใช้ ledger ที่มี double-entry accounting (บัญชีคู่) เป็นโมเดลข้อมูล ข้อมูลประเภทนี้แทนในฐานข้อมูลเชิงสัมพันธ์ได้ แต่ก็มีฐานข้อมูล (เช่น TigerBeetle) ที่เชี่ยวชาญโมเดลข้อมูลนี้โดยเฉพาะ cryptocurrency และ blockchain มักตั้งอยู่บน distributed ledger ซึ่งมีการโอนมูลค่าฝังอยู่ในโมเดลข้อมูลของมันด้วยเช่นกัน

  • Full-text search อาจกล่าวได้ว่าเป็น โมเดลข้อมูลชนิดหนึ่งที่มักใช้ควบคู่กับฐานข้อมูล information retrieval (การสืบค้นสารสนเทศ) เป็นหัวข้อเฉพาะทางขนาดใหญ่ที่เราจะไม่ลงรายละเอียดมากนักในหนังสือเล่มนี้ แต่เราจะแตะเรื่อง search index และ vector search ใน "Full-Text Search"

เราต้องหยุดไว้แค่นี้ก่อน ในบทถัดไปเราจะพูดถึง trade-off บางอย่างที่เข้ามาเกี่ยวข้องเมื่อ implement โมเดลข้อมูลที่อธิบายในบทนี้