Skip to content

Add external compression hypertable config: column_codec - #10255

Open
kvc0 wants to merge 2 commits into
timescale:mainfrom
kvc0:externalcompression
Open

Add external compression hypertable config: column_codec#10255
kvc0 wants to merge 2 commits into
timescale:mainfrom
kvc0:externalcompression

Conversation

@kvc0

@kvc0 kvc0 commented Jul 14, 2026

Copy link
Copy Markdown

This change lets users define custom functions to do compression for arbitrary types.

Some custom types are composites, and some come from plugins. Having the ability to define a codec gives you better control over compression.

I did this because I needed to define a pgrx type, and the default array/toast compression did not do as good as I hoped. I spent a little time on it, and with this change to timescaledb I was able to get my data type into 33-45% less space. That's a major difference for me and my small ssds!

The idea for External codecs is basically to extend custom data types with serialization codecs. They're resolved by configuration after being set up. You make an opclass on the type with 2 functions:

  • .compress([]) RETURNS bytea
  • .decompress(bytea) RETURNS []

When timescale goes to compress a column, it checks if column_codec is configured. If so, it uses it to compress the row batch. If column_codec is not configured, it just falls through and serializes via the pre-existing rules.

I serialized a table of custom rust histograms I have via these functions. That's where I learned the opportunity for compression is 1/3 to 1/2 the size of toast compression.

By way of example, these are the stubs for a type called exphist in pgrx flavor, to take advantage of the feature in this PR:

fn exphist_compress(batch: pgrx::Array<ExpHist>) -> Vec<u8> {
}

fn exphist_decompress(blob: &[u8]) -> Vec<ExpHist> {
}

Then some sql to set it up:

CREATE  FUNCTION "exphist_compress"(
	"batch" ExpHist[]
) RETURNS bytea
IMMUTABLE STRICT PARALLEL SAFE
LANGUAGE c
AS 'MODULE_PATHNAME', 'exphist_compress_wrapper';

CREATE  FUNCTION "exphist_decompress"(
	"blob" bytea
) RETURNS ExpHist[]
IMMUTABLE STRICT PARALLEL SAFE
LANGUAGE c
AS 'MODULE_PATHNAME', 'exphist_decompress_wrapper';

CREATE OPERATOR CLASS exphist_codec_ops
FOR TYPE exphist USING ts_compression_codec AS
	FUNCTION 1 exphist_compress(exphist[]),
	FUNCTION 2 exphist_decompress(bytea);

and in my create table I include a configuration mention:

    tsdb.column_codec = 'histogram:exphist_codec_ops'

Resolves: #1524

This change lets users define custom functions to do compression
for arbitrary types.

Some custom types are composites, and some come from plugins.
Having the ability to define a codec gives you better control over
compression.

I did this because I needed to define a pgrx type, and the default
array/toast compression did not do as good as I hoped. I spent a
little time on it, and with this change to timescaledb I was able
to get my data type into 33-45% less space. That's a major
difference for me and my small ssds!

The idea for External codecs is basically to extend custom data
types with intrinsic serialization codecs. They're resolved by a
name rather than a whole registry. You make a custom data type,
and 2 functions:
* <schema>.<typename>_compress(<type>[]) RETURNS bytea
* <schema>.<typename>_decompress(bytea)  RETURNS <type>[]

When timescale goes to compress a column that is a "fallback"
type, you first try to look up (cached) the codec function pair
for the data type. If it exists, you use it. If it does not
exist, you just fall through and serialize exactly how you do
today.

I serialized a table of custom rust histograms I have via these
functions. That's where I learned the compression is 1/3 to 1/2
the size of toast compression.

By way of example, these are the stubs for a type called
`exphist` in pgrx flavor, to take advantage of the feature in
this PR:

```rust
fn exphist_compress(batch: pgrx::Array<ExpHist>) -> Vec<u8> {
}

fn exphist_decompress(blob: &[u8]) -> Vec<ExpHist> {
}
```

Resolves: timescale#1524
@github-actions

Copy link
Copy Markdown

@natalya-aksman, @melihmutlu: please review this pull request.

Powered by pull-review

Comment thread tsl/src/compression/README.md Outdated

The external method delegates compression to the column's data type. A type
opts in by providing two SQL functions in its own schema,
`<typename>_compress(<type>[]) RETURNS bytea` and

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't thought about this feature and don't know if it's feasible/desired. It is a very high-impact area, I'm not sure we have resources now to address it with the care it requires. In general, at this stage it might be best to write a design document instead of code.

One thing catches my attention here: instead of name interpolation, this should probably use the standard Postgres approach with operator classes (access method support functions).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw a willingness to entertain a contribution in the earlier linked issue, so I figured I’d give it a shot. Thanks for the feedback. You make a great point about operator classes. I’m going to take a look at switching to that kind of approach instead. I’m not that deep on postgres :-)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, this is much easier than I expected. And it's going to help with upgrade/downgrade. Thanks again for this awesome suggestion

@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.85068% with 60 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
tsl/src/compression/algorithms/external.c 73.23% 36 Missing and 21 partials ⚠️
tsl/src/compression/compression.c 62.50% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@dbeck

dbeck commented Jul 14, 2026

