在時間序列工作負載中,應用程式(例如一些實時應用程式查詢最近的資訊,同時歸檔舊資訊。
為了處理這種工作負載,單節點 PostgreSQL
資料庫通常會使用表分割槽將一個按時間排序的大資料表分解為多個繼承表,每個表包含不同的時間範圍。
將資料儲存在多個物理表中會加速資料過期。 在單個大表中,刪除行會產生掃描以查詢要刪除的行,然後清理清空空間的成本。 另一方面,刪除分割槽是一種與資料大小無關的快速操作。 這相當於簡單地刪除磁碟上包含資料的檔案。
將資料儲存在多個物理表中會加快資料過期的速度。在一個大表中,刪除行需要掃描以找到要刪除的行,然後清空空的空間。另一方面,刪除分割槽是一種與資料大小無關的快速操作。這相當於簡單地刪除磁碟上包含資料的檔案。
對錶進行分割槽還可以使每個日期範圍內的索引更小更快。 對最近資料進行的查詢很可能對適合記憶體的 hot
索引進行操作。這加快了讀取速度。
插入也有更小的索引要更新,所以它們也更快。
在以下情況下,基於時間的分割槽最有意義:
- 大多數查詢只訪問最近資料的一個非常小的子集
- 舊資料定期過期(刪除/丟棄)
請記住,在錯誤的情況下,讀取所有這些分割槽對開銷的傷害大於幫助。 但是,在正確的情況下,它非常有幫助。 例如,保留一年的時間序列資料並定期僅查詢最近一週。
擴充套件 Citus 上的時間序列資料
我們可以將單節點表分割槽技術與 Citus
的分散式分片相結合,形成一個可擴充套件的時間序列資料庫。這是兩全其美的。它在 Postgres
的宣告性表分割槽之上特別優雅。
例如,讓我們 distribute
和 partition
一個包含歷史 GitHub 事件資料的表。
- GitHub 事件資料
此 GitHub
資料集中的每條記錄代表在 GitHub
中建立的事件,以及有關事件的關鍵資訊,例如事件型別、建立日期和建立事件的使用者。
第一步是按時間建立和分割槽(partition)
表,就像我們在單節點 PostgreSQL
資料庫中一樣:
-- 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);
注意 PARTITION BY RANGE (created_at)
。這告訴 Postgres
該表將由 created_at
列在有序範圍內進行分割槽。不過,我們還沒有為特定範圍建立任何分割槽。
在建立特定分割槽之前,讓我們在 Citus
中分佈表。我們將按 repo_id
進行分片,這意味著事件將被聚集到每個儲存庫的分片中。
SELECT create_distributed_table('github_events', 'repo_id');
此時 Citus
已跨工作節點為該表建立分片。在內部,每個分片是一個表,每個分片識別符號 N
的名稱為 github_events_N
。此外,Citus
傳播了分割槽資訊,每個分片都宣告瞭 Partition key: RANGE (created_at)
。
分割槽表不能直接包含資料,它更像是跨分割槽的檢視。因此,分片還沒有準備好儲存資料。 我們需要建立分割槽並指定它們的時間範圍,之後我們可以插入與範圍匹配的資料。
自動建立分割槽
Citus
為分割槽管理提供了輔助函式。我們可以使用 create_time_partitions()
建立一批每月分割槽:
SELECT create_time_partitions(
table_name := 'github_events',
partition_interval := '1 month',
end_at := now() + '12 months'
);
Citus
還包括一個檢視 time_partitions
,以方便地調查它建立的分割槽。
隨著時間的推移,您將需要進行一些維護以建立新分割槽並刪除舊分割槽。最好設定一個定期 job
來執行帶有 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 */
);
$$);
一旦設定了定期維護,您就不必再考慮分割槽了,它們可以正常工作。
請注意,Postgres
中的原生分割槽仍然很新,並且有一些怪癖。 對分割槽表的維護操作將獲取可能會短暫停止查詢的激進鎖。目前在 postgres
社群中正在進行大量工作來解決這些問題,因此預計 Postgres
中的 time
分割槽只會變得更好。
使用列式儲存歸檔
一些應用程式的資料在邏輯上分為可更新的小部分和“凍結(frozen)”
的較大部分。 示例包括日誌、點選流或銷售記錄。 在這種情況下,我們可以將分割槽與列式表儲存(在 Citus 10
中引入)結合起來壓縮磁碟上的歷史分割槽。Citus 柱狀表目前是僅追加的,這意味著它們不支援更新或刪除,但我們可以將它們用於不可變的歷史分割槽。
分割槽表
可以由行分割槽
和列分割槽
的任意組合組成。在 timestamp
key 上使用範圍分割槽時,我們可以將最新的分割槽製作成行表,並定期將最新的分割槽滾動到另一個歷史列式分割槽中。
讓我們看一個例子,再次使用 GitHub 事件
。我們將建立一個名為 github_columnar_events
的新表,以消除前面示例中的歧義。 為了完全專注於列式儲存方面,我們不會分佈此表。
接下來,下載示例資料:
wget http://examples.citusdata.com/github_archive/github_events-2015-01-01-{0..5}.csv.gz
gzip -c -d github_events-2015-01-01-*.gz >> github_events.csv
-- our new table, same structure as the example in
-- the previous section
CREATE TABLE github_columnar_events ( LIKE github_events )
PARTITION BY RANGE (created_at);
-- create partitions to hold two hours of data each
SELECT create_time_partitions(
table_name := 'github_columnar_events',
partition_interval := '2 hours',
start_from := '2015-01-01 00:00:00',
end_at := '2015-01-01 08:00:00'
);
-- fill with sample data
-- (note that this data requires the database to have UTF8 encoding)
\COPY github_columnar_events FROM 'github_events.csv' WITH (format CSV)
-- list the partitions, and confirm they're
-- using row-based storage (heap access method)
SELECT partition, access_method
FROM time_partitions
WHERE parent_table = 'github_columnar_events'::regclass;
-- convert older partitions to use columnar storage
CALL alter_old_partitions_set_access_method(
'github_columnar_events',
'2015-01-01 06:00:00' /* older_than */,
'columnar'
);
-- the old partitions are now columnar, while the
-- latest uses row storage and can be updated
SELECT partition, access_method
FROM time_partitions
WHERE parent_table = 'github_columnar_events'::regclass;
要檢視柱狀表的壓縮率,請使用 VACUUM VERBOSE
。我們三個柱狀分割槽的壓縮比相當不錯:
VACUUM VERBOSE github_columnar_events;
INFO: statistics for "github_columnar_events_p2015_01_01_0000":
storage id: 10000000003
total file size: 4481024, total data size: 4444425
compression rate: 8.31x
total row count: 15129, stripe count: 1, average rows per stripe: 15129
chunk count: 18, containing data for dropped columns: 0, zstd compressed: 18
INFO: statistics for "github_columnar_events_p2015_01_01_0200":
storage id: 10000000004
total file size: 3579904, total data size: 3548221
compression rate: 8.26x
total row count: 12714, stripe count: 1, average rows per stripe: 12714
chunk count: 18, containing data for dropped columns: 0, zstd compressed: 18
INFO: statistics for "github_columnar_events_p2015_01_01_0400":
storage id: 10000000005
total file size: 2949120, total data size: 2917407
compression rate: 8.51x
total row count: 11756, stripe count: 1, average rows per stripe: 11756
chunk count: 18, containing data for dropped columns: 0, zstd compressed: 18
分割槽表 github_columnar_events
的一個強大之處在於它可以像普通表一樣被完整地查詢。
SELECT COUNT(DISTINCT repo_id)
FROM github_columnar_events;
只要分割槽鍵上有一個 WHERE
子句,它可以完全過濾到行表分割槽中,條目就可以被更新或刪除。
將行分割槽歸檔到列式儲存
當行分割槽已填滿其範圍時,您可以將其歸檔到壓縮的列式儲存中。我們可以使用 pg_cron
自動執行此操作,如下所示:
-- a monthly cron job
SELECT cron.schedule('compress-partitions', '0 0 1 * *', $$
CALL alter_old_partitions_set_access_method(
'github_columnar_events',
now() - interval '6 months' /* older_than */,
'columnar'
);
$$);
有關詳細資訊,請參閱列式儲存。