คุณจะแสดงการสืบทอดในฐานข้อมูลได้อย่างไร


236

ฉันคิดเกี่ยวกับวิธีแสดงโครงสร้างที่ซับซ้อนในฐานข้อมูล SQL Server

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

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

ฉันเห็นว่ามีสองตัวเลือกหลัก:

  1. สร้างตารางนโยบายจากนั้นตารางส่วนที่มีฟิลด์ทั้งหมดที่จำเป็นสำหรับรูปแบบที่เป็นไปได้ทั้งหมดซึ่งส่วนใหญ่จะเป็นโมฆะ

  2. สร้างตารางนโยบายและตารางส่วนต่างๆจำนวนมากหนึ่งตารางสำหรับความคุ้มครองแต่ละประเภท

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

แนวปฏิบัติที่ดีที่สุดสำหรับสถานการณ์นี้คืออะไร


คำตอบ:


430

@Bill KarwinอธิบายโมเดลการสืบทอดสามแบบในหนังสือSQL Antipatternsของเขาเมื่อเสนอวิธีแก้ปัญหาให้กับantipattern ของEntity-Attribute-Valueของ SQL นี่คือภาพรวมคร่าวๆ:

การสืบทอดตารางเดี่ยว (aka ตารางต่อการสืบทอดลำดับชั้น):

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

+------+---------------------+----------+----------------+------------------+
| id   | date_issued         | type     | vehicle_reg_no | property_address |
+------+---------------------+----------+----------------+------------------+
|    1 | 2010-08-20 12:00:00 | MOTOR    | 01-A-04004     | NULL             |
|    2 | 2010-08-20 13:00:00 | MOTOR    | 02-B-01010     | NULL             |
|    3 | 2010-08-20 14:00:00 | PROPERTY | NULL           | Oxford Street    |
|    4 | 2010-08-20 15:00:00 | MOTOR    | 03-C-02020     | NULL             |
+------+---------------------+----------+----------------+------------------+

\------ COMMON FIELDS -------/          \----- SUBTYPE SPECIFIC FIELDS -----/

การออกแบบให้เรียบง่ายเป็นสิ่งที่ดี แต่ปัญหาหลักของวิธีนี้คือ:

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

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

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

มรดกตารางคอนกรีต:

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

--// Table: policies_motor
+------+---------------------+----------------+
| id   | date_issued         | vehicle_reg_no |
+------+---------------------+----------------+
|    1 | 2010-08-20 12:00:00 | 01-A-04004     |
|    2 | 2010-08-20 13:00:00 | 02-B-01010     |
|    3 | 2010-08-20 15:00:00 | 03-C-02020     |
+------+---------------------+----------------+
                          
--// Table: policies_property    
+------+---------------------+------------------+
| id   | date_issued         | property_address |
+------+---------------------+------------------+
|    1 | 2010-08-20 14:00:00 | Oxford Street    |   
+------+---------------------+------------------+

การออกแบบนี้โดยทั่วไปจะแก้ปัญหาที่ระบุสำหรับวิธีตารางเดียว:

  • สามารถบังคับใช้แอตทริบิวต์บังคับNOT NULLได้

  • การเพิ่มประเภทย่อยใหม่จำเป็นต้องเพิ่มตารางใหม่แทนการเพิ่มคอลัมน์ลงในที่มีอยู่

  • นอกจากนี้ยังไม่มีความเสี่ยงที่จะมีการตั้งค่าแอตทริบิวต์ที่ไม่เหมาะสมสำหรับประเภทย่อยเฉพาะเช่นvehicle_reg_noฟิลด์สำหรับนโยบายคุณสมบัติ

  • ไม่จำเป็นต้องมีtypeแอตทริบิวต์เช่นเดียวกับวิธีในตารางเดียว ชนิดนี้ถูกกำหนดโดยเมทาดาทา: ชื่อตาราง

อย่างไรก็ตามรุ่นนี้มาพร้อมกับข้อเสีย:

  • แอตทริบิวต์ทั่วไปจะผสมกับแอตทริบิวต์เฉพาะของชนิดย่อยและไม่มีวิธีที่ง่ายในการระบุ ฐานข้อมูลจะไม่ทราบเช่นกัน

  • เมื่อกำหนดตารางคุณจะต้องทำซ้ำคุณสมบัติทั่วไปสำหรับแต่ละตารางย่อย ที่แน่นอนไม่แห้ง

  • การค้นหานโยบายทั้งหมดโดยไม่คำนึงถึงประเภทย่อยจะยากและจะต้องใช้จำนวนUNIONมาก

