The securities industry produces more data, and reads and writes frequently.
Take stock trading as an example, there are thousands of stocks. There are about 240 trading days in a year, with trading hours ranging from 10:00 to 16:00.
1. Data writing requirements:
Real-time data writing, and real-time merging by query dimension (For example, second-level data is written in real time. Minute-level data is merged in real time .)
Data is divided into time-sharing data of different granularity. (The data is exact to seconds, minutes, hours, days )
2. Dimensions of data:
Each stock contains more than 10 indicators, such as time, number of orders, volume, average price, highest price, lowest price, starting price, and ending price.
3. Database storage:
All the data needs to be stored from the listing of each stock onward, with an average of at least ten years of data.
4. Data query requirements:
Query the data within a certain period of time; for example, query the minute-level data of a stock on July 1, 2016.
5. Analysis requirements:
For example, linear regression analysis.
PostgreSQL provides many features to better meet these requirements.
1. BRIN block-level indexes. The data is inserted in time order, so the time field has a strong linear correlation with HEAP storage. Using BRIN block-level indexes can reduce index size exponentially while providing excellent range query performance.
2. JSON, HSTORE, and array type. You can use a record to store a range of data, such as a record to store time-sharing data for a day. This way, you can reduce the number of records scanned by the database, and this speeds up the scan of the range data.
3. Range type and gist index. With the second feature, the range type can be used to indicate the start time for this record. Why not use two fields to represent it? Because the range type is more efficient, and you can refer to the following article.
Talk About the Problem of BETWEEN AND and the Solution
4. Analysis requirements, such as linear regression, machine learning (MADlib) library, multidimensional analysis, and language extension (plpgsql,plpython,plr). The data analysis needs of the securities industry can be well supported.
PostgreSQL has 10 numerical types, three of which may be related to the financial industry.
1. numeric (131,072 digits before the decimal point and 16,383 digits after the decimal point)
2. float8 (15 valid digits)
Numeric is a variable length type and has a palloc (memory transfer) when used, which is less efficient than float8.
3. decimal128 extension type, which is more frequently used in the financial field. (Both performance and the range of data representation can meet the requirements)
PostgreSQL decimal64 decimal128 Efficient Numerical Type Extension
Second-level data tables, requiring fast insertion
(Each stock has 5,184 million records every 10 years, so partitioning is not considered)
create table tbl_sec_股票代码 -- 每只股票一张表,可以达到最高的查询效率,弊端是表多,需要动态拼接表名,变更表结构时,需要调整较多的表(可以使用继承来管理,减少管理复杂度)。
(
id serial8 primary key, -- 序列(可选字段)
时间 timestamp(0), -- 值的生成时间
指标1 numeric, -- 数据指标列
指标2 numeric,
...
指标10 numeric
);
create index idx_xx on tbl_sec_股票代码 (时间);
或
create index idx_xx on tbl_sec_股票代码 using brin (时间);
Minute-level data tables, requiring fast query by time period (less data, no partition required)
create table tbl_min_股票代码
(
id serial8 primary key, -- 序列(可选字段)
时间 timestamp(0), -- 值的生成时间
指标1 numeric, -- 数据指标列
指标2 numeric,
...
指标10 numeric
);
create index idx_xx on tbl_min_股票代码 (时间);
或
create index idx_xx on tbl_min_股票代码 using brin (时间);
The BRIN index. When range query requirements are high, the BRIN index can significantly reduce the index size while improving insert performance. Examples of BRIN indexes are as follows:
PostgreSQL 9.5 New Feature - BRIN (Block Range Index) Index
The client usually has data cache, so the query frequency is not very high. For scenarios where the range (massive data) query frequency is very high, the following optimal methods can be used:
Aggregate again by time period.
More extreme optimization method (optional)
If you want to perform range queries frequently (for example, querying the minute-level details of a day) and use the previous design, you need a range of 360 records. For better query efficiency, you can use aggregation (for example, aggregation by day), then only one record is returned when querying data for one day.
Minute-level data tables, requiring fast query by time period (a real-time aggregation table and a delayed aggregation table, reducing garbage)
create table tbl_min_股票代码_实时聚合
(
id serial8 primary key, -- 序列(可选字段)
时间 timestamp(0), -- 表示当前记录的写入时间
指标1 numeric, -- 数据指标列
指标2 numeric,
...
指标10 numeric
);
create index idx_xx on tbl_min_股票代码_实时聚合 (时间);
create table tbl_min_股票代码_延时聚合
(
id serial8 primary key, -- 序列(可选字段)
时间区间 tsrange, -- 表示当前记录的时间区间
指标 jsonb -- 数据指标列
);
create index idx_xx on tbl_min_股票代码_延时聚合 using gist(时间区间);
Indicators can be represented by jsonbhstorearray, and they can always be freely chosen. Take jsonb as an example:
{指标1: {时间点1:value, 时间点2:value, ......} , 指标2: {时间点1:value, 时间点2:value, ......}, ......}
tsrange supports the gist index. Data from 2017-01-01 to 2017-01-05 can be quickly (in milliseconds) located to the record through the index. The example is as follows:
Talk About the Problem of BETWEEN AND and the Solution
Other time-sharing data table designs are similar to the minute-level design.
Delayed aggregation process
Aggregation process
Second-level table -> (real-time aggregation) real-time aggregation minute-level table -> (One day delay for aggregation) delayed aggregation minute-level table
Query process
(Query 1 real-time aggregation minute-level table) union all (Query 2 delayed aggregation minute-level table)
If you do not want to have a table for each stock at the business level, you can also use the PostgreSQL partitioned table feature, taking the stock ID as the partition field and using hash partitions.
10.0 currently supports range and list partitions, but hash partitions have not been merged into the master branch yet.
https://www.postgresql.org/docs/devel/static/sql-createtable.html
However, pg_pathman already supports hash partitions and users can choose freely.
https://github.com/postgrespro/pg_pathman
PostgreSQL 9.6 sharding based on FDW & pg_pathman
PostgreSQL 9.5+ Efficient Partitioned Table Implementation – pg_pathman
1. Insert
The second-level data of each stock is inserted in real time.
2. Real-time merging
Data of different dimensions, such as grouping data and daily data, of each stock is merged into the corresponding real-time aggregation table in real time after reaching the time point through the second-level data.
For example, after reaching the point of 2017-01-01 11:00:00, the second-level data of 2017-01-01 10:59:00 to 2017-01-01 10:59:59 is merged into the minute-level real-time aggregation table.
3. Delayed merging (optional)
Delayed merging, which aggregates the results of real-time merging into one record by interval.
For example, each stock has 360 minute-level records per day, which are merged into one record per day and represented by jsonb.
The purpose of merging is to reduce the number of scanned records for range queries, such as returning only one record per day.
4. Query
Precise query, which specifies the time to query a record.
Range query, which specifies the time range to query the records within this range.
Create a test base table
create table tbl_sec
(
crt_time timestamp(0),
c1 float8,
c2 float8,
c3 float8,
c4 float8,
c5 float8,
c6 float8,
c7 float8,
c8 float8,
c9 float8,
c10 float8
);
create index idx_tbl_sec_time on tbl_sec using brin (crt_time) with (pages_per_range=1);
Create 3,000 stock tables
do language plpgsql
$$
declare
sql text;
begin
for i in 1..3000 loop
sql := format('create table %I (like tbl_sec including all)', 'tbl_sec_'||lpad(i::text, 6, '0') );
execute sql;
end loop;
end;
$$
;
Data insertion performance test for each stock
vi test.sql
\set c1 random(1,1000)
\set c2 random(1,1000)
\set c3 random(1,1000)
\set c4 random(1,1000)
\set c5 random(1,1000)
\set c6 random(1,1000)
\set c7 random(1,1000)
\set c8 random(1,1000)
\set c9 random(1,1000)
\set c10 random(1,1000)
insert into tbl_sec_000001 values (now(),:c1,:c2,:c3,:c4,:c5,:c6,:c7,:c8,:c9,:c10);
Insertion delay is 0.043 milliseconds
pgbench -M prepared -n -r -P 1 -f ./test.sql -c 1 -j 1 -T 10
tps = 21714.908797 (including connections establishing)
tps = 21719.144013 (excluding connections establishing)
script statistics:
- statement latencies in milliseconds:
0.000 \set c1 random(1,1000)
0.000 \set c2 random(1,1000)
0.000 \set c3 random(1,1000)
0.000 \set c4 random(1,1000)
0.000 \set c5 random(1,1000)
0.000 \set c6 random(1,1000)
0.000 \set c7 random(1,1000)
0.000 \set c8 random(1,1000)
0.000 \set c9 random(1,1000)
0.000 \set c10 random(1,1000)
0.043 insert into tbl_sec_000001 values (now(),:c1,:c2,:c3,:c4,:c5,:c6,:c7,:c8,:c9,:c10);
postgres=# select * from tbl_sec_000001 limit 10;
crt_time | c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c9 | c10
---------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
2017-04-17 14:14:00 | 480 | 60 | 918 | 563 | 168 | 457 | 129 | 887 | 870 | 457
2017-04-17 14:14:00 | 189 | 894 | 707 | 598 | 701 | 418 | 191 | 287 | 688 | 668
2017-04-17 14:14:00 | 492 | 423 | 972 | 101 | 28 | 847 | 919 | 698 | 594 | 430
2017-04-17 14:14:00 | 781 | 38 | 816 | 467 | 96 | 2 | 762 | 8 | 271 | 577
2017-04-17 14:14:00 | 225 | 126 | 828 | 158 | 447 | 12 | 691 | 693 | 272 | 995
2017-04-17 14:14:00 | 125 | 18 | 589 | 472 | 424 | 884 | 177 | 754 | 463 | 468
2017-04-17 14:14:00 | 156 | 412 | 784 | 40 | 126 | 100 | 727 | 851 | 80 | 513
2017-04-17 14:14:00 | 320 | 75 | 485 | 10 | 481 | 592 | 594 | 227 | 658 | 810
2017-04-17 14:14:00 | 678 | 199 | 155 | 325 | 212 | 977 | 170 | 696 | 895 | 679
2017-04-17 14:14:00 | 413 | 512 | 535 | 319 | 99 | 520 | 39 | 502 | 207 | 160
(10 rows)
For a single stock, insert 10 million pieces of second-level data. Therefore, 3,000 stocks have about 30 billion pieces of test data.
postgres=# insert into tbl_sec_000001 select now()+(i||' sec')::interval , 1,1,1,1,1,1,1,1,1,1 from generate_series(1,10000000) t(i);
INSERT 0 10000000
postgres=# select * from tbl_sec_000001 limit 10;
crt_time | c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c9 | c10
---------------------+----+----+----+----+----+----+----+----+----+-----
2017-04-17 14:20:17 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
2017-04-17 14:20:18 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
2017-04-17 14:20:19 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
2017-04-17 14:20:20 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
2017-04-17 14:20:21 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
2017-04-17 14:20:22 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
2017-04-17 14:20:23 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
2017-04-17 14:20:24 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
2017-04-17 14:20:25 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
2017-04-17 14:20:26 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
(10 rows)
The index size and table size are 4,808 MB and 1,116 KB, respectively.
The BRIN index saves a lot of space.
public | tbl_sec_000001 | table | postgres | 1116 MB |
public | tbl_sec_000001_crt_time_idx | index | postgres | tbl_sec_000001 | 4808 kB |
It takes about 47 milliseconds to query data for 1 minute (60 pieces) and 2 hours (7,200 pieces).
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_sec_000001 where crt_time between '2017-04-17 14:21:00' and '2017-04-17 14:21:59';
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on public.tbl_sec_000001 (cost=777.40..778.72 rows=1 width=88) (actual time=46.612..46.628 rows=60 loops=1)
Output: crt_time, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10
Recheck Cond: ((tbl_sec_000001.crt_time >= '2017-04-17 14:21:00'::timestamp without time zone) AND (tbl_sec_000001.crt_time <= '2017-04-17 14:21:59'::timestamp without time zone))
Rows Removed by Index Recheck: 80
Heap Blocks: lossy=2
Buffers: shared hit=809
-> Bitmap Index Scan on idx_tbl_sec_000001_time (cost=0.00..777.40 rows=1 width=0) (actual time=46.597..46.597 rows=20 loops=1)
Index Cond: ((tbl_sec_000001.crt_time >= '2017-04-17 14:21:00'::timestamp without time zone) AND (tbl_sec_000001.crt_time <= '2017-04-17 14:21:59'::timestamp without time zone))
Buffers: shared hit=807
Planning time: 0.077 ms
Execution time: 46.664 ms
(11 rows)
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_sec_000001 where crt_time between '2017-04-17 14:21:00' and '2017-04-17 16:20:59';
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on public.tbl_sec_000001 (cost=834.57..7710.56 rows=5578 width=88) (actual time=46.194..47.437 rows=7200 loops=1)
Output: crt_time, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10
Recheck Cond: ((tbl_sec_000001.crt_time >= '2017-04-17 14:21:00'::timestamp without time zone) AND (tbl_sec_000001.crt_time <= '2017-04-17 16:20:59'::timestamp without time zone))
Rows Removed by Index Recheck: 80
Heap Blocks: lossy=104
Buffers: shared hit=911
-> Bitmap Index Scan on idx_tbl_sec_000001_time (cost=0.00..833.18 rows=5578 width=0) (actual time=46.182..46.182 rows=1040 loops=1)
Index Cond: ((tbl_sec_000001.crt_time >= '2017-04-17 14:21:00'::timestamp without time zone) AND (tbl_sec_000001.crt_time <= '2017-04-17 16:20:59'::timestamp without time zone))
Buffers: shared hit=807
Planning time: 0.060 ms
Execution time: 47.862 ms
(11 rows)
The BRIN index is a block-level index, so not much space is occupied and it is very suitable for scenarios where field values have a good correlation with the physical order of HEAP table storage.
However, BRIN is not a precise index, so the query efficiency may not be as good as B-Tree.
Users can use different indexing methods according to actual business requirements.
The comparison is as follows:
postgres=# drop index tbl_sec_000001_crt_time_idx;
cDROP INDEX
postgres=# create index tbl_sec_000001_crt_time_idx on tbl_sec_000001(crt_time);
CREATE INDEX
public | tbl_sec_000001_crt_time_idx | index | postgres | tbl_sec_000001 | 214 MB |
1. Comparison of space occupancy
Number of records | Table | B-Tree | BRIN |
10 million | 1116 MB | 214 MB | 4.8 MB |
2. Comparison of query efficiency
Number of returned records | B-Tree | BRIN |
60 pieces | 0.04 milliseconds | 46.7 milliseconds |
7,200 pieces | 1.96 milliseconds | 47.9 milliseconds |
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_sec_000001 where crt_time between '2017-04-17 14:21:00' and '2017-04-17 14:21:59';
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Index Scan using tbl_sec_000001_crt_time_idx on public.tbl_sec_000001 (cost=0.43..4.11 rows=54 width=88) (actual time=0.007..0.022 rows=60 loops=1)
Output: crt_time, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10
Index Cond: ((tbl_sec_000001.crt_time >= '2017-04-17 14:21:00'::timestamp without time zone) AND (tbl_sec_000001.crt_time <= '2017-04-17 14:21:59'::timestamp without time zone))
Buffers: shared hit=5
Planning time: 0.095 ms
Execution time: 0.040 ms
(6 rows)
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_sec_000001 where crt_time between '2017-04-17 14:21:00' and '2017-04-17 16:20:59';
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Index Scan using tbl_sec_000001_crt_time_idx on public.tbl_sec_000001 (cost=0.43..252.61 rows=6609 width=88) (actual time=0.022..1.539 rows=7200 loops=1)
Output: crt_time, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10
Index Cond: ((tbl_sec_000001.crt_time >= '2017-04-17 14:21:00'::timestamp without time zone) AND (tbl_sec_000001.crt_time <= '2017-04-17 16:20:59'::timestamp without time zone))
Buffers: shared hit=126
Planning time: 0.119 ms
Execution time: 1.957 ms
(6 rows)
Each stock has a table with a suffix of the stock code. You can splice the table names at the application side or use FUNCTION to encapsulate the spliced code in the database.
The market trading time is usually 4 hours a day and the trading market is not open on holidays. If a stock creates one record per second, there will be 4 60 60 52 5 = 3.744 million records in one day, and only 37.44 million records in 10 years.
So if you partition by stock, the time dimension is completely unnecessary.
1. The base table is as follows:
create table tbl
(
gid text,
crt_time timestamp,
c1 float8,
c2 float8,
c3 float8,
c4 float8,
c5 float8,
c6 float8,
c7 float8,
c8 float8,
c9 float8,
c10 float8
) PARTITION BY list (gid)
;
-- create index idx_tbl_sec_time on tbl_sec using btree (crt_time);
2. Data estimation
-- CREATE TABLE tbl_000000 PARTITION OF tbl FOR VALUES IN ('000000') PARTITION BY RANGE (crt_time);
-- 4*60*60*52*5=374.4万,10年才3744万条记录。
3. Create 3,000 stock tables
do language plpgsql
$$
declare
sql text;
begin
for i in 1..3000 loop
sql := format('create table %I PARTITION OF tbl for values in (%L)', 'tbl_'||lpad(i::text, 6, '0'), lpad(i::text, 6, '0'));
execute sql;
sql := format('create index %I on %I (crt_time)', 'idx_tbl_'||lpad(i::text, 6, '0')||'_1', 'tbl_'||lpad(i::text, 6, '0'));
execute sql;
end loop;
end;
$$
;
4. Create a data insertion function to encapsulate the table name inside PG
create or replace function ins_tbl(
i_gid text,
i_crt_time timestamp,
i_c1 float8,
i_c2 float8,
i_c3 float8,
i_c4 float8,
i_c5 float8,
i_c6 float8,
i_c7 float8,
i_c8 float8,
i_c9 float8,
i_c10 float8
) returns void as
$$
declare
begin
execute format('insert into %I values (%L, %L, %L, %L, %L, %L, %L, %L, %L, %L, %L, %L)', 'tbl_'||i_gid, i_gid, i_crt_time, i_c1, i_c2, i_c3, i_c4, i_c5, i_c6, i_c7, i_c8, i_c9, i_c10);
end;
$$
language plpgsql strict;
5. The stress testing shows that the writing speed is about 220,000/s, which fully meets the business requirements.
vi test.sql
\set id random(1,3000)
select ins_tbl(lpad(:id, 6, '0'), now()::timestamp, 1::float8,1::float8,1::float8,1::float8,1::float8,1::float8,1::float8,1::float8,1::float8,1::float8);
nohup pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -T 15000 > /dev/null 2>&1 &
6. Query performance
We do not recommend using the PG partitioned table to query directly, and the run and planning time is slightly longer.
postgres=# explain (analyze) select * from tbl where gid='000001' and crt_time between '2017-07-17 15:17:42.336503' and '2017-07-17 15:38:08.981317';
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Append (cost=0.29..20.47 rows=286 width=95) (actual time=0.017..0.033 rows=30 loops=1)
-> Index Scan using idx_tbl_000001_1 on tbl_000001 (cost=0.29..20.47 rows=286 width=95) (actual time=0.016..0.030 rows=30 loops=1)
Index Cond: ((crt_time >= '2017-07-17 15:17:42.336503'::timestamp without time zone) AND (crt_time <= '2017-07-17 15:38:08.981317'::timestamp without time zone))
Filter: (gid = '000001'::text)
Planning time: 141.484 ms
Execution time: 0.116 ms
(6 rows)
Querying partitions directly is very fast.
postgres=# explain (analyze) select * from tbl_000001 where gid='000001' and crt_time between '2017-07-17 15:17:42.336503' and '2017-07-17 15:38:08.981317';
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
Index Scan using idx_tbl_000001_1 on tbl_000001 (cost=0.29..21.60 rows=290 width=95) (actual time=0.009..0.016 rows=30 loops=1)
Index Cond: ((crt_time >= '2017-07-17 15:17:42.336503'::timestamp without time zone) AND (crt_time <= '2017-07-17 15:38:08.981317'::timestamp without time zone))
Filter: (gid = '000001'::text)
Planning time: 0.199 ms
Execution time: 0.036 ms
(5 rows)
The query code is encapsulated, and the table name is automatically spliced for query according to the stock code entered.
create or replace function sel_tbl(
i_gid text,
begin_crt_time timestamp,
end_crt_time timestamp
) returns setof tbl as
$$
declare
begin
return query execute format('select * from %I where crt_time between %L and %L', 'tbl_'||i_gid, begin_crt_time, end_crt_time);
end;
$$
language plpgsql strict;
The performance of using UDF to query data is very good, and takes 0.298 milliseconds.
postgres=# select * from sel_tbl('000001', '2017-07-17 15:17:42.336503', '2017-07-17 15:38:08.981317');
gid | crt_time | c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c9 | c10
--------+----------------------------+----+----+----+----+----+----+----+----+----+-----
000001 | 2017-07-17 15:17:42.336503 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:17:47.083672 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:17:53.633412 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:17:54.092175 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:17:55.452835 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:17:55.55255 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:17:59.689178 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:04.051391 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:11.255866 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:12.217447 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:12.456304 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:19.640116 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:22.022434 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:27.141344 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:33.709304 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:34.285168 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:52.501981 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:19:21.891636 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:19:36.091745 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:19:37.481345 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:37:43.894333 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:37:44.921234 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:37:45.317703 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:38:08.799772 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:38:08.897194 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:38:08.938029 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:38:08.953457 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:38:08.954542 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:38:08.959182 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:38:08.981317 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
(30 rows)
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from sel_tbl('000001', '2017-07-17 15:17:42.336503', '2017-07-17 15:38:08.981317');
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Function Scan on postgres.sel_tbl (cost=0.25..10.25 rows=1000 width=120) (actual time=0.277..0.279 rows=30 loops=1)
Output: gid, crt_time, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10
Function Call: sel_tbl('000001'::text, '2017-07-17 15:17:42.336503'::timestamp without time zone, '2017-07-17 15:38:08.981317'::timestamp without time zone)
Buffers: shared hit=9
Planning time: 0.030 ms
Execution time: 0.298 ms
(6 rows)
Usually, users have the demand for optional stocks, so they may need to enter data for more than one stock at a time.
select * from tbl where gid in ('000001','002999','001888') and crt_time between ? and ?
The UDF can also be used to meet users' query requirements.
create or replace function sel_tbl(
i_gid text[],
begin_crt_time timestamp,
end_crt_time timestamp
) returns setof tbl as
$$
declare
v_gid text;
begin
foreach v_gid in array i_gid
loop
return query execute format('select * from %I where crt_time between %L and %L', 'tbl_'||v_gid, begin_crt_time, end_crt_time);
end loop;
end;
$$
language plpgsql strict;
The performance is also excellent, and takes 0.662 milliseconds.
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from sel_tbl(array['000001','002999','001888'], '2017-07-17 15:17:42.336503', '2017-07-17 15:38:08.981317');
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Function Scan on postgres.sel_tbl (cost=0.25..10.25 rows=1000 width=120) (actual time=0.632..0.638 rows=86 loops=1)
Output: gid, crt_time, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10
Function Call: sel_tbl('{000001,002999,001888}'::text[], '2017-07-17 15:17:42.336503'::timestamp without time zone, '2017-07-17 15:38:08.981317'::timestamp without time zone)
Buffers: shared hit=30
Planning time: 0.048 ms
Execution time: 0.662 ms
(6 rows)
postgres=# select * from sel_tbl(array['000001','002999','001888'], '2017-07-17 15:17:42.336503', '2017-07-17 15:38:08.981317');
gid | crt_time | c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c9 | c10
--------+----------------------------+----+----+----+----+----+----+----+----+----+-----
000001 | 2017-07-17 15:17:42.336503 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:17:47.083672 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:17:53.633412 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:17:54.092175 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:17:55.452835 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:17:55.55255 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:17:59.689178 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:04.051391 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:11.255866 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:12.217447 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:12.456304 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:19.640116 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:22.022434 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:27.141344 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:33.709304 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:34.285168 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:18:52.501981 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:19:21.891636 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:19:36.091745 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:19:37.481345 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
000001 | 2017-07-17 15:37:43.894333 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:37:44.921234 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:37:45.317703 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:38:08.799772 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:38:08.897194 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:38:08.938029 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:38:08.953457 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:38:08.954542 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:38:08.959182 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
000001 | 2017-07-17 15:38:08.981317 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
002999 | 2017-07-17 15:18:04.116816 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
002999 | 2017-07-17 15:18:08.720714 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
002999 | 2017-07-17 15:18:11.021059 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
002999 | 2017-07-17 15:18:13.17118 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
002999 | 2017-07-17 15:18:19.349304 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
002999 | 2017-07-17 15:18:20.525734 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
002999 | 2017-07-17 15:18:38.480529 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
002999 | 2017-07-17 15:18:42.462302 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
002999 | 2017-07-17 15:18:42.81403 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
002999 | 2017-07-17 15:19:11.211989 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
002999 | 2017-07-17 15:19:14.861736 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
002999 | 2017-07-17 15:19:20.240403 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
002999 | 2017-07-17 15:19:32.747798 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
002999 | 2017-07-17 15:19:35.191558 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
002999 | 2017-07-17 15:29:58.143158 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
002999 | 2017-07-17 15:38:08.800312 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
002999 | 2017-07-17 15:38:08.801949 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
002999 | 2017-07-17 15:38:08.824119 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
002999 | 2017-07-17 15:38:08.835612 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
002999 | 2017-07-17 15:38:08.860339 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
002999 | 2017-07-17 15:38:08.918502 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
002999 | 2017-07-17 15:38:08.9365 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
002999 | 2017-07-17 15:38:08.944578 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
002999 | 2017-07-17 15:38:08.951397 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
002999 | 2017-07-17 15:38:08.963564 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
002999 | 2017-07-17 15:38:08.980547 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
002999 | 2017-07-17 15:38:08.980656 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
001888 | 2017-07-17 15:17:42.353113 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
001888 | 2017-07-17 15:17:43.15402 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
001888 | 2017-07-17 15:17:46.316366 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
001888 | 2017-07-17 15:17:51.982603 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
001888 | 2017-07-17 15:18:07.32869 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
001888 | 2017-07-17 15:18:16.798675 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
001888 | 2017-07-17 15:18:36.947117 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
001888 | 2017-07-17 15:18:39.629393 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
001888 | 2017-07-17 15:18:42.56243 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
001888 | 2017-07-17 15:18:48.777822 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
001888 | 2017-07-17 15:18:50.850458 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
001888 | 2017-07-17 15:18:51.693084 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
001888 | 2017-07-17 15:18:55.660418 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
001888 | 2017-07-17 15:19:07.735869 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
001888 | 2017-07-17 15:19:32.331744 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
001888 | 2017-07-17 15:19:34.409026 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
001888 | 2017-07-17 15:29:56.634906 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
001888 | 2017-07-17 15:38:08.749017 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
001888 | 2017-07-17 15:38:08.801824 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
001888 | 2017-07-17 15:38:08.829437 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
001888 | 2017-07-17 15:38:08.855895 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
001888 | 2017-07-17 15:38:08.857959 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
001888 | 2017-07-17 15:38:08.858431 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
001888 | 2017-07-17 15:38:08.882241 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
001888 | 2017-07-17 15:38:08.930556 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
001888 | 2017-07-17 15:38:08.938661 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
001888 | 2017-07-17 15:38:08.942828 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
001888 | 2017-07-17 15:38:08.9459 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
001888 | 2017-07-17 15:38:08.966001 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
(86 rows)
7. Ten million records are written for each stock, a total of 30 billion pieces of test data.
postgres=# select count(*) from tbl_000001;
count
----------
10135995
(1 row)
The test queries the records of a stock in a certain period of time, and the response time is within 1 ms.
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from sel_tbl('000001', '2017-07-17 15:17:42.336503', '2017-07-17 15:38:08.981317');
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Function Scan on postgres.sel_tbl (cost=0.25..10.25 rows=1000 width=120) (actual time=0.303..0.305 rows=30 loops=1)
Output: gid, crt_time, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10
Function Call: sel_tbl('000001'::text, '2017-07-17 15:17:42.336503'::timestamp without time zone, '2017-07-17 15:38:08.981317'::timestamp without time zone)
Buffers: shared hit=12
Planning time: 0.040 ms
Execution time: 0.328 ms
(6 rows)
8. Implement schemaless solution through to the end - PostgreSQL Schemaless Implementation (Class Mongodb Collection)
The schemaless solution solves the problem that the HEAP PAGE I/O scales up (each sensor is active, so checking several records of a sensor ID actually requires scanning as many HEAP PAGEs as returning records). Using the schemaless solution, the data of the sensor ID is redistributed and stored, which directly solves the problem that the I/O scales up.
Using the BRIN index, the problem with a large BTREE index can be solved (but you can still continue to use BTREE index).
This is similar to the query requirement for second-level data, so the test is ignored.
For securities scenarios, in addition to the common requirements for data query, insertion, and merging, there are more stringent requirements for data analysis.
PostgreSQL provides a lot of analysis functions:
1. Aggregation
PostgreSQL aggregate function 1 : General-Purpose Aggregate Functions
PostgreSQL aggregate function 2 : Aggregate Functions for Statistics
PostgreSQL aggregate function 3 : Aggregate Functions for Ordered-Set
PostgreSQL aggregate function 4 : Hypothetical-Set Aggregate Functions
2. Window query
https://www.postgresql.org/docs/9.6/static/sql-expressions.html#SYNTAX-WINDOW-FUNCTIONS
https://www.postgresql.org/docs/9.6/static/functions-window.html
3. Multidimensional analysis
PostgreSQL 9.5 new feature - Support GROUPING SETS, CUBE and ROLLUP.
4. MADlib (machine learning library)
See What MADlib can Do Through a Picture
Prediction of linear regression data
Using Linear Regression Analysis in PostgreSQL - Implementing Data Prediction
PostgreSQL Linear Regression - Stock Price Forecast 1
PostgreSQL Multiple Linear Regression - 1 MADLib Installed in PostgreSQL 9.2
PostgreSQL Multiple Linear Regression - Stock Forecast 2
Other features provided by PostgreSQL that can be used by the securities and financial industries:
In addition to some of the basic functions of the online transaction database mentioned earlier, PostgreSQL also has strong analysis capabilities, such as the ability to stream data. It can also efficiently process large amounts of data (including column-store, CPU multi-core parallel computing, JIT, use of CPU vector computing instructions, and time-series data plug-ins).
StreamCompute can help the securities industry to count data in various dimensions in real time, set agents (for example, when data reaches certain conditions), and trigger events. It is very useful for transaction events.
PostgreSQL StreamCompute Application
In terms of analysis capability, PostgreSQL combines the current CPU multi-core capability and the CPU vector computing capability, giving analysis performance an exponential improvement over the traditional database.
PostgreSQL also has corresponding plug-ins that can efficiently process time-series data.
1. When calculated using ten years of stock data, there are about 30 billion pieces of data.
We have achieved a query response time of about 0.04 ms for any stock in any time period by using this solution, and a response speed of about 0.2 ms by using schemaless UDF encapsulation.
With these performance indicators, do you still need a time-series database? PostgreSQL itself is a top-level time-series database.
2. The writing speed of stock data is about 220,000 lines/s. This far exceeds the business requirements.
3. Analysis requirements. The JIT of PostgreSQL and multi-core parallel computing provide strong computing power support for AP business. Many users are already using PG parallel computing to solve the mixed requirements of OLTP+OLAP.
4. For I/O scaling-up, this can also be solved by using a method similar to the aggregation table (index-only scan), which does not impact the business. Please see:
PostgreSQL IoT Internet of Vehicles (IoV) - Real-Time Trajectory - Travel Practice 2
PostgreSQL Time-Series Database Plug-in TimescaleDB Deployment Practices
PostgreSQL Business Data Quality Real-time Monitoring Practices
digoal - May 16, 2019
digoal - May 16, 2019
digoal - May 16, 2019
Alibaba Cloud Storage - March 1, 2021
digoal - September 17, 2019
Alibaba Clouder - February 15, 2018
An on-demand database hosting service for PostgreSQL with automated monitoring, backup and disaster recovery capabilities
Learn MoreAlibaba Cloud PolarDB for PostgreSQL is an in-house relational database service 100% compatible with PostgreSQL and highly compatible with the Oracle syntax.
Learn MoreAn on-demand database hosting service for MySQL with automated monitoring, backup and disaster recovery capabilities
Learn MoreApsaraDB RDS for MariaDB supports multiple storage engines, including MySQL InnoDB to meet different user requirements.
Learn MoreMore Posts by digoal
5295094317782498 July 9, 2020 at 6:03 pm
Thanks for the great article!What postgres configuration was used and on what hardware did it run?