Skip to content

Commit d82db52

Browse files
committed
new doc
1 parent c4e0e86 commit d82db52

File tree

9 files changed

+332
-4
lines changed

9 files changed

+332
-4
lines changed

201705/20170522_01.md

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -750,7 +750,69 @@ SSD UTIL: 67%
750750
查询qps:1.5/s
751751
```
752752

753-
## 小结
753+
## 扩展方案 - PG + ES
754+
ES对审计日志的字段进行全文检索,由于数据是追加写入(没有更新),可以使用行号和审计日志进行关联。在ES中建立 审计日志+行号(全文索引)。
755+
756+
注意,ES建的索引必须能区分出PG中对应的表名,否则不同的表,行号是会重复的。
757+
758+
如果ES中不能区分表名,那么建议使用 审计日志+全局唯一ID的全文索引,这样才能和数据关联起来。
759+
760+
### 按行号拖数据,BUILD ES全文索引
761+
从PG将数据拖到ES,使用行号拖,可以省掉一个PK字段以及索引,极大的提升PG的数据写入性能。
762+
763+
使用行号拖数据,需要支持按数据块查询。
764+
765+
```
766+
-- 一次拖一万条
767+
768+
create or replace function gen_tids(blkid int) returns tid[] as $$
769+
declare
770+
res tid[] := '{}'::tid[];
771+
begin
772+
for x in blkid..(blkid+199) loop
773+
select array_cat(res, array(
774+
SELECT ('('||x||',' || s.i || ')')::tid
775+
FROM generate_series(0, 50) AS s(i)
776+
)
777+
) into res;
778+
end loop;
779+
return res;
780+
end;
781+
$$ language plpgsql strict immutable;
782+
```
783+
784+
同时需要注意空洞(漏建索引)的问题,比如
785+
786+
不要查最后一个数据块,除非这个表不再写入,否则可能导致查询空洞。
787+
788+
并行写入PG时,可能导致某个数据块的部分数据还没有提交,而拖数据的程序如果读到这个数据块的时候,下一次跳过这个数据块,会导致空洞。
789+
790+
拖数据测试脚本
791+
792+
```
793+
一次请求拖1万条
794+
795+
#!/bin/bash
796+
797+
for ((i=1;i<13;i++))
798+
do
799+
. /home/dege.zzz/env_pg10.sh ${i}
800+
for ((i=1;i<=1000000;i=i+200))
801+
do
802+
echo "start: `date` $((50*$i-50))"
803+
psql -c "explain (analyze,verbose,timing,costs,buffers) select * from rds_logs_1_20170607 where ctid = any (gen_tids($i));" >/dev/null
804+
echo "end: `date` "
805+
done
806+
done
807+
```
808+
809+
为了提高拖数据的性能,拖数据和写数据尽量保持一个速度水平,这样写入的数据还没有被刷到磁盘,可以在缓存中命中,拖数据的效率更高。
810+
811+
原理详见
812+
813+
[《块级扫描在IoT(物联网)极限写和消费读并存场景的应用》](../201706/20170607_01.md)
814+
815+
## 小结
754816
### 性能指标
755817
1\. 数据量:
756818

201706/20170607_01.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## 块级扫描在IoT(物联网)极限写和消费读并存场景的应用
1+
## 块级(ctid)扫描在IoT(物联网)极限写和消费读并存场景的应用
22
33
### 作者
44
digoal
@@ -109,6 +109,8 @@ postgres=# select current_setting('block_size')::int/31;
109109
(1 row)
110110
```
111111

112+
如果需要评估更精确的行数,可以加上字段的固定长度,变长字段的头(4BYTE)。
113+
112114
## 例子
113115
### 生成指定block TID的函数
114116
```
@@ -190,6 +192,9 @@ postgres=# select ctid,* from test where ctid = any(gen_tids(9));
190192
(909 rows)
191193
```
192194

195+
## 扩展场景
196+
如果数据没有更新,删除;那么CTID还可以作为索引来使用,例如全文检索(ES),可以在建立索引时使用ctid来指向数据库中的记录,而不需要另外再建一个PK,也能大幅度提升写入性能。
197+
193198
## 参考
194199
https://www.citusdata.com/blog/2016/03/30/five-ways-to-paginate/
195200