นี่คือวิธีที่คุณจะต้องค้นหานโยบายทั้งหมดโดยไม่คำนึงถึงประเภท:

SELECT     date_issued, other_common_fields, 'MOTOR' AS type
FROM       policies_motor
UNION ALL
SELECT     date_issued, other_common_fields, 'PROPERTY' AS type
FROM       policies_property;

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

การสืบทอดตารางคลาส (aka ตารางต่อการสืบทอดประเภท):

นี้เป็นวิธีการที่@ David กล่าวถึงในคำตอบอื่น คุณสร้างตารางเดียวสำหรับคลาสพื้นฐานของคุณซึ่งรวมถึงแอตทริบิวต์ทั่วไปทั้งหมด จากนั้นคุณจะสร้างตารางเฉพาะสำหรับแต่ละประเภทย่อยซึ่งมีคีย์หลักทำหน้าที่เป็นforeign keyไปยังตารางฐาน ตัวอย่าง:

CREATE TABLE policies (
   policy_id          int,
   date_issued        datetime,

   -- // other common attributes ...
);

CREATE TABLE policy_motor (
    policy_id         int,
    vehicle_reg_no    varchar(20),

   -- // other attributes specific to motor insurance ...

   FOREIGN KEY (policy_id) REFERENCES policies (policy_id)
);

CREATE TABLE policy_property (
    policy_id         int,
    property_address  varchar(20),

   -- // other attributes specific to property insurance ...

   FOREIGN KEY (policy_id) REFERENCES policies (policy_id)
);

วิธีนี้แก้ปัญหาที่ระบุในอีกสองการออกแบบ:

  • NOT NULLแอตทริบิวต์บังคับสามารถบังคับกับ

  • การเพิ่มประเภทย่อยใหม่จำเป็นต้องเพิ่มตารางใหม่แทนการเพิ่มคอลัมน์ลงในที่มีอยู่

  • ไม่มีความเสี่ยงที่มีการตั้งค่าแอตทริบิวต์ที่ไม่เหมาะสมสำหรับประเภทย่อยเฉพาะ

  • ไม่จำเป็นต้องมีtypeแอตทริบิวต์

  • ตอนนี้แอตทริบิวต์ทั่วไปจะไม่ผสมกับแอตทริบิวต์เฉพาะของชนิดย่อยอีกต่อไป

  • เราสามารถอยู่แห้งได้ในที่สุด ไม่จำเป็นต้องทำซ้ำคุณสมบัติทั่วไปสำหรับแต่ละตารางย่อยเมื่อสร้างตาราง

  • การจัดการการเพิ่มอัตโนมัติidสำหรับนโยบายจะง่ายขึ้นเนื่องจากสามารถจัดการได้โดยตารางฐานแทนที่จะเป็นตารางย่อยแต่ละประเภทที่สร้างขึ้นมาโดยอิสระ

  • กำลังหานโยบายทั้งหมดโดยไม่คำนึงถึงชนิดย่อยในขณะนี้จะกลายเป็นเรื่องง่ายมาก: ไม่มีUNIONs จำเป็น - SELECT * FROM policiesเพียง

ฉันคิดว่าวิธีตารางเรียนเป็นวิธีที่เหมาะสมที่สุดในสถานการณ์ส่วนใหญ่


ชื่อของทั้งสามรุ่นมาจากมาร์ตินฟาวเลอร์ของหนังสือรูปแบบของสถาปัตยกรรม Enterprise Application


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

19
+1 สำหรับความคิดเห็นของ @ Tibo นั่นเป็นปัญหาร้ายแรง การสืบทอดคลาสตารางทำให้สคีมาไม่ปกติ ฉันไม่เห็นด้วยกับการโต้แย้งว่า Concrete Table Inheritance เป็นอุปสรรคต่อ DRY SQLขัดขวาง DRY เนื่องจากไม่มีเครื่องมืออำนวยความสะดวก ทางออกคือการใช้ Database Toolkit (หรือเขียนของคุณเอง) เพื่อยกของหนักแทนที่จะเขียน SQL โดยตรง (จำไว้ว่าจริงๆแล้วมันเป็นเพียงภาษา DB interface) ท้ายที่สุดคุณยังไม่ได้เขียนแอปพลิเคชันองค์กรของคุณในการประกอบ
โจดังนั้น

