Timeseries Data
In a time-series workload, applications (such as some Real-Time Apps) query recent information, while archiving old information.
To deal with this workload, a single-node LightDB database would typically use table partitioning to break a big table of time-ordered data into multiple inherited tables with each containing different time ranges.
Storing data in multiple physical tables speeds up data expiration. In a single big table, deleting rows incurs the cost of scanning to find which to delete, and then vacuuming the emptied space. On the other hand, dropping a partition is a fast operation independent of data size. It’s the equivalent of simply removing files on disk that contain the data.
Partitioning a table also makes indices smaller and faster within each date range. Queries operating on recent data are likely to operate on “hot” indices that fit in memory. This speeds up reads.
Also inserts have smaller indices to update, so they go faster too.
Time-based partitioning makes most sense when:
Most queries access a very small subset of the most recent data
Older data is periodically expired (deleted/dropped)
Keep in mind that, in the wrong situation, reading all these partitions can hurt overhead more than it helps. However, in the right situations it is quite helpful. For example, when keeping a year of time series data and regularly querying only the most recent week.
Scaling Timeseries Data on Canopy
We can mix the single-node table partitioning techniques with Canopy’ distributed sharding to make a scalable time-series database. It’s the best of both worlds. It’s especially elegant atop LightDB’s declarative table partitioning.
For example, let’s distribute and partition a table holding historical GitHub events data.
Each record in this GitHub data set represents an event created in GitHub, along with key information regarding the event such as event type, creation date, and the user who created the event.
The first step is to create and partition the table by time as we would in a single-node LightDB database:
-- declaratively partitioned table
CREATE TABLE github_events (
event_id bigint,
event_type text,
event_public boolean,
repo_id bigint,
payload jsonb,
repo jsonb,
actor jsonb,
org jsonb,
created_at timestamp
) PARTITION BY RANGE (created_at);
Notice the PARTITION BY RANGE (created_at)
. This tells LightDB that the table will be partitioned by the created_at
column in ordered ranges. We have not yet created any partitions for specific ranges, though.
Before creating specific partitions, let’s distribute the table in Canopy. We’ll shard by repo_id
, meaning the events will be clustered into shards per repository.
SELECT create_distributed_table('github_events', 'repo_id');
At this point Canopy has created shards for this table across worker nodes. Internally each shard is a table with the name github_events_N
for each shard identifier N. Also, Canopy propagated the partitioning information, and each of these shards has Partition key: RANGE (created_at)
declared.
A partitioned table cannot directly contain data, it is more like a view across its partitions. Thus the shards are not yet ready to hold data. We need to create partitions and specify their time ranges, after which we can insert data that match the ranges.
Automating Partition Creation
Canopy provides helper functions for partition management. We can create a batch
of monthly partitions using create_time_partitions()
:
SELECT create_time_partitions(
table_name := 'github_events',
partition_interval := '1 month',
end_at := now() + '12 months'
);
Canopy also includes a view, time_partitions, for an easy way to investigate the partitions it has created.
SELECT partition
FROM time_partitions
WHERE parent_table = 'github_events'::regclass;
┌────────────────────────┐
│ partition │
├────────────────────────┤
│ github_events_p2021_10 │
│ github_events_p2021_11 │
│ github_events_p2021_12 │
│ github_events_p2022_01 │
│ github_events_p2022_02 │
│ github_events_p2022_03 │
│ github_events_p2022_04 │
│ github_events_p2022_05 │
│ github_events_p2022_06 │
│ github_events_p2022_07 │
│ github_events_p2022_08 │
│ github_events_p2022_09 │
│ github_events_p2022_10 │
└────────────────────────┘
As time progresses, you will need to do some maintenance to create new partitions and drop old ones. It’s best to set up a periodic job to run the maintenance functions with an extension like pg_cron:
-- set two monthly cron jobs:
-- 1. ensure we have partitions for the next 12 months
SELECT cron.schedule('create-partitions', '0 0 1 * *', $$
SELECT create_time_partitions(
table_name := 'github_events',
partition_interval := '1 month',
end_at := now() + '12 months'
)
$$);
-- 2. (optional) ensure we never have more than one year of data
SELECT cron.schedule('drop-partitions', '0 0 1 * *', $$
CALL drop_old_time_partitions(
'github_events',
now() - interval '12 months' /* older_than */
);
$$);
Once periodic maintenance is set up, you no longer have to think about the partitions, they just work.
备注
Be aware that native partitioning in LightDB is still quite new and has a few quirks. Maintenance operations on partitioned tables will acquire aggressive locks that can briefly stall queries.