PostgreSQL 多值类型(如数组),元素值全局唯一 - 约束如何实现

简介:

标签

PostgreSQL , 多值类型 , 数组 , 全局约束


背景

《PostgreSQL 11 preview - 支持 数组外键约束》

在PostgreSQL中,应用可以非常方便使用多值类型,比如数组、全文检索、范围类型等。

对于多值类型的操作也越来越方便,比如查询相交、包含可以使用索引,更新、替换、APPEND的操作也都有对应的UDF和OP来支撑。

可能应用会有这样的需求,对于多值类型,能否做到全局唯一约束呢?

比如数组内的元素,要求整张表唯一。

对于单值类型,很容易做到,加个unique, primary key约束就可以,使用b-tree很方便判断值是否已存在。但是对于多值类型,怎么判断呢?

1、采用排他约束

PostgreSQL的排他约束可以支持空间、范围、普通数据的排他,采用gist索引,使用能支持左右互换操作数并且返回boolean值不变的操作符。

例如:

1、会议室预定系统,EXCLUDE约束,确保一个会议室在某个时间点不会被多人预定。

《会议室预定系统实践(解放开发) - PostgreSQL tsrange(时间范围类型) + 排他约束》

2、空间EXCLUDE约束,比如国土面积,不会出现相交。

https://www.postgresql.org/docs/10/static/sql-createtable.html#SQL-CREATETABLE-EXCLUDE

对于数组类型有一个操作符为&&,可以支持操作数互换,结果不变,代表的含义是两个array是否overlap。

但是目前GIN还不支持EXCLUDE约束,相信后面会支持。

postgres=# \set VERBOSITY verbose  
postgres=# create table t_unique_arr (id int, arr int[], exclude using gin (arr with &&));   
ERROR:  0A000: access method "gin" does not support exclusion constraints  
LOCATION:  DefineIndex, indexcmds.c:580  

所以对于int类型,我们可以加一个intarray插件,让gist接口来支持int[]。

postgres=# create table t_unique_arr (id int, arr int[], exclude using gist (arr with &&));   
ERROR:  data type integer[] has no default operator class for access method "gist"  
HINT:  You must specify an operator class for the index or define a default operator class for the data type.  
  
postgres=# create extension intarray;  
CREATE EXTENSION  

创建数组排他约束

postgres=# create table t_unique_arr (id int, arr int[], exclude using gist (arr with &&));   
CREATE TABLE  

现在,这个约束可以保证数组内的元素,在整个表的所有行中,全局唯一。

postgres=# insert into t_unique_arr values (1,array[1,2,3]);  
INSERT 0 1  
postgres=# insert into t_unique_arr values (1,array[1,2,3]);  
ERROR:  conflicting key value violates exclusion constraint "t_unique_arr_arr_excl"  
DETAIL:  Key (arr)=({1,2,3}) conflicts with existing key (arr)=({1,2,3}).  
postgres=# insert into t_unique_arr values (1,array[1,4,5]);  
ERROR:  conflicting key value violates exclusion constraint "t_unique_arr_arr_excl"  
DETAIL:  Key (arr)=({1,4,5}) conflicts with existing key (arr)=({1,2,3}).  
postgres=# insert into t_unique_arr values (1,array[4,5,6]);  
INSERT 0 1  
  
postgres=# insert into t_unique_arr select id, array[id] from generate_series(7,10000) t(id);  
ERROR:  conflicting key value violates exclusion constraint "t_unique_arr_arr_excl"  
DETAIL:  Key (arr)=({7}) conflicts with existing key (arr)=({7}).  
Time: 3.131 ms  
postgres=# update t_unique_arr set arr=array[1,2,3] where id=2;  
UPDATE 0  
Time: 1.405 ms  
postgres=# update t_unique_arr set arr=array[1,2,3] where id=7;  
ERROR:  conflicting key value violates exclusion constraint "t_unique_arr_arr_excl"  
DETAIL:  Key (arr)=({1,2,3}) conflicts with existing key (arr)=({1,2,3}).  
Time: 2.126 ms  

目前使用intarray,exclude约束来实现数组内元素全局唯一,性能不怎么样。

postgres=# insert into t_unique_arr select id, array[id] from generate_series(7,10000) t(id);  
INSERT 0 9994  
Time: 122861.074 ms (02:02.861)  

所以我们可以用另外的方法,比如rule, trigger。

2、采用rule

原理是这样的,当数据写入一个包含数组的表时,如果你想让某个数组列全局唯一,那么可以把数据展开,放到一个单值列,并用B-TREE的unique, primary key这类约束来约束其唯一性。

使用规则,即可完成透明展开。

drop table t_unique_arr;  
  
create table t_unique_arr(id int, arr int[]);  
create index idx_t_unique_arr on t_unique_arr(id);  

创建展开表

create table check_t_unique_arr(arr int unique);  

创建insert, update, delete的规则,在操作源表时,自动展开多值列。