18
@Tibo ประมาณ 3 จุดที่คุณสามารถใช้วิธีการอธิบายที่นี่: sqlteam.com/article/... , ตรวจสอบการจำกัด การสร้างแบบจำลองแบบทั้งสองส่วน
แอนดรู

4
@DanielVassallo ก่อนอื่นขอขอบคุณสำหรับคำตอบที่น่าทึ่ง 1 ข้อสงสัยว่าบุคคลนั้นมีกรมธรรม์ฉันจะรู้ได้อย่างไรว่า policy_motor หรือ policy_property วิธีหนึ่งคือการค้นหา policyId ในตารางย่อยทั้งหมด แต่ฉันเดาว่านี่เป็นวิธีที่ไม่ดีใช่ไหมวิธีการที่ถูกต้องควรเป็นอย่างไร
ThomasBecker

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

14

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

การตัดสินใจที่ดีที่สุดนั้นขึ้นอยู่กับจำนวนฟิลด์ที่คุณมีและวิธีที่คุณต้องการเขียน SQL ของคุณ พวกเขาทั้งหมดจะทำงาน หากคุณมีเพียงไม่กี่ฟิลด์ฉันก็อาจจะไปกับ # 1 ด้วยฟิลด์ "ล็อต" ฉันจะเอนตัวไปทาง # 2 หรือ # 3


+1: ตัวเลือกที่ 3 เป็นรูปแบบการถ่ายทอดที่ใกล้เคียงที่สุดและ IMO ที่ได้รับการปรับมาตรฐานมากที่สุด
RedFilter

ตัวเลือกของคุณ # 3 เป็นสิ่งที่ฉันหมายถึงโดยตัวเลือก # 2 มีหลายสาขาและบางส่วนก็จะมีเอนทิตีรองด้วย
Steve Jones

9

ด้วยข้อมูลที่ให้มาฉันจะสร้างแบบจำลองฐานข้อมูลเพื่อให้มีสิ่งต่อไปนี้:

นโยบาย

  • POLICY_ID (คีย์หลัก)

หนี้สิน

  • LIABILITY_ID (คีย์หลัก)
  • POLICY_ID (รหัสที่ต่างประเทศ)

คุณสมบัติ

  • PROPERTY_ID (คีย์หลัก)
  • POLICY_ID (รหัสที่ต่างประเทศ)

... และต่อไปเพราะฉันคาดว่าจะมีแอตทริบิวต์ที่แตกต่างกันที่เกี่ยวข้องกับแต่ละส่วนของนโยบาย มิฉะนั้นอาจมีSECTIONSตารางเดียวและนอกจากpolicy_idนั้นจะมีsection_type_code...

ไม่ว่าจะด้วยวิธีใดก็ตามสิ่งนี้จะช่วยให้คุณสามารถสนับสนุนส่วนเพิ่มเติมได้ตามนโยบาย ...

ฉันไม่เข้าใจสิ่งที่คุณพบว่าไม่น่าพึงพอใจเกี่ยวกับวิธีการนี้ - นี่คือวิธีที่คุณเก็บข้อมูลในขณะที่รักษาความสมบูรณ์ของการอ้างอิงและไม่ทำซ้ำข้อมูล คำว่า "ปกติ" ...

เนื่องจาก SQL เป็นฐานของ SET จึงค่อนข้างแปลกกับแนวคิดการเขียนโปรแกรมตามขั้นตอน / OO & ต้องการรหัสเพื่อเปลี่ยนจากอาณาจักรหนึ่งไปเป็นอีกอาณาจักรหนึ่ง บ่อยครั้งที่ ORM นั้นได้รับการพิจารณาแล้ว แต่มันทำงานได้ไม่ดีในระบบที่ซับซ้อนและมีปริมาณมาก


