Tạo ràng buộc duy nhất với các cột null


252

Tôi có một bảng với bố cục này:

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

Tôi muốn tạo một ràng buộc duy nhất tương tự như thế này:

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

Tuy nhiên, điều này sẽ cho phép nhiều hàng với cùng (UserId, RecipeId), nếu MenuId IS NULL. Tôi muốn cho phép NULLtrong MenuIdđể lưu trữ một yêu thích đã không đơn liên quan, nhưng tôi chỉ muốn tối đa là một trong những hàng mỗi cặp người dùng / công thức.

Những ý tưởng tôi có cho đến nay là:

  1. Sử dụng một số UUID được mã hóa cứng (như tất cả các số không) thay vì null.
    Tuy nhiên, MenuIdcó một ràng buộc FK trên mỗi menu của người dùng, do đó tôi phải tạo một menu "null" đặc biệt cho mọi người dùng, điều này thật rắc rối.

  2. Kiểm tra sự tồn tại của một mục rỗng bằng cách sử dụng một kích hoạt thay thế.
    Tôi nghĩ rằng đây là một rắc rối và tôi thích tránh kích hoạt bất cứ nơi nào có thể. Ngoài ra, tôi không tin tưởng họ để đảm bảo dữ liệu của tôi không bao giờ ở trạng thái xấu.

  3. Chỉ cần quên nó đi và kiểm tra sự tồn tại trước đó của mục nhập null trong kho trung gian hoặc trong chức năng chèn và không có ràng buộc này.

Tôi đang sử dụng Postgres 9.0.

Có phương pháp nào tôi đang xem không?


Tại sao nó sẽ cho phép nhiều hàng có cùng ( UserId, RecipeId), nếu MenuId IS NULL?
Drux

Câu trả lời:


382

Tạo hai chỉ mục một phần :

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;

Bằng cách này, chỉ có thể có một sự kết hợp của (user_id, recipe_id)nơi menu_id IS NULL, thực hiện hiệu quả các ràng buộc mong muốn.

Hạn chế có thể có: bạn không thể có tham chiếu khóa ngoài (user_id, menu_id, recipe_id), bạn không thể dựa CLUSTERvào chỉ mục một phần và các truy vấn không có WHEREđiều kiện khớp có thể sử dụng chỉ mục một phần. (Có vẻ như bạn không muốn có một cột tham chiếu FK rộng ba cột - thay vào đó hãy sử dụng cột PK).

Nếu bạn cần một chỉ mục hoàn chỉnh , bạn có thể thay thế WHEREđiều kiện từ đó favo_3col_uni_idxvà các yêu cầu của bạn vẫn được thực thi.
Chỉ số, hiện bao gồm toàn bộ bảng, trùng lặp với bảng khác và trở nên lớn hơn. Tùy thuộc vào các truy vấn thông thường và tỷ lệ phần trăm của các NULLgiá trị, điều này có thể có hoặc không hữu ích. Trong các tình huống cực đoan, nó thậm chí có thể giúp duy trì cả ba chỉ số (hai chỉ số một phần và tổng số trên cùng).

Ngoài ra: Tôi khuyên không nên sử dụng các định danh trường hợp hỗn hợp trong PostgreSQL .


1
@Erwin Brandsetter: liên quan đến nhận xét "số nhận dạng trường hợp hỗn hợp ": Miễn là không sử dụng dấu ngoặc kép, sử dụng số nhận dạng hỗn hợp là hoàn toàn tốt. Không có sự khác biệt trong việc sử dụng tất cả các số nhận dạng chữ thường (một lần nữa: chỉ khi không có dấu ngoặc kép được sử dụng)
a_horse_with_no_name

14
@a_horse_with_no_name: Tôi giả sử bạn biết rằng tôi biết điều đó. Đó thực sự là một trong những lý do tôi khuyên chống lại việc sử dụng nó. Những người không biết chi tiết cụ thể rất bối rối, như trong các định danh RDBMS khác là (một phần) phân biệt chữ hoa chữ thường. Đôi khi người ta nhầm lẫn chính mình. Hoặc họ xây dựng SQL động và sử dụng quote_ident () như họ nên và quên chuyển định danh dưới dạng chuỗi chữ thường ngay bây giờ! Không sử dụng các định danh trường hợp hỗn hợp trong PostgreSQL, nếu bạn có thể tránh nó. Tôi đã thấy một số yêu cầu tuyệt vọng ở đây xuất phát từ sự điên rồ này.
Erwin Brandstetter

