数据流
流(Stream)是一种抽象,允许客户端通过连续视图推送有时间顺序的数据。数据行(或简单的事件)与常规关系表的行完全相同,即,将数据写入流的接口与写入表的接口相同。然而,流的语义与表有根本的不同。
也就是说,事件只存在于流中,直到它被连续视图从流中读取所消耗,因此,用户不能从流中进行数据的选择,只能将流作为连续视图的输入。
创建Stream
流在PipelineDB中表示为由外部服务器管理的外部表。创建外表的语法与创建普通PostgreSQL表的语法相似:
CREATE FOREIGN TABLE stream_name ( [
{ column_name data_type [ COLLATE collation ] } [, ... ]
] )
SERVER pipelinedb;
stream_name:要创建的流的名称。
column_name:在新表中创建的列的名称。
data_type:列的数据类型,可以包括数组以及绝大部分PostgreSQL支持的类型。
COLLATE collation:COLLATE子句将排序规则分配给列(必须是可排序数据类型)。如果未指定,则使用列数据类型的默认排序规则。
可以使用ALTER STREAM将列添加到流中,但无法从流中删除字段:
ALTER FOREIGN TABLE stream ADD COLUMN x integer;
可以使用DROP FOREIGN TABLE命令删除流:
DROP FOREIGN TABLE stream;
数据流输入
INSERT
流写入使用常规的PostgreSQL插入语句,语法如下:
INSERT INTO stream_name ( column_name [, ...] )
{ VALUES ( expression [, ...] ) [, ...] | query };
其中,query可以是一个SELECT查询。
流的写入可以是在某个时间的一个单一事件:
INSERT INTO stream (x, y, z) VALUES (0, 1, 2);
INSERT INTO json_stream (payload) VALUES (
'{"key": "value", "arr": [92, 12, 100, 200], "obj": { "nested": "value" } }'
);
也可以是批量处理的语句,以便获得更好的性能:
INSERT INTO stream (x, y, z) VALUES (0, 1, 2), (3, 4, 5), (6, 7, 8)
(9, 10, 11), (12, 13, 14), (15, 16, 17), (18, 19, 20), (21, 22, 23), (24, 25, 26);
流写入也可以包含表达式或者子查询:
INSERT INTO geo_stream (id, coords) VALUES (42, a_function(-72.09, 41.40));
INSERT INTO udf_stream (result) VALUES (my_user_defined_function('foo'));
INSERT INTO str_stream (encoded, location) VALUES
(encode('encode me', 'base64'), position('needle' in 'haystack'));
INSERT INTO rad_stream (circle, sphere) VALUES
(pi() * pow(11.2, 2), 4 / 3 * pi() * pow(11.2, 3));
INSERT INTO ss_stream (x) SELECT generate_series(1, 10) AS x;
INSERT INTO tab_stream (x) SELECT x FROM some_table;
Prepared INSERT
流写入还可以通过Prepared INSERT执行,以减少网络开销:
PREPARE write_to_stream AS INSERT INTO stream (x, y, z) VALUES ($1, $2, $3);
EXECUTE write_to_stream(0, 1, 2);
EXECUTE write_to_stream(3, 4, 5);
EXECUTE write_to_stream(6, 7, 8);
COPY
还可以使用COPY将数据从文件写入流:
COPY stream (data) FROM '/some/file.csv'
对于将归档数据回溯的填充连续视图的场景,COPY非常有用。例如,将压缩的归档数据从S3写入PipelineDB:
aws s3 cp s3://bucket/logfile.gz - | gunzip | pipeline -c "COPY stream (data) FROM STDIN"
其他输入端
由于PipelineDB是PostgreSQL的一个扩展,任何使用PostgreSQL的客户端都可以对流进行写入操作,甚至是大多数使用SQL数据库的客户端,也能构造流的写入。
数据流输出
由于流输出是常规的PipelineDB流,因此可以被其他连续视图或转换读取,也使得从流中读取对任意连续视图或连续转换的选择数据行进行增量更改成为可能,即,通过在连续视图或转换上调用的output_of函数来访问的。
对于连续视图,流输出中的每一行总是包含一个旧元组和一个新元组,用来表示对底层连续视图所做的更改。如果更改对应于连续的视图插入,则旧的元组为NULL。如果更改对应于删除,则新元组为NULL。
假设有这样一个连续视图,它简单的对数据流的某列进行求和:
CREATE VIEW v_sum AS SELECT sum(x) FROM stream;
现在要求记录每次Sum变化超过10时的情况,可以通过创建另一个连续视图,从v_sum的输出流中读取数据:
CREATE VIEW v_deltas AS SELECT abs((new).sum - (old).sum) AS delta
FROM output_of('v_sum')
WHERE abs((new).sum - (old).sum) > 10;
关键字new与old必须使用括号。
滑动窗口场景
对于非滑动窗口的连续视图,只要流的写入导致连续视图结果的更改,流输出就会被直接写入。由于滑动窗口连续视图的结果依赖于时间,当结果随时间变化时,它们的流输出会被自动写入。换言之,滑动窗口连续视图的流输出将接收写操作,即使它的流输入并没有新的数据被写入。
Delta流
除了将旧元组和新元组写入连续视图的流输出之外,每次对连续视图进行的增量更改也会产生增量元组。Delta元组包含表示新旧元组之间差异的值。类似Sum这样的普通聚合函数,旧值和新值之间的增量只是(new).sum - (old).sum。
结合下方示例理解Delta流:
CREATE VIEW v AS SELECT COUNT(*) FROM stream;
CREATE VIEW v_real_deltas AS SELECT (delta).sum FROM output_of('v');
INSERT INTO stream (x) VALUES (1);
SELECT * FROM v_real_deltas;
sum
-----
1
(1 row)
INSERT INTO stream (x) VALUES (2);
INSERT INTO stream (x) VALUES (3);
SELECT * FROM v_real_deltas;
sum
-----
1
2
3
(3 rows)
可以看到,v_real_deltas记录了每次插入所产生的增量值。这只是一个简单的示例,Delta流真正的价值在于,它们适用于所有聚合,甚至可以与Combine一起使用,可以在不同粒度或分组上高效地聚合连续视图的输出。
再看一个计算每分钟不同用户的数量的例子:
CREATE VIEW uniques_1m AS
SELECT minute(arrival_timestamp) AS ts, COUNT(DISTINCT user_id) AS uniques
FROM s GROUP BY ts;
出于数据归档和性能的考虑,需要在一段时间后将这个连续视图调整为每小时的粒度。对于COUNT(DISTINCT)这样的聚合,显然不能简单地将一小时内所有分钟的计数相加,因为用户可能会被重复统计。因此,可以通过组合分钟级连续视图的输出所产生的不同Delta值来统计:
CREATE VIEW uniques_hourly AS
SELECT hour((new).ts) AS ts, combine((delta).uniques) AS uniques
FROM output_of('uniques_1m') GROUP BY ts;
uniques_hourly连续视图统计每小时的uniques,这些行与原始信息一致,就像所有原始值都按小时级别汇总一样,由combine结合权重计算均值。 只是,不必通过读取原始事件来执行,只需要进一步聚合分钟级聚合的输出。
stream_targets
有时,在写入流时,可能只需要更新一组选定的连续查询视图或转换,可以使用stream_targets配置参数(要使用事件的连续查询或转换名称列表,逗号分隔)来指定应从当前会话读取写入流的事件的连续查询:
CREATE VIEW v0 AS SELECT COUNT(*) FROM stream;
CREATE VIEW v1 AS SELECT COUNT(*) FROM stream;
INSERT INTO stream (x) VALUES (1);
SET stream_targets TO v0;
INSERT INTO stream (x) VALUES (1);
SET stream_targets TO DEFAULT;
INSERT INTO stream (x) VALUES (1);
SELECT count FROM v0;
count
-------
3
(1 row)
SELECT count FROM v1;
count
-------
2
(1 row)
到达顺序
PipelineDB使用到达顺序对事件进行排序。这意味着事件在到达PipelineDB服务器时会加上时间戳,并被赋予包含该时间戳的附加属性arrival_timestamp。因此,可以在带有时间组件的连续视图中使用arrival_timestamp,例如,滑动窗口。
过期事件
在每个事件到达PipelineDB服务器之后,将生成一个位图,标记仍需要读取该事件的所有连续视图。当连续视图完成读取事件后,它将位图对应的标记重新置位。当位图中的所有位都为1时,该事件将被丢弃并且永远无法再次访问。