本文是《PostgreSQL技术问答》系列文章中的一篇。关于这个系列的由来,可以参阅开篇文章:
《PostgreSQL技术问答00 - Why Postgres》
文章的编号只是一个标识,在系列中没有明确的逻辑顺序和意义。读者进行阅读时,不用太关注这个方面。
本文讨论的内容是PostgreSQL中一个非常重要而常用的功能集合:Hstore,键值存储。
什么是hstore
hstore是PostgreSQL中的一个扩展,用于存储键值对数据。hstore这个名字的来源应当是"Hash Store",就是哈希存储。在信息技术中哈希存储(哈希表)是一种常见的数据结构,用于存储处理键值对形式的数据。任何一个数据的键,都可以转换和表示成为哈希的形式,然后在哈希表中,可以快速的排序和访问,这样,它就可以保证在存储结构中,可以高效的对数据进行访问和处理。
可以看到,hstore并不是一个完全官方的特性,而是作为一个扩展模块(当然也是内置,可以直接加载和使用),有一点半官方的感觉。那么,Postgres社区,作为一个关系型数据库的技术系统,为什么要设计和实现hstore呢?
笔者认为,这是为了响应Web应用的技术和发展趋势。随着Web应用的发展,人们已经渐渐发现,原有的关系型数据库系统,并不是特别适合以文本对象结构数据为主和并发性能为主要诉求的Web应用。这样,就诞生了一些不同于传统关系数据库系统的数据存储和处理技术方案,典型的比如MongoDB,Redis等等。这些系统的设计目标和实现模式非常简单,就是通过提供键值对象存储,可以很好的满足大部分简单和高度模式化的Web应用场景。此外这类系统开发、部署和使用简单,并发性能强大,很快就得到了广泛的应用和发展。
可能是看到这个模型的高效和优势,Postgres社区意识到,可以在关系性数据库技术的基础之上,引入对象数据处理的机制,来扩展PG的使用场景,特别是在Web应用开发支撑方面,提供更多的灵活和可能性。
基于键值数据库系统的逻辑,hstore在Postgres,对于普通的关系数据模型,进行的扩展和优势在于:
- 无架构的数据,不用实现设置数据结构,应用中按照需求调整和扩展
- 相对字符串存储信息的方式,hstore使用编码的二进制结构,存储效率、压缩和处理性能更好
- 使用键值模式,简单易用
笔者觉得,这个规划和想法是很好的,但它的实现有一点问题。最大的问题应该是,它还是作为一个关系数据库的数据类型,其存储结构还是依托于关系数据库表(字段)而存在的。给人的感觉有点多余,易用性也不是很好。其实它完全可以设计成为数据库级别的对象,同时保留作为字段的数据类型,都可以使用标准的SQL来操作,或者提供专门的语法和命令(类似Redis?)。
此外,可能是随后,通过JSON技术的应用和实现,也在另一方面实现了对象数据的处理模型,hstore的必要性好像就没有那么迫切和不可替代。再加上JSON的本身的技术优势和应用方面的强势,慢慢的hstore的应用就没有那么热门,逐渐的被边缘化了。
如何使用hstore
hstore的使用方式,其实和JSON或者Array这种特性是差不多的。在Postgres中,hstore体现为一种数据类型,依附于数据库的字段,然后配合这个数据类型,设计了一套相关的操作符和函数可以来对此种类型的数据进行操作。并且完整的实现了通用的数据管理生命周期就是增查改删这类操作。
下面就是这个方式的一般过程:
- PG扩展
默认情况下,hstore不是标准PG内置原生的特性,需要先作为扩展进行加载:
CREATE EXTENSION hstore;
- hstore数据类型
hstore扩展安装完成后,可以作为数据类型,来定义数据表中的字段:
sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
data HSTORE
);
- 插入数据
在Postgres中,hstore数据的外在表现形式,还是字符串,但它有自己的语法规范,简单而言就是 "键=>值"。在示例中我们可以看到,和JSON相比,hstore作为简单键值对的数据结构,它只有简单的一层,而且无需使用双引号将键和值包围起来,其表示形式是比较简单的。下面就是是插入hstore数据的代码:
sql
INSERT INTO users (name, data) VALUES ('Alice', 'age => 25, gender => female');
- 更新数据
更新数据涉及两种情况,一种是完全更新,一种是键的值更新。完全更新使用普通字段值的处理方式;键的值更新使用hstore数据合并的方式。
sql
-- 完全更新
update user set data = 'age => 26, gender => female' where name = 'alice';
-- 更新键或者增加的值
update user set data || 'age => 27 ' where name = 'alice' returning *;
- 删除键值
update user set data = data - 'age => 27 ' where name = 'alice' returning *;
- 删除数据
删除数据和更新一样,也分为两种情况,一种是删除键值数据(上例),一种是作为普通的记录进行删除。
- 数据查询
可以将hstore字段,作为普通的字符串数据(hstore格式)进行查询,也可以按照hstore的语法和操作符,查询特定的键的值,当然也可以以hstore的键值作为查询条件,参与到普通的SQL查询条件之中:
sql
-- 查询hstore数据
select name, data from user where name = 'alice';
-- 查询hstore键值
select name, data->"age" from user where name = 'alice';
-- 查询多个键值
select name, data->Array["age","gender"} from user where name = 'alice';
-- 作为查询条件
select name, data from user where data->'age'= 26;
如何构造一个hstore数据呢?
hstore的应用,一般从数据构造开始。构造hstore数据,除了前面提到的字符串表述之外,可以用很多种方法,但主要可以分成构造函数和类型转换两种大的类型:
- hstore函数
支持调用hstore函数,从记录,数组,二维数组,键值数组,键值等方式创建hstore数据,下面是一些示例方式:
sql
select 'record',hstore(ROW(1,2))
union all
select 'array', hstore(ARRAY['a','1','b','2'])
union all
select 'array2dim', hstore(ARRAY[['c','3'],['d','4']])
union all
select 'array key value', hstore(ARRAY['a','b'], ARRAY['1','2'])
union all
select 'keyvalue', hstore('a', 'b')
;
?column? | hstore
-----------------+----------------------
record | "f1"=>"1", "f2"=>"2"
array | "a"=>"1", "b"=>"2"
array2dim | "c"=>"3", "d"=>"4"
array key value | "a"=>"1", "b"=>"2"
keyvalue | "a"=>"b"
(5 rows)
- 字符串转换
hstore可以直接使用字符串的表示形式,但要声明为hstore数据类型,需要显式的转换。
sql
select 'a=>b,c=>d'::hstore;
hstore
--------------------
"a"=>"b", "c"=>"d"
(1 row)
总结一下hstore的相关函数和操作符
hstore相关操作的函数和操作符,也是随着hstore扩展安装后,才能被加载、定义和使用的。
hstore的相关操作符包括:
- hstore -> text → text, 键值引用
- hstore -> text[] → text[], 键值数组引用
- hstore || hstore → hstore, hstore数据合并
- hstore ? text → boolean, 检查是否包含键
- hstore ?& text[] → boolean, 检查是否包括多个键
- hstore ?| text[] → boolean, 检查是否包含某个键
- hstore @> hstore → boolean, 检查是否包含另一个hstore对象
- hstore <@ hstore → boolean, 检查hstore被包含
- hstore - text → hstore, 删除某个键
- hstore - text[] → hstore, 删除某些键
- %% hstore → text[], 将hstore转为键值对值数组,一维形式
- %# hstore → text[], 将hstore转为键值对值数组,二维形式
- anyelement #= hstore → anyelement, 将复合值中的值,替换为hstore值
sql
'a=>x, b=>y'::hstore -> 'a' → x
'a=>x, b=>y, c=>z'::hstore -> ARRAY['c','a'] → {"z","x"}
'a=>b, c=>d'::hstore || 'c=>x, d=>q'::hstore → "a"=>"b", "c"=>"x", "d"=>"q"
'a=>1'::hstore ? 'a' → t
'a=>1,b=>2'::hstore ?& ARRAY['a','b'] → t
'a=>1,b=>2'::hstore ?| ARRAY['b','c'] → t
'a=>b, b=>1, c=>NULL'::hstore @> 'b=>1' → t
'a=>c'::hstore <@ 'a=>b, b=>1, c=>NULL' → f
'a=>1, b=>2, c=>3'::hstore - 'b'::text → "a"=>"1", "c"=>"3"
'a=>1, b=>2, c=>3'::hstore - ARRAY['a','b'] → "c"=>"3
'a=>1, b=>2, c=>3'::hstore - 'a=>4, b=>2'::hstore → "a"=>"1", "c"=>"3"
%% 'a=>foo, b=>bar'::hstore → {a,foo,b,bar}
%# 'a=>foo, b=>bar'::hstore → {{a,foo},{b,bar}}
ROW(1,3) #= 'f1=>11'::hstore → (11,3)
hstore的相关函数包括:
- akeys(hstore) → text[]: 获取hstore字段的键,并输出为文本数组
- avals ( hstore ) → text[]: 将hstore字段的值转为文本数组
- skeys(hstore) → setof text: 获取hstore字段的键并转换为记录集
- svals ( hstore ) → setof text: 转换hstore字段的值为记录集
- hstore_to_array ( hstore ) → text[]: 将hstore数据展开为键值相间的数组
- hstore_to_matrix ( hstore ) → text[]: 将hstore数据转换为二维数组
- hstore_to_json ( hstore ) → json: 将hstore数据转为JSON(非空值)
- hstore_to_jsonb ( hstore ) → jsonb: 将hstore数据转为JSONB(非空值)
- hstore_to_json_loose(hstore) → json: 转为json,但保留null
- hstore_to_jsonb_loose ( hstore ) → jsonb: 转为jsonb,保留null
- slice ( hstore, text[] ) → hstore: 提取并保留指定的键
- each ( hstore ) → setof record (key text, value text): 转为键值对记录集
- exist ( hstore, text ) → boolean: 检查键的存在性
- defined ( hstore, text ) → boolean: 检查键对应值是否定义(非空)
- delete ( hstore, text ) → hstore: 删除匹配键
- delete ( hstore, text[] ) → hstore: 删除多个匹配键
- delete ( hstore, hstore ) → hstore: 删除在另一个hstore数据中有的键
- populate_record ( anyelement, hstore ) → anyelement: 替换hstore中对应键的值
笔者感觉,这些操作符和函数的设计,和JSON或者Array都很像啊,这样大大的降低了理解和学习的门槛。
hstore如何使用索引
hstore可以使用多种索引方式。对于如果使用 = 来进行条件过滤的操作,可以使用普通的btree和hash索引;如果针对其他操作符进行过滤操作,则应当使用GIN或者GiST索引,这些操作符包括@>、?、?& 、 ?| ,其实就是类似全文索引的意思。对应的函数操作,也是类似的情况。
下面是一些索引创建的例子:
sql
-- bree索引
CREATE INDEX hidx ON testhstore USING BTREE (h);
-- hash索引
CREATE INDEX hidx ON testhstore USING HASH (h);
-- GIN / GiST索引
CREATE INDEX hidx ON testhstore USING GIN (h);
CREATE INDEX hidx ON testhstore USING GIST (h);
CREATE INDEX hidx ON testhstore USING GIST (h gist_hstore_ops(siglen=32));
关于这些索引是如何对hstore数据造成影响的,其实应该是属于索引技术的范畴了。笔者会在其他的博文中深入探讨,这里就不再展开,只是说明hstore使用索引的方式,其实和其他数据类型没有太大的差异。
如何进行键或者值的统计
在使用hstore数据类型中,对数据键或者值进行统计,是一个常见的场景和需求。因为hstore作为键值存储是非常灵活的,不能使用一般的字段值统计方式。可以考虑将其转换为标准的记录集的形式,然后在进行处理。下面是一个简单的例子:
sql
defaultdb=> with H(h) as (values ('aaa=>bq, b=>NULL, ""=>1'), ('bbb=>bq, cb=>NULL, x=>2, b=>3' ) )
select key, count(*) from
(select (each(h::hstore)).key from H) stat
group by 1 order by 2 desc, 1;
key | count
-----+-------
b | 2
| 1
aaa | 1
bbb | 1
cb | 1
x | 1
(6 rows)
还有什么需要注意的问题吗?
有一些可能在hstore应用过程中需要注意的问题。
hstore的键和值,本质上都是字符串。值可以是null(未设置)。
原始的hstore文本表达,是不需要引号的。而如果操作字符串中带有引号或者反斜杠的时候,需要进行正确的转义。而在查询输出的时候,可能会使用双引号来表示hstore的键和值。
如果要你设计hstore,怎么设计比较好?
我想,可能可以扩展一下,增加可以创建一种hstore的表对象,然后在配合可以操作标准键值数据的操作符和函数,以及配套的SQL语句,应该可以比现在的实现更简单易用一点。
例如下面的一些概念性的代码:
sql
-- 创建 hstore数据表
create hstoretable hdata;
-- 插入或更新数据
upsert hdata ('key', 'value');
-- 删除键值
delete 'key' from hdata;
-- 获取键、值和实体
select keys, values, entities from hdata;
-- 查询键和值
select value, entity from hdata where key = 'key';
select value, entity from hdata where key in ('key1','key2');
小结
本文讨论了postgres的一个扩展功能模块和数据类型-hstore。它可以用于存储和处理键值类型的数据。postgres为此提供了相关的操作符和函数,来实现hstore数据的管理和操作。