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

Ludwig Wittgenstein, Tractatus Logico-Philosophicus (1922)

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

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

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

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

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

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

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

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

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 ความสามารถในการปรับขนาด และการเคลื่อนตัวไปสู่โมเดลลิขสิทธิ์แบบ Open Source ฐานข้อมูลบางแห่งสร้างแบรนด์ให้ตัวเองเป็น NewSQL ซึ่งสะท้อนถึงเป้าหมายในการให้ความสามารถในการปรับขนาดของระบบ NoSQL พร้อมกับโมเดลข้อมูลและการรับประกัน transactional ของฐานข้อมูลเชิงสัมพันธ์แบบดั้งเดิม แนวคิด 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 reference ไปยังตาราง 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 ช่วยลดความไม่สอดคล้องระหว่างอิมพีแดนซ์ระหว่างโค้ดแอปพลิเคชันและชั้นจัดเก็บข้อมูล การไม่มี schema มักถูกยกให้เป็นข้อดีเช่นกัน เราจะพูดถึงเรื่องนี้ใน "Schema flexibility in the document model" อย่างไรก็ตาม ดังที่เราจะเห็นใน บทที่ 5 ก็มีปัญหาเกี่ยวกับ JSON ในฐานะรูปแบบการเข้ารหัสข้อมูลเช่นกัน

การแสดงแบบ 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 และ Joins)

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

ถ้า UI มีช่องกรอกข้อความอิสระสำหรับป้อนภูมิภาค การจัดเก็บเป็นข้อความธรรมดาก็สมเหตุสมผล แต่การมีรายการภูมิภาคทางภูมิศาสตร์ที่ได้มาตรฐานและให้ผู้ใช้เลือกจากรายการแบบเลื่อนลงหรือ 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 มักจะเขียนได้เร็วกว่า (เนื่องจากมีสำเนาเดียว) แต่สืบค้นได้ช้ากว่า (เนื่องจากต้องใช้ 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

อย่างไรก็ตาม การ implement materialized timeline ของ X (เดิมคือ Twitter) ไม่ได้จัดเก็บข้อความจริงของแต่ละโพสต์ แต่ละรายการจัดเก็บเฉพาะ 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 ]

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

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

ถ้าคุณต้องตัดสินใจว่าจะ denormalize อะไรบางอย่างในแอปพลิเคชันของคุณหรือไม่ กรณีศึกษาเครือข่ายสังคมแสดงให้เห็นว่าตัวเลือกนั้นไม่ชัดเจนในทันที วิธีการที่ปรับขนาดได้มากที่สุดอาจเกี่ยวข้องกับการ denormalize บางอย่างและปล่อยให้สิ่งอื่นเป็น normalized คุณจะต้องพิจารณาอย่างรอบคอบว่าข้อมูลเปลี่ยนแปลงบ่อยแค่ไหนและค่าใช้จ่ายของการอ่านและการเขียน (ซึ่งอาจถูกครอบงำโดยค่าผิดปกติ เช่น ผู้ใช้ที่มีผู้ติดตาม/ผู้ติดตามจำนวนมากในกรณีของเครือข่ายสังคมทั่วไป) Normalization และ denormalization ไม่ใช่สิ่งที่ดีหรือไม่ดีโดยเนื้อแท้—มันเพียงแค่แสดงถึงข้อแลกเปลี่ยนในแง่ของประสิทธิภาพการอ่านและการเขียน และความพยายามในการ 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 ดังกล่าวเป็นไปได้คือการจัดเก็บ ID reference ทั้งสองด้าน โดยที่ประวัติย่อรวม ID ของแต่ละองค์กรที่บุคคลนั้นเคยทำงาน และเอกสารองค์กรรวม ID ของประวัติย่อที่กล่าวถึงองค์กรนั้น การแสดงนี้เป็นแบบ denormalized เนื่องจากความสัมพันธ์ถูกจัดเก็บในสองที่ ซึ่งอาจทำให้ไม่สอดคล้องกัน

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

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

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

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

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

