8000 Bitpacking mode info by arjenpdevries · Pull Request #15623 · duckdb/duckdb · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Bitpacking mode info #15623

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Feb 6, 2025
Merged

Bitpacking mode info #15623

merged 23 commits into from
Feb 6, 2025

Conversation

arjenpdevries
Copy link
Contributor

Version of #15622 without (I think) merge conflicts, which was a continuation of #15544

This implements the suggested serialize/deserialize, in line with the suggestions by @Tishj (and building on his initials steps).

More detailed info in #15544

…Santa did not bring it;

so I tried myself ;-)

This pull request will do:

- Create a BitpackingSegmentState that overrides GetSegmentInfo to return mode info
- Add BitpackingInitSegment that returns segment-info (used in ColumnData)
- Add BitpackingInitSegment / init_segment to the compressionfunction

(I also renamed BitpackingCompressState to BitpackingCompressionState for
consistency with other compression code.)

The approach implemented seems to work correctly and passes tests locally.

However, the new information is only shown when you first close the database and reopen it.
If you only use `CHECKPOINT`, `init_segment` is not called and the modes not reported.
I am stuck where to fix this; it seems that this would be a step related to `ConvertToPersistent`
that I overlook but I do not understand what I need to do to make it work.

I have tested using the following SQL (but because of this incomplete behaviour not yet added as a test):

```
pragma force_compression = 'BitPacking';
CREATE or replace TABLE test (a INTEGER, b INTEGER);
insert into test values (10,12), (11,12), (12,11), (NULL,NULL);
insert into test values (10,12), (33,33), (33,33), (10,12);
select segment_id, row_group_id, block_id, block_offset, compression, segment_info FROM pragma_storage_info('test') order by segment_id, row_group_id  asc;
checkpoint;
select segment_id, row_group_id, block_id, block_offset, compression, segment_info FROM pragma_storage_info('test') order by segment_id, row_group_id  asc;
```

After closing the CLI and restarting it on the same database, and then doing
```
select segment_id, row_group_id, block_id, block_offset, compression, segment_info FROM pragma_storage_info('test') order by segment_id, row_group_id  asc;
```

the `pragma_storage_info` reports `segment_info` that includes the bitpacking mode.

Hoping for some DuckDB compression guru advice, A.

```
D select segment_id, row_group_id, block_id, block_offset, compression, segment_info FROM pragma_storage_info('test') order by segment_id, row_group_id  asc;
┌────────────┬──────────────┬──────────┬──────────────┬──────────────┬──────────────┐
│ segment_id │ row_group_id │ block_id │ block_offset │ compression  │ segment_info │
│   int64    │    int64     │  int64   │    int64     │   varchar    │   varchar    │
├────────────┼──────────────┼──────────┼──────────────┼──────────────┼──────────────┤
│          0 │            0 │        1 │            0 │ BitPacking   │ Mode: for    │
│          0 │            0 │        1 │           48 │ Uncompressed │              │
│          0 │            0 │        1 │          304 │ BitPacking   │ Mode: for    │
│          0 │            0 │        1 │          352 │ Uncompressed │              │
└────────────┴──────────────┴──────────┴──────────────┴──────────────┴──────────────┘
```
@duckdb-draftbot duckdb-draftbot marked this pull request as draft January 8, 2025 19:33
@arjenpdevries arjenpdevries marked this pull request as ready for review January 8, 2025 20:17
@duckdb-draftbot duckdb-draftbot marked this pull request as draft January 8, 2025 21:52
@arjenpdevries arjenpdevries marked this pull request as ready for review January 8, 2025 21:53
return std::move(result);
}

