web-dev-qa-db-ja.com

md5(NULL)がpostgresqlでNULL以外の値を返すようにする

_(content_md5 UUID, content TEXT)_という構造のテーブルがあり、_content_md5_(md5(content)の値)を主キーとして使用し、外部キーとして使用したい他のテーブルのキー。

これは「静的」テーブルであり、コンテンツ(一部の大きなドキュメント)は、単純化のために_md5_値によって参照され、テーブル内の重複を防ぎます(単純なSERIALでは指定されません)。 PKEY)。

ただし、contentNULLにすることができます。これは、参照するテーブルに存在しないコンテンツフィールドを宣言する空の値とは異なります。
md5(NULL)NULLを返し、NULLは主キー制約では使用できないため、md5(NULL)NULLではなくすべてゼロを返すようにしたい。

例:

_-- setup
CREATE TABLE example (content_md5 UUID PRIMARY KEY, content TEXT);
CREATE TABLE test (id SERIAL PRIMARY KEY, tags TEXT, content_md5 UUID REFERENCES example(content_md5) ON DELETE RESTRICT);
INSERT INTO  example VALUES ('00000000000000000000000000000000'::uuid, NULL);

-- usage
INSERT INTO example VALUES (md5('some text')::uuid, 'some text');
INSERT INTO test (tags, content_md5) VALUES ('some content defining tags', md5('some text')::uuid);

SELECT tags, content FROM test LEFT JOIN example USING (content_md5);

-- QUESTION: Having an md5-like function to return zero-filled "md5"/uuid?
INSERT INTO example VALUES (md5(NULL)::uuid, NULL); -- ignored, because already existing record
INSERT INTO test (tags, content_md5) VALUES ('non-existing-document', md5(NULL)::uuid);
_

戻り値をゼロで埋められた文字列にキャストしたり、md5()に基づいてカスタム関数を作成したりして、NULLを_00000000000000000000000000000000_に置き換えるなど、この結果を達成する方法はありますか?

/ edit:または、おそらくこのテーブルにNULL値は必要ありません。参照する外部キー列をNULLに設定するだけで同じ結果を得ることができますか?

1
nyov

私はこの代替設計を提案します:

_-- setup
CREATE TABLE example (content_id serial PRIMARY KEY, content text);
CREATE TABLE test (id serial PRIMARY KEY, tags TEXT, content_id int REFERENCES example);

CREATE UNIQUE INDEX ON example ((md5(content)::uuid)) INCLUDE (content_id); -- !

-- usage
INSERT INTO example(content) VALUES (NULL);        -- allowed multiple times
INSERT INTO example(content) VALUES ('some text');

INSERT INTO test (tags, content_id)
SELECT 'some content defining tags', content_id
FROM   example
WHERE  md5(content)::uuid = md5('some text')::uuid;
_

db <> fiddle ここ

主なポイント

シリアル列(_content_id_)をテーブルexampleのサロゲートPKとして、およびどこでもFK参照として使用します。 16ではなく4バイト。

md5(example)::uuidの一意のインデックスを使用して一意性を適用します。ハッシュの衝突が発生する可能性があることに注意してください(テーブルが大きくない場合は非常にまれです)。

そのとき、serial PK列をINCLUDE句を使用してインデックスに追加し(Postgres 11以降)、インデックスのみを高速に検索するためのカバーするインデックスにします。

PK列とは異なり、これはNULLを許可し、NULLNULLの複製とは見なされないため、ユースケースをカバーする必要があります。見る:

Postgres 10以前では、_content_id_をインデックスに追加しないでください。もちろん、インデックスのみのスキャンは取得できません。

_CREATE UNIQUE INDEX ON example ((md5(content)::uuid));
_

以下の場合を除きNULLの単一インスタンスのみを許可します。これは、投稿したような関数で適用できます(衝突のリスクを導入-たとえありそうもない)または上記のものに加えて小さな部分インデックス:

_CREATE UNIQUE INDEX ON example (content_id)
WHERE md5(content)::uuid IS NULL;
_

見る:

Md5値をテーブル列として(冗長に)まったく保存しないでください。


もしあなたが投稿した関数を使い続けたい場合あなたの答え 、それを最適化することを検討してください:

_CREATE OR REPLACE FUNCTION pg_temp.md5zero(data text)
  RETURNS uuid PARALLEL SAFE IMMUTABLE LANGUAGE sql AS
$func$
SELECT COALESCE(md5(data)::uuid, '00000000000000000000000000000000')
$func$
_

より速く、インライン化できます。見る:

2

あなたのリクエストは、主キーの概念を少し壊します-主キーを別の列に依存させる必要があります(他の列--content-あなたの場合-主キーにしないでください)。それと同時に、その派生列を一意にする必要があります。この設定をすることは可能ですが、設計は混乱を招きます(つまり、将来のDBA /開発者は、設計の決定内容を解読する必要があるでしょう)。

また、md5()UUIDタイプを返しません(ただし、UUIDにキャストするつもりだと思います)。

そうは言っても、シーケンスと一緒にCOALESCE()を使用できると思います。

edb=# create sequence abc_seq;
CREATE SEQUENCE
edb=# create table abc (content_md5 text primary key, content text);
CREATE TABLE
edb=# insert into abc values (md5(coalesce('mycontent',nextval('abc_seq')::text)),'mycontent');
INSERT 0 1
edb=# insert into abc values (md5(coalesce(null,nextval('abc_seq')::text)),null);
INSERT 0 1
edb=# select * from abc;
           content_md5            |  content  
----------------------------------+-----------
 c8afdb36c52cf4727836669019e69222 | mycontent
 c4ca4238a0b923820dcc509a6f75849b | 
(2 rows)

_ [にDEFAULTを設定できないことにも注意してくださいcontent_md5以下のため:

edb=# create table abc (content_md5 text primary key default md5(coalesce(content,nextval('abc_seq')::text)), content text);
ERROR:  cannot use column references in default expression
1
richyen

自分のやりたいことを行うカスタム関数を作成する方法を見つけました。
これがそれを解決するための最良の方法であるかどうかはわかりませんが、私にとってはうまくいくので、次のようにします:

CREATE OR REPLACE FUNCTION md5zero(data text) RETURNS text AS $$
BEGIN
    IF data IS NULL
    THEN
        RETURN '00000000000000000000000000000000';
    ELSE
        RETURN md5(data);
    END IF;
END;
$$ LANGUAGE plpgsql;

-- TEST: 
INSERT INTO example VALUES (md5zero(NULL)::uuid, NULL); -- ignored, because already existing record

-- ERROR:  duplicate key value violates unique constraint "example_pkey"
-- DETAIL:  Key (content_md5)=(00000000-0000-0000-0000-000000000000) already exists.
-- Time: 0.477 ms

INSERT INTO test (tags, content_md5) VALUES ('non-existing-document', md5zero(NULL)::uuid);

-- id |         tags          |             content_md5              
------+-----------------------+--------------------------------------
--  4 | non-existing-document | 00000000-0000-0000-0000-000000000000
0
nyov