3
@a_horse_with_no_name: Vâng, điều đó tất nhiên là đúng. Nhưng nếu bạn có thể tránh chúng: bạn không muốn các định danh trường hợp hỗn hợp . Họ không phục vụ mục đích. Nếu bạn có thể tránh chúng: đừng sử dụng chúng. Vả lại: họ chỉ đơn giản là xấu xí. Trích dẫn nhận dạng là xấu xí, quá. Các định danh SQL92 có khoảng trắng trong đó là một sai lầm được tạo bởi một ủy ban. Đừng sử dụng chúng.
wildplasser

2
@Mike: Tôi nghĩ rằng bạn phải nói chuyện với ủy ban tiêu chuẩn SQL về điều đó, chúc may mắn :)
mu quá ngắn

1
@buffer: Chi phí bảo trì và tổng dung lượng lưu trữ về cơ bản là như nhau (ngoại trừ chi phí cố định nhỏ trên mỗi chỉ số). Mỗi hàng chỉ được đại diện trong một chỉ mục. Hiệu suất: Nếu kết quả của bạn kéo dài cả hai trường hợp, tổng chỉ số đơn giản có thể trả. Nếu không, một chỉ mục một phần thường nhanh hơn một chỉ mục hoàn chỉnh, chủ yếu là do kích thước nhỏ hơn. Thêm điều kiện chỉ mục vào các truy vấn (dự phòng) nếu Postgres không tìm ra nó có thể tự sử dụng một chỉ mục một phần. Thí dụ.
Erwin Brandstetter

75

Bạn có thể tạo một chỉ mục duy nhất với sự kết hợp trên MenuId:

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

Bạn chỉ cần chọn một UUID cho COALESCE sẽ không bao giờ xảy ra trong "cuộc sống thực". Bạn có thể không bao giờ thấy UUID bằng 0 trong cuộc sống thực nhưng bạn có thể thêm một ràng buộc KIỂM TRA nếu bạn bị hoang tưởng (và vì họ thực sự ra ngoài để giúp bạn ...):

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

1
Điều này mang lỗ hổng (lý thuyết), rằng các mục có menu_id = '00000000-0000-0000-0000-000000000000' có thể kích hoạt các vi phạm duy nhất sai - nhưng bạn đã giải quyết điều đó trong nhận xét của mình.
Erwin Brandstetter

2
@muistooshort: Yup, đó là một giải pháp thích hợp. Đơn giản hóa để (MenuId <> '00000000-0000-0000-0000-000000000000')mặc dù. NULLđược cho phép theo mặc định. Btw, có ba loại người. Những người hoang tưởng và những người không làm cơ sở dữ liệu. Loại thứ ba thỉnh thoảng đăng câu hỏi về SO trong sự hoang mang. ;)
Erwin Brandstetter

2
@Erwin: Ý bạn là "những người hoang tưởng và những người có cơ sở dữ liệu bị hỏng"?
mu quá ngắn

2
Giải pháp tuyệt vời này giúp dễ dàng bao gồm một cột null thuộc loại đơn giản hơn, chẳng hạn như số nguyên, trong một ràng buộc duy nhất.
Markus Pscheidt

2
Đúng là một UUID sẽ không xuất hiện với chuỗi cụ thể đó, không chỉ vì các xác suất liên quan, mà còn bởi vì đó không phải là một UUID hợp lệ . Trình tạo UUID không được tự do sử dụng bất kỳ chữ số hex nào ở bất kỳ vị trí nào, ví dụ: một vị trí được dành riêng cho số phiên bản của UUID.
Toby 1 Kenobi

1

Bạn có thể lưu trữ các mục yêu thích mà không có menu liên quan trong một bảng riêng biệt:

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