ใช่ฉันได้รับ normalization สิ่ง ;-) สำหรับโครงสร้างที่ซับซ้อนเช่นบางส่วนมีความเรียบง่ายและบางส่วนมีโครงสร้างย่อยที่ซับซ้อนของตัวเองดูเหมือนว่าไม่น่าเป็นไปได้ที่ ORM จะใช้ได้แม้ว่ามันจะดี
Steve Jones

6

นอกจากนี้ที่โซลูชัน Daniel Vassallo ถ้าคุณใช้ SQL Server 2016+ มีวิธีแก้ไขปัญหาอื่นที่ฉันใช้ในบางกรณีโดยไม่สูญเสียประสิทธิภาพ

คุณสามารถสร้างเพียงตารางที่มีเพียงฟิลด์ทั่วไปและเพิ่มคอลัมน์เดียวด้วยสตริงJSONที่มีฟิลด์เฉพาะประเภทย่อยทั้งหมด

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


1
นั่นเป็นแนวคิดที่น่าสนใจ ฉันยังไม่ได้ใช้ JSON ใน SQL Server แต่ใช้ที่อื่นมาก ขอบคุณสำหรับหัวขึ้น.
Steve Jones

5

อีกวิธีในการทำคือใช้INHERITSส่วนประกอบ ตัวอย่างเช่น:

CREATE TABLE person (
    id int ,
    name varchar(20),
    CONSTRAINT pessoa_pkey PRIMARY KEY (id)
);

CREATE TABLE natural_person (
    social_security_number varchar(11),
    CONSTRAINT pessoaf_pkey PRIMARY KEY (id)
) INHERITS (person);


CREATE TABLE juridical_person (
    tin_number varchar(14),
    CONSTRAINT pessoaj_pkey PRIMARY KEY (id)
) INHERITS (person);

ดังนั้นจึงเป็นไปได้ที่จะกำหนดมรดกระหว่างตาราง


DBs อื่นรองรับINHERITSนอกเหนือจากPostgreSQLหรือไม่ ตัวอย่างเช่นMySQL ?
giannis christofakis

1
@giannischristofakis: MySQL เป็นเพียงฐานข้อมูลเชิงสัมพันธ์ในขณะที่ Postgres เป็นฐานข้อมูลเชิงวัตถุ ดังนั้นไม่มี MySQL ไม่สนับสนุนสิ่งนี้ อันที่จริงฉันคิดว่า Postgres เป็น DBMS ปัจจุบันเท่านั้นที่สนับสนุนการสืบทอดประเภทนี้
a_horse_with_no_name

2
@ marco-paulo-ollivier คำถามของ OP เกี่ยวกับ SQL Server ดังนั้นฉันไม่เข้าใจว่าทำไมคุณจึงให้โซลูชันที่ใช้งานได้กับ Postgres เท่านั้น เห็นได้ชัดว่าไม่ได้แก้ไขปัญหา
mapto

@ แผนที่สำหรับคำถามนี้ได้กลายเป็นสิ่งที่ "มีลักษณะอย่างไรในการสืบทอดลักษณะ OO ในฐานข้อมูล" วิธีการกำหนดเป้ แต่เดิมเกี่ยวกับ sql server นั้นตอนนี้ไม่เกี่ยวข้อง
Caius Jard

0

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

นอกจากนี้ฉันไม่ทราบว่าคุณใช้ SQL Server รุ่นใด แต่ในปี 2008+ คอลัมน์แบบกระจายช่วยเพิ่มประสิทธิภาพในสถานการณ์ที่ค่าจำนวนมากในคอลัมน์จะเป็นค่า NULL

ท้ายที่สุดคุณจะต้องตัดสินใจว่า "คล้ายกัน" ในส่วนของนโยบายอย่างไร ฉันคิดว่าทางออกที่เป็นมาตรฐานมากขึ้นอาจมีปัญหามากกว่าที่ควรค่า ... แต่คุณเท่านั้นที่จะโทรออกได้ :)


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

คำว่า "ตารางส่วนที่รวมเป็นหนึ่ง" มาจากที่ใด Google แทบจะไม่แสดงผลลัพธ์ใด ๆ และมีคำศัพท์ที่สับสนอยู่แล้วที่นี่
Stephan-v

-1

หรือลองใช้ฐานข้อมูลเอกสาร (เช่น MongoDB) ซึ่งรองรับโครงสร้างข้อมูลที่หลากหลายและการซ้อนกัน


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