web-dev-qa-db-ja.com

テーブル全体で一意

ユーザーを含むテーブルがあります。各ユーザーには、プライマリメールと、ユーザーが削除されているかどうかを示すフラグがあります(ユーザーを完全に削除することはありません)。

ただし、各ユーザーは追加のメールを受け取ることもできます。

いずれにせよ、電子メールアドレスは一意である必要があり、これをデータベースレベルで適用したいと思います。簡単なメインのメールアドレスの場合。 WHERE not is_deleted制限付きのUNIQUEインデックスを追加するだけで、削除されたユーザーのメールを再利用できます。

ただし、より注意が必要な予備のメールについては、.

  • それらを配列フィールドの同じテーブルに格納した場合、おそらくそれらをインデックス化する機能を失うだけでなく(ユーザー数が5万人を超え、電子メールで検索できる必要があるため、これは立ち入り禁止です)、私が知っているように、配列に入る一意のインデックス/制約を置くこともできません。
  • 別のテーブルを使用する場合(これは明らかにすっきりしています)、is_deletedフラグをユーザーからそのテーブルに複製する必要がありますが、これも非常に醜いですが、WHERE not is_deleted

私がやろうとしていることを達成するためのより良い解決策はありますか?

3
ThiefMaster

これを理解する1つの方法は、わずかに異なるルールを持つ2つのユーザークラスを持っているということです。削除されたユーザーの電子メールは衝突する可能性があり、削除されたユーザーの電子メールは一意でなければなりません。

これら2つのクラスには異なる規則(つまり、制約)があるため、ユーザーを削除するかどうかを示すフラグを使用する代わりに、テーブルを複製します。1つは削除されたユーザー用、もう1つは削除されていないユーザー用です。次に、削除されていないユーザーの電子メールのテーブルに一意の制約を作成します。

利点:

  • ほとんどの操作が「削除されていない」ユーザーに対するものであるという想定の下で、このモデルはより優れたパフォーマンスを提供します。 「削除されていない」ユーザーテーブルは、現在のアクティブユーザーのセットを表すだけなので、それに応じて拡大および縮小します。削除されたユーザーのテーブルは、ユーザーの削除を取り消すことを許可しない限り、成長するだけです。
  • すべてのユーザーのクエリはまだ比較的簡単です。以前と同じように簡単にしたい場合は、2つのセットを結合するビューを作成できます(行のソースに応じてis_deletedフラグを生成します)。
  • これはCREATE INDEX...WHEREに依存しません。私がこれに言及する理由は、部分インデックスが普遍的にサポートされていないためです(たとえば、MS-SQLおよびDB2)。明らかに、PostgreSQLに問題はありませんが、将来の移行の潜在的なハードルを回避することは決して害にはなりません。

短所:

  • 削除された(または削除を取り消す)ユーザーはさらに複雑になります。単純なフラグフィールドを反転する代わりに、行を移動する必要があります。ただし、SQLAlchemyを使用しているとおっしゃっていました(良い選択です!)。私が正しく思い出せば、これは INSERT..SELECT FROM のようなステートメントを構築するのに問題はありません。これにより、エントリを削除されていないものから削除されたものに(またはその逆に)移動することが簡単になります。
  • ユーザーの2倍の数のテーブル(+ユニオンのオプションのビュー)。これはパフォーマンスには影響しませんが、スキーマ図が複雑になる可能性があります。
3
Dave Jones

一意のメールアドレスを適用するには、remove競合するすべてのメール列を1つの中央のemailに格納しますすべてのアクティブなメールのテーブル。削除されたメールの別のテーブル:

_CREATE TABLE users (
  user_id  serial PRIMARY KEY
, username text UNIQUE NOT NULL
, email    text UNIQUE -- FK added below  -- can also be NOT NULL
);

CREATE TABLE email (
  email    text PRIMARY KEY
, user_id  int NOT NULL REFERENCES users ON DELETE CASCADE
, UNIQUE (user_id, email)  -- seems redundant, but required for FK
);

ALTER TABLE users ADD CONSTRAINT users_primary_email_fkey
FOREIGN KEY (user_id, email) REFERENCES email (user_id, email);