Copy link
Copy Markdown
Member

My concern with this approach is about the compatibility between external algorithm versions. What happens when someone assigns a different/new algorithm for a type?

Same question about the migration between the current Timescaledb Array/Dictionary and the external compression?

The Timescaledb strategy for this was that we haven't retired any previous compression algorithm from the code, so we can always read all previous algorithms on upgrades. On downgrades, we recompress the data with the algorithm available in the previous version and then we can safely downgrade.

How would this work with an external algorithm?

@kvc0

kvc0 commented Jul 14, 2026

Copy link
Copy Markdown
Author

My concern with this approach is about the compatibility between external algorithm versions. What happens when someone assigns a different/new algorithm for a type?

It’s going to be up to the user to decide compression compatibility. My codec uses a version byte so I can make updates in the future.

Broadly speaking, I think all compression codec authors have to consider this: Timescale doesn’t control external users’ deployment schedules, so the user’s versioning contract can only be with the codec author. I think that’s a necessary fallout of external compression. Well, either that or some machinery to force re-compression on update; but I actively do not want that! Decompress old, compress new is a good approach for lazy decompression with a new format generally.

Same question about the migration between the current Timescaledb Array/Dictionary and the external compression?

I’m thinking of this more like the chunk time interval setting. That only applies to new chunks (obviously you know all this). So the external compression algorithm should apply to new chunks.

Interestingly, you deal with this problem today between the array and dictionary codecs. You write an algorithm byte in the batch header. It’s the same strategy, from timescale’s perspective, as my external algorithm. Depending on the system state, an algorithm is chosen at write time. At read time, the deserializer is chosen by algorithm byte. Array/dictionary/external should all work fine in the same hypertatable by virtue of your batch header.

The Timescaledb strategy for this was that we haven't retired any previous compression algorithm from the code, so we can always read all previous algorithms on upgrades. On downgrades, we recompress the data with the algorithm available in the previous version and then we can safely downgrade.

How would this work with an external algorithm?

I think the opclass approach akuzm mentioned has a path forward on this. I should be able to do the same thing for a timescale downgrade. (Read external, recompress array/dictionary) I should have an update this week.

Separately, upgrade/downgrade/change of compression algorithm is between a user and their codec author.

Thank you both for your thoughtful comments! I realize this change comes a bit out of left field, but I can’t justify paying double to store my histograms as toast without trying to upstream a feature :-)

Comment thread tsl/src/compression/compression.c Outdated
Comment on lines +3357 to +3361
/* Prefer the type's own registered compress/decompress function pair */
if (external_codec_lookup(typeoid, NULL, NULL))
{
return COMPRESSION_ALGORITHM_EXTERNAL;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this behavior be default when the codec exists or be more explicit with a knob like:

alter table metrics
set (
    timescaledb.compress_column_codec = 'somecol:some_schema.some_codec'
)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that was top of mind for me too, but I was worried the hypertable config section would be harder to merge 😅 than a niche leaf. I will look into this also!

Instead of using types and default opclasses, the EXTERNAL compresison
algorithm now uses regular opclasses and only applies when explicitly
configured.

Users alter or otherwise set the timescaledb.column_codec to a
`column_name:some.compression_opclass`. The opclass, as in the
previous commit, is a virtual namespace with 2 support functions.
(1 and 2, compress and decompress respectively).
@kvc0 kvc0 changed the title Add external compression algorithm Add external compression hypertable config: column_codec Jul 18, 2026
@kvc0

kvc0 commented Jul 18, 2026

Copy link
Copy Markdown
Author

@surister -> I have added a tsdb.column_codec configuration. There is no more implicit EXTERNAL type resolution. This touched many test files, but it's mostly just empty columns from a select *. I think this is a much better approach, even if touched file count is greater.

@akuzm -> I have switched to using opclass support functions, with amvalidate. It's a much more robust system. I considered skipping it with the tsdb.column_codec change, but it still seemed useful. It's a nice way to declare 1 identifier that has rules, and that can map to multiple functions. It's kind of an "interface" of sorts. This was a great suggestion, thank you!

@dbeck
Different/new algorithm is already handled by your pre-existing header. For EXTERNAL algorithm, I've documented that folks can leave old algorithms in their opclass list until they're done with them. I've also documented how to migrate. It should be familiar: You just configure what you want, then decompress_chunk(), compress_chunk() all the chunks that hold configuration you don't want.

It works the same irrespective of EXTERNAL algorithms or dictionary/array.

For downgrade, you will have to commit to never retiring the EXTERNAL function call approach. There are 2 broad classes of upgrade/downgrade to consider:

  1. For users who want to use EXTERNAL, then downgrade to a tsdb version without EXTERNAL support, they follow the runbook generated by the downgrade script. It occurs to me that you could possibly prefer to label the EXTERNAL algorithm beta (or undocumented) for a release or two to make that downgrade path less likely to be exercised. Up to you, of course!
  2. For users who want to change their EXTERNAL algorithm implementation, that's outside of your control. It's between the user and their codec author. The author may say the new version is backward compatible. Or they may say it requires a new opclass. Either way, this feature supports both.

@kvc0
kvc0 requested review from akuzm and surister July 18, 2026 07:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Public API for array compression?

4 participants