Một ý tưởng thú vị. Nó làm cho việc chèn một chút phức tạp hơn. Tôi sẽ cần kiểm tra nếu một hàng đã tồn tại trong FavoriteWithoutMenuđầu tiên. Nếu vậy, tôi chỉ cần thêm một liên kết menu - nếu không tôi tạo FavoriteWithoutMenuhàng trước và sau đó liên kết nó với menu nếu cần thiết. Điều này cũng khiến việc chọn tất cả các mục yêu thích trong một truy vấn trở nên rất khó khăn: Tôi phải làm điều gì đó kỳ lạ như chọn tất cả các liên kết menu trước, sau đó chọn tất cả các Mục ưa thích có ID không tồn tại trong truy vấn đầu tiên. Tôi không chắc là tôi thích điều đó.
Mike Christensen

Tôi không nghĩ việc chèn càng phức tạp. Nếu bạn muốn chèn một bản ghi với NULL MenuId, bạn chèn vào bảng này. Nếu không, để Favoritesbàn. Nhưng truy vấn, vâng, nó sẽ phức tạp hơn.
ypercubeᵀᴹ

Trên thực tế, việc chọn tất cả các mục yêu thích sẽ chỉ là một tham gia TRÁI PHIẾU để có được menu. Hmm yea đây có thể là con đường để đi ..
Mike Christensen

INSERT trở nên phức tạp hơn nếu bạn muốn thêm cùng một công thức vào nhiều menu, vì bạn có một ràng buộc KHÔNG GIỚI HẠN đối với UserId / RecipeId trên FavoritesWithoutMothy. Tôi chỉ cần tạo hàng này nếu nó chưa tồn tại.
Mike Christensen

1
Cảm ơn! Câu trả lời này xứng đáng được +1 vì nó giống như một thứ SQL thuần cơ sở dữ liệu chéo .. Tuy nhiên, trong trường hợp này tôi sẽ đi theo con đường chỉ mục một phần vì nó không yêu cầu thay đổi lược đồ của tôi và tôi thích nó :)
Mike Christensen

-1

Tôi nghĩ rằng có một vấn đề ngữ nghĩa ở đây. Theo quan điểm của tôi, người dùng có thể có một (nhưng chỉ một ) công thức yêu thích để chuẩn bị một thực đơn cụ thể. (OP có menu và công thức trộn lẫn; nếu tôi sai: vui lòng trao đổi MenuId và RecipeId bên dưới) Điều đó ngụ ý rằng {user, menu} phải là một khóa duy nhất trong bảng này. Và nó nên chỉ ra chính xác một công thức. Nếu người dùng không có công thức yêu thích cho menu cụ thể này thì không có hàng nào tồn tại cho cặp khóa {user, menu} này. Ngoài ra: khóa thay thế (FaVouRiteId) là không cần thiết: các khóa chính tổng hợp hoàn toàn hợp lệ cho các bảng ánh xạ quan hệ.

Điều đó sẽ dẫn đến định nghĩa bảng giảm:

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
Vâng, điều này đúng. Ngoại trừ, trong trường hợp của tôi, tôi muốn hỗ trợ có một món ưa thích không thuộc về bất kỳ menu nào. Hãy tưởng tượng nó giống như Dấu trang của bạn trong trình duyệt của bạn. Bạn chỉ có thể "đánh dấu" một trang. Hoặc, bạn có thể tạo các thư mục con của dấu trang và đặt tiêu đề cho chúng những thứ khác nhau. Tôi muốn cho phép người dùng yêu thích một công thức hoặc tạo các thư mục con của các mục yêu thích được gọi là menu.
Mike Christensen

1
Như tôi đã nói: đó là tất cả về ngữ nghĩa. (Tôi đã suy nghĩ về thực phẩm, rõ ràng) Có một yêu thích "không thuộc về bất kỳ thực đơn nào" không có ý nghĩa với tôi. Bạn không thể ủng hộ một cái gì đó không tồn tại, IMHO.
wildplasser

Có vẻ như một số chuẩn hóa db có thể giúp đỡ. Tạo một bảng thứ hai liên quan đến công thức nấu ăn với các menu (hoặc không). Mặc dù nó khái quát hóa vấn đề và cho phép nhiều hơn một menu mà một công thức có thể là một phần của. Bất kể, câu hỏi là về các chỉ mục duy nhất trong PostgreSQL. Cảm ơn.
Chris
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.