列が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。メニューが関連付けられていないお気に入りを保存できるようNULLMenuIdしたいのですが、ユーザー/レシピのペアごとにこれらの行の最大1つだけが必要です。

私のこれまでの考えは次のとおりです。

  1. nullではなく、ハードコードされたUUID(すべてゼロなど)を使用します。
    ただし、MenuId各ユーザーのメニューにはFK制約があるため、ユーザーごとに特別な「null」メニューを作成する必要があり、面倒です。

  2. 代わりにトリガーを使用して、nullエントリの存在を確認してください。
    これは面倒で、トリガーをできるだけ避けたいと思います。加えて、私は彼らが私のデータが決して悪い状態にあることを保証することを信頼していません。

  3. それを忘れて、ミドルウェアまたは挿入関数にnullエントリが以前に存在するかどうかを確認してください。この制約はありません。

Postgres 9.0を使用しています。

私が見落としている方法はありますか?


なぜ同じ(UserIdRecipeId)の複数の行を許可するのMenuId IS NULLですか?
Drux

回答:


382

2つの部分インデックスを作成します

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、whereの組み合わせは1つしかなく、目的の制約を効果的に実装できます。

考えられる欠点:外部キー参照(user_id, menu_id, recipe_id)を使用できずCLUSTER、部分インデックスに基づくことができず、一致WHERE条件のないクエリでは部分インデックスを使用できません。(3列幅のFK参照が必要になる可能性は低いようです。代わりにPK列を使用してください)。

完全なインデックスが必要な場合は、代わりにWHERE条件を削除しfavo_3col_uni_idxても、要件は引き続き適用されます。
インデックスはテーブル全体で構成され、他のインデックスと重複して大きくなります。一般的なクエリとNULL値のパーセンテージによっては、これが役立つ場合と役に立たない場合があります。極端な状況では、3つすべてのインデックス(2つの部分的なインデックスと全体のインデックス)を維持することも役立つ場合があります。

余談ですが、PostgreSQLでは大文字と小文字が混在する識別子を使用しないことをお勧めします


1
@Erwin Brandsetter:「大文字と小文字が混在する識別子について」備考:二重引用符が使用されていない限り、大文字と小文字が混在する識別子を使用しても問題ありません。すべての小文字の識別子の使用に違いはありません(ここでも、引用符が使用されていない場合のみ
a_horse_with_no_name

14
@a_horse_with_no_name:私はあなたがそれを知っていることを知っていると思います。それが、私がその使用に反対する理由の1つです。他のRDBMS識別子のように(一部)大文字と小文字が区別されるため、詳細をあまり知らない人は混乱します。時々人々は混乱します。または、動的SQLを構築し、必要に応じてquote_ident()を使用して、識別子を小文字の文字列として渡すことを忘れます!回避できる場合は、PostgreSQLで大文字と小文字が混在する識別子を使用しないでください。私はこの愚かさに起因する多くの絶望的な要求をここで見ました。
Erwin Brandstetter、2011年

3
@a_horse_with_no_name:はい、もちろんそうです。しかし、それらを避けることができる場合は、大文字と小文字が混在する識別子は必要ありません。彼らは何の役にも立ちません。それらを回避できる場合は、使用しないでください。その上、それらは単に醜いです。引用された識別も醜いです。スペースが含まれているSQL92識別子は、委員会が犯した誤りです。それらを使用しないでください。
wildplasser

2
@マイク:それについてSQL標準委員会と話し合う必要があると思います。幸運を祈ります:)
muは短すぎます

1
@buffer:メンテナンスコストと合計ストレージは基本的に同じです(インデックスごとのマイナーな固定オーバーヘッドを除く)。各行は1つのインデックスでのみ表されます。パフォーマンス:結果が両方のケースにまたがる場合、追加のプレーンインデックスの合計が支払われる場合があります。そうでない場合、主にサイズが小さいため、部分インデックスは通常、完全インデックスよりも高速です。Postgresが単独で部分インデックスを使用できることがわからない場合は、インデックス条件をクエリに追加します(冗長)。例。
Erwin Brandstetter 2014年

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、2011年