void CleanupState(ColumnSegment &segment) {
Copy link
Contributor
@Tishj Tishj Jan 8, 2025

Choose a reason for hiding this comment

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

I don't think we need this, cleanup state is necessary for state that holds lifetimes outside of the scope of the segment, such as additional blocks that it has created - which is what the UncompressedStringSegmentState uses it for.

UncompressedStringStorage can create additional blocks to hold "overflow" strings, which are bigger than a certain threshold (I think it's 4096 by default)
When the column segment that these additional blocks are a part of gets dropped, we need to inform the block manager that these additional blocks can now also be safely reused.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Of course, should have realized. Will remove.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just out of curiosity - we now write extra information, could it happen that this requires an extra block (in the case where everything was filled completely with data and I want to write out the bitmode histogram) or does this extra serialization data get written to a separate place in storage?

Copy link
Contributor
@Tishj Tishj left a comment

Choose a reason for hiding this comment

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

Thanks, this looks like it'll work from reading the code 👍
I do however want to see at least one test making sure this is working properly

Looking at:

static constexpr const idx_t BITPACKING_METADATA_GROUP_SIZE = STANDARD_VECTOR_SIZE > 512 ? STANDARD_VECTOR_SIZE : 2048;

We should be deciding per 2048 values which mode to use, so given 10000 tuples to compress, this should result in a total of 5 groups
(also your test should probably use require vector_size 2048 because the vector size will impact the group size)

@duckdb-draftbot duckdb-draftbot marked this pull request as draft January 9, 2025 07:56
@arjenpdevries arjenpdevries marked this pull request as ready for review January 9, 2025 07:58
Copy link
Contributor
@Tishj Tishj left a comment

Choose a reason for hiding this comment

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

Thanks, looks great! 👍

Copy link
Collaborator
@Mytherin Mytherin left a comment

Choose a reason for hiding this comment

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

Thanks for the PR!

Unless I'm misunderstanding the PR - this seems to store more information in the database only to provide the segment information during the storage_info callback. The new serialize/deserialize logic will also hurt forwards compatibility of database files.

Given this information exists elsewhere already in the blocks, perhaps we can instead introduce a separate get_segment_info callback to the compression method so that we don't need to introduce the new compressed segment state?

@Tishj
Copy link
Contributor
Tishj commented Jan 9, 2025

The PR adds segment_info for the bitpacked column segments

query I
SELECT
	segment_info
FROM
	pragma_storage_info('test_bp')
WHERE segment_type NOT IN ('VALIDITY')
----
CONSTANT: 9, DELTA_FOR: 1

I'm not sure about the serialization / forwards compatibility complications, but this sounds like a welcome change for observability ?
But now that I think about it, I guess this does get complicated by the fact that old versions don't produce this information, and we need to safely handle this

I also feel like Logging this information during checkpointing would be a nicer way to provide this observability, waiting patiently on #15119 🙏


I think I just understood this part:

Given this information exists elsewhere already in the blocks, perhaps we can instead introduce a separate get_segment_info callback to the compression method so that we don't need to introduce the new compressed segment state?

This is the existing code to get segment info
Because we have access to the segment, we could scan the segment here instead.
I thought that was too expensive, but perhaps introducing serialization is more expensive in terms of backwards compatibility
(as shown in later comments scanning the block for this information is pretty trivial)

		auto segment_state = segment->GetSegmentState();
		if (segment_state) {
			column_info.segment_info = segment_state->GetSegmentInfo();
			column_info.additional_blocks = segment_state->GetAdditionalBlocks();
		}

We can add a get_segment_info method to the CompressionFunction, when it's set we can call that instead of getting the segment state

@Mytherin
Copy link
Collaborator
Mytherin commented Jan 9, 2025

The change itself is welcome for sure - my point is that this information is already stored in the (compressed) data itself. We should be able to just read it from the blocks. That then (1) also works with older database files, (2) does not require storing additional data in the database file, increasing the size, and (3) does not break the ability of older DuckDB versions from reading this compression due to the introduction of new information they do not understand.

@arjenpdevries
Copy link
Contributor Author

Thanks Mark!

My initial approach used only init-segment, but didn't work at the moment of checkpointing; in discussion we switched to this design.

I guess I can alternatively scan all segments in init-segment to recreate this information upon the column being loaded from disk. Have to look into the exact mechanics but will try.

(It may imply a longer load time?)

@Tishj
Copy link
Contributor
Tishj commented Jan 9, 2025
	void LoadNextGroup() {
		D_ASSERT(bitpacking_metadata_ptr > handle.Ptr() &&
		         bitpacking_metadata_ptr < handle.Ptr() + current_segment.GetBlockManager().GetBlockSize());
		current_group_offset = 0;
		current_group = DecodeMeta(reinterpret_cast<bitpacking_metadata_encoded_t *>(bitpacking_metadata_ptr));

		bitpacking_metadata_ptr -= sizeof(bitpacking_metadata_encoded_t);

All the group metadata is neatly packed together, so scanning just that portion should be cheap and efficient

The ColumnSegment has a count to tell us how many tuples are stored on it
As discussed before, BITPACKING_METADATA_GROUP_SIZE is the amount of tuples stored in a group

map<BitpackingMode, idx_t> counts;
auto tuple_count = segment.count.load();
BitpackingScanState<T> scan_state(segment);
for (idx_t i = 0; i < tuple_count; i += BITPACKING_METADATA_GROUP_SIZE) {
    counts[scan_state.current_group.mode]++;
}

// stringify the counts and return

should be all you need 👍

@Mytherin
Copy link
Collaborator
Mytherin commented Jan 9, 2025

Thanks Mark!

My initial approach used only init-segment, but didn't work at the moment of checkpointing; in discussion we switched to this design.

I guess I can alternatively scan all segments in init-segment to recreate this information upon the column being loaded from disk. Have to look into the exact mechanics but will try.

(It may imply a longer load time?)

GetSegmentInfo is only called very rarely (when the storage_info function is called) - and is not very performance sensitive. Ideally the code introduced in this PR is only ever run when the user calls storage_info - and does not impact any other (much more common) code paths (i.e. does not slow down compression, load, etc).

@Tishj
Copy link
Contributor
Tishj commented Jan 9, 2025

Thanks Mark!

My initial approach used only init-segment, but didn't work at the moment of checkpointing; in discussion we switched to this design.

I guess I can alternatively scan all segments in init-segment to recreate this information upon the column being loaded from disk. Have to look into the exact mechanics but will try.

(It may imply a longer load time?)

Yes, so you were mostly on the right track to do what Mark is suggesting, but the place it was being done was wrong.
You were doing this in InitSegment, which receives both fresh and existing blocks
For an existing block this information is available, but for a fresh one it doesn't exist yet

See the second part of #15623 (comment)

6D40
@duckdb-draftbot duckdb-draftbot marked this pull request as draft January 16, 2025 14:56
@arjenpdevries arjenpdevries marked this pull request as ready for review January 18, 2025 17:29
@arjenpdevries
Copy link
Contributor Author

So followed the suggestion to use the map but kept segment_info a column of VARCHAR for now. LMK if you'd rather see that changed. Otherwise seems good to go.

Copy link
Contributor
@Tishj Tishj left a comment

Choose a reason for hiding this comment

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

Thanks, I think it's fine to keep it as VARCHAR for now 👍

@duckdb-draftbot duckdb-draftbot marked this pull request as draft January 22, 2025 17:58
@arjenpdevries arjenpdevries marked this pull request as ready for review January 22, 2025 17:58
@arjenpdevries
Copy link
Contributor Author

(only removed two includes, it should pass tests as before)

@Mytherin
Copy link
Collaborator

Looks like they were not unnecessary :)

Turn return type into a InsertionOrderPreservingMap<string> similar to PhysicalOperator::ParamsToString (following suggestion by Mark).
Keep segment_info column of type VARCHAR to limit changes.
@arjenpdevries
Copy link
Contributor Author

OK don't believe the LSP without checking twice...

@duckdb-draftbot duckdb-draftbot marked this pull request as draft January 23, 2025 01:50
@arjenpdevries arjenpdevries marked this pull request as ready for review January 23, 2025 07:58
@arjenpdevries
Copy link
Contributor Author

Will not touch it any more! 😀

@Mytherin Mytherin merged commit 670e905 into duckdb:main Feb 6, 2025
48 checks passed
@Mytherin
Copy link
Collaborator
Mytherin commented Feb 6, 2025

Thanks!

@arjenpdevries arjenpdevries deleted the bitpacking-mode-info branch February 21, 2025 08:26
Antonov548 added a commit to Antonov548/duckdb-r that referenced this pull request Feb 26, 2025
krlmlr pushed a commit to duckdb/duckdb-r that referenced this pull request Mar 5, 2025
krlmlr added a commit to duckdb/duckdb-r that referenced this pull request May 15, 2025
krlmlr added a commit to duckdb/duckdb-r that referenced this pull request May 15, 2025
krlmlr added a commit to duckdb/duckdb-r that referenced this pull request May 17, 2025
krlmlr added a commit to duckdb/duckdb-r that referenced this pull request May 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
0