Data Warehouse (ดู "Data Warehousing" ) มักจะเป็นแบบเชิงสัมพันธ์ และมีแบบแผนที่ใช้กันอย่างแพร่หลายสำหรับโครงสร้างของตารางใน Data Warehouse รวมถึง star schema, snowflake schema, dimensional modeling [ 14 ] และ one big table (OBT) โครงสร้างเหล่านี้ถูกปรับให้เหมาะสมกับความต้องการของนักวิเคราะห์ธุรกิจ กระบวนการ ETL แปลงข้อมูลจากระบบปฏิบัติการไปยัง 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 เป็นแอตทริบิวต์ เช่น ราคาที่สินค้าถูกขายและต้นทุนในการซื้อจากซัพพลายเออร์ (ทำให้สามารถคำนวณอัตรากำไรได้) คอลัมน์อื่นๆ ใน fact table เป็น foreign key reference ไปยังตารางอื่นๆ ที่เรียกว่า 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 มีการ normalization มากกว่า 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 ]

ในบริบทของการวิเคราะห์ การทำ denormalization ดังกล่าวไม่มีปัญหา เนื่องจากข้อมูลโดยทั่วไปแทน log ของข้อมูลในประวัติที่จะไม่เปลี่ยนแปลง (ยกเว้นบางครั้งเพื่อแก้ไขข้อผิดพลาด) ปัญหาความสอดคล้องของข้อมูลและ overhead การเขียนที่เกิดขึ้นกับการทำ denormalization ในระบบ 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 หมายความว่าสามารถเพิ่มคีย์และค่าใดๆ ลงในเอกสารได้ และเมื่ออ่าน ไคลเอ็นต์ไม่มีการรับประกันว่าเอกสารอาจมีฟิลด์อะไรบ้าง

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

Schema-on-read คล้ายกับการตรวจสอบชนิดแบบ dynamic (runtime) ในภาษาโปรแกรม ในขณะที่ schema-on-write คล้ายกับการตรวจสอบชนิดแบบ static (compile-time) เช่นเดียวกับที่ผู้สนับสนุนการตรวจสอบชนิดแบบ static และ dynamic มีการถกเถียงกันอย่างมากเกี่ยวกับข้อดีของพวกมัน [ 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 ] แต่การทำ migration ดังกล่าวบนฐานข้อมูลขนาดใหญ่ยังคงท้าทายในเชิงปฏิบัติการ การ migration ที่ซับซ้อนสามารถหลีกเลี่ยงได้โดยการเพิ่มคอลัมน์ first_name ด้วยค่าเริ่มต้นเป็น NULL (ซึ่งรวดเร็ว) และเติมค่าในตอนอ่าน เหมือนกับที่คุณทำกับฐานข้อมูลเอกสาร

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

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

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

ในสถานการณ์เช่นนี้ schema อาจสร้างปัญหามากกว่าช่วยเหลือ และเอกสารแบบ schemaless สามารถเป็นโมเดลข้อมูลที่เป็นธรรมชาติมากกว่า แต่เมื่อ record ทั้งหมดคาดว่าจะมีโครงสร้างเดียวกัน schema ก็เป็นกลไกที่มีประโยชน์สำหรับการจัดทำเอกสารและการบังคับใช้โครงสร้างนั้น เราจะพูดถึง schema และวิวัฒนาการของ schema ในรายละเอียดเพิ่มเติมใน บทที่ 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 และ operators การสืบค้น และความสามารถในการทำ 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 น้อย โมเดลเอกสารก็เหมาะสม

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

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

Social graph

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

Web graph

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

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

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

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