create rule r1 as on insert to t_unique_arr do also insert into check_t_unique_arr select unnest(NEW.arr);  
create rule r2 as on update to t_unique_arr do also delete from check_t_unique_arr where arr = any (OLD.arr);  
create rule r3 as on update to t_unique_arr do also insert into check_t_unique_arr select unnest(NEW.arr);  
create rule r4 as on delete to t_unique_arr do also delete from check_t_unique_arr where arr = any (OLD.arr);  

创建一个测试函数,用于对源表进行DML操作。

create or replace function ins_t_unique_arr(int,int,int,int,int,int) returns void as $$  
declare  
begin  
  insert into t_unique_arr values ($1,array[$2,$3,$4,$5,$6]);  
  update t_unique_arr set arr=array[$2,$3,$4,$5,$6] where id=$1+1;  
  delete from t_unique_arr where id=$1+100;  
  exception when others then  
    return;  
end;  
$$ language plpgsql strict;  

创建压测脚本

vi test.sql  
\set id1 random(100001,200000)  
\set id2 random(200001,300000)  
\set id3 random(300001,400000)  
\set id4 random(400001,500000)  
\set id5 random(500001,600000)  
\set id random(1,200000000)  
select ins_t_unique_arr(:id,:id1,:id2,:id3,:id4,:id5);  

开始DML压测

pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -T 120  
transaction type: ./test.sql  
scaling factor: 1  
query mode: prepared  
number of clients: 64  
number of threads: 64  
duration: 120 s  
number of transactions actually processed: 19510269  
latency average = 0.394 ms  
latency stddev = 1.115 ms  
tps = 162325.823786 (including connections establishing)  
tps = 162351.285338 (excluding connections establishing)  
statement latencies in milliseconds:  
         0.001  \set id1 random(100001,200000)  
         0.000  \set id2 random(200001,300000)  
         0.000  \set id3 random(300001,400000)  
         0.000  \set id4 random(400001,500000)  
         0.000  \set id5 random(500001,600000)  
         0.000  \set id random(1,200000000)  
         0.391  select ins_t_unique_arr(:id,:id1,:id2,:id3,:id4,:id5);  

检查结果一致性,是否达到了唯一要求

postgres=# select count(*) from t_unique_arr ;  
 count   
-------  
 91429  
(1 row)  
  
postgres=# select count(*) from (select unnest(arr) from t_unique_arr) t;  
 count    
--------  
 417541  
(1 row)  
  
postgres=# select count(*) from check_t_unique_arr ;  
 count    
--------  
 417541  
(1 row)  

缺陷:

1、rule 不支持copy,即COPY不会触发RULE。

2、RULE 不支持 truncate。 即TRUNCATE不会触发RULE。

可以改成trigger,trigger支持copy,支持truncate。

3、采用trigger

创建触发器函数

create or replace function tg_t_unique_arr() returns trigger as $$  
declare  
begin  
  case TG_OP    
  when 'INSERT' then  
    insert into check_t_unique_arr select unnest(NEW.arr);  
    return NEW;  
  when 'UPDATE' then  
    delete from check_t_unique_arr where arr = any (OLD.arr);  
    insert into check_t_unique_arr select unnest(NEW.arr);  
    return NEW;  
  when 'DELETE' then  
    delete from check_t_unique_arr where arr = any (OLD.arr);  
    return OLD;  
  when 'TRUNCATE' then  
    truncate check_t_unique_arr;  
  end case;  
  return null;  
end;  
$$ language plpgsql strict;  

删除规则

postgres=# drop rule r1 on t_unique_arr ;  
DROP RULE  
postgres=# drop rule r2 on t_unique_arr ;  
DROP RULE  
postgres=# drop rule r3 on t_unique_arr ;  
DROP RULE  
postgres=# drop rule r4 on t_unique_arr ;  
DROP RULE  

创建DML触发器

postgres=# create trigger tg1 before insert or update or delete on t_unique_arr for each row execute procedure tg_t_unique_arr();  
CREATE TRIGGER  

创建TRUNCATE触发器

postgres=# create trigger tg2 after truncate on t_unique_arr for each statement execute procedure tg_t_unique_arr();  
CREATE TRIGGER  

压测

transaction type: ./test.sql  
scaling factor: 1  
query mode: prepared  
number of clients: 64  
number of threads: 64  
duration: 120 s  
number of transactions actually processed: 16580381  
latency average = 0.463 ms  
latency stddev = 0.408 ms  
tps = 137735.702798 (including connections establishing)  
tps = 137754.497564 (excluding connections establishing)  
statement latencies in milliseconds:  
         0.001  \set id1 random(100001,200000)  
         0.000  \set id2 random(200001,300000)  
         0.000  \set id3 random(300001,400000)  
         0.000  \set id4 random(400001,500000)  
         0.000  \set id5 random(500001,600000)  
         0.000  \set id random(1,200000000)  
         0.460  select ins_t_unique_arr(:id,:id1,:id2,:id3,:id4,:id5);  

