널 (null) 열로 고유 제한 조건 작성


251

이 레이아웃이있는 테이블이 있습니다.

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)if 가 동일한 여러 행이 허용 됩니다 MenuId IS NULL. 내가 허용 할 NULLMenuId어떤 메뉴를 관련 지을 수있는 즐겨 찾기를 저장하지만, 난 단지 사용자 / 조리법 쌍 당이 행의 대부분 하나를 원한다.

지금까지 내가 가진 아이디어는 다음과 같습니다.

  1. null 대신 하드 코딩 된 UUID (예 : 모든 0)를 사용하십시오.
    그러나 MenuId각 사용자의 메뉴에 FK 제약이 있으므로 번거로운 모든 사용자에 대해 특별한 "널"메뉴를 만들어야합니다.

  2. 대신 트리거를 사용하여 널 항목이 있는지 확인하십시오.
    나는 이것이 번거롭고 가능한 한 트리거를 피하는 것을 좋아합니다. 또한 데이터가 결코 나쁜 상태에 있지 않다는 것을 보증하지 않습니다.

  3. 이를 잊고 미들웨어 또는 삽입 함수에 널 항목이 존재하는지 점검하십시오.이 제한 조건이 없습니다.

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)where menu_id IS NULL제약 조건을 효과적으로 구현하는 where 조합은 하나만 가능 합니다.

가능한 단점 : 외래 키 참조 (user_id, menu_id, recipe_id)를 가질 수없고 CLUSTER부분 인덱스를 기반 으로 할 수 없으며 일치하는 WHERE조건이없는 쿼리 는 부분 인덱스를 사용할 수 없습니다. (3 열 너비의 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

MenuId에서 통합 된 고유 인덱스를 만들 수 있습니다.

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

"실제"에서는 발생하지 않는 COALESCE에 대한 UUID를 선택하기 만하면됩니다. 실생활에서는 UUID가 전혀 보이지 않지만 편집증이라면 CHECK 제약 조건을 추가 할 수 있습니다 ( 실제로 당신을 얻을 수 있기 때문에 ...) :

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

1
이것은 menu_id = '00000000-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 생성기는 임의의 위치에서 16 진수를 자유롭게 사용할 수 없습니다. 예를 들어 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가없는 모든 즐겨 찾기를 선택하는 것과 같은 이상한 일을해야합니다. 내가 좋아하는지 모르겠습니다.
마이크 크리스텐슨

삽입이 더 복잡하다고 생각하지 않습니다. 로 레코드를 삽입 NULL MenuId하려면이 테이블에 삽입하십시오. 그렇지 않은 경우 Favorites테이블로. 그러나 쿼리는 더 복잡 할 것입니다.
ypercubeᵀᴹ

실제로 모든 즐겨 찾기를 선택하면 하나의 왼쪽 조인이 메뉴를 얻는 것입니다. 흠 그래, 이것은 갈 길일지도 모른다.
Mike Christensen

FavoriteWithoutMenu의 UserId / RecipeId에 ​​대한 UNIQUE 제약 조건이 있으므로 동일한 레시피를 둘 이상의 메뉴에 추가하려는 경우 INSERT가 더 복잡해집니다. 이 행이 아직없는 경우에만이 행을 작성해야합니다.
마이크 크리스텐슨

1
감사! 이 답변은 데이터베이스 간 순수한 SQL 일이기 때문에 +1이 필요합니다. 그러나이 경우 스키마에 대한 변경이 필요하지 않기 때문에 부분 인덱스 경로를 사용하고 싶습니다.)
Mike Christensen

-1

여기에 의미 론적 문제가 있다고 생각합니다. 내 관점에서, 사용자는 (하지만 할 수 있습니다 하나의 특정 메뉴를 준비하는) 마음에 드는 조리법을. (OP에는 메뉴와 레시피가 섞여 있습니다. 틀린 경우 아래의 MenuId와 RecipeId를 바꾸십시오.) 이는 {user, menu}가이 테이블에서 고유 한 키 여야 함을 의미합니다. 그리고 정확히 하나의 레시피를 가리켜 야합니다 . 사용자가이 특정 메뉴에 대해 선호하는 레시피가없는 경우이 {user, menu} 키 쌍에 대한 행이 없어야합니다 . 또한 대리 키 (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
네 맞습니다. 내 경우에는 메뉴에 속하지 않는 즐겨 찾기를 지원하고 싶습니다. 브라우저의 책갈피와 같다고 상상해보십시오. 페이지를 "책갈피"할 수도 있습니다. 또는 책갈피의 하위 폴더를 작성하고 다른 제목을 지정할 수 있습니다. 사용자가 레시피를 좋아하거나 메뉴라는 즐겨 찾기의 하위 폴더를 만들도록하고 싶습니다.
마이크 크리스텐슨

1
내가 말했듯이 : 그것은 의미론에 관한 것입니다. (나는 분명히 음식에 대해 생각하고 있었다) "어떤 메뉴에 속하지 않는"좋아하는 것을 갖는 것은 나에게 의미가 없다. 당신은 존재하지 않는 것을 좋아할 수 없습니다, IMHO.
wildplasser

일부 DB 정규화가 도움이 될 것 같습니다. 레시피를 메뉴에 관련시키는 (또는 관련이없는) 두 번째 테이블을 만듭니다. 그것은 문제를 일반화하고 레시피가 포함될 수있는 하나 이상의 메뉴를 허용합니다. 그럼에도 불구하고 문제는 PostgreSQL의 고유 인덱스에 관한 것입니다. 감사.
Chris
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.