Optimizing Time Series Querying on Alibaba Cloud RDS for PostgreSQL
By: Alibaba Cloud
Tags: Database, Postgresql, Subquery, Alibaba Cloud, Programming

By Digoal

Data merging and data cleaning are required in many scenarios. Example scenarios:

1. For recorded table change details (insert, update, and delete), you must merge details and quickly obtain the latest value of each PK based on these details.

2. When we have a large number of sensors all continuously reporting data, we need to quickly obtain each sensor’s latest reading.

We can use window query for this kind of operation, but how can we make it faster and quickly retrieve batch data?

PostgreSQL is the best open-source database. It’s optimized in ways you probably haven’t even thought of yet.

Time Sequence Data Value Optimization

Here is a quick summary of the common methods of optimizing time sequence data querying:

1. Recursion is used when there are few unique values and an unknown range.

2. Use subquery when the number of unique values is relatively small and you know the specific range of the unique values.

3. Window query is more appropriate than the above method when there are many unique values.

4. However, stream computing is even better in the same scenarios.

This document will compare the first three methods.

Method 4, stream computing does not need to be compared because it is the most powerful. It is the best method in all scenarios. Wait for the pipelineDB plugin. Alibaba Cloud RDS PG 10 will then be integrated with the pipelineDB functionality.

Recursion vs Subquery vs Window

Take 5 million pieces of data as an example and compare these methods in the following situations.

1. Recursion

Scenario I. a Large Variety of Unique Values Are Available (1 Million Unique Values)

1. Create a table

\timing

drop table test;

create unlogged table test(id int , info text, crt_time timestamp);

2. Construct data

insert into test select ceil(random()*1000000), md5(random()::text), clock_timestamp() from generate_series(1,5000000);

3. Create an index

create index idx_test_1 on test (id, crt_time desc);

4. Recursive query efficiency

explain (analyze,verbose,timing,costs,buffers) with recursive skip as (

(

select test as v from test where id in (select id from test where id is not null order by id,crt_time desc limit 1) limit 1

)

union all

(

select (

select t as v from test t where t.id>(s.v).id and t.id is not null order by id,crt_time desc limit 1

) from skip s where (s.v).id is not null

) -- "where (s.v).id is not null" must be added, otherwise it will end up with an endless loop.

)

select (t.v).id, (t.v).info, (t.v).crt_time from skip t where t.* is not null;

QUERY PLAN

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

CTE Scan on skip t (cost=54.35..56.37 rows=100 width=44) (actual time=0.042..6626.084 rows=993288 loops=1)

Output: (t.v).id, (t.v).info, (t.v).crt_time

Filter: (t.* IS NOT NULL)

Rows Removed by Filter: 1

Buffers: shared hit=3976934

CTE skip

-> Recursive Union (cost=0.91..54.35 rows=101 width=69) (actual time=0.034..6006.615 rows=993289 loops=1)

Buffers: shared hit=3976934

-> Limit (cost=0.91..0.93 rows=1 width=69) (actual time=0.033..0.033 rows=1 loops=1)

Output: test.*

Buffers: shared hit=8

-> Nested Loop (cost=0.91..10.19 rows=500 width=69) (actual time=0.032..0.032 rows=1 loops=1)

Output: test.*

Buffers: shared hit=8

-> HashAggregate (cost=0.48..0.49 rows=1 width=4) (actual time=0.021..0.021 rows=1 loops=1)

Output: test_1.id

Group Key: test_1.id

Buffers: shared hit=4

-> Limit (cost=0.43..0.47 rows=1 width=12) (actual time=0.016..0.016 rows=1 loops=1)

Output: test_1.id, test_1.crt_time

Buffers: shared hit=4

-> Index Only Scan using idx_test_1 on public.test test_1 (cost=0.43..173279.36 rows=5000002 width=12) (actual time=0.015..0.015 rows=1 loops=1)

Output: test_1.id, test_1.crt_time

Index Cond: (test_1.id IS NOT NULL)

Heap Fetches: 1

Buffers: shared hit=4

-> Index Scan using idx_test_1 on public.test (cost=0.43..9.64 rows=6 width=73) (actual time=0.009..0.009 rows=1 loops=1)

Output: test.*, test.id

Index Cond: (test.id = test_1.id)

Buffers: shared hit=4

-> WorkTable Scan on skip s (cost=0.00..5.14 rows=10 width=32) (actual time=0.006..0.006 rows=1 loops=993289)

Output: (SubPlan 1)

Filter: ((s.v).id IS NOT NULL)

Rows Removed by Filter: 0

Buffers: shared hit=3976926

SubPlan 1

-> Limit (cost=0.43..0.49 rows=1 width=81) (actual time=0.005..0.005 rows=1 loops=993288)

Output: t_1.*, t_1.id, t_1.crt_time

Buffers: shared hit=3976926

-> Index Scan using idx_test_1 on public.test t_1 (cost=0.43..102425.17 rows=1666667 width=81) (actual time=0.005..0.005 rows=1 loops=993288)

Output: t_1.*, t_1.id, t_1.crt_time