检查约束是否生效

postgres=# select count(*) from t_unique_arr ;  
 count   
-------  
 80307  
(1 row)  
  
postgres=# select count(*) from (select unnest(arr) from t_unique_arr) t;  
 count    
--------  
 401535  
(1 row)  
  
postgres=# select count(*) from check_t_unique_arr ;  
 count    
--------  
 401535  
(1 row)  

TRUNCATE测试

postgres=# truncate t_unique_arr ;  
TRUNCATE TABLE  
postgres=# select count(*) from t_unique_arr ;  
 count   
-------  
     0  
(1 row)  
  
postgres=# select count(*) from check_t_unique_arr ;  
 count   
-------  
     0  
(1 row)  

使用trigger满足了最终的要求,性能也不错。

小结

1、目前使用exclude排他约束来对多值类型实现元素的全局唯一约束,使用intarry的gist && 操作符,性能并不理想。期待PostgreSQL后续版本开通gin接口的exclude支持。

2、使用rule可以实现数组元素全局唯一约束,但缺陷是: rule 不支持copy, 不支持 truncate。

3、使用trigger可以实现数组元素全局唯一约束,并且性能很不错,支持copy, truncate。

空间、。。。

4、排他约束,不仅仅能支持多值类型,同时还支持空间、范围等异构类型。广泛应用于面积不相交、范围不相交等约束场景。

《会议室预定系统实践(解放开发) - PostgreSQL tsrange(时间范围类型) + 排他约束》

5、PostgreSQL对多值类型的支持细节做得越来越不错,除了业务上常用的“查询相交、包含可以使用索引,更新、替换、APPEND的操作”也都有对应的UDF和OP来支撑。

《PostgreSQL 11 preview - 支持 数组外键约束》

参考

《会议室预定系统实践(解放开发) - PostgreSQL tsrange(时间范围类型) + 排他约束》

《PostgreSQL 11 preview - 支持 数组外键约束》

https://www.postgresql.org/docs/10/static/sql-createtable.html#SQL-CREATETABLE-EXCLUDE

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍如何基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
SQL 关系型数据库 PostgreSQL
|
关系型数据库 数据库 PostgreSQL
开发踩坑记录之三:PostgreSQL数据库表唯一性约束失效
在设计数据库表过程中,我们通常会对数据库表进行唯一性约束,以防止事务不一致导致的相同数据的重复插入问题。但是在实际开发中发现,即使设置了数据库表的唯一性约束,仍然出现了相同数据重复插入的问题。
|
弹性计算 关系型数据库 测试技术
PostgreSQL 分区表如何支持多列唯一约束 - 枚举、hash哈希 分区, 多列唯一, insert into on conflict, update, upsert, merge insert
标签 PostgreSQL , 分区表 , native partition , 唯一 , 非分区键唯一 , 组合唯一 , insert into on conflict , upsert , merge insert 背景 PG 11开始支持HASH分区,10的分区如果要支持hash分区,可以通过枚举绕道实现。 《PostgreSQL 9.x, 10, 11 hash分区表 用法举例
3410 0
|
存储 关系型数据库 PostgreSQL
PostgreSQL PostGIS 空间数据约束使用
标签 PostgreSQL , PostGIS , 空间数据约束 背景 空间数据有一定的规范,例如SRID的规范。空间类型geometry包罗万象,除了能存储POINT,还能存储多边形,线段等。 这就带来一个有意思的烦恼,当我们业务不够规范时,你可以往GEOMETRY里面存储任意SRID的数据,存储任意的空间对象。
1445 0
|
SQL 关系型数据库 数据库
PostgreSQL 10.1 手册_部分 II. SQL 语言_第 5 章 数据定义_5.3. 约束
5.3. 约束 5.3.1. 检查约束 5.3.2. 非空约束 5.3.3. 唯一约束 5.3.4. 主键 5.3.5. 外键 5.3.6. 排他约束 数据类型是一种限制能够存储在表中数据类别的方法。
1159 0
|
关系型数据库 测试技术 C语言
PostgreSQL 另类advisory lock保证唯一约束法
在没有唯一约束或者主键约束时,数据库是不保证唯一性的。那么有什么手段来保证呢? 方法 1. 串行操作,先查询,如果没有查到记录,则插入。 这种方法效率非常低: 测试如下: postgres=# create table tbl(c1 text); CREATE TABLE
4736 0
|
关系型数据库 测试技术 PostgreSQL
postgresql 排它约束
--pg支持 EXCLUSION Constraint,排它约束是约束中定义的操作计算结果为false,则不允许插入 Exclusion constraints ensure that if...
1170 0

相关产品

  • 云原生数据库 PolarDB
  • 云数据库 RDS PostgreSQL 版
  • 推荐镜像

    更多