สร้างข้อ จำกัด ที่ไม่ซ้ำกับคอลัมน์ null


252

ฉันมีตารางที่มีเค้าโครงนี้:

CREATE TABLE Favorites
(
  FavoriteId uuid NOT NULL PRIMARY KEY,
  UserId uuid NOT NULL,
  RecipeId uuid NOT NULL,
  MenuId uuid
)

ฉันต้องการสร้างข้อ จำกัด ที่ไม่ซ้ำแบบนี้:

ALTER TABLE Favorites
ADD CONSTRAINT Favorites_UniqueFavorite UNIQUE(UserId, MenuId, RecipeId);

แต่นี้จะช่วยให้หลายแถวด้วยเหมือนกันถ้า(UserId, RecipeId) MenuId IS NULLผมต้องการที่จะช่วยให้NULLในMenuIdการจัดเก็บที่ชื่นชอบที่ไม่มีเมนูที่เกี่ยวข้อง แต่ฉันเพียงต้องการมากที่สุดคนหนึ่งของแถวเหล่านี้ต่อผู้ใช้ / คู่สูตร

ความคิดที่ฉันมีคือ:

  1. ใช้ UUID ที่กำหนดค่าตายตัวบางอย่าง (เช่นศูนย์ทั้งหมด) แทนค่า Null
    อย่างไรก็ตามMenuIdมีข้อ จำกัด FK ในเมนูของผู้ใช้แต่ละคนดังนั้นฉันต้องสร้างเมนู "null" พิเศษสำหรับผู้ใช้ทุกคนที่สร้างความยุ่งยาก

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

  3. เพียงแค่ลืมมันและตรวจสอบการมีอยู่ของรายการ null ก่อนหน้านี้ในมิดเดิลแวร์หรือในฟังก์ชั่นแทรกและไม่มีข้อ จำกัด นี้

ฉันใช้ Postgres 9.0

มีวิธีใดบ้างที่ฉันมองเห็น?


ทำไมมันจึงเป็นที่จะช่วยให้หลายแถวเดียวกับที่ ( UserId, RecipeId) ถ้าMenuId IS NULL?
Drux

คำตอบ:


382

สร้างดัชนีบางส่วนสองรายการ :

CREATE UNIQUE INDEX favo_3col_uni_idx ON favorites (user_id, menu_id, recipe_id)
WHERE menu_id IS NOT NULL;

CREATE UNIQUE INDEX favo_2col_uni_idx ON favorites (user_id, recipe_id)
WHERE menu_id IS NULL;

ด้วยวิธีนี้สามารถมีได้เพียงหนึ่งการรวมกันของ(user_id, recipe_id)ที่ซึ่งmenu_id IS NULLการใช้ข้อ จำกัด ที่ต้องการอย่างมีประสิทธิภาพ

ข้อเสียที่เป็นไปได้: คุณไม่สามารถอ้างอิงคีย์ต่างประเทศ(user_id, menu_id, recipe_id)ได้คุณไม่สามารถอ้างอิงCLUSTERดัชนีบางส่วนได้และแบบสอบถามที่ไม่มีWHEREเงื่อนไขการจับคู่ไม่สามารถใช้ดัชนีบางส่วนได้ (ดูเหมือนว่าคุณไม่ต้องการให้มีการอ้างอิง FK สามคอลัมน์กว้าง ๆ ให้ใช้คอลัมน์ PK แทน)

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

นอกเหนือ: ผมแนะนำที่จะไม่ใช้ตัวระบุกรณีที่ผสมใน PostgreSQL


1
@Erwin Brandsetter: เกี่ยวกับคำพูด " ตัวระบุตัวพิมพ์เล็กและตัวน้อย ": ตราบใดที่ไม่มีการใช้เครื่องหมายคำพูดคู่ ไม่มีความแตกต่างในการใช้ตัวระบุตัวพิมพ์เล็กทั้งหมด (อีกครั้ง: เฉพาะเมื่อไม่มีการใช้เครื่องหมายคำพูด)
a_horse_with_no_name

14
@a_horse_with_no_name: ฉันถือว่าคุณรู้ว่าฉันรู้ ที่เป็นจริงหนึ่งในเหตุผลที่ผมแนะนำให้กับการใช้งานของมัน ผู้ที่ไม่ทราบรายละเอียดต่าง ๆ สับสนอย่างมากเช่นเดียวกับในตัวระบุ RDBMS อื่น ๆ (บางส่วน) เป็นกรณี ๆ ไป บางครั้งคนสับสนตัวเอง หรือพวกเขาสร้างไดนามิก SQLและใช้quote_ident ()เท่าที่ควรและลืมส่งตัวระบุเป็นสตริงตัวพิมพ์เล็กตอนนี้! อย่าใช้ตัวบ่งชี้ตัวพิมพ์ผสมใน PostgreSQL หากคุณสามารถหลีกเลี่ยงได้ ฉันได้เห็นคำขอที่สิ้นหวังจำนวนมากที่นี่เกิดจากความเขลานี้
Erwin Brandstetter