201706/20170607_02.md

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
## 多字段,任意组合条件查询(无需建模) - 毫秒级实时圈人 最佳实践
2+
3+
### 作者
4+
digoal
5+
6+
### 日期
7+
2017-06-07
8+
9+
### 标签
10+
PostgreSQL , 数组 , GIN索引 , 任意字段组合查询 , 圈人 , ToB分析型业务 , 建模
11+
12+
----
13+
14+
## 背景
15+
你也许在一家ToB的数据分析公司,你可能设计了一张表(包括用户标识,及若干已经统计好的的属性值),你也许收集了一些用户的数据,你也许要为客户提供报表,你也许需要为客户提供任意属性值的组合查询,并快速的返回结果给用户。
16+
17+
这些需求应该是非常常见的ToB的数据平台公司的形态,头痛的问题无法建模,因为B端的需求无法捉摸,任意组合查询、要求实时响应。
18+
19+
你的客户数据也许有几十亿上百亿,客户数据也许有几百个属性,用户可能需要的是任意属性组合的结果。
20+
21+
如果要快速响应,你的第一反应是不是对查询条件建立索引呢?
22+
23+
比如
24+
25+
```
26+
where col1=? and col2=? and col3<>? or col4=?;
27+
```
28+
29+
这样的SQL,你准备怎么做到实时响应呢?(col1,col2)建立索引,col4建立索引,这样是吗?
30+
31+
但是用户下次的请求肯又换条件了
32+
33+
```
34+
where col3=1 or col100=?
35+
```
36+
37+
是不是又要建col3, col100的索引呢?
38+
39+
你会发现根本没有办法优化,因为对应查询的索引组合可能是成千上万的。
40+
41+
## PostgreSQL 对付任意字段检索的黑科技
42+
我在之前写过一些关于任意字段查询的实践文章,广泛应用于广告营销平台的圈人,ToB的圈人,前端页面的任意组合筛选等场景。
43+
44+
## 方法1,GIN复合索引
45+
对需要参与查询的字段,建立GIN的复合索引。
46+
47+
![pic](20170607_02_pic_004.jpg)
48+
49+
CASE如下:
50+
51+
[《任意组合字段等效查询, 探探PostgreSQL多列展开式B树 (GIN)》](../201702/20170205_01.md)
52+
53+
这个场景针对任意字段匹配的场景,PostgreSQL对于多个查询条件,内部会使用索引+bitmapAnd或bitmapOr来筛选BLOCK,得到中间结果。
54+
55+
```
56+
+---------------------------------------------+
57+
|100000000001000000010000000000000111100000000| bitmap 1
58+
|000001000001000100010000000001000010000000010| bitmap 2
59+
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
60+
|000000000001000000010000000000000010000000000| Combined bitmap
61+
+-----------+-------+--------------+----------+
62+
| | |
63+
v v v
64+
Used to scan the heap only for matching pages:
65+
+---------------------------------------------+
66+
|___________X_______X______________X__________|
67+
+---------------------------------------------+
68+
```
69+
70+
这种方法为什么快呢?
71+
72+
原因是GIN索引实现了内部bitmapAnd or bitmapOr,实际上等效于对每个字段建立单独的B-Tree索引(PostgreSQL对多个B-Tree索引也支持bitmapAnd, bitmapOr的合并)。
73+
74+
bitmapand,or原理如下:
75+
76+
[《PostgreSQL bitmapAnd, bitmapOr, bitmap index scan, bitmap heap scan》](../201702/20170221_02.md)
77+
78+
GIN的复合索引这种方法可以满足以上需求,但是,当数据量非常庞大或者列非常多时,GIN索引会比较大。
79+
80+
### 方法1 优化技巧
81+
建议可以拆成多张表(例如随机拆分,或者按强制条件拆分)。降低GIN索引的大小,同时还可以利用PostgreSQL 10的多表并行特性,提升查询性能。
82+
83+
#### PostgreSQL并行计算特性
84+
PostgreSQL支持单表多核并行,也支持多表的并行查询
85+
86+
单表并行,指一条SQL,在处理单张表的数据时,可以使用多个CPU进行运算。
87+
88+
多表并行,指的是一条SQL涉及到多张表的处理(例如APPEND SCAN)时,可以并行的处理多个表的SCAN。
89+
90+
多表并行是在PG 10版本加入的,PostgreSQL 10 append scan 并行
91+
92+
[《PostgreSQL 10.0 preview sharding增强 - 支持Append节点并行》](../201703/20170312_11.md)
93+
94+
![pic](20170607_02_pic_001.jpg)
95+
96+
## 方法2,行级全文检索
97+
将整行记录转换为一个大大的字符串,然后对整行记录建立全文索引(PostgreSQL内置了全文索引的功能),搜索时即覆盖了任意字段任意匹配的场景。
98+
99+
[《PostgreSQL 如何高效解决 按任意字段分词检索的问题 - case 1》](../201607/20160725_05.md)
100+
101+
此方法适用于不限定列,但是限定查询条件的场景。
102+
103+
比如搜索 迪奥香水 , 可以在表的任意字段匹配,(例如 店铺名字、商品名字、用户名字)。
104+
105+
## 方法3,bloom过滤
106+
bloom方法过滤的效果有限,目前算是一种预览特性,建议观察使用。
107+
108+
[《PostgreSQL 9.6 黑科技 bloom 算法索引,一个索引支撑任意列组合查询》](../201605/20160523_01.md)
109+
110+
## 方法4,数组化(同类应用 1 - 电商圈人)
111+
每个用户对应若干个标签,商家根据标签的组合筛选人群。这是广告商的惯用手法。
112+
113+
主要利用了PostgreSQL的数组类型、倒排索引,性能也是杠杠的。
114+
115+
[《万亿级营销(圈人)潇洒的迈入毫秒时代 - 万亿user_tags级实时推荐系统数据库设计》](../201612/20161225_01.md)
116+
117+
![pic](../201612/20161225_01_pic_001.png)
118+
119+
![pic](20170607_02_pic_002.jpg)
120+
121+
这种方法为什么快呢?
122+
123+
它采用了ARRAY元素倒排的方法,在查询时,对查询条件进行块级BITMAP筛选,筛选后的数据落到少量的数据块,再进行recheck选出最终结果。
124+
125+
![pic](../201701/20170112_02_pic_002.jpg)
126+
127+
### 本案例(多字段,任意组合条件 毫秒级实时圈人) 方法4 实践
128+
实际上文章开头提到的场景,和电商圈人非常相似,所以也能使用电商圈人的方法。
129+
130+
怎么实现呢?
131+
132+
#### 1 多个字段转换为数组
133+
首先,将多个字段,转换为一个数组字段。
134+
135+
![pic](20170607_02_pic_003.jpg)
136+
137+
例如
138+
139+
create table test(uid int8 primary key, tag1 int, tag2 text, tag3 int, tag4 text, tag5 timestamp, tag6 date, ...);
140+
141+
转换为
142+
143+
create table test(uid int8 primary key, tag text[]);
144+
145+
例子
146+
147+
1, 1, 'man', 1023, 'football', '2017-01-01 10:00:00', '1989-09-01'
148+
149+
转换为
150+
151+
1, array['tag1:1', 'tag2:man', 'tag3:1023', 'tag4:football', 'tag5:...', tag6:...']
152+
153+
#### 2 阶梯化(可选)
154+
如果查询中包含 =, <> 以外的查询(如某些字段为年龄、销售额、收入等,那么可能有大于,小于的范围查询需求),那么我们需要阶梯化对应TAG的VALUE,阶梯化的方法请参考
155+
156+
[《恭迎万亿级营销(圈人)潇洒的迈入毫秒时代 - 万亿user_tags级实时推荐系统数据库设计》](../201612/20161225_01.md)
157+
158+
#### 3 拆表(可选)
159+
拆表的目的是并行,保持每个表的体量。
160+
161+
拆表的方法很多,可以随机,可以按UID进行哈希。
162+
163+
拆表后,扫描所有的分区表,聚合结果即可。
164+
165+
拆表可以是本地拆分、也可以是跨库拆分。本地拆分,实际上就是分区表。跨库拆分则涉及到数据分发、聚合的过程。
166+
167+
跨库分发和聚合的方法也很多:例如postgres_fdw+pg_pathman, plproxy, 程序实现等方法。请参考如下文档
168+
169+
[《PostgreSQL 9.6 sharding based on FDW & pg_pathman》](../201610/20161027_01.md)
170+
171+
[《PostgreSQL 最佳实践 - 水平分库(基于plproxy)》](../201608/20160824_02.md)
172+
173+
[《阿里云ApsaraDB RDS for PostgreSQL 最佳实践 - 2 教你RDS PG的水平分库》](../201512/20151220_02.md)
174+
175+
[《阿里云ApsaraDB RDS for PostgreSQL 最佳实践 - 3 水平分库 vs 单机 性能》](../201512/20151220_03.md)
176+
177+
[《阿里云ApsaraDB RDS for PostgreSQL 最佳实践 - 4 水平分库 之 节点扩展》](../201512/20151220_04.md)
178+
179+
#### 4 建立数组GIN索引
180+
对数组字段建立GIN索引,建立GIN索引实际上就是倒排索引,数组元素作为KEY,行号作为VALUE的B树。
181+
182+
例如搜索包含某个TAG的用户,从GIN索引得到HEAP表行号,然后获取记录即可,速度非常快。
183+
184+
多个TAG组合查询时,内部进行BITMAP and/or的合并,过滤到数据块级别,然后通过数据块获取记录,通过查询条件FILTER,得到最终结果,速度也非常快。
185+
186+
关于GIN,多个TAG组合查询的原理,可以参考这篇文档。
187+
188+
[《电商内容去重\内容筛选应用(实时识别转载\盗图\侵权?) - 文本、图片集、商品集、数组相似判定的优化和索引技术》](../201701/20170112_02.md)
189+
190+
#### 5 圈人 <=> 数组组合查询
191+
将多个字段数组化后,圈人就变成了数组的操作,例如
192+
193+
```
194+
where tag1=? and tag2=? or tag3=?
195+
```
196+
197+
转换为数组操作如下:
198+
199+
```
200+
where arraycol @> array[tag1:?, tag2:?] or arraycol && [tag3:?]
201+
```
202+
203+
数组查询会走GIN索引扫描,速度快得惊人。
204+
205+
## 方法5,bitmap化(同类应用 2 - 圈人(基于阿里云 RDS PostgreSQL varbitx))
206+
这个用到的是bit的方法,当所有属性的VALUE可以被穷举时,例如可以穷举到100万或者多少,那么我们可以使用这样的方法来优化圈人的应用。
207+
208+
BIT相比数组的方法,BIT空间下降25倍左右,性能稳定。
209+
210+
但是BIT方法要求数据的写入是合并式的,最好使用UDF完成,实际案例如下(包含数据合并的DEMO代码)。
211+
212+
[《阿里云RDS for PostgreSQL varbitx插件与实时画像应用场景介绍》](../201705/20170502_01.md)
213+
214+
[《基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统》](../201610/20161021_01.md)
215+
216+
varbitx这种方法与bitmap数据库pilosa如出一辙,但是PG有更强大的功能背景,推荐使用PG。
217+
218+
https://www.pilosa.com/docs/introduction/
219+
220+
## 小结
221+
在PostgreSQL中,圈人业务场景的优化方法非常多,得益于PostgreSQL强大的功能。下面小结一下每一种方法,
222+
223+
1\. gin 复合索引,对需要参与查询的列,构建GIN复合索引即可。PostgreSQL内部会对GIN的多个条件使用bitmapAnd, bitmapOr进行合并。
224+
225+
这种方法的使用最为简便,但是当数据量或列非常多时,GIN索引会很大,GIN索引很大带来一个问题,建立索引的速度较慢,将来维护索引的速度也较慢。
226+
227+
使用这种方法,建议对表进行本地分区、或者垮库分区,将单表的数据量降低。(多列展开后的记录数建议在1亿左右,例如10列,则单表记录数控制在1000万)(经验值,随着硬件发展,以后可能更多)
228+
229+
同时GIN建议使用fastupdate和延迟合并的特性,加速插入、删除、更新操作。
230+
231+
2\. 独立B-tree索引,需要参与查询的列,每列单独建立B-Tree索引(或者对应类型的其他索引例如brin, gin, gist, sp-gist, hash),PostgreSQL内部会对多个索引查询的结果使用bitmapAnd, bitmapOr进行合并。
232+
233+
这种方法使用也非常便捷,使用这种方法,建议对表进行本地分区、或者垮库分区,将单表的数据量降低。单表记录数建议控制在1亿左右(经验值,随着硬件发展,以后可能更多)。
234+
235+
3\. 数组化+GIN,类似与电商的圈人场景,查询时使用数组的包含,相交操作符,实现索引检索。
236+
237+
这种方法特别适合于已经构建好标签(使用PostgreSQL数组)的场景,直接使用数组索引、数组的操作即可实现圈人。
238+
239+
万亿user_tags级,毫秒响应。
240+
241+
[《恭迎万亿级营销(圈人)潇洒的迈入毫秒时代 - 万亿user_tags级实时推荐系统数据库设计》](../201612/20161225_01.md)
242+
243+
4\. bit化,当标签可以穷举时,可以将标签作为KEY,将USERID作为bit进行存储,使用BIT的方法相比ARRAY,空间需求下降25倍,效率可以保持平稳。
244+
245+
这种方法将用户和标签做了一次倒转,好处很明显,支持任意组合的高效圈人。但是比较烧脑,也需要更多的开发工作量(这部分已经有UDF DEMO)。
246+
247+
### 建议
248+
249+
1\. 省钱、高效、不怕烧脑
250+
251+
bit化。
252+
253+
2\. 有钱、高效、能折腾
254+
255+
数组化+GIN
256+
257+
3\. 有钱、高效、不能折腾
258+
259+
独立B-Tree, GIN复合索引

201706/20170607_02_pic_001.jpg

86.2 KB
Loading

201706/20170607_02_pic_002.jpg

148 KB
Loading

201706/20170607_02_pic_003.jpg

156 KB
Loading

201706/20170607_02_pic_004.jpg

146 KB
Loading

201706/readme.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
### 文章列表
22
----
3-
##### 20170607_01.md [《块级扫描在IoT(物联网)极限写和消费读并存场景的应用》](20170607_01.md)
3+
##### 20170607_02.md [《多字段,任意组合条件查询(无需建模) - 毫秒级实时圈人 最佳实践》](20170607_02.md)
4+
##### 20170607_01.md [《块级(ctid)扫描在IoT(物联网)极限写和消费读并存场景的应用》](20170607_01.md)
45
##### 20170605_02.md [《PostgreSQL UDF实现IF NOT EXISTS语法》](20170605_02.md)
56
##### 20170605_01.md [《AI(OtterTune)引波澜 - AI会洗牌数据库行业吗? DBA如何转变思想》](20170605_01.md)
67
##### 20170604_01.md [《JSONB 压缩版本 ZSON》](20170604_01.md)

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ digoal's|PostgreSQL|文章|归类
2828

2929
### 未归类文档如下
3030
----
31-
##### 201706/20170607_01.md [《块级扫描在IoT(物联网)极限写和消费读并存场景的应用》](201706/20170607_01.md)
31+
##### 201706/20170607_02.md [《多字段,任意组合条件查询(无需建模) - 毫秒级实时圈人 最佳实践》](201706/20170607_02.md)
32+
##### 201706/20170607_01.md [《块级(ctid)扫描在IoT(物联网)极限写和消费读并存场景的应用》](201706/20170607_01.md)
3233
##### 201706/20170605_02.md [《PostgreSQL UDF实现IF NOT EXISTS语法》](201706/20170605_02.md)
3334
##### 201706/20170605_01.md [《AI(OtterTune)引波澜 - AI会洗牌数据库行业吗? DBA如何转变思想》](201706/20170605_01.md)
3435
##### 201706/20170604_01.md [《JSONB 压缩版本 ZSON》](201706/20170604_01.md)

0 commit comments

Comments
 (0)