CREATE TABLE email_deleted (
  email_id serial PRIMARY KEY
, email    text NOT NULL  -- not necessarily unique
, user_id  int NOT NULL REFERENCES users ON DELETE CASCADE
);
_

こちらです:

  • アクティブ電子メールは一意であり、emailのPK制約によって強制されます。
  • 各ユーザーはアクティブなメールと削除されたメールをいくつでも持つことができますが...
  • 各ユーザーは、1つのプライマリ電子メールのみを持つことができます。
  • すべてのメールは常に1人のユーザーが所有し、ユーザーと共に削除されます。
  • メールとそのユーザーへの所属を失うことなくソフト削除するには、行をemailから_email_deleted_に移動します。
    • ユーザーのメインのメールはこの方法では削除できません。メインのメールは削除してはならないためです。
  • FK制約_users_primary_email_fkey_が_(user_id, email)_にまたがるように設計しましたが、最初は冗長に見えます。ただし、この方法では、メインのメールは、実際に同じユーザーが所有しているメールのみとなります。
    FK制約のデフォルトの_MATCH SIMPLE_動作により、いずれかの列がnullの場合はFK制約が適用されないため、プライマリメールなしでユーザーを入力できます。
    詳細:

_users.email_のUNIQUE制約はこのソリューションでは冗長ですが、他の理由で役立つ場合があります。自動的に作成されたインデックスが役立つはずです(この回答の最後のクエリなど)。

この方法で強制されない唯一のことは、everyユーザーがプライマリメールを持っているということです。あなたもこれを行うことができます。 _NOT NULL_制約を_users.email_に追加します

FK制約にはUNIQUE (user_id, email)が必要です:

上記のモデルで循環参照を見つけたことは間違いありません。予想とは逆に、これはうまくいきます。

_users.email_がNULLである限り、それは自明です。

  1. 電子メールなしのINSERTユーザー。
  2. 所有する_user_id_を参照するINSERTメール。
  3. 必要に応じて、UPDATEユーザーがメインのメールアドレスを設定します。

_users.email_を_NOT NULL_に設定しても機能します。ただし、ユーザーとメールを同時に挿入する必要があります。

_WITH u AS (
   INSERT INTO users(username, email)
   VALUES ('user_foo', '[email protected]')
   RETURNING email, user_id
   )
INSERT INTO email (email, user_id)
SELECT email, user_id
FROM   u;
_

IMMEDIATE FK制約(デフォルト)は、各ステートメントの最後でチェックされます。上記はoneステートメントです。これが、2つの別々のステートメントが失敗する場所で機能する理由です。詳細な説明:

ユーザーのすべてのメールを配列として取得するには、最初にメインのメールを送信します。

_SELECT u.*, e.emails
FROM   users u
     , LATERAL (
      SELECT ARRAY (
      SELECT email
      FROM   email
      WHERE  user_id = u.user_id
      ORDER  BY (email <> u.email)  -- sort primary email first
      ) AS emails
   ) e
WHERE  user_id = 1;
_

これを使用すると、使いやすくするためにVIEWを作成できます。
LATERALにはPostgres 9.3が必要です。 pg 9.2で相関サブクエリを使用します。

_SELECT *, ARRAY (
             SELECT email
             FROM   email
             WHERE  user_id = u.user_id
             ORDER  BY (email <> u.email)  -- sort primary email first
             ) AS emails
FROM   users u
WHERE  user_id = 1;
_

メールを一時削除するには:

_WITH del AS (
   DELETE FROM email
   WHERE  email = '[email protected]'
   RETURNING email, user_id
   )
INSERT INTO email_deleted (email, user_id)
SELECT email, user_id FROM del;
_

特定のユーザーのプライマリ電子メールをソフト削除するには:

_WITH upd AS (
   UPDATE users u
   SET    email = NULL
   FROM   (SELECT user_id, email FROM users WHERE user_id = 123 FOR UPDATE) old
   WHERE  old.user_id = u.user_id
   AND    u.user_id = 1
   RETURNING old.*
   )
,    del AS (
   DELETE FROM email
   USING  upd
   WHERE  email.email = upd.email
   )
INSERT INTO email_deleted (email, user_id)
SELECT email, user_id FROM upd;
_

詳細:

上記すべてのクイックテスト: SQL Fiddle

3