3
@a_horse_with_no_name: ใช่นั่นเป็นเรื่องจริง แต่ถ้าคุณสามารถหลีกเลี่ยงพวกเขา: คุณไม่ต้องการตัวระบุกรณีผสม พวกเขาไม่มีจุดประสงค์ หากคุณสามารถหลีกเลี่ยงพวกเขา: อย่าใช้พวกเขา นอกจากนี้: พวกเขาเป็นเพียงน่าเกลียดธรรมดา การระบุที่อ้างถึงนั้นน่าเกลียดเช่นกัน ตัวระบุ SQL92 ที่มีช่องว่างในนั้นเป็นความผิดพลาดที่จัดทำโดยคณะกรรมการ อย่าใช้มัน
wildplasser

2
@ ไมค์: ฉันคิดว่าคุณต้องพูดคุยกับคณะกรรมการมาตรฐาน SQL เกี่ยวกับเรื่องนี้โชคดี :)
mu สั้นเกินไป

1
@buffer: ค่าใช้จ่ายในการบำรุงรักษาและการจัดเก็บทั้งหมดนั้นเท่ากัน (ยกเว้นค่าใช้จ่ายคงที่เล็กน้อยต่อดัชนี) แต่ละแถวจะแสดงในดัชนีเดียวเท่านั้น ประสิทธิภาพการทำงาน: หากผลลัพธ์ของคุณครอบคลุมทั้งสองกรณีดัชนีธรรมดาทั้งหมดเพิ่มเติมอาจจ่าย หากไม่เป็นเช่นนั้นดัชนีบางส่วนจะเร็วกว่าดัชนีสมบูรณ์เนื่องจากส่วนใหญ่มีขนาดเล็กกว่า เพิ่มเงื่อนไขดัชนีลงในคิวรี (ซ้ำซ้อน) หาก Postgres ไม่เข้าใจว่าสามารถใช้ดัชนีบางส่วนได้ด้วยตัวเอง ตัวอย่าง.
Erwin Brandstetter

75

คุณสามารถสร้างดัชนีที่ไม่ซ้ำกับ coalesce ใน MenuId:

CREATE UNIQUE INDEX
Favorites_UniqueFavorite ON Favorites
(UserId, COALESCE(MenuId, '00000000-0000-0000-0000-000000000000'), RecipeId);

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

alter table Favorites
add constraint check
(MenuId <> '00000000-0000-0000-0000-000000000000')

1
สิ่งนี้มีข้อบกพร่อง (ตามทฤษฎี) ว่ารายการที่มี menu_id = '00000000-0000-0000-0000-0000-000000000000' สามารถก่อให้เกิดการละเมิดที่ไม่ซ้ำกันได้ แต่คุณได้ระบุไว้ในความคิดเห็นของคุณแล้ว
Erwin Brandstetter

2
@muistooshort: ใช่นั่นเป็นวิธีแก้ปัญหาที่เหมาะสม ทำให้ง่ายขึ้น(MenuId <> '00000000-0000-0000-0000-000000000000')แม้ว่า NULLได้รับอนุญาตโดยค่าเริ่มต้น Btw มีสามคน คนหวาดระแวงและคนที่ไม่ได้ทำฐานข้อมูล ประเภทที่สามเป็นครั้งคราวโพสต์คำถามเกี่ยวกับ SO ในความสับสน ;)
Erwin Brandstetter

2
@Erwin: คุณไม่ได้หมายถึง "คนหวาดระแวงและคนที่มีฐานข้อมูลที่เสียหาย"
mu สั้นเกินไป

2
โซลูชันที่ยอดเยี่ยมนี้ทำให้การรวมคอลัมน์ null เป็นประเภทที่ง่ายกว่าเช่นจำนวนเต็มในข้อ จำกัด ที่ไม่ซ้ำกันนั้นง่ายมาก
Markus Pscheidt

2
มันเป็นความจริงว่า UUID เคยชินเกิดขึ้นกับสตริงที่โดยเฉพาะอย่างยิ่งไม่เพียงเพราะความน่าจะเป็นที่เกี่ยวข้อง แต่ยังเพราะมันไม่ได้เป็น UUID ตัวสร้าง UUID ไม่สามารถใช้เลขฐานสิบหกใดก็ได้ในตำแหน่งใด ๆ ตัวอย่างเช่นตำแหน่งหนึ่งสงวนไว้สำหรับหมายเลขเวอร์ชันของ UUID
Toby 1 Kenobi

1

คุณสามารถจัดเก็บรายการโปรดโดยไม่มีเมนูที่เกี่ยวข้องในตารางแยก:

CREATE TABLE FavoriteWithoutMenu
(
  FavoriteWithoutMenuId uuid NOT NULL, --Primary key
  UserId uuid NOT NULL,
  RecipeId uuid NOT NULL,
  UNIQUE KEY (UserId, RecipeId)
)