กราฟสามารถแสดงได้หลายวิธี ใน โมเดล adjacency list vertex แต่ละตัวจะจัดเก็บ ID ของ vertex ใกล้เคียงที่ห่างกันหนึ่ง edge หรือคุณสามารถใช้ adjacency matrix ซึ่งเป็นอาร์เรย์สองมิติที่แต่ละแถวและคอลัมน์ตรงกับ vertex โดยค่าจะเป็น 0 เมื่อไม่มี edge ระหว่าง vertex แถวและคอลัมน์ และ 1 เมื่อมี edge Adjacency list เหมาะสำหรับการ traversal กราฟ และ 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 คุณสามารถสืบค้นตาราง 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 หรือ join table แบบหลายต่อหลายที่เราเห็นใน "Many-to-One and Many-to-Many Relationships" ที่ถูกทำให้ทั่วไปเพื่อให้ความสัมพันธ์หลายประเภทสามารถจัดเก็บในตารางเดียวกัน นอกจากนี้ยังอาจมี index บน label และคุณสมบัติต่างๆ ทำให้สามารถค้นหา vertex หรือ edge ที่มีคุณสมบัติบางอย่างได้อย่างมีประสิทธิภาพ

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 แนะนำว่าข้อมูลกราฟสามารถแสดงในฐานข้อมูลเชิงสัมพันธ์ได้ แต่ถ้าเราใส่ข้อมูลกราฟในโครงสร้างเชิงสัมพันธ์ เราจะสามารถสืบค้นมันโดยใช้ 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 และ 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 ตามที่ envision ไว้แต่แรกจะไม่ประสบความสำเร็จ [ 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 สามารถเข้ารหัสด้วยวิธีอื่นๆ รวมถึง (verbose กว่า) XML ดังที่แสดงใน ตัวอย่างที่ 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") มันเกิดก่อน Cypher และเนื่องจากการ match รูปแบบของ 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 ไม่แยกความแตกต่างระหว่าง properties และ edges แต่ใช้ predicate สำหรับทั้งสองอย่าง คุณสามารถใช้ syntax เดียวกันสำหรับการ matching properties ใน 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 ของสหรัฐฯ โดยทั่วไป คำสั่ง table(val1, val2, …​ ) หมายความว่า table มีแถวที่คอลัมน์แรกมีค่า val1 , คอลัมน์ที่สองมีค่า val2 และอื่นๆ

ตัวอย่างที่ 3-11 แสดงวิธีการเขียนข้อมูลจากด้านซ้ายมือของ รูปที่ 3-6 ใน Datalog ขอบของกราฟ ( within , born_in และ lives_in ) แสดงเป็นตาราง join สองคอลัมน์ ตัวอย่างเช่น 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 พื้นฐาน ตารางที่ derive นี้เหมือนกับ (virtual) SQL view: มันไม่ได้ถูกจัดเก็บในฐานข้อมูล แต่คุณสามารถสืบค้นมันได้ในลักษณะเดียวกับตารางที่เก็บ facts

ใน ตัวอย่างที่ 3-12 เรากำหนดตารางที่ derive สามตาราง: 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' และคืนเฉพาะชื่อของคนที่ตรงกับการค้นหา โดยการสืบค้นเนื้อหาของตารางเสมือน us_to_europe ระบบ Datalog ก็จะได้คำตอบเดียวกับ query Cypher และ SPARQL ก่อนหน้านี้

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

GraphQL

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

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

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

อย่างไรก็ตาม GraphQL ก็มีประโยชน์ ตัวอย่างที่ 3-13 แสดงวิธีที่คุณอาจ implement แอปพลิเคชันแชทกลุ่มเช่น Discord หรือ Slack โดยใช้ GraphQL query ขอทุกช่องที่ผู้ใช้สามารถเข้าถึงได้ รวมถึงชื่อช่องและ 50 ข้อความล่าสุดในแต่ละช่อง สำหรับแต่ละข้อความ 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 แสดงให้เห็นว่าการตอบสนองต่อ query ใน ตัวอย่างที่ 3-13 อาจมีลักษณะอย่างไร การตอบสนองเป็นเอกสาร JSON ที่สะท้อนโครงสร้างของ query: มันประกอบด้วยแอตทริบิวต์ที่ถูกร้องขอเท่านั้น ไม่มากไม่น้อย วิธีการนี้มีข้อดีที่เซิร์ฟเวอร์ไม่จำเป็นต้องรู้ว่าแอตทริบิวต์ใดที่ไคลเอ็นต์ต้องการเพื่อแสดง UI ไคลเอ็นต์สามารถร้องขอสิ่งที่ต้องการได้ ตัวอย่างเช่น query นี้ไม่ได้ขอ URL รูปโปรไฟล์สำหรับผู้ส่งข้อความ replyTo แต่ถ้า UI ถูกเปลี่ยนให้รวมรูปโปรไฟล์นั้น มันก็ง่ายสำหรับไคลเอ็นต์ที่จะเพิ่มแอตทริบิวต์ imageUrl ที่ต้องการลงใน query โดยไม่ต้องเปลี่ยนแปลงฝั่งเซิร์ฟเวอร์

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

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

แม้ว่า response ของ GraphQL query จะดูคล้ายกับ response จาก document database และถึงแม้ว่ามันจะมีคำว่า "graph" อยู่ในชื่อ แต่ GraphQL สามารถถูก implement บนฐานข้อมูลประเภทใดก็ได้ ไม่ว่าจะเป็น relational, document หรือ graph

Event Sourcing and CQRS (Event Sourcing และ CQRS)

ในทุก data model ที่เราได้พูดถึงมาจนถึงตอนนี้ ข้อมูลถูก query ในรูปแบบเดียวกันกับที่เขียน ไม่ว่าจะเป็น JSON documents, ตารางใน relational database, หรือ vertices และ edges ใน graph อย่างไรก็ตาม ในแอปพลิเคชันที่ซับซ้อน บางครั้งอาจยากที่จะหารูปแบบข้อมูลเดียวที่สามารถตอบสนองทุกวิธีที่ข้อมูลจำเป็นต้องถูก query และนำเสนอ ในสถานการณ์เช่นนี้ การเขียนข้อมูลในรูปแบบหนึ่งแล้วสร้าง representation ที่ถูก optimize สำหรับการอ่านแบบต่างๆ จากข้อมูลนั้น อาจเป็นประโยชน์

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

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

Figure 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 ของ immutable events เป็น source of truth และสร้าง materialized views จากมัน

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

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

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

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

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

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

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

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

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

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

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

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

  • โดยทั่วไปแล้ว event logs สามารถรองรับ write throughput ที่สูงกว่า databases เนื่องจากรูปแบบการเข้าถึงแบบเรียงตามลำดับ ถ้ามี events จำนวนมากชั่วคราว log สามารถรองรับมันได้ และระบบปลายน้ำที่ดูแล materialized views สามารถตามทันได้ตามจังหวะของตัวเองโดยไม่ถูก overwhelm

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

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

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

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

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

ข้อกำหนดที่สำคัญเพียงอย่างเดียวคือระบบจัดเก็บ events ต้องรับประกันว่า materialized views ทั้งหมดประมวลผล events ในลำดับเดียวกันกับที่ปรากฏใน log อย่างแน่นอน อย่างที่เราจะเห็นใน Chapter 10 สิ่งนี้ไม่ใช่เรื่องง่ายเสมอไปที่จะทำให้สำเร็จในระบบกระจาย

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

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

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

เมื่อมองแวบแรก DataFrame คล้ายกับตารางใน relational database หรือ spreadsheet DataFrame รองรับ operators แบบ relational ที่ดำเนินการ bulk กับเนื้อหาของมัน ตัวอย่างเช่น การใช้ฟังก์ชันกับทุกแถว การกรองแถวตามเงื่อนไข การจัดกลุ่มแถวตาม columns บางตัวและรวม columns อื่นๆ และการ join แถวใน DataFrame หนึ่งกับอีก DataFrame หนึ่งตามคีย์ (สิ่งที่ relational database เรียกว่า join โดยทั่วไปจะเรียกว่า merge บน DataFrames)

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

DataFrame APIs ยังมี operations หลากหลายที่เกินกว่าที่ relational databases เสนอให้ และ data model มักถูกใช้ในวิธีที่แตกต่างจากการทำ data modeling แบบ relational ทั่วไป [69] ตัวอย่างเช่น การใช้งาน DataFrames ทั่วไปคือการแปลงข้อมูลจากรูปแบบคล้าย relational ให้เป็น matrix หรือ multidimensional array ซึ่งเป็นรูปแบบที่ ML algorithms จำนวนมากคาดหวังให้ input ของพวกเขาอยู่ในรูปแบบนี้

ตัวอย่างง่ายๆ ของการแปลงดังกล่าวแสดงใน Figure 3-9 ทางซ้ายเรามีตาราง relational ที่แสดงคะแนน rating ของผู้ใช้ต่อภาพยนตร์ต่างๆ (ในระดับ 1 ถึง 5) และทางขวาข้อมูลถูกแปลงเป็น matrix ที่แต่ละคอลัมน์คือภาพยนตร์และแต่ละแถวคือผู้ใช้ (คล้ายกับ pivot table ใน spreadsheet) matrix นี้เป็น sparse ซึ่งหมายความว่าไม่มีข้อมูลสำหรับคู่ผู้ใช้-ภาพยนตร์หลายคู่ แต่นี่ก็ไม่เป็นปัญหา matrix นี้อาจมีหลายพันคอลัมน์และจึงอาจไม่เหมาะสมกับ relational database แต่ DataFrames และ libraries ที่มี sparse arrays (เช่น 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. การแปลงตาราง relational ของ rating ภาพยนตร์เป็น matrix

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

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

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

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

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

Summary (สรุป)

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

Relational model ถึงแม้จะมีอายุมากกว่าครึ่งศตวรรษแล้ว แต่ก็ยังคงเป็น data model ที่สำคัญสำหรับหลายแอปพลิเคชัน โดยเฉพาะใน data warehousing และ business analytics ที่ star หรือ snowflake schemas แบบ relational และ SQL queries เป็นสิ่งที่แพร่หลาย อย่างไรก็ตาม ทางเลือกหลายอย่างแทน relational data ก็ได้รับความนิยมในโดเมนอื่นๆ:

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

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

  • DataFrames ขยาย generalization ของ relational data ไปยัง columns จำนวนมาก โดยให้สะพานเชื่อมระหว่าง databases และ multidimensional arrays ที่เป็นพื้นฐานของ machine learning, การวิเคราะห์ข้อมูลทางสถิติ และการคำนวณทางวิทยาศาสตร์

ในระดับหนึ่ง model หนึ่งมักสามารถถูกเลียนแบบในรูปของอีก model หนึ่งได้ เช่น ข้อมูล graph สามารถแสดงใน relational database ได้ แต่ผลลัพธ์อาจดูไม่เป็นธรรมชาติ อย่างที่เราเห็นกับการรองรับ recursive queries ใน SQL

ดังนั้นจึงมีการพัฒนาฐานข้อมูลเฉพาะทางต่างๆ สำหรับแต่ละ data model โดยให้ query languages และ storage engines ที่ถูก optimize สำหรับ model นั้นๆ อย่างไรก็ตาม ก็มีแนวโน้มที่ databases จะขยายไปสู่ niches ใกล้เคียงโดยเพิ่มการรองรับ data models อื่นๆ เช่น relational databases เพิ่มการรองรับ document data ในรูปแบบ JSON columns, document databases เพิ่ม relational-like joins และการรองรับ graph data ภายใน SQL ก็ค่อยๆ ดีขึ้น

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

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

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

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

  • ระบบการเงินจำนวนมากใช้ ledgers ร่วมกับบัญชีคู่ (double-entry accounting) เป็น data model ข้อมูลประเภทนี้สามารถแสดงใน relational databases ได้ แต่ก็มี databases (เช่น TigerBeetle) ที่เชี่ยวชาญใน data model นี้ Cryptocurrencies และ blockchains โดยทั่วไปจะใช้ distributed ledgers ซึ่งก็มีการโอนมูลค่าสร้างอยู่ใน data model ของพวกมันเช่นกัน

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

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