On Thu, 6 Mar 2025 at 08:43, Nikhil Kumar Veldanda
<veldanda.nikhilkuma...@gmail.com> wrote:
>
> Hi all,
>
> The ZStandard compression algorithm [1][2], though not currently used for 
> TOAST compression in PostgreSQL, offers significantly improved compression 
> ratios compared to lz4/pglz in both dictionary-based and non-dictionary 
> modes. Attached find for review my patch to add ZStandard compression to 
> Postgres. In tests this patch used with a pre-trained dictionary achieved up 
> to four times the compression ratio of LZ4, while ZStandard without a 
> dictionary outperformed LZ4/pglz by about two times during compression of 
> data.
>
> Notably, this is the first compression algorithm for Postgres that can make 
> use of a dictionary to provide higher levels of compression, but dictionaries 
> have to be generated and maintained, and so I’ve had to break new ground in 
> that regard. To use the dictionary support requires training and storing a 
> dictionary for a given variable-length column type. On a variable-length 
> column, a SQL function will be called. It will sample the column’s data and 
> feed it into the ZStandard training API which will return a dictionary. In 
> the example, the column is of JSONB type. The SQL function takes the table 
> name and the attribute number as inputs. If the training is successful, it 
> will return true; otherwise, it will return false.
>
> ‘’‘
> test=# select build_zstd_dict_for_attribute('"public"."zstd"', 1);
> build_zstd_dict_for_attribute
> -------------------------------
> t
> (1 row)
> ‘’‘
>
> The sampling logic and data to feed to the ZStandard training API can vary by 
> data type. The patch includes an method to write other type-specific training 
> functions and includes a default for JSONB, TEXT and BYTEA. There is a new 
> option called ‘build_zstd_dict’ that takes a function name as input in 
> ‘CREATE TYPE’. In this way anyone can write their own type-specific training 
> function by handling sampling logic and returning the necessary information 
> for the ZStandard training API in “ZstdTrainingData” format.
>
> ```
> typedef struct ZstdTrainingData
> {
> char *sample_buffer; /* Pointer to the raw sample buffer */
> size_t *sample_sizes; /* Array of sample sizes */
> int nitems; /* Number of sample sizes */
> } ZstdTrainingData;
> ```
> This information is feed into the ZStandard train API, which generates a 
> dictionary and inserts it into the dictionary catalog table. Additionally, we 
> update the ‘pg_attribute’ attribute options to include the unique dictionary 
> ID for that specific attribute. During compression, based on the available 
> dictionary ID, we retrieve the dictionary and use it to compress the 
> documents. I’ve created standard training function 
> (`zstd_dictionary_builder`) for JSONB, TEXT, and BYTEA.
>
> We store dictionary and dictid in the new catalog table ‘pg_zstd_dictionaries’
>
> ```
> test=# \d pg_zstd_dictionaries
> Table "pg_catalog.pg_zstd_dictionaries"
> Column | Type | Collation | Nullable | Default
> --------+-------+-----------+----------+---------
> dictid | oid | | not null |
> dict | bytea | | not null |
> Indexes:
> "pg_zstd_dictionaries_dictid_index" PRIMARY KEY, btree (dictid)
> ```
>
> This is the entire ZStandard dictionary infrastructure. A column can have 
> multiple dictionaries. The latest dictionary will be identified by the 
> pg_attribute attoptions. We never delete dictionaries once they are 
> generated. If a dictionary is not provided and attcompression is set to zstd, 
> we compress with ZStandard without dictionary. For decompression, the 
> zstd-compressed frame contains a dictionary identifier (dictid) that 
> indicates the dictionary used for compression. By retrieving this dictid from 
> the zstd frame, we then fetch the corresponding dictionary and perform 
> decompression.
>
> #############################################################################
>
> Enter toast compression framework changes,
>
> We identify a compressed datum compression algorithm using the top two bits 
> of va_tcinfo (varattrib_4b.va_compressed).
> It is possible to have four compression methods. However, based on previous 
> community email discussions regarding toast compression changes[3], the idea 
> of using it for a new compression algorithm has been rejected, and a 
> suggestion has been made to extend it which I’ve implemented in this patch. 
> This change necessitates an update to ‘varattrib_4b’ and ‘varatt_external’ on 
> disk structures. I’ve made sure that this changes are backward compatible.
>
> ```
> typedef union
> {
> struct /* Normal varlena (4-byte length) */
> {
> uint32 va_header;
> char va_data[FLEXIBLE_ARRAY_MEMBER];
> } va_4byte;
> struct /* Compressed-in-line format */
> {
> uint32 va_header;
> uint32 va_tcinfo; /* Original data size (excludes header) and
> * compression method; see va_extinfo */
> char va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
> } va_compressed;
> struct
> {
> uint32 va_header;
> uint32 va_tcinfo;
> uint32 va_cmp_alg;
> char va_data[FLEXIBLE_ARRAY_MEMBER];
> } va_compressed_ext;
> } varattrib_4b;
>
> typedef struct varatt_external
> {
> int32 va_rawsize; /* Original data size (includes header) */
> uint32 va_extinfo; /* External saved size (without header) and
> * compression method */
> Oid va_valueid; /* Unique ID of value within TOAST table */
> Oid va_toastrelid; /* RelID of TOAST table containing it */
> uint32 va_cmp_alg; /* The additional compression algorithms
> * information. */
> } varatt_external;
> ```
>
> As I need to update this structs, I’ve made changes to the existing macros. 
> Additionally added compression and decompression routines related to 
> ZStandard as needed. These are major design changes in the patch to 
> incorporate ZStandard with dictionary compression.
>
> Please let me know what you think about all this. Are there any concerns with 
> my approach? In particular, I would appreciate your thoughts on the on-disk 
> changes that result from this.
>
> kind regards,
>
> Nikhil Veldanda
> Amazon Web Services: https://aws.amazon.com
>
> [1] https://facebook.github.io/zstd/
> [2] https://github.com/facebook/zstd
> [3] https://www.postgresql.org/message-id/flat/YoMiNmkztrslDbNS%40paquier.xyz
>

Hi!
I generally love this idea, however I am not convinced in-core support
this is the right direction here. Maybe we can introduce some API
infrastructure here to allow delegating compression to extension's?
This is merely my opinion; perhaps dealing with a redo is not
worthwhile.

I did a brief lookup on patch v1. I feel like this is too much for a
single patch. Take, for example this change:

```
-#define NO_LZ4_SUPPORT() \
+#define NO_METHOD_SUPPORT(method) \
  ereport(ERROR, \
  (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), \
- errmsg("compression method lz4 not supported"), \
- errdetail("This functionality requires the server to be built with
lz4 support.")))
+ errmsg("compression method %s not supported", method), \
+ errdetail("This functionality requires the server to be built with
%s support.", method)))
 ```

This could be a separate preliminary refactoring patch in series.
Perhaps we need to divide the patch into smaller pieces if we follow
the suggested course of this thread (in-core support).

I will try to give another in-depth look here soon.

-- 
Best regards,
Kirill Reshke


Reply via email to