ความคิดที่น่าสนใจ มันทำให้การแทรกซับซ้อนขึ้นเล็กน้อย ฉันจะต้องตรวจสอบว่าแถวมีอยู่แล้วในFavoriteWithoutMenuครั้งแรก ถ้าเป็นเช่นนั้นฉันเพิ่งเพิ่มลิงค์เมนู - ไม่เช่นนั้นฉันจะสร้างFavoriteWithoutMenuแถวแรกแล้วเชื่อมโยงไปยังเมนูถ้าจำเป็น นอกจากนี้ยังทำให้การเลือกรายการโปรดทั้งหมดในแบบสอบถามเดียวยากมาก: ฉันต้องทำสิ่งแปลก ๆ เช่นเลือกลิงก์เมนูทั้งหมดก่อนจากนั้นเลือกรายการโปรดทั้งหมดที่ไม่มี ID ในการสืบค้นแรก ฉันไม่แน่ใจว่าฉันชอบที่
Mike Christensen

ฉันไม่คิดว่าการแทรกจะซับซ้อนเท่าไหร่ ถ้าคุณต้องการที่จะแทรกบันทึกด้วยNULL MenuIdคุณแทรกลงในตารางนี้ ถ้าไม่ไปที่Favoritesตาราง แต่การสืบค้นใช่มันจะซับซ้อนมากขึ้น
ypercubeᵀᴹ

ในความเป็นจริงแล้วการเลือกรายการโปรดทั้งหมดจะเป็นการเข้าร่วมด้านซ้ายเพียงครั้งเดียวเพื่อรับเมนู อืมใช่นี่อาจเป็นวิธีที่จะไป ..
Mike Christensen

INSERT จะซับซ้อนมากขึ้นถ้าคุณต้องการเพิ่มสูตรเดียวกันในเมนูมากกว่าหนึ่งเมนูเนื่องจากคุณมีข้อ จำกัด UNIQUE ใน UserId / RecipeId บน FavoriteWithoutMenu ฉันต้องการสร้างแถวนี้เฉพาะเมื่อยังไม่มีอยู่
Mike Christensen

1
ขอบคุณ! คำตอบนี้ควรได้รับ +1 เพราะมันเป็นมากกว่าสิ่งบริสุทธิ์ SQL ข้ามฐานข้อมูล .. อย่างไรก็ตามในกรณีนี้ฉันจะไปตามเส้นทางดัชนีบางส่วนเพราะมันไม่จำเป็นต้องเปลี่ยนแปลง schema ของฉันและฉันชอบ :)
Mike Christensen

-1

ฉันคิดว่ามีปัญหาความหมายที่นี่ ในมุมมองของฉันผู้ใช้สามารถมีสูตรที่ชื่นชอบ(แต่เพียงหนึ่ง ) ในการเตรียมเมนูเฉพาะ (OP มีเมนูและสูตรผสมกันถ้าฉันผิด: โปรดแลกเปลี่ยน MenuId และ RecipeId ด้านล่าง) ซึ่งหมายความว่า {ผู้ใช้เมนู} ควรเป็นคีย์เฉพาะในตารางนี้ และควรชี้ไปที่สูตรหนึ่งอย่างแน่นอน หากผู้ใช้ไม่มีสูตรอาหารที่โปรดปรานสำหรับเมนูนี้ไม่ควรมีแถวสำหรับคู่คีย์ {ผู้ใช้เมนู} นี้ นอกจากนี้: คีย์ตัวแทน (FaVouRiteId) ไม่จำเป็น: คีย์หลักแบบคอมโพสิตใช้ได้อย่างสมบูรณ์แบบสำหรับตารางการทำแผนที่สัมพันธ์

นั่นจะนำไปสู่คำจำกัดความของตารางที่ลดลง:

CREATE TABLE Favorites
( UserId uuid NOT NULL REFERENCES users(id)
, MenuId uuid NOT NULL REFERENCES menus(id)
, RecipeId uuid NOT NULL REFERENCES recipes(id)
, PRIMARY KEY (UserId, MenuId)
);

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

1
อย่างที่ฉันพูด: มันเกี่ยวกับความหมาย (ฉันกำลังคิดเกี่ยวกับอาหารชัด ๆ ) มีรายการโปรด "ที่ไม่ได้อยู่ในเมนูใด ๆ " ทำให้ฉัน คุณไม่สามารถชอบสิ่งที่ไม่มีอยู่ IMHO
wildplasser

ดูเหมือนว่าการปรับสภาพฐานข้อมูลบางอย่างอาจช่วยได้ สร้างตารางที่สองที่สัมพันธ์กับสูตรอาหาร (หรือไม่) แม้ว่ามันจะทำให้ปัญหาทั่วไปและอนุญาตให้มีมากกว่าหนึ่งเมนูที่สูตรอาจเป็นส่วนหนึ่งของ ไม่ว่าคำถามจะเกี่ยวกับดัชนีเฉพาะใน PostgreSQL ขอบคุณ
Chris
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.