2
@muistooshort:うん、それは適切な解決策です。(MenuId <> '00000000-0000-0000-0000-000000000000')しかし、単純化してください。NULLデフォルトで許可されています。ところで、人には3種類あります。妄想的な人、そしてデータベースをしない人。3番目の種類は時々困惑してSOに質問を投稿します。;)
Erwin Brandstetter、2011年

2
@アーウィン:「パラノイアなものとデータベースが壊れているもの」という意味ではないですか?
muが短すぎる

2
この優れたソリューションにより、整数などのより単純なタイプのnull列を一意制約に非常に簡単に含めることができます。
Markus Pscheidt、2015

2
関係する確率のためだけでなく、有効なUUIDではないため、UUIDがその特定の文字列を思い付かないことは事実です。UUIDジェネレーターは、任意の位置で16進数字を自由に使用できません。たとえば、1つの位置がUUIDのバージョン番号用に予約されています。
トビー1ケノービ

1

メニューが関連付けられていないお気に入りを別のテーブルに保存できます。

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

面白いアイデア。挿入が少し複雑になります。FavoriteWithoutMenu最初に行が既に存在するかどうかを確認する必要があります。その場合は、メニューリンクを追加するだけです。それ以外の場合は、FavoriteWithoutMenu最初に行を作成し、必要に応じてメニューにリンクします。また、1つのクエリですべてのお気に入りを選択することは非常に困難になります。最初にすべてのメニューリンクを選択し、次に最初のクエリ内にIDが存在しないすべてのお気に入りを選択するなど、奇妙なことを行う必要があります。それが好きかどうかわかりません。
Mike Christensen

挿入はそれほど複雑ではないと思います。でレコードを挿入する場合はNULL MenuId、このテーブルに挿入します。そうでない場合は、Favoritesテーブルに。しかし、クエリを実行すると、はい、より複雑になります。
ypercubeᵀᴹ

実際にそれをスクラッチして、すべてのお気に入りを選択すると、メニューを取得するための単一の左結合になります。うーんこれは行く方法かもしれない..
マイク・クリステンセン

FavoriteWithoutMenuのUserId / RecipeIdにUNIQUE制約があるため、同じレシピを複数のメニューに追加する場合、INSERTはより複雑になります。この行がまだ存在していない場合にのみ、この行を作成する必要があります。
Mike Christensen

1
ありがとう!この答えは、クロスデータベースの純粋なSQLに近いため、+ 1に値します。ただし、この場合は、スキーマに変更を加える必要がないため、部分インデックスルートを使用します。気に入ったのは:)
Mike Christensen

-1

ここには意味上の問題があると思います。私の見解では、ユーザーは特定のメニューを準備するためのお気に入りのレシピ(1つのみ)を持つことができます。(OPにはメニューとレシピが混在しています。間違っている場合は、以下のMenuIdとRecipeIdを入れ替えてください)これは、{user、menu}がこのテーブルの一意のキーであることを意味します。そして、それは正確に1つのレシピを指す必要があります。ユーザーがこの特定のメニューのお気に入りのレシピを持っていない場合、この{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
私が言ったように:それはすべて意味論です。(明らかに、食べ物について考えていました)「どのメニューにも属さない」というお気に入りがあることは、私には意味がありません。あなたは存在しないものを好むことはできません、私見。
wildplasser

いくつかのdb正規化が役立つと思われます。レシピとメニューを関連付ける(または関連付けない)2番目のテーブルを作成します。それは問題を一般化し、レシピが含まれる可能性がある複数のメニューを可能にしますが。とにかく、質問はPostgreSQLの一意のインデックスに関するものでした。ありがとう。
Chris
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.