Index Cond: ((t_1.id > (s.v).id) AND (t_1.id IS NOT NULL))

Buffers: shared hit=3976926

Planning time: 0.354 ms

Execution time: 6706.105 ms

(45 rows)

Scenario 2: A Small Number of Unique Values (1,000 Unique Values)

1. Create a table

\timing

drop table test;

create unlogged table test(id int , info text, crt_time timestamp);

2. Construct data

insert into test select ceil(random()*1000), md5(random()::text), clock_timestamp() from generate_series(1,5000000);

3. Create an index

create index idx_test_1 on test (id, crt_time desc);

4. Recursive query efficiency

The query statement stays unchanged

QUERY PLAN

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

CTE Scan on skip t (cost=55.09..57.11 rows=100 width=44) (actual time=0.046..8.859 rows=1000 loops=1)

Output: (t.v).id, (t.v).info, (t.v).crt_time

Filter: (t.* IS NOT NULL)

Rows Removed by Filter: 1

Buffers: shared hit=4007

CTE skip

-> Recursive Union (cost=0.91..55.09 rows=101 width=69) (actual time=0.039..8.203 rows=1001 loops=1)

Buffers: shared hit=4007

-> Limit (cost=0.91..1.67 rows=1 width=69) (actual time=0.038..0.038 rows=1 loops=1)

Output: test.*

Buffers: shared hit=8

-> Nested Loop (cost=0.91..6335.47 rows=8333 width=69) (actual time=0.038..0.038 rows=1 loops=1)

Output: test.*

Buffers: shared hit=8

-> HashAggregate (cost=0.48..0.49 rows=1 width=4) (actual time=0.021..0.021 rows=1 loops=1)

Output: test_1.id

Group Key: test_1.id

Buffers: shared hit=4

-> Limit (cost=0.43..0.47 rows=1 width=12) (actual time=0.016..0.017 rows=1 loops=1)

Output: test_1.id, test_1.crt_time

Buffers: shared hit=4

-> Index Only Scan using idx_test_1 on public.test test_1 (cost=0.43..173279.55 rows=5000002 width=12) (actual time=0.015..0.015 rows=1 loops=1)

Output: test_1.id, test_1.crt_time

Index Cond: (test_1.id IS NOT NULL)

Heap Fetches: 1

Buffers: shared hit=4

-> Index Scan using idx_test_1 on public.test (cost=0.43..6284.98 rows=5000 width=73) (actual time=0.015..0.015 rows=1 loops=1)

Output: test.*, test.id

Index Cond: (test.id = test_1.id)

Buffers: shared hit=4

-> WorkTable Scan on skip s (cost=0.00..5.14 rows=10 width=32) (actual time=0.008..0.008 rows=1 loops=1001)

Output: (SubPlan 1)

Filter: ((s.v).id IS NOT NULL)

Rows Removed by Filter: 0

Buffers: shared hit=3999

SubPlan 1

-> Limit (cost=0.43..0.49 rows=1 width=81) (actual time=0.007..0.007 rows=1 loops=1000)

Output: t_1.*, t_1.id, t_1.crt_time

Buffers: shared hit=3999

-> Index Scan using idx_test_1 on public.test t_1 (cost=0.43..102425.80 rows=1666667 width=81) (actual time=0.007..0.007 rows=1 loops=1000)

Output: t_1.*, t_1.id, t_1.crt_time

Index Cond: ((t_1.id > (s.v).id) AND (t_1.id IS NOT NULL))

Buffers: shared hit=3999

Planning time: 0.353 ms

Execution time: 8.980 ms

(45 rows)

Scenario 1: A Large Number of Unique Values (1 Million Unique Values)

1. Subquery query efficiency

Subquery is inefficient if the value range of an ID is too broad.

A unique ID table needs to be maintained. Here we use generate_series as a replacement for testing.

explain (analyze,verbose,timing,costs,buffers) select (select test from test where id=t.id order by crt_time desc limit 1) from generate_series(1,1000000) t(id);

QUERY PLAN

---------------------------------------------------------------------------------------------------------------------------------------------

Function Scan on pg_catalog.generate_series t (cost=0.00..1976.65 rows=1000 width=32) (actual time=70.682..2835.109 rows=1000000 loops=1)

Output: (SubPlan 1)

Function Call: generate_series(1, 1000000)

Buffers: shared hit=3997082

SubPlan 1

-> Limit (cost=0.43..1.97 rows=1 width=77) (actual time=0.002..0.002 rows=1 loops=1000000)

Output: test.*, test.crt_time

Buffers: shared hit=3997082

-> Index Scan using idx_test_1 on public.test (cost=0.43..9.64 rows=6 width=77) (actual time=0.002..0.002 rows=1 loops=1000000)

Output: test.*, test.crt_time

Index Cond: (test.id = t.id)

Buffers: shared hit=3997082

Planning time: 0.119 ms

Execution time: 2892.712 ms

(14 rows)

Scenario 2: A Small Number of Unique Values (1,000 Unique Values)

1. Subquery efficiency

Query statement changes

explain (analyze,verbose,timing,costs,buffers) select (select test from test where id=t.id order by crt_time desc limit 1) from generate_series(1,1000) t(id);

QUERY PLAN

------------------------------------------------------------------------------------------------------------------------------------------------

Function Scan on pg_catalog.generate_series t (cost=0.00..1699.41 rows=1000 width=32) (actual time=0.107..7.041 rows=1000 loops=1)

Output: (SubPlan 1)

Function Call: generate_series(1, 1000)

Buffers: shared hit=4000

SubPlan 1

-> Limit (cost=0.43..1.69 rows=1 width=77) (actual time=0.006..0.007 rows=1 loops=1000)

Output: test.*, test.crt_time

Buffers: shared hit=4000

-> Index Scan using idx_test_1 on public.test (cost=0.43..6284.98 rows=5000 width=77) (actual time=0.006..0.006 rows=1 loops=1000)

Output: test.*, test.crt_time

Index Cond: (test.id = t.id)

Buffers: shared hit=4000

Planning time: 0.131 ms

Execution time: 7.126 ms

(14 rows)

2. Window Query

Scenario 1: A Large Number of Unique Values (1 Million Unique Values)

1. Window query efficiency

explain (analyze,verbose,timing,costs,buffers) select id,info,crt_time from (select row_number() over (partition by id order by crt_time desc) as rn, * from test) t where rn=1;

postgres=# explain (analyze,verbose,timing,costs,buffers) select id,info,crt_time from (select row_number() over (partition by id order by crt_time desc) as rn, * from test) t where rn=1;

QUERY PLAN

---------------------------------------------------------------------------------------------------------------------------------------------------------

Subquery Scan on t (cost=0.43..310779.41 rows=25000 width=45) (actual time=0.027..6398.308 rows=993288 loops=1)

Output: t.id, t.info, t.crt_time

Filter: (t.rn = 1)

Rows Removed by Filter: 4006712

Buffers: shared hit=5018864

-> WindowAgg (cost=0.43..248279.39 rows=5000002 width=53) (actual time=0.026..5973.497 rows=5000000 loops=1)

Output: row_number() OVER (?), test.id, test.info, test.crt_time

Buffers: shared hit=5018864

-> Index Scan using idx_test_1 on public.test (cost=0.43..160779.35 rows=5000002 width=45) (actual time=0.019..4058.476 rows=5000000 loops=1)

Output: test.id, test.info, test.crt_time

Buffers: shared hit=5018864

Planning time: 0.121 ms

Execution time: 6446.901 ms

(13 rows)

Scenario 2: A Small Number of Unique Values (1,000 Unique Values)

1. Window query efficiency

The query statement stays unchanged

QUERY PLAN

---------------------------------------------------------------------------------------------------------------------------------------------------------

Subquery Scan on t (cost=0.43..310779.61 rows=25000 width=45) (actual time=0.027..6176.801 rows=1000 loops=1)

Output: t.id, t.info, t.crt_time

Filter: (t.rn = 1)

Rows Removed by Filter: 4999000

Buffers: shared hit=4744850 read=18157

-> WindowAgg (cost=0.43..248279.58 rows=5000002 width=53) (actual time=0.026..5822.576 rows=5000000 loops=1)

Output: row_number() OVER (?), test.id, test.info, test.crt_time

Buffers: shared hit=4744850 read=18157

-> Index Scan using idx_test_1 on public.test (cost=0.43..160779.55 rows=5000002 width=45) (actual time=0.020..4175.082 rows=5000000 loops=1)

Output: test.id, test.info, test.crt_time

Buffers: shared hit=4744850 read=18157

Planning time: 0.108 ms

Execution time: 6176.924 ms

(13 rows)

Efficiency Comparison Table

Data volumeNumber of Unique ValuesWindow Query (ms)Subquery (ms)Recursive Query (ms)5 million1 million6,4462,8926,7065 million1,0006,17679

Summary

With the rise of the Internet of Things, the world is generating increasing amounts of time sequenced data, and in situations where we have to provide services based on that data, calculating the newest values in the data and those in the sliding window is crucial.

PostgreSQL is the best choice in open-source databases as it provides several solutions to the same problems. It leaves you free to choose the most appropriate solution for you and your individual needs.

1. Use recursion when the number of unique values is relatively small and the range of the unique values is unknown.

2. Use subquery when the number of unique values is relatively small and the range of the unique values is determined. For example, if the total range is 1 million pieces of data, but only 500,000 pieces of data are included in this batch, then the performance is optimal if you have the IDs for these 500,000 entries. Otherwise you need to scan 1 million pieces of data. Another example is that there are a total of 100 million users, but an interval includes only tens of thousands of active users.

3. Window query is more appropriate if the number of unique values is relatively large.

4. Steaming computing is better than method 3 if the number of unique values is relatively large.

Reference:https://www.alibabacloud.com/blog/optimizing-time-series-querying-on-alibaba-cloud-rds-for-postgresql_594867?spm=a2c41.12952090.0.0