如何创建包含多个列的 MD5 的 GENERATED 列?

How to create a GENERATED column containing the MD5 of multiple columns?

我尝试在 PostgreSQL 14.3 中添加以下 table:

CREATE TABLE client_cache (
    id            BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    request       VARCHAR COMPRESSION lz4 NOT NULL CHECK (LENGTH (request) <= 10240),
    request_body  BYTEA COMPRESSION lz4 NOT NULL CHECK (LENGTH (request_body) <= 1048576),
    request_hash VARCHAR GENERATED ALWAYS AS (MD5(ROW(request::BYTEA, request_body)::VARCHAR)) STORED
);

但是 Postgres 抱怨:

[42P17] ERROR: generation expression is not immutable

我看过很多讨论如何创建包含单列 MD5GENERATED 列的答案,但是一旦您添加 ROW() 来计算 MD5 在多个列上,表达式不再是 immutable.

我可以使用 ROW(MD5(A), MD5(B)) 创建一个 GENERATED 列,但不能使用 MD5(ROW(A, B))

我该怎么做才能在不同类型的多个列(如上所示)上创建单个 MD5 值?

我知道我可以使用触发器创建视图或填充列,但如果可能的话,我真的很想坚持使用 GENERATED 列。

我想我找到了解决办法!

Postgres 不喜欢:

request_hash VARCHAR GENERATED ALWAYS AS (MD5(ROW(request, request_body)::VARCHAR)) STORED

request_hash VARCHAR GENERATED ALWAYS AS (MD5(request || request_body::VARCHAR)) STORED 工作正常。

万岁!

我建议使用 immutable 辅助函数:

CREATE OR REPLACE FUNCTION f_request_md5(_request text, _request_body bytea)
  RETURNS uuid
  LANGUAGE sql IMMUTABLE PARALLEL SAFE AS 
'SELECT md5(textin(record_out((md5(_request_body), _request))))::uuid';

还有一个 table 这样的:

CREATE TABLE client_cache (
  id           bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY
, request      text   COMPRESSION lz4 NOT NULL CHECK (length(request) <= 10240)
, request_body bytea  COMPRESSION lz4 NOT NULL CHECK (length(request_body) <= 1048576)
, request_hash uuid   GENERATED ALWAYS AS (f_request_md5(request, request_body)) STORED
);

db<>fiddle here

注意效率更高的 uuid 而不是 varchar。参见:

背景

在 Postgres 14(或任何支持的版本)中有两个 md5() 的重载变体:

test=> SELECT (proargtypes::regtype[])[0], prorettype::regtype, provolatile
test-> FROM   pg_proc
test-> WHERE  proname = 'md5';
 proargtypes | prorettype | provolatile 
-------------+------------+-------------
 bytea       | text       | i
 text        | text       | i
(2 rows)

一个bytea,一个text,都是IMMUTABLE和returntext。所以这个表达式是 immutable:

ROW(MD5(request), MD5(request_body))

但事实并非如此,就像您通过艰难的方式发现的那样:

MD5(ROW(A, B)::varchar)

record 的文本表示不是 immutable。原因有很多。手头案例的一个明显原因是:bytea 输出可以是(默认)hex 格式或过时的 escape 格式。一个普通的

SET bytea_output = 'escape'; 

... 会破坏您生成的列。

要获得 bytea 值的 immutable 文本表示,您需要 运行 通过 encode(request_body, 'hex')。但是不要去那里。 md5(request_body) 为我们的目的提供了更快的 immutable 文本“表示”。

我们仍然无法录制。所以我创建了包装函数。请务必阅读此相关答案以获得更多解释:

就像那个答案中讨论的那样,新的 built-in 函数 hash_record_extended() 很多 更有效率为目的。因此,如果 bigint 足够好,请考虑以下:

CREATE TABLE client_cache2 (
  id           bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY
, request      text   COMPRESSION lz4 NOT NULL CHECK (length(request) <= 10240)
, request_body bytea  COMPRESSION lz4 NOT NULL CHECK (length(request_body) <= 1048576)
, request_hash bigint GENERATED ALWAYS AS (hash_record_extended((request, request_body), 0)) STORED
);

相同数据库<>fiddle here

在 Postgres 14 或更高版本中开箱即用。

相关:

  • Computed / calculated / virtual / derived columns in PostgreSQL