如果您在Postgres中使用大型数据库,这个故事听起来会很熟悉。随着您的 Postgres 数据库不断增长,您的性能开始下降,您开始担心存储空间,或者更准确地说,您将为此支付多少费用。您喜欢 PostgreSQL,但您希望拥有一些东西:高效的数据压缩机制。
PostgreSQL确实有某种压缩机制:
即使它可能会减少数据集的大小,TOAST(超大属性存储技术)也不是传统的数据压缩机制。要理解什么是 TOAST,我们首先要讲一下
Postgres 的存储单位称为页,它们具有固定大小(默认为 8 kB)。固定页面大小为 Postgres 带来了许多优势,即数据管理简单、高效和一致性,但它也有一个缺点:某些数据值可能不适合该页面。
这就是 TOAST 的用武之地。TOAST 指的是 PostgreSQL 用于有效存储和管理 Postgres 中不适合页面的值的自动机制。为了处理这些值,Postgres TOAST 默认情况下将使用内部算法压缩它们。如果压缩后这些值仍然太大,Postgres 会将它们移动到一个单独的表(称为 TOAST 表),将指针保留在原始表中。
正如我们将在本文后面看到的,您实际上可以作为用户修改此策略,例如,通过告诉 Postgres 避免压缩特定列中的数据。
可能受到 TOAST 影响的数据类型主要是可变长度的数据类型,有可能超出标准 PostgreSQL 页面的大小限制。另一方面,固定长度的数据类型,如integer
、 float
或timestamp
,不受 TOAST 的影响,因为它们可以轻松地放入页面中。
可能受到 TOAST 影响的数据类型的一些示例包括:
json
和jsonb
大text
字符串
varchar
和varchar(n)
(如果varchar(n)
中指定的长度足够小,则该列的值可能始终低于 TOAST 阈值。)
bytea
存储二进制数据
path
和polygon
等几何数据以及geometry
或geography
等 PostGIS 类型
理解 TOAST 不仅涉及页面大小的概念,还涉及另一个 Postgres 存储概念:元组。元组是 PostgreSQL 表中的行。通常,如果元组中的所有字段的总大小大约超过 2 kB,则 TOAST 机制就会启动。
如果您一直在关注,您可能会想,“等等,但页面大小约为 8 kB — 为什么会有这样的开销?”这是因为 PostgreSQL 喜欢确保它可以在单个页面上存储多个元组:如果元组太大,每个页面上适合的元组就会减少,从而导致 I/O 操作增加并降低性能。
Postgres 还需要保留可用空间来容纳其他操作数据:每个页面不仅存储元组数据,还存储用于管理数据的其他信息,例如项目标识符、标头和事务信息。
因此,当元组中所有字段的总大小超过大约 2 kB(或 TOAST 阈值参数,我们稍后会看到)时,PostgreSQL 会采取措施确保数据有效存储。 TOAST 通过两种主要方式处理这个问题:
压缩。 PostgreSQL 可以使用我们将在本文后面介绍的压缩算法来压缩元组中的大字段值以减小其大小。默认情况下,如果压缩足以使元组的总大小低于阈值,则数据将保留在主表中,尽管采用压缩格式。
离线存储。如果单独压缩不足以有效地减少大字段值的大小,Postgres 会将它们移动到单独的 TOAST 表中。此过程称为“外联”存储,因为主表中的原始元组不再保存大字段值。相反,它包含一个“指针”或指向 TOAST 表中大数据位置的引用。
我们在本文中稍微简化了一些事情——
pglz
我们已经提到 TOAST 可以压缩 PostgreSQL 中的大值。但是 PostgreSQL 使用哪种压缩算法,效果如何?
pglz
(PostgreSQL Lempel-Ziv) 是 PostgreSQL 使用的默认内部压缩算法,专为 TOAST 量身定制。
以下是它的工作原理,非常简单:
pglz
尽量避免重复的数据。当它看到重复的数据时,它不会再次写入相同的内容,而是指向之前写入的位置。这种“避免重复”有助于节省空间。
当pglz
读取数据时,它会记住最近看到的一些数据。这个最近的记忆被称为“滑动窗口”。
当新数据进入时, pglz
会检查最近是否看到过该数据(在其滑动窗口内)。如果是,它会写入一个简短的引用而不是重复数据。
如果数据是新的或者没有重复足够多的次数来使引用比实际数据短, pglz
就会按原样写下来。
当需要读取压缩数据时, pglz
使用其引用来获取原始数据。这个过程非常直接,因为它查找引用的数据并将其放置在其所属的位置。
pglz
不需要单独的内存存储(滑动窗口);它在压缩时随时构建它,并在解压缩时执行相同的操作。
此实现旨在在 TOAST 机制内的压缩效率和速度之间提供平衡。就压缩率而言, pglz
的有效性很大程度上取决于数据的性质。
例如,高度重复的数据比高熵数据(如随机数据)压缩得更好。您可能会看到压缩率在 25% 到 50% 的范围内,但这是一个非常笼统的估计,结果会根据数据的确切性质而有很大差异。
默认情况下,PostgreSQL 将按照前面解释的过程执行 TOAST 机制(如果压缩不够,则先压缩,然后再进行线外存储)。尽管如此,在某些情况下,您可能希望在每列的基础上微调此行为。 PostgreSQL 允许您使用 TOAST 策略PLAIN
、 EXTERNAL
、 EXTENDED
和MAIN
来做到这一点。
EXTENDED
:这是默认策略。这意味着如果数据对于常规表页来说太大,则数据将被存储在单独的 TOAST 表中。在将数据移动到 TOAST 表之前,将对其进行压缩以节省空间。
EXTERNAL
:如果数据太大而无法放入常规表页,此策略告诉 PostgreSQL 将该列的数据存储在行外,并且我们要求 PostgreSQL 不要压缩数据——该值将被移动到TOAST 表按原样。
MAIN
:这个策略是一个中间立场。它试图通过压缩使数据在主表中保持一致;如果数据确实太大,它会将数据移动到 TOAST 表以避免错误,但 PostgreSQL 不会移动压缩数据。相反,它会以其原始形式将该值存储在 TOAST 表中。
PLAIN
:在列中使用PLAIN
告诉 PostgreSQL 始终将列的数据存储在主表中,确保它不会移动到外联 TOAST 表中。请考虑到,如果数据增长超出页面大小,则INSERT
将失败,因为数据无法容纳。
如果您想检查特定表的当前策略,可以运行以下命令:
\d+ your_table_name
您将得到如下输出:
=> \d+ example_table Table "public.example_table" Column | Data Type | Modifiers | Storage | Stats target | Description ---------+------------------+-----------+----------+--------------+------------- bar | varchar(100000) | | extended | |
如果您想修改存储设置,可以使用以下命令进行操作:
-- Sets EXTENDED as the TOAST strategy for bar_column ALTER TABLE example_blob ALTER COLUMN bar_column SET STORAGE EXTENDED;
除了上述策略之外,这两个参数对于控制 TOAST 行为也很重要:
TOAST_TUPLE_THRESHOLD
该参数用于设置何时考虑对超大元组进行 TOAST 操作(压缩和离线存储)的大小阈值。
正如我们之前提到的,默认情况下, TOAST_TUPLE_THRESHOLD
设置为大约 2 kB。
TOAST_COMPRESSION_THRESHOLD
该参数指定 Postgres 在 TOAST 过程中考虑压缩某个值之前的最小大小。
如果某个值超过此阈值,PostgreSQL 将尝试对其进行压缩。然而,仅仅因为一个值高于压缩阈值,并不自动意味着它将被压缩:TOAST 策略将指导 PostgreSQL 如何根据数据是否被压缩以及相对于元组的结果大小来处理数据页面限制,我们将在下一节中看到。
TOAST_TUPLE_THRESHOLD
是触发点。当元组的数据字段组合的大小超过这个阈值时,PostgreSQL将根据为其列设置的TOAST策略来评估如何管理它,并考虑压缩和线外存储。所采取的确切操作还取决于列数据是否超过TOAST_COMPRESSION_THRESHOLD
:
EXTENDED
(默认策略):如果元组的大小超过TOAST_TUPLE_THRESHOLD
,PostgreSQL 将首先尝试压缩超大列(如果它们也超过TOAST_COMPRESSION_THRESHOLD
)。如果压缩使元组大小低于阈值,它将保留在主表中。如果没有,数据将被移动到外联 TOAST 表,并且主表将包含指向该外部数据的指针。
MAIN
:如果元组大小超过TOAST_TUPLE_THRESHOLD
,PostgreSQL 将尝试压缩超大列(前提是它们超过TOAST_COMPRESSION_THRESHOLD
)。如果压缩允许元组适合主表的元组,则它会保留在那里。如果不是,数据将以未压缩的形式移至 TOAST 表。
EXTERNAL
:无论TOAST_COMPRESSION_THRESHOLD
是多少,PostgreSQL 都会跳过压缩。如果元组的大小超出TOAST_TUPLE_THRESHOLD
,则过大的列将存储在 TOAST 表中。
PLAIN
:数据始终存储在主表中。如果元组的大小超过页面大小(由于列非常大),则会引发错误。
战略 | 如果元组 > TOAST_COMPRESSION_THRESHOLD 则压缩 | 如果元组 > TOAST_TUPLE_THRESHOLD 则存储外线 | 描述 |
---|---|---|---|
扩展 | 是的 | 是的 | 默认策略。首先压缩,然后检查是否需要线外存储。 |
主要的 | 是的 | 仅以未压缩形式 | 首先压缩,如果仍然过大,则移至 TOAST 表而不压缩。 |
外部的 | 不 | 是的 | 如果尺寸过大,则始终移动到 TOAST,而不进行压缩。 |
清楚的 | 不 | 不 | 数据始终保留在主表中。如果元组超过页面大小,则会发生错误。 |
现在,您可能会明白为什么 TOAST 不是您希望在 PostgreSQL 中拥有的数据压缩机制。现代应用程序意味着每天都会摄取大量数据,这意味着数据库会快速增长。
当我们心爱的 Postgres 几十年前构建时,这样的问题并不那么突出,但今天的开发人员需要压缩解决方案来减少数据集的存储占用。
虽然 TOAST 将压缩作为其技术之一,但重要的是要了解它的主要作用不是充当传统意义上的数据库压缩机制。 TOAST 主要是解决一个问题:在 Postgres 页面的结构范围内管理大值。
虽然这种方法可以由于压缩特定的大值而节省一些存储空间,但其主要目的并不是全面优化存储空间。
例如,如果您有一个由小元组组成的 5 TB 数据库,TOAST 将无法帮助您将这 5 TB 转换为 1 TB。虽然 TOAST 中有一些可以调整的参数,但这不会将 TOAST 转变为通用的存储节省解决方案。
在 PostgreSQL 中使用 TOAST 作为传统压缩机制还存在其他固有问题,例如:
访问 TOAST 数据会增加开销,尤其是当数据存储在行外时。当频繁访问许多大文本或其他可 TOAST 的数据类型时,这一点变得更加明显。
TOAST 缺乏一个高级的、用户友好的机制来指定压缩策略。它不是为了优化存储成本或促进存储管理而构建的。
TOAST 的压缩并不是为了提供特别高的压缩比而设计的。它仅使用一种算法 ( pglz
),压缩率通常在 25-50% 之间变化。
通过向大型表添加压缩策略,
通过定义基于时间的压缩策略,您可以指示何时应压缩数据。例如,您可以选择自动压缩七 (7) 天之前的数据:
-- Compress data older than 7 days SELECT add_compression_policy('my_hypertable', INTERVAL '7 days');
通过这种压缩策略,Timescale 将转换表
Gorilla 压缩浮标
对具有一些重复值的列进行整行字典压缩(+ 顶部 LZ 压缩)
适用于所有其他类型的基于 LZ 的数组压缩
这种列式压缩设计为 PostgreSQL 中的大型数据集问题提供了高效且可扩展的解决方案。它允许您使用更少的存储来存储更多的数据,而不会损害查询性能(它可以提高查询性能)。在最新版本的 TimescaleDB 中,您还可以直接对压缩数据进行INSERT
、 DELETE
和UPDATE
。
我们希望本文能帮助您理解,虽然 TOAST 是一种经过深思熟虑的机制,用于管理 PostgreSQL 页面中的大值,但它对于优化现代应用程序领域内的数据库存储使用并不起作用。
如果您正在寻找能够显着节省存储空间的有效数据压缩,请尝试一下 Timescale。您可以尝试我们的云平台,它将 PostgreSQL 推向新的性能高度,使其更快、更猛烈 —
卡洛塔·索托撰写。