我想要一种相当有效的方法来将整个表压缩为哈希值。
我有一些工具可以生成整个数据表,然后可以使用这些工具生成更多表,等等。我正在尝试实现一个简单的构建系统来协调构建运行并避免重复工作。我希望能够记录输入表的哈希值,以便稍后检查它们是否已更改。构建表需要几分钟或几小时,因此花几秒钟构建哈希是可以接受的。
我使用的一个 hack 是将 pg_dump 的输出通过管道传输到 md5sum,但这需要通过网络传输整个表转储以将其散列到本地机器上。理想情况下,我想在数据库服务器上生成哈希值。
在 postgresql 中查找一行的哈希值给了我一种一次计算一行的哈希值的方法,然后可以以某种方式组合。
任何提示将不胜感激。
编辑发布我最终得到的结果:tinychen的答案并没有直接为我工作,因为我显然无法使用“plpgsql”。当我用 SQL 实现该函数时,它可以工作,但对于大型表来说效率非常低。因此,我没有连接所有行哈希然后对其进行哈希,而是改用“滚动哈希”,其中前一个哈希与行的文本表示连接,然后对其进行哈希以生成下一个哈希。这样就好多了。显然,在短字符串上额外运行 md5 数百万次比连接短字符串数百万次要好。
create function zz_concat(text, text) returns text as
'select md5($1 || $2);' language 'sql';
create aggregate zz_hashagg(text) (
sfunc = zz_concat,
stype = text,
initcond = '');
我知道这是老问题,但这是我的解决方案:
SELECT
md5(CAST((array_agg(f.* order by id))AS text)) /* id is a primary key of table (to avoid random sorting) */
FROM
foo f;
SELECT md5(array_agg(md5((t.*)::varchar))::varchar)
FROM (
SELECT *
FROM my_table
ORDER BY 1
) AS t
就这样创建一个哈希表聚合函数。
create function pg_concat( text, text ) returns text as '
begin
if $1 isnull then
return $2;
else
return $1 || $2;
end if;
end;' language 'plpgsql';
create function pg_concat_fin(text) returns text as '
begin
return $1;
end;' language 'plpgsql';
create aggregate pg_concat (
basetype = text,
sfunc = pg_concat,
stype = text,
finalfunc = pg_concat_fin);
然后你可以使用 pg_concat 函数来计算表的哈希值。
select md5(pg_concat(md5(CAST((f.*)AS text)))) from f order by id
我有类似的要求,在测试专门的表复制解决方案时使用。
@Ben 的滚动 MD5 解决方案(他附加到问题中)似乎非常有效,但有几个陷阱让我绊倒。
第一个(在其他一些答案中提到)是您需要确保在您正在检查的表上以已知的顺序执行聚合。其语法是例如。
select zz_hashagg(CAST((example.*)AS text) order by id) from example;
注意
order by
在聚合内部。
第二个是,使用
CAST((example.*)AS text
不会为具有相同列内容的两个表提供相同的结果,除非以相同的顺序创建列。就我而言,这并不能得到保证,因此为了获得真正的比较,我必须单独列出各列,例如:
select zz_hashagg(CAST((example.id, example.a, example.c)AS text) order by id) from example;
为了完整性(以防后续编辑应删除它),这里是 @Ben 问题中 zz_hashagg 的定义:
create function zz_concat(text, text) returns text as
'select md5($1 || $2);' language 'sql';
create aggregate zz_hashagg(text) (
sfunc = zz_concat,
stype = text,
initcond = '');
Tomas Greif 的解决方案很好。但对于足够大的表无效的内存分配请求大小错误将会发生。因此,可以通过两种选择来克服。
选项 1. 无批次
如果表不够大,请使用
string_agg
和 bytea
数据类型。
select
md5(string_agg(c.row_hash, '' order by c.row_hash)) table_hash
from
foo f
cross join lateral(select ('\x' || md5(f::text))::bytea row_hash) c
;
选项 2. 批量
如果上一个选项中的查询以类似错误结束
SQL 错误 [54000]:错误:内存不足 详细信息:无法将包含 1073741808 字节的字符串缓冲区扩大 16 个字节。
行数限制为
1073741808 / 16 = 67108863
,并且表应分为批次。
select
md5(string_agg(t.batch_hash, '' order by t.batch_hash)) table_hash
from(
select
md5(string_agg(c.row_hash, '' order by c.row_hash)) batch_hash
from
foo f
cross join lateral(select ('\x' || md5(f::text))::bytea row_hash) c
group by substring(row_hash for 3)
) t
;
其中
3
子句中的 group by
将行哈希划分为 16 777 216 个批次(2
:65 536,1
:256)。其他批处理方法(例如严格ntile
)也可以使用。
附注如果您需要比较两个表这篇文章可能会有所帮助。
很好的答案。
万一有人要求不使用聚合函数但保持对大小为几 GiB 的表的支持,您可以使用它,在最大表的情况下,它比最佳答案具有很少的性能损失。
CREATE OR REPLACE FUNCTION table_md5(
table_name CHARACTER VARYING
, VARIADIC order_key_columns CHARACTER VARYING [])
RETURNS CHARACTER VARYING AS $$
DECLARE
order_key_columns_list CHARACTER VARYING;
query CHARACTER VARYING;
first BOOLEAN;
i SMALLINT;
working_cursor REFCURSOR;
working_row_md5 CHARACTER VARYING;
partial_md5_so_far CHARACTER VARYING;
BEGIN
order_key_columns_list := '';
first := TRUE;
FOR i IN 1..array_length(order_key_columns, 1) LOOP
IF first THEN
first := FALSE;
ELSE
order_key_columns_list := order_key_columns_list || ', ';
END IF;
order_key_columns_list := order_key_columns_list || order_key_columns[i];
END LOOP;
query := (
'SELECT ' ||
'md5(CAST(t.* AS TEXT)) ' ||
'FROM (' ||
'SELECT * FROM ' || table_name || ' ' ||
'ORDER BY ' || order_key_columns_list ||
') t');
OPEN working_cursor FOR EXECUTE (query);
-- RAISE NOTICE 'opened cursor for query: ''%''', query;
first := TRUE;
LOOP
FETCH working_cursor INTO working_row_md5;
EXIT WHEN NOT FOUND;
IF first THEN
first := FALSE;
SELECT working_row_md5 INTO partial_md5_so_far;
ELSE
SELECT md5(working_row_md5 || partial_md5_so_far)
INTO partial_md5_so_far;
END IF;
-- RAISE NOTICE 'partial md5 so far: %', partial_md5_so_far;
END LOOP;
-- RAISE NOTICE 'final md5: %', partial_md5_so_far;
RETURN partial_md5_so_far :: CHARACTER VARYING;
END;
$$ LANGUAGE plpgsql;
用作:
SELECT table_md5(
'table_name', 'sorting_col_0', 'sorting_col_1', ..., 'sorting_col_n'
);
至于算法,您可以对所有单独的 MD5 哈希值进行异或,或者将它们连接起来并对连接进行哈希处理。
如果您想完全在服务器端执行此操作,您可能必须创建自己的聚合函数,然后您可以调用该函数。
select my_table_hash(md5(CAST((f.*)AS text)) from f order by id
作为中间步骤,您可以只选择所有行的 MD5 结果,然后通过 md5sum 运行这些结果,而不是将整个表复制到客户端。
无论哪种方式,您都需要建立固定的排序顺序,否则即使对于相同的数据,您也可能会得到不同的校验和。
这是比较整个表/视图的无连接有效方法:
with t1 as (
select sum(('x' || md5(table1::text))::bit(64)::bigint) as hash, count(*) as count from table1
), t2 as (
select sum(('x' || md5(table2::text))::bit(64)::bigint) as hash, count(*) as count from table2
)
select 'table1' as source, hash, count from t1
union all
select 'table2', hash, count from t2
union all
select 'diff', t2.hash - t1.hash, t2.count - t1.count
from t1 cross join t2