diff --git a/src/duckdb/src/common/enum_util.cpp b/src/duckdb/src/common/enum_util.cpp index 8bc6b84dd..9dda8d0dc 100644 --- a/src/duckdb/src/common/enum_util.cpp +++ b/src/duckdb/src/common/enum_util.cpp @@ -1502,6 +1502,25 @@ DefaultOrderByNullType EnumUtil::FromString(const char * return static_cast(StringUtil::StringToEnum(GetDefaultOrderByNullTypeValues(), 5, "DefaultOrderByNullType", value)); } +const StringUtil::EnumStringLiteral *GetDeleteIdStateValues() { + static constexpr StringUtil::EnumStringLiteral values[] { + { static_cast(DeleteIdState::CONSTANT), "CONSTANT" }, + { static_cast(DeleteIdState::MASKED), "MASKED" }, + { static_cast(DeleteIdState::ARRAY), "ARRAY" } + }; + return values; +} + +template<> +const char* EnumUtil::ToChars(DeleteIdState value) { + return StringUtil::EnumToString(GetDeleteIdStateValues(), 3, "DeleteIdState", static_cast(value)); +} + +template<> +DeleteIdState EnumUtil::FromString(const char *value) { + return static_cast(StringUtil::StringToEnum(GetDeleteIdStateValues(), 3, "DeleteIdState", value)); +} + const StringUtil::EnumStringLiteral *GetDependencyEntryTypeValues() { static constexpr StringUtil::EnumStringLiteral values[] { { static_cast(DependencyEntryType::SUBJECT), "SUBJECT" }, @@ -5542,6 +5561,25 @@ VerifyExistenceType EnumUtil::FromString(const char *value) return static_cast(StringUtil::StringToEnum(GetVerifyExistenceTypeValues(), 3, "VerifyExistenceType", value)); } +const StringUtil::EnumStringLiteral *GetVersionCompressionResultValues() { + static constexpr StringUtil::EnumStringLiteral values[] { + { static_cast(VersionCompressionResult::FULLY_COMPRESSED), "FULLY_COMPRESSED" }, + { static_cast(VersionCompressionResult::PENDING), "PENDING" }, + { static_cast(VersionCompressionResult::SETTLED), "SETTLED" } + }; + return values; +} + +template<> +const char* EnumUtil::ToChars(VersionCompressionResult value) { + return StringUtil::EnumToString(GetVersionCompressionResultValues(), 3, "VersionCompressionResult", static_cast(value)); +} + +template<> +VersionCompressionResult EnumUtil::FromString(const char *value) { + return static_cast(StringUtil::StringToEnum(GetVersionCompressionResultValues(), 3, "VersionCompressionResult", value)); +} + const StringUtil::EnumStringLiteral *GetVertexTypeValues() { static constexpr StringUtil::EnumStringLiteral values[] { { static_cast(VertexType::XY), "XY" }, diff --git a/src/duckdb/src/function/table/version/pragma_version.cpp b/src/duckdb/src/function/table/version/pragma_version.cpp index 27ac50d9d..253616363 100644 --- a/src/duckdb/src/function/table/version/pragma_version.cpp +++ b/src/duckdb/src/function/table/version/pragma_version.cpp @@ -1,5 +1,5 @@ #ifndef DUCKDB_PATCH_VERSION -#define DUCKDB_PATCH_VERSION "6-dev72" +#define DUCKDB_PATCH_VERSION "6-dev80" #endif #ifndef DUCKDB_MINOR_VERSION #define DUCKDB_MINOR_VERSION 5 @@ -8,10 +8,10 @@ #define DUCKDB_MAJOR_VERSION 1 #endif #ifndef DUCKDB_VERSION -#define DUCKDB_VERSION "v1.5.6-dev72" +#define DUCKDB_VERSION "v1.5.6-dev80" #endif #ifndef DUCKDB_SOURCE_ID -#define DUCKDB_SOURCE_ID "52b98657c0" +#define DUCKDB_SOURCE_ID "a14d147c9f" #endif #include "duckdb/function/table/system_functions.hpp" #include "duckdb/main/database.hpp" diff --git a/src/duckdb/src/include/duckdb/common/enum_util.hpp b/src/duckdb/src/include/duckdb/common/enum_util.hpp index 4533ca0f8..a02107128 100644 --- a/src/duckdb/src/include/duckdb/common/enum_util.hpp +++ b/src/duckdb/src/include/duckdb/common/enum_util.hpp @@ -158,6 +158,8 @@ enum class DecimalBitWidth : uint8_t; enum class DefaultOrderByNullType : uint8_t; +enum class DeleteIdState : uint8_t; + enum class DependencyEntryType : uint8_t; enum class DeprecatedIndexType : uint8_t; @@ -494,6 +496,8 @@ enum class VerificationType : uint8_t; enum class VerifyExistenceType : uint8_t; +enum class VersionCompressionResult : uint8_t; + enum class VertexType : uint8_t; enum class WALType : uint8_t; @@ -696,6 +700,9 @@ const char* EnumUtil::ToChars(DecimalBitWidth value); template<> const char* EnumUtil::ToChars(DefaultOrderByNullType value); +template<> +const char* EnumUtil::ToChars(DeleteIdState value); + template<> const char* EnumUtil::ToChars(DependencyEntryType value); @@ -1200,6 +1207,9 @@ const char* EnumUtil::ToChars(VerificationType value); template<> const char* EnumUtil::ToChars(VerifyExistenceType value); +template<> +const char* EnumUtil::ToChars(VersionCompressionResult value); + template<> const char* EnumUtil::ToChars(VertexType value); @@ -1408,6 +1418,9 @@ DecimalBitWidth EnumUtil::FromString(const char *value); template<> DefaultOrderByNullType EnumUtil::FromString(const char *value); +template<> +DeleteIdState EnumUtil::FromString(const char *value); + template<> DependencyEntryType EnumUtil::FromString(const char *value); @@ -1912,6 +1925,9 @@ VerificationType EnumUtil::FromString(const char *value); template<> VerifyExistenceType EnumUtil::FromString(const char *value); +template<> +VersionCompressionResult EnumUtil::FromString(const char *value); + template<> VertexType EnumUtil::FromString(const char *value); diff --git a/src/duckdb/src/include/duckdb/common/types.hpp b/src/duckdb/src/include/duckdb/common/types.hpp index b6e24805c..3fd4c890a 100644 --- a/src/duckdb/src/include/duckdb/common/types.hpp +++ b/src/duckdb/src/include/duckdb/common/types.hpp @@ -514,7 +514,7 @@ struct MapType { }; struct UnionType { - DUCKDB_API static const idx_t MAX_UNION_MEMBERS = 256; + DUCKDB_API static const idx_t MAX_UNION_MEMBERS = 255; DUCKDB_API static idx_t GetMemberCount(const LogicalType &type); DUCKDB_API static const LogicalType &GetMemberType(const LogicalType &type, idx_t index); DUCKDB_API static const string &GetMemberName(const LogicalType &type, idx_t index); diff --git a/src/duckdb/src/include/duckdb/storage/table/chunk_info.hpp b/src/duckdb/src/include/duckdb/storage/table/chunk_info.hpp index abe1b3bba..b7b115bdc 100644 --- a/src/duckdb/src/include/duckdb/storage/table/chunk_info.hpp +++ b/src/duckdb/src/include/duckdb/storage/table/chunk_info.hpp @@ -10,6 +10,7 @@ #include "duckdb/execution/index/index_pointer.hpp" #include "duckdb/common/enums/scan_options.hpp" +#include "duckdb/common/types/validity_mask.hpp" namespace duckdb { class RowGroup; @@ -21,97 +22,54 @@ class Serializer; class Deserializer; class FixedSizeAllocator; +//! The type of a serialized chunk info entry +//! In-memory all chunk infos are represented by ChunkVectorInfo - CONSTANT_INFO (a fully deleted vector) and +//! EMPTY_INFO (no deletes) only exist as tags in the serialized format enum class ChunkInfoType : uint8_t { CONSTANT_INFO, VECTOR_INFO, EMPTY_INFO }; -class ChunkInfo { +//! ChunkVectorInfo holds the version information (the insert and delete ids) of the rows of a single vector within +//! a row group. The insert and delete ids are stored as constants when all rows share the same id, and as per-row +//! id arrays otherwise. + +//! The result of a CompressVersionIds pass over a vector +enum class VersionCompressionResult : uint8_t { + //! No per-row id arrays remain - there is nothing left to compress + FULLY_COMPRESSED, + //! Some ids are not yet visible to all transactions - compression can succeed once the + //! lowest active start advances past them, without any further modifications + PENDING, + //! Per-row arrays remain that cannot compress without further modifications + //! (some rows are not deleted) + SETTLED +}; + +//! The storage state of the delete side of a ChunkVectorInfo. Exhaustive and mutually exclusive: +//! CONSTANT - all rows share constant_delete_id (NOT_DELETED_ID for none-deleted, or a single delete id) +//! MASKED - partially deleted, all deleted rows share one committed id (mask_delete_id): which rows are +//! deleted is stored in deleted_mask, deleted_data freed +//! ARRAY - per-row delete ids materialized in deleted_data +enum class DeleteIdState : uint8_t { CONSTANT, MASKED, ARRAY }; + +class ChunkVectorInfo { public: - ChunkInfo(idx_t start, ChunkInfoType type) : start(start), type(type) { - } - virtual ~ChunkInfo() { - } + explicit ChunkVectorInfo(FixedSizeAllocator &allocator, idx_t start, transaction_t insert_id = 0); + ChunkVectorInfo(FixedSizeAllocator &allocator, idx_t start, transaction_t insert_id, transaction_t delete_id); + ~ChunkVectorInfo(); //! The row index of the first row idx_t start; - //! The ChunkInfo type - ChunkInfoType type; public: //! Gets up to max_count entries from the chunk info. If the ret is 0>ret>max_count, the selection vector is filled //! with the tuples - virtual idx_t GetSelVector(ScanOptions options, optional_ptr sel_vector, - idx_t max_count) const = 0; + idx_t GetSelVector(ScanOptions options, optional_ptr sel_vector, idx_t max_count) const; idx_t GetCheckpointRowCount(TransactionData transaction, idx_t max_count); - //! Returns whether or not a single row in the ChunkInfo should be used or not for the given transaction - virtual bool Fetch(TransactionData transaction, row_t row) = 0; - virtual void CommitAppend(transaction_t commit_id, idx_t start, idx_t end) = 0; idx_t GetCommittedDeletedCount(idx_t max_count) const; - virtual bool Cleanup(transaction_t lowest_transaction) const; - virtual string ToString(idx_t max_count) const = 0; - - virtual bool HasDeletes(transaction_t transaction_id = MAX_TRANSACTION_ID) const = 0; - - virtual void Write(WriteStream &writer, transaction_t transaction_id) const; - static unique_ptr Read(FixedSizeAllocator &allocator, ReadStream &reader); - -public: - template - TARGET &Cast() { - if (type != TARGET::TYPE) { - throw InternalException("Failed to cast chunk info to type - query result type mismatch"); - } - return reinterpret_cast(*this); - } - - template - const TARGET &Cast() const { - if (type != TARGET::TYPE) { - throw InternalException("Failed to cast chunk info to type - query result type mismatch"); - } - return reinterpret_cast(*this); - } -}; - -class ChunkConstantInfo : public ChunkInfo { -public: - static constexpr const ChunkInfoType TYPE = ChunkInfoType::CONSTANT_INFO; - -public: - explicit ChunkConstantInfo(idx_t start); - - transaction_t insert_id; - transaction_t delete_id; - -public: - idx_t GetSelVector(ScanOptions options, optional_ptr sel_vector, idx_t max_count) const override; - bool Fetch(TransactionData transaction, row_t row) override; - void CommitAppend(transaction_t commit_id, idx_t start, idx_t end) override; - bool Cleanup(transaction_t lowest_transaction) const override; - string ToString(idx_t max_count) const override; - - bool HasDeletes(transaction_t transaction_id = MAX_TRANSACTION_ID) const override; - - void Write(WriteStream &writer, transaction_t transaction_id) const override; - static unique_ptr Read(ReadStream &reader); - -private: - template - idx_t TemplatedGetSelVector(transaction_t start_time, transaction_t transaction_id, idx_t max_count) const; -}; - -class ChunkVectorInfo : public ChunkInfo { -public: - static constexpr const ChunkInfoType TYPE = ChunkInfoType::VECTOR_INFO; - -public: - explicit ChunkVectorInfo(FixedSizeAllocator &allocator, idx_t start, transaction_t insert_id = 0); - ~ChunkVectorInfo() override; - -public: - idx_t GetSelVector(ScanOptions options, optional_ptr sel_vector, idx_t max_count) const override; - bool Fetch(TransactionData transaction, row_t row) override; - void CommitAppend(transaction_t commit_id, idx_t start, idx_t end) override; - bool Cleanup(transaction_t lowest_transaction) const override; - string ToString(idx_t max_count) const override; + //! Returns whether or not a single row in the ChunkVectorInfo should be used or not for the given transaction + bool Fetch(TransactionData transaction, row_t row); + void CommitAppend(transaction_t commit_id, idx_t start, idx_t end); + bool Cleanup(transaction_t lowest_transaction) const; + string ToString(idx_t max_count) const; void Append(idx_t start, idx_t end, transaction_t commit_id); @@ -123,13 +81,27 @@ class ChunkVectorInfo : public ChunkInfo { idx_t Delete(transaction_t transaction_id, row_t rows[], idx_t count); void CommitDelete(transaction_t commit_id, const DeleteInfo &info); - bool HasDeletes(transaction_t transaction_id = MAX_TRANSACTION_ID) const override; + //! Attempts to compress the per-row insert/delete ids into constants + //! This is possible when the ids behave identically for all transactions with a start time of at least + //! lowest_active_start (i.e. all active and future transactions) + VersionCompressionResult CompressVersionIds(transaction_t lowest_active_start); + //! Whether a compression pass could achieve anything for this vector (see recheck_compression) + bool RecheckCompression() const { + return recheck_compression; + } + //! Verifies (in DEBUG) that a disarmed recheck_compression matches the actual ids: nothing may be + //! compressible now or in the future without a modification that re-arms the check + void VerifyCachedCompressionState() const; + + bool HasDeletes(transaction_t transaction_id = MAX_TRANSACTION_ID) const; bool AnyDeleted() const; bool HasConstantInsertionId() const; transaction_t ConstantInsertId() const; + bool HasConstantDeleteId() const; + transaction_t ConstantDeleteId() const; - void Write(WriteStream &writer, transaction_t transaction_id) const override; - static unique_ptr Read(FixedSizeAllocator &allocator, ReadStream &reader); + void Write(WriteStream &writer, transaction_t transaction_id) const; + static unique_ptr Read(FixedSizeAllocator &allocator, ReadStream &reader); private: template @@ -140,6 +112,13 @@ class ChunkVectorInfo : public ChunkInfo { IndexPointer GetDeletedPointer() const; IndexPointer GetInitializedInsertedPointer(); IndexPointer GetInitializedDeletedPointer(); + //! Frees the per-row delete ids (if any) + void FreeDeleteData(); + //! ARRAY -> MASKED: record alive rows as invalid bits, free the per-row delete array. mask_id is the + //! shared committed id of the deleted rows (0 when they are already visible to all transactions) + void CompressDeleteToMask(transaction_t mask_id); + //! MASKED -> ARRAY: re-materialize the per-row delete array from the bitmask + void DecompressDeleteMask(); private: FixedSizeAllocator &allocator; @@ -150,6 +129,23 @@ class ChunkVectorInfo : public ChunkInfo { //! The transaction ids of the transactions that deleted the tuples (if any) IndexPointer deleted_data; + //! The constant delete id (if there is only one, e.g. because the entire vector was deleted in one transaction) + transaction_t constant_delete_id; + //! Bitmask used in the MASKED state: valid bit == the row is deleted, invalid bit == the row is alive. + //! Matches the on-disk VECTOR_INFO orientation. Only meaningful when delete_state == DeleteIdState::MASKED. + ValidityMask deleted_mask; + //! The single committed id shared by every deleted row in the MASKED state. 0 means the deletes are + //! visible to all transactions (the value used when read from disk); a non-zero committed id means the + //! mask was folded from one committed transaction whose delete is not yet visible to every snapshot. + //! Only meaningful when delete_state == DeleteIdState::MASKED. + transaction_t mask_delete_id = 0; + //! The current delete-side storage state - the single source of truth for the delete side + DeleteIdState delete_state = DeleteIdState::CONSTANT; + //! Whether a compression pass could achieve anything for this vector: armed by any id modification, + //! disarmed when a pass compresses the vector fully or finds it settled (live rows block the collapse + //! until a further delete re-arms it). CompressVersionIds returns the cached SETTLED without + //! re-scanning the ids while this is false. + bool recheck_compression = true; }; } // namespace duckdb diff --git a/src/duckdb/src/include/duckdb/storage/table/row_group.hpp b/src/duckdb/src/include/duckdb/storage/table/row_group.hpp index d4fa5ec8f..aede55584 100644 --- a/src/duckdb/src/include/duckdb/storage/table/row_group.hpp +++ b/src/duckdb/src/include/duckdb/storage/table/row_group.hpp @@ -229,6 +229,10 @@ class RowGroup : public SegmentBase { idx_t GetColumnCount() const; vector CheckpointDeletes(RowGroupWriter &writer); + //! Attempts to compress the version information of the row group + //! Per-row insert/delete ids that behave identically for all transactions with a start time of at least + //! lowest_active_start (i.e. all active and future transactions) are compressed into constants + void CompressVersionInfo(transaction_t lowest_active_start); //! Direct accessors, fall outside of general use but can be useful to some extensions ColumnData &GetRawColumnData(const StorageIndex &c) const; diff --git a/src/duckdb/src/include/duckdb/storage/table/row_version_manager.hpp b/src/duckdb/src/include/duckdb/storage/table/row_version_manager.hpp index 6791b8cfa..9ef9eaf75 100644 --- a/src/duckdb/src/include/duckdb/storage/table/row_version_manager.hpp +++ b/src/duckdb/src/include/duckdb/storage/table/row_version_manager.hpp @@ -36,6 +36,13 @@ class RowVersionManager { idx_t DeleteRows(idx_t vector_idx, transaction_t transaction_id, row_t rows[], idx_t count); void CommitDelete(idx_t vector_idx, transaction_t commit_id, const DeleteInfo &info); + //! Attempts to compress the per-row insert/delete ids of each vector into constants + //! This is possible when the ids behave identically for all transactions with a start time of at least + //! lowest_active_start (i.e. all active and future transactions) + //! Cheap when nothing can have changed: the pass only runs when version ids were modified since the + //! last pass, or when a previous pass left ids that can still compress once older transactions finish + void CompressVersionIds(transaction_t lowest_active_start); + vector Checkpoint(RowGroupWriter &writer); static shared_ptr Deserialize(MetaBlockPointer delete_pointer, MetadataManager &manager); @@ -45,15 +52,19 @@ class RowVersionManager { private: mutex version_lock; FixedSizeAllocator allocator; - vector> vector_info; + vector> vector_info; optional_idx uncheckpointed_delete_commit; vector storage_pointers; + //! Whether a compression pass may achieve anything: set when version ids are modified, cleared when a + //! pass finds no ids that could still compress. For deserialized version info this is derived from the + //! deserialized content (with the current storage format checkpointed ids are always settled). + bool needs_compression_check = false; private: FixedSizeAllocator &GetAllocator() { return allocator; } - optional_ptr GetChunkInfo(idx_t vector_idx); + optional_ptr GetChunkInfo(idx_t vector_idx); ChunkVectorInfo &GetVectorInfo(idx_t vector_idx); void FillVectorInfo(idx_t vector_idx); }; diff --git a/src/duckdb/src/storage/table/chunk_info.cpp b/src/duckdb/src/storage/table/chunk_info.cpp index 22676a47d..d768743f9 100644 --- a/src/duckdb/src/storage/table/chunk_info.cpp +++ b/src/duckdb/src/storage/table/chunk_info.cpp @@ -44,29 +44,16 @@ static bool UseVersion(TransactionData transaction, transaction_t id) { return StandardInsertOperator::UseInsertedVersion(transaction.start_time, transaction.transaction_id, id); } -bool ChunkInfo::Cleanup(transaction_t lowest_transaction) const { - return false; +ChunkVectorInfo::ChunkVectorInfo(FixedSizeAllocator &allocator_p, idx_t start, transaction_t insert_id_p) + : ChunkVectorInfo(allocator_p, start, insert_id_p, NOT_DELETED_ID) { } -void ChunkInfo::Write(WriteStream &writer, transaction_t checkpoint_id) const { - writer.Write(type); +ChunkVectorInfo::ChunkVectorInfo(FixedSizeAllocator &allocator_p, idx_t start, transaction_t insert_id_p, + transaction_t delete_id_p) + : start(start), allocator(allocator_p), constant_insert_id(insert_id_p), constant_delete_id(delete_id_p) { } -unique_ptr ChunkInfo::Read(FixedSizeAllocator &allocator, ReadStream &reader) { - auto type = reader.Read(); - switch (type) { - case ChunkInfoType::EMPTY_INFO: - return nullptr; - case ChunkInfoType::CONSTANT_INFO: - return ChunkConstantInfo::Read(reader); - case ChunkInfoType::VECTOR_INFO: - return ChunkVectorInfo::Read(allocator, reader); - default: - throw SerializationException("Could not deserialize Chunk Info Type: unrecognized type"); - } -} - -idx_t ChunkInfo::GetCommittedDeletedCount(idx_t max_count) const { +idx_t ChunkVectorInfo::GetCommittedDeletedCount(idx_t max_count) const { ScanOptions options(TransactionData(0, TRANSACTION_ID_START)); options.insert_type = InsertedScanType::ALL_ROWS; options.delete_type = DeletedScanType::OMIT_COMMITTED_DELETES; @@ -74,116 +61,14 @@ idx_t ChunkInfo::GetCommittedDeletedCount(idx_t max_count) const { return max_count - not_deleted_count; } -idx_t ChunkInfo::GetCheckpointRowCount(TransactionData transaction, idx_t max_count) { +idx_t ChunkVectorInfo::GetCheckpointRowCount(TransactionData transaction, idx_t max_count) { ScanOptions options(transaction); options.delete_type = DeletedScanType::INCLUDE_ALL_DELETED; return GetSelVector(options, nullptr, max_count); } -//===--------------------------------------------------------------------===// -// Constant info -//===--------------------------------------------------------------------===// -ChunkConstantInfo::ChunkConstantInfo(idx_t start) - : ChunkInfo(start, ChunkInfoType::CONSTANT_INFO), insert_id(0), delete_id(NOT_DELETED_ID) { -} - -template -idx_t ChunkConstantInfo::TemplatedGetSelVector(transaction_t start_time, transaction_t transaction_id, - idx_t max_count) const { - if (INSERT_OP::UseInsertedVersion(start_time, transaction_id, insert_id) && - !DELETE_OP::IsDeleted(start_time, transaction_id, delete_id)) { - return max_count; - } - return 0; -} - -idx_t ChunkConstantInfo::GetSelVector(ScanOptions options, optional_ptr sel_vector, - idx_t max_count) const { - auto &transaction = options.transaction; - if (options.insert_type == InsertedScanType::STANDARD) { - if (!StandardInsertOperator::UseInsertedVersion(transaction.start_time, transaction.transaction_id, - insert_id)) { - return 0; - } - } - if (options.delete_type == DeletedScanType::STANDARD) { - if (StandardDeleteOperator::IsDeleted(transaction.start_time, transaction.transaction_id, delete_id)) { - return 0; - } - } else if (options.delete_type == DeletedScanType::OMIT_COMMITTED_DELETES) { - if (CommittedDeleteOperator::IsDeleted(transaction.start_time, transaction.transaction_id, delete_id)) { - return 0; - } - } - return max_count; -} - -bool ChunkConstantInfo::Fetch(TransactionData transaction, row_t row) { - return UseVersion(transaction, insert_id) && !UseVersion(transaction, delete_id); -} - -void ChunkConstantInfo::CommitAppend(transaction_t commit_id, idx_t start, idx_t end) { - D_ASSERT(start == 0 && end == STANDARD_VECTOR_SIZE); - insert_id = commit_id; -} - -bool ChunkConstantInfo::HasDeletes(transaction_t transaction_id) const { - if (transaction_id == MAX_TRANSACTION_ID) { - transaction_id = TRANSACTION_ID_START - 1; - } - bool is_deleted = insert_id >= TRANSACTION_ID_START || delete_id <= transaction_id; - return is_deleted; -} - -bool ChunkConstantInfo::Cleanup(transaction_t lowest_transaction) const { - if (delete_id != NOT_DELETED_ID) { - // the chunk info is labeled as deleted - we need to keep it around - return false; - } - if (insert_id > lowest_transaction) { - // there are still transactions active that need this ChunkInfo - return false; - } - return true; -} - -void ChunkConstantInfo::Write(WriteStream &writer, transaction_t checkpoint_id) const { - D_ASSERT(HasDeletes(checkpoint_id)); - ChunkInfo::Write(writer, checkpoint_id); - writer.Write(start); -} - -unique_ptr ChunkConstantInfo::Read(ReadStream &reader) { - auto start = reader.Read(); - auto info = make_uniq(start); - info->insert_id = 0; - info->delete_id = 0; - return std::move(info); -} - -string ChunkConstantInfo::ToString(idx_t max_count) const { - string result; - result += "Constant [Count: " + to_string(max_count); - result += ", "; - result += "Insert Id: " + to_string(insert_id); - if (delete_id != NOT_DELETED_ID) { - result += ", Delete Id: " + to_string(delete_id); - } - result += "]"; - return result; -} - -//===--------------------------------------------------------------------===// -// Vector info -//===--------------------------------------------------------------------===// -ChunkVectorInfo::ChunkVectorInfo(FixedSizeAllocator &allocator_p, idx_t start, transaction_t insert_id_p) - : ChunkInfo(start, ChunkInfoType::VECTOR_INFO), allocator(allocator_p), constant_insert_id(insert_id_p) { -} - ChunkVectorInfo::~ChunkVectorInfo() { - if (AnyDeleted()) { - allocator.Free(deleted_data); - } + FreeDeleteData(); if (!HasConstantInsertionId()) { allocator.Free(inserted_data); } @@ -192,24 +77,29 @@ ChunkVectorInfo::~ChunkVectorInfo() { template idx_t ChunkVectorInfo::TemplatedGetSelVector(transaction_t start_time, transaction_t transaction_id, optional_ptr sel_vector, idx_t max_count) const { - if (HasConstantInsertionId()) { - if (!AnyDeleted()) { - // all tuples have the same inserted id: and no tuples were deleted + switch (delete_state) { + case DeleteIdState::CONSTANT: { + // all tuples have the same deleted id + if (DELETE_OP::IsDeleted(start_time, transaction_id, ConstantDeleteId())) { + // all tuples are deleted + return 0; + } + // no tuples are deleted: we only have to check the inserted ids + if (HasConstantInsertionId()) { + // all tuples have the same inserted id as well if (INSERT_OP::UseInsertedVersion(start_time, transaction_id, ConstantInsertId())) { return max_count; } else { return 0; } } - if (!INSERT_OP::UseInsertedVersion(start_time, transaction_id, ConstantInsertId())) { - return 0; - } - // have to check deleted flag + // have to check inserted flag + auto insert_segment = allocator.GetHandle(GetInsertedPointer()); + auto inserted = insert_segment.GetPtr(); + idx_t count = 0; - auto segment = allocator.GetHandle(GetDeletedPointer()); - auto deleted = segment.GetPtr(); for (idx_t i = 0; i < max_count; i++) { - if (DELETE_OP::IsDeleted(start_time, transaction_id, deleted[i])) { + if (!INSERT_OP::UseInsertedVersion(start_time, transaction_id, inserted[i])) { continue; } if (sel_vector) { @@ -219,16 +109,62 @@ idx_t ChunkVectorInfo::TemplatedGetSelVector(transaction_t start_time, transacti } return count; } - if (!AnyDeleted()) { - // have to check inserted flag + case DeleteIdState::MASKED: { + // every deleted row shares mask_delete_id and alive rows are NOT_DELETED_ID (never deleted), so the + // delete decision is a single constant for the whole vector + const bool masked_deleted = DELETE_OP::IsDeleted(start_time, transaction_id, mask_delete_id); + if (HasConstantInsertionId()) { + if (!INSERT_OP::UseInsertedVersion(start_time, transaction_id, ConstantInsertId())) { + return 0; + } + if (!masked_deleted) { + // the delete is not visible to this transaction - every row is visible + return max_count; + } + // only the alive (mask-invalid) rows are visible + if (!sel_vector) { + return max_count - deleted_mask.CountValid(max_count); + } + // scan the mask a word at a time: skip fully-deleted words, take fully-alive words wholesale, + // only extract bits for mixed words + idx_t count = 0; + const idx_t entry_count = ValidityMask::EntryCount(max_count); + for (idx_t entry_idx = 0; entry_idx < entry_count; entry_idx++) { + auto entry = deleted_mask.GetValidityEntry(entry_idx); + if (ValidityMask::AllValid(entry)) { + // every row in this word is deleted - skip + continue; + } + const idx_t base = entry_idx * ValidityMask::BITS_PER_VALUE; + const idx_t entry_end = MinValue(base + ValidityMask::BITS_PER_VALUE, max_count); + if (ValidityMask::NoneValid(entry)) { + // every row in this word is alive - select them all + for (idx_t i = base; i < entry_end; i++) { + sel_vector->set_index(count++, i); + } + continue; + } + for (idx_t i = base; i < entry_end; i++) { + if (!ValidityMask::RowIsValid(entry, i - base)) { + sel_vector->set_index(count++, i); + } + } + } + return count; + } + // per-row insert ids: the mask cannot collapse the insert check, but the delete decision is still + // the constant masked_deleted auto insert_segment = allocator.GetHandle(GetInsertedPointer()); auto inserted = insert_segment.GetPtr(); - idx_t count = 0; for (idx_t i = 0; i < max_count; i++) { if (!INSERT_OP::UseInsertedVersion(start_time, transaction_id, inserted[i])) { continue; } + if (masked_deleted && deleted_mask.RowIsValid(i)) { + // the row is deleted and the delete is visible to this transaction + continue; + } if (sel_vector) { sel_vector->set_index(count, i); } @@ -236,27 +172,51 @@ idx_t ChunkVectorInfo::TemplatedGetSelVector(transaction_t start_time, transacti } return count; } + case DeleteIdState::ARRAY: { + if (HasConstantInsertionId()) { + if (!INSERT_OP::UseInsertedVersion(start_time, transaction_id, ConstantInsertId())) { + return 0; + } + // have to check deleted flag + idx_t count = 0; + auto segment = allocator.GetHandle(GetDeletedPointer()); + auto deleted = segment.GetPtr(); + for (idx_t i = 0; i < max_count; i++) { + if (DELETE_OP::IsDeleted(start_time, transaction_id, deleted[i])) { + continue; + } + if (sel_vector) { + sel_vector->set_index(count, i); + } + count++; + } + return count; + } - idx_t count = 0; - // have to check both flags - auto insert_segment = allocator.GetHandle(GetInsertedPointer()); - auto inserted = insert_segment.GetPtr(); + idx_t count = 0; + // have to check both flags + auto insert_segment = allocator.GetHandle(GetInsertedPointer()); + auto inserted = insert_segment.GetPtr(); - auto delete_segment = allocator.GetHandle(GetDeletedPointer()); - auto deleted = delete_segment.GetPtr(); - for (idx_t i = 0; i < max_count; i++) { - if (!INSERT_OP::UseInsertedVersion(start_time, transaction_id, inserted[i])) { - continue; - } - if (DELETE_OP::IsDeleted(start_time, transaction_id, deleted[i])) { - continue; - } - if (sel_vector) { - sel_vector->set_index(count, i); + auto delete_segment = allocator.GetHandle(GetDeletedPointer()); + auto deleted = delete_segment.GetPtr(); + for (idx_t i = 0; i < max_count; i++) { + if (!INSERT_OP::UseInsertedVersion(start_time, transaction_id, inserted[i])) { + continue; + } + if (DELETE_OP::IsDeleted(start_time, transaction_id, deleted[i])) { + continue; + } + if (sel_vector) { + sel_vector->set_index(count, i); + } + count++; } - count++; + return count; + } + default: + throw InternalException("Unknown DeleteIdState in TemplatedGetSelVector"); } - return count; } idx_t ChunkVectorInfo::GetSelVector(ScanOptions options, optional_ptr sel_vector, @@ -281,6 +241,10 @@ idx_t ChunkVectorInfo::GetSelVector(ScanOptions options, optional_ptr( transaction.start_time, transaction.transaction_id, sel_vector, max_count); } + if (options.delete_type == DeletedScanType::INCLUDE_ALL_DELETED) { + // include all rows + return max_count; + } if (options.delete_type == DeletedScanType::OMIT_COMMITTED_DELETES) { return TemplatedGetSelVector( transaction.start_time, transaction.transaction_id, sel_vector, max_count); @@ -291,7 +255,6 @@ idx_t ChunkVectorInfo::GetSelVector(ScanOptions options, optional_ptr(); fetch_insert_id = inserted[row]; } - if (!AnyDeleted()) { - fetch_deleted_id = NOT_DELETED_ID; - } else { + transaction_t fetch_deleted_id; + switch (delete_state) { + case DeleteIdState::CONSTANT: + fetch_deleted_id = ConstantDeleteId(); + break; + case DeleteIdState::MASKED: + fetch_deleted_id = deleted_mask.RowIsValid(row) ? mask_delete_id : NOT_DELETED_ID; + break; + case DeleteIdState::ARRAY: { auto delete_segment = allocator.GetHandle(GetDeletedPointer()); - auto deleted = delete_segment.GetPtr(); - fetch_deleted_id = deleted[row]; + fetch_deleted_id = delete_segment.GetPtr()[row]; + break; + } + default: + throw InternalException("Unknown DeleteIdState in Fetch"); } return UseVersion(transaction, fetch_insert_id) && !UseVersion(transaction, fetch_deleted_id); @@ -318,8 +290,9 @@ IndexPointer ChunkVectorInfo::GetInsertedPointer() const { } IndexPointer ChunkVectorInfo::GetDeletedPointer() const { - if (!AnyDeleted()) { - throw InternalException("ChunkVectorInfo: deleted id requested but deletions were not initialized"); + if (delete_state != DeleteIdState::ARRAY) { + throw InternalException( + "ChunkVectorInfo: deleted id array requested but delete side is not in the ARRAY state"); } return deleted_data; } @@ -340,19 +313,100 @@ IndexPointer ChunkVectorInfo::GetInitializedInsertedPointer() { } IndexPointer ChunkVectorInfo::GetInitializedDeletedPointer() { - if (!AnyDeleted()) { + if (delete_state == DeleteIdState::MASKED) { + // re-materialize the per-row array so callers can write into it + DecompressDeleteMask(); + } + if (HasConstantDeleteId()) { + transaction_t constant_id = ConstantDeleteId(); + deleted_data = allocator.New(); deleted_data.SetMetadata(1); auto segment = allocator.GetHandle(deleted_data); auto deleted = segment.GetPtr(); for (idx_t i = 0; i < STANDARD_VECTOR_SIZE; i++) { - deleted[i] = NOT_DELETED_ID; + deleted[i] = constant_id; } + delete_state = DeleteIdState::ARRAY; } return deleted_data; } +void ChunkVectorInfo::FreeDeleteData() { + if (delete_state == DeleteIdState::ARRAY) { + allocator.Free(deleted_data); + deleted_data = IndexPointer(); + } + deleted_mask.Reset(); + delete_state = DeleteIdState::CONSTANT; +} + +void ChunkVectorInfo::CompressDeleteToMask(transaction_t mask_id) { + D_ASSERT(delete_state == DeleteIdState::ARRAY); + // the mask can only carry a single committed id shared by every deleted row + D_ASSERT(mask_id < TRANSACTION_ID_START); + // start all-valid (== all deleted), then mark the alive rows invalid + deleted_mask.Initialize(STANDARD_VECTOR_SIZE); + { + auto segment = allocator.GetHandle(deleted_data); + auto deleted = segment.GetPtr(); + for (idx_t i = 0; i < STANDARD_VECTOR_SIZE; i++) { + if (deleted[i] == NOT_DELETED_ID) { + deleted_mask.SetInvalid(i); + } + } + } // release the read handle before freeing the buffer + allocator.Free(deleted_data); + deleted_data = IndexPointer(); + mask_delete_id = mask_id; + delete_state = DeleteIdState::MASKED; +} + +void ChunkVectorInfo::DecompressDeleteMask() { + D_ASSERT(delete_state == DeleteIdState::MASKED); + // re-materialize the per-row array: deleted rows == mask_delete_id, alive rows == NOT_DELETED_ID + deleted_data = allocator.New(); + deleted_data.SetMetadata(1); + { + auto segment = allocator.GetHandle(deleted_data); + auto deleted = segment.GetPtr(); + for (idx_t i = 0; i < STANDARD_VECTOR_SIZE; i++) { + deleted[i] = deleted_mask.RowIsValid(i) ? mask_delete_id : NOT_DELETED_ID; + } + } + deleted_mask.Reset(); + delete_state = DeleteIdState::ARRAY; + recheck_compression = true; +} + +static bool DeletesEntireVector(const row_t rows[], idx_t count) { + D_ASSERT(count == STANDARD_VECTOR_SIZE); + for (idx_t i = 0; i < count; i++) { + if (rows[i] != row_t(i)) { + return false; + } + } + return true; +} + idx_t ChunkVectorInfo::Delete(transaction_t transaction_id, row_t rows[], idx_t count) { + if (HasConstantDeleteId() && ConstantDeleteId() != NOT_DELETED_ID) { + // all rows in this vector share the same deleted id - the rows we are trying to delete are already deleted + if (ConstantDeleteId() == transaction_id) { + // the rows were deleted by this transaction already - skip + return 0; + } + // the rows were deleted by another transaction - conflict + throw TransactionException("Conflict on tuple deletion!"); + } + if (HasConstantDeleteId() && count == STANDARD_VECTOR_SIZE && DeletesEntireVector(rows, count)) { + // no rows were deleted yet and we are deleting the entire vector + // all rows share the same deleted id - store it as a constant instead of materializing per-row delete ids + constant_delete_id = transaction_id; + return count; + } + // we are materializing / modifying per-row delete ids - re-arm the compression check + recheck_compression = true; auto segment = allocator.GetHandle(GetInitializedDeletedPointer()); auto deleted = segment.GetPtr(); @@ -379,19 +433,165 @@ idx_t ChunkVectorInfo::Delete(transaction_t transaction_id, row_t rows[], idx_t } void ChunkVectorInfo::CommitDelete(transaction_t commit_id, const DeleteInfo &info) { - auto segment = allocator.GetHandle(GetDeletedPointer()); - auto deleted = segment.GetPtr(); + if (info.is_consecutive && info.count == STANDARD_VECTOR_SIZE) { + // the delete covers the entire vector - all rows share the same deleted id + // we can store the deleted id as a constant and free any per-row delete ids + FreeDeleteData(); + constant_delete_id = commit_id; + return; + } + if (HasConstantDeleteId() && ConstantDeleteId() == commit_id) { + // all rows already share this exact deleted id - nothing to do + return; + } + // we are materializing / modifying per-row delete ids - re-arm the compression check + recheck_compression = true; + bool all_equal = true; + { + auto segment = allocator.GetHandle(GetInitializedDeletedPointer()); + auto deleted = segment.GetPtr(); - if (info.is_consecutive) { - for (idx_t i = 0; i < info.count; i++) { - deleted[i] = commit_id; + if (info.is_consecutive) { + for (idx_t i = 0; i < info.count; i++) { + deleted[i] = commit_id; + } + } else { + auto rows = info.GetRows(); + for (idx_t i = 0; i < info.count; i++) { + deleted[rows[i]] = commit_id; + } } - } else { - auto rows = info.GetRows(); - for (idx_t i = 0; i < info.count; i++) { - deleted[rows[i]] = commit_id; + // check if all rows now share the same deleted id + for (idx_t i = 0; i < STANDARD_VECTOR_SIZE; i++) { + if (deleted[i] != commit_id) { + all_equal = false; + break; + } + } + } + if (all_equal) { + // all rows share the same deleted id - compress the per-row delete ids into a constant + FreeDeleteData(); + constant_delete_id = commit_id; + } +} + +void ChunkVectorInfo::VerifyCachedCompressionState() const { +#ifdef DEBUG + if (recheck_compression) { + // armed - the next pass re-derives everything from the ids, there is no cached claim to verify + return; + } + // a disarmed check claims that nothing can compress, now or as older transactions finish, until a + // modification re-arms it. Both conditions below are independent of the lowest active start: + // per-row insert ids all become visible (or are reverted) eventually, so they must already be compressed + D_ASSERT(HasConstantInsertionId()); + if (delete_state != DeleteIdState::CONSTANT) { + // a settled, non-constant delete side can only be the terminal masked state + D_ASSERT(delete_state == DeleteIdState::MASKED); + // the mask must hold at least one deleted row and one alive row: a fully deleted vector + // collapses to a constant, and a vector with no deletes carries no delete-side info + idx_t deleted_rows = deleted_mask.CountValid(STANDARD_VECTOR_SIZE); + D_ASSERT(deleted_rows > 0 && deleted_rows < STANDARD_VECTOR_SIZE); + } +#endif +} + +VersionCompressionResult ChunkVectorInfo::CompressVersionIds(transaction_t lowest_active_start) { + if (!recheck_compression) { + // no ids were modified since this vector last settled - only a further modification + // (which re-arms the check) can make it compressible, so skip re-scanning the ids +#ifdef DEBUG + VerifyCachedCompressionState(); +#endif + return HasConstantDeleteId() && HasConstantInsertionId() ? VersionCompressionResult::FULLY_COMPRESSED + : VersionCompressionResult::SETTLED; + } + bool pending = false; + if (delete_state == DeleteIdState::ARRAY) { + // scan the per-row delete ids to decide how far they can collapse + bool rows_alive = false; + bool deletes_pending = false; + bool deletes_uncommitted = false; + bool deletes_equal = true; + transaction_t max_delete_id = 0; + transaction_t shared_delete_id = NOT_DELETED_ID; + { + auto segment = allocator.GetHandle(GetDeletedPointer()); + auto deleted = segment.GetPtr(); + for (idx_t i = 0; i < STANDARD_VECTOR_SIZE; i++) { + if (deleted[i] == NOT_DELETED_ID) { + // the row is not deleted - the ids cannot fully collapse until it is + rows_alive = true; + continue; + } + if (deleted[i] >= lowest_active_start) { + // deleted, but the delete is not yet visible to all transactions + deletes_pending = true; + if (deleted[i] >= TRANSACTION_ID_START) { + // the delete is not even committed yet - the array must be kept + deletes_uncommitted = true; + } + } else { + max_delete_id = MaxValue(max_delete_id, deleted[i]); + } + // track whether every deleted row shares a single id + if (shared_delete_id == NOT_DELETED_ID) { + shared_delete_id = deleted[i]; + } else if (deleted[i] != shared_delete_id) { + deletes_equal = false; + } + } + } + if (!rows_alive && !deletes_pending) { + // entire vector deleted and visible to all - collapse to a constant + FreeDeleteData(); + constant_delete_id = max_delete_id; + } else if (!rows_alive) { + // entire vector deleted but a delete is still pending - retry next pass + pending = true; + } else if (!deletes_pending) { + // partially deleted, every delete visible to all - compress to a mask (terminal) + CompressDeleteToMask(0); + } else if (deletes_equal && !deletes_uncommitted) { + // partially deleted, every delete committed by the same transaction but not yet visible to + // all - compress to a mask carrying that single committed id (terminal). Older snapshots still + // see the rows via the id comparison, and on reload every id is visible so it becomes 0. + CompressDeleteToMask(shared_delete_id); + } else { + // partially deleted with pending deletes from multiple or uncommitted transactions - retry + pending = true; + } + } + if (!HasConstantInsertionId()) { + // check if all inserts are visible to all active and future transactions + // if so, the per-row insert ids are equivalent to a single constant insert id + bool can_compress = true; + { + auto segment = allocator.GetHandle(GetInsertedPointer()); + auto inserted = segment.GetPtr(); + for (idx_t i = 0; i < STANDARD_VECTOR_SIZE; i++) { + if (inserted[i] >= lowest_active_start) { + // the insert is not yet visible to all transactions + can_compress = false; + break; + } + } } + if (can_compress) { + allocator.Free(inserted_data); + inserted_data = IndexPointer(); + constant_insert_id = 0; + } else { + // insert ids become visible to all transactions (or are reverted) eventually + pending = true; + } + } + recheck_compression = pending; + if (HasConstantDeleteId() && HasConstantInsertionId()) { + return VersionCompressionResult::FULLY_COMPRESSED; } + return pending ? VersionCompressionResult::PENDING : VersionCompressionResult::SETTLED; } void ChunkVectorInfo::Append(idx_t start, idx_t end, transaction_t commit_id) { @@ -405,6 +605,8 @@ void ChunkVectorInfo::Append(idx_t start, idx_t end, transaction_t commit_id) { return; } + // we are materializing / modifying per-row insert ids - re-arm the compression check + recheck_compression = true; auto segment = allocator.GetHandle(GetInitializedInsertedPointer()); auto inserted = segment.GetPtr(); for (idx_t i = start; i < end; i++) { @@ -417,6 +619,8 @@ void ChunkVectorInfo::CommitAppend(transaction_t commit_id, idx_t start, idx_t e constant_insert_id = commit_id; return; } + // we are modifying per-row insert ids - re-arm the compression check + recheck_compression = true; auto segment = allocator.GetHandle(GetInsertedPointer()); auto inserted = segment.GetPtr(); @@ -451,31 +655,60 @@ bool ChunkVectorInfo::Cleanup(transaction_t lowest_transaction) const { } bool ChunkVectorInfo::HasDeletes(transaction_t transaction_id) const { + if (HasConstantInsertionId() && ConstantInsertId() >= TRANSACTION_ID_START) { + // the vector was inserted by a transaction that has not committed yet + // the rows have to be masked as deleted when writing a checkpoint + return true; + } if (!AnyDeleted()) { return false; } if (transaction_id == MAX_TRANSACTION_ID) { return true; } - auto segment = allocator.GetHandle(deleted_data); - auto deleted = segment.GetPtr(); - - for (idx_t i = 0; i < STANDARD_VECTOR_SIZE; i++) { - if (deleted[i] <= transaction_id) { - return true; + switch (delete_state) { + case DeleteIdState::CONSTANT: + return ConstantDeleteId() <= transaction_id; + case DeleteIdState::MASKED: + // AnyDeleted() above guaranteed at least one deleted row; they all share mask_delete_id + return mask_delete_id <= transaction_id; + case DeleteIdState::ARRAY: { + auto segment = allocator.GetHandle(GetDeletedPointer()); + auto deleted = segment.GetPtr(); + for (idx_t i = 0; i < STANDARD_VECTOR_SIZE; i++) { + if (deleted[i] <= transaction_id) { + return true; + } } + return false; + } + default: + throw InternalException("Unknown DeleteIdState in HasDeletes"); } - return false; } bool ChunkVectorInfo::AnyDeleted() const { - return deleted_data.HasMetadata(); + switch (delete_state) { + case DeleteIdState::CONSTANT: + return ConstantDeleteId() != NOT_DELETED_ID; + case DeleteIdState::MASKED: + // a masked vector always contains at least one deleted row + return true; + case DeleteIdState::ARRAY: + return true; + default: + throw InternalException("Unknown DeleteIdState in AnyDeleted"); + } } bool ChunkVectorInfo::HasConstantInsertionId() const { return !inserted_data.HasMetadata(); } +bool ChunkVectorInfo::HasConstantDeleteId() const { + return delete_state == DeleteIdState::CONSTANT; +} + string ChunkVectorInfo::ToString(idx_t max_count) const { string result; result += "Vector [Count: " + to_string(max_count); @@ -495,6 +728,41 @@ string ChunkVectorInfo::ToString(idx_t max_count) const { } result += "]"; } + switch (delete_state) { + case DeleteIdState::CONSTANT: + if (ConstantDeleteId() != NOT_DELETED_ID) { + result += ", Delete Id: " + to_string(constant_delete_id); + } + break; + case DeleteIdState::MASKED: { + result += ", Delete Id: " + to_string(mask_delete_id); + result += ", Deleted (mask): ["; + for (idx_t idx = 0; idx < max_count; idx++) { + if (idx > 0) { + result += ", "; + } + result += deleted_mask.RowIsValid(idx) ? "1" : "0"; + } + result += "]"; + break; + } + case DeleteIdState::ARRAY: { + result += ", Delete Ids: ["; + auto segment = allocator.GetHandle(GetDeletedPointer()); + auto deleted = segment.GetPtr(); + + for (idx_t idx = 0; idx < max_count; idx++) { + if (idx > 0) { + result += ", "; + } + result += to_string(deleted[idx]); + } + result += "]"; + break; + } + default: + throw InternalException("Unknown DeleteIdState in ToString"); + } result += "]"; return result; } @@ -506,6 +774,13 @@ transaction_t ChunkVectorInfo::ConstantInsertId() const { return constant_insert_id; } +transaction_t ChunkVectorInfo::ConstantDeleteId() const { + if (!HasConstantDeleteId()) { + throw InternalException("ConstantDeleteId() called but vector info does not have a constant delete id"); + } + return constant_delete_id; +} + void ChunkVectorInfo::Write(WriteStream &writer, transaction_t checkpoint_id) const { SelectionVector sel(STANDARD_VECTOR_SIZE); transaction_t start_time = checkpoint_id == MAX_TRANSACTION_ID ? TRANSACTION_ID_START - 1 : checkpoint_id + 1; @@ -523,7 +798,7 @@ void ChunkVectorInfo::Write(WriteStream &writer, transaction_t checkpoint_id) co return; } // write a boolean vector - ChunkInfo::Write(writer, checkpoint_id); + writer.Write(ChunkInfoType::VECTOR_INFO); writer.Write(start); ValidityMask mask(STANDARD_VECTOR_SIZE); mask.Initialize(STANDARD_VECTOR_SIZE); @@ -533,20 +808,46 @@ void ChunkVectorInfo::Write(WriteStream &writer, transaction_t checkpoint_id) co mask.Write(writer, STANDARD_VECTOR_SIZE); } -unique_ptr ChunkVectorInfo::Read(FixedSizeAllocator &allocator, ReadStream &reader) { - auto start = reader.Read(); - auto result = make_uniq(allocator, start); - ValidityMask mask; - mask.Read(reader, STANDARD_VECTOR_SIZE); - - auto segment = allocator.GetHandle(result->GetInitializedDeletedPointer()); - auto deleted = segment.GetPtr(); - for (idx_t i = 0; i < STANDARD_VECTOR_SIZE; i++) { - if (mask.RowIsValid(i)) { - deleted[i] = 0; +unique_ptr ChunkVectorInfo::Read(FixedSizeAllocator &allocator, ReadStream &reader) { + auto type = reader.Read(); + switch (type) { + case ChunkInfoType::EMPTY_INFO: + // no rows are deleted + return nullptr; + case ChunkInfoType::CONSTANT_INFO: { + // a fully deleted vector - the constant insert and delete ids of 0 are visible to all transactions + auto start = reader.Read(); + auto result = make_uniq(allocator, start, 0, 0); + // both ids are constant - there is nothing left to compress + result->recheck_compression = false; + return result; + } + case ChunkInfoType::VECTOR_INFO: { + // a partially deleted vector - the deleted rows are stored as a boolean mask, all committed and + // visible to every transaction. The on-disk orientation (valid == deleted) matches the in-memory + // MASKED state, so load it straight into deleted_mask without materializing a per-row array. + auto start = reader.Read(); + auto result = make_uniq(allocator, start); + result->deleted_mask.Read(reader, STANDARD_VECTOR_SIZE); + // Write only emits VECTOR_INFO for a partial delete: an all-deleted vector becomes + // CONSTANT_INFO and an undeleted one becomes EMPTY_INFO, so the mask must have at least one + // deleted (valid) and one alive (invalid) bit - never all-valid, never all-invalid. + if (result->deleted_mask.CheckAllValid(STANDARD_VECTOR_SIZE) || + result->deleted_mask.CheckAllInvalid(STANDARD_VECTOR_SIZE)) { + throw SerializationException( + "Partial-delete vector info mask marks either all rows deleted or all rows alive, but a " + "VECTOR_INFO block must always encode a partial delete. The database file may be corrupted."); } + result->delete_state = DeleteIdState::MASKED; + // on-disk deletes are all committed and visible to every transaction - the shared id is 0 + result->mask_delete_id = 0; + // every id is already visible to all transactions - nothing left to compress + result->recheck_compression = false; + return result; + } + default: + throw SerializationException("Could not deserialize Chunk Info Type: unrecognized type"); } - return std::move(result); } } // namespace duckdb diff --git a/src/duckdb/src/storage/table/row_group.cpp b/src/duckdb/src/storage/table/row_group.cpp index f55126348..c2ee81d24 100644 --- a/src/duckdb/src/storage/table/row_group.cpp +++ b/src/duckdb/src/storage/table/row_group.cpp @@ -1651,6 +1651,18 @@ PersistentRowGroupData RowGroup::SerializeRowGroupInfo(idx_t row_group_start) co return result; } +void RowGroup::CompressVersionInfo(transaction_t lowest_active_start) { + if (HasUnloadedDeletes()) { + // deletes were not loaded - they are still stored in their compact serialized form + return; + } + auto vinfo = GetVersionInfo(); + if (!vinfo) { + return; + } + vinfo->CompressVersionIds(lowest_active_start); +} + vector RowGroup::CheckpointDeletes(RowGroupWriter &writer) { if (HasUnloadedDeletes()) { // deletes were not loaded so they cannot be changed diff --git a/src/duckdb/src/storage/table/row_group_collection.cpp b/src/duckdb/src/storage/table/row_group_collection.cpp index c80f9974d..2c3f17de6 100644 --- a/src/duckdb/src/storage/table/row_group_collection.cpp +++ b/src/duckdb/src/storage/table/row_group_collection.cpp @@ -21,6 +21,7 @@ #include "duckdb/storage/table_storage_info.hpp" #include "duckdb/main/settings.hpp" #include "duckdb/execution/index/art/art.hpp" +#include "duckdb/transaction/duck_transaction_manager.hpp" #include "duckdb/common/type_visitor.hpp" namespace duckdb { @@ -1538,6 +1539,8 @@ void RowGroupCollection::Checkpoint(TableDataWriter &writer, TableStatistics &gl writer.SetRowIdsChanged(); } + auto &transaction_manager = DuckTransactionManager::Get(GetAttached()); + auto lowest_active_start = transaction_manager.LowestActiveStart(); try { // schedule tasks idx_t total_vacuum_tasks = 0; @@ -1562,6 +1565,8 @@ void RowGroupCollection::Checkpoint(TableDataWriter &writer, TableStatistics &gl if (!RefersToSameObject(row_group.GetCollection(), *this)) { throw InternalException("RowGroup Vacuum - row group collection of row group changed"); } + // the row group is kept as-is: try to compress its version information + row_group.CompressVersionInfo(lowest_active_start); if (writer.GetCheckpointOptions().type != CheckpointType::VACUUM_ONLY) { DUCKDB_LOG(checkpoint_state.writer.GetDatabase(), CheckpointLogType, GetAttached(), *info, segment_idx, row_group, vacuum_state.row_start); diff --git a/src/duckdb/src/storage/table/row_version_manager.cpp b/src/duckdb/src/storage/table/row_version_manager.cpp index 78b9a933e..188b13fc4 100644 --- a/src/duckdb/src/storage/table/row_version_manager.cpp +++ b/src/duckdb/src/storage/table/row_version_manager.cpp @@ -28,7 +28,7 @@ idx_t RowVersionManager::GetCommittedDeletedCount(idx_t count) { return deleted_count; } -optional_ptr RowVersionManager::GetChunkInfo(idx_t vector_idx) { +optional_ptr RowVersionManager::GetChunkInfo(idx_t vector_idx) { if (vector_idx >= vector_info.size()) { return nullptr; } @@ -68,6 +68,7 @@ void RowVersionManager::FillVectorInfo(idx_t vector_idx) { void RowVersionManager::AppendVersionInfo(TransactionData transaction, idx_t count, idx_t row_group_start, idx_t row_group_end) { lock_guard lock(version_lock); + needs_compression_check = true; idx_t start_vector_idx = row_group_start / STANDARD_VECTOR_SIZE; idx_t end_vector_idx = (row_group_end - 1) / STANDARD_VECTOR_SIZE; @@ -81,27 +82,16 @@ void RowVersionManager::AppendVersionInfo(TransactionData transaction, idx_t cou idx_t vector_end = vector_idx == end_vector_idx ? row_group_end - end_vector_idx * STANDARD_VECTOR_SIZE : STANDARD_VECTOR_SIZE; if (vector_start == 0 && vector_end == STANDARD_VECTOR_SIZE) { - // entire vector is encapsulated by append: append a single constant - auto constant_info = make_uniq(vector_idx * STANDARD_VECTOR_SIZE); - constant_info->insert_id = transaction.transaction_id; - constant_info->delete_id = NOT_DELETED_ID; - vector_info[vector_idx] = std::move(constant_info); + // entire vector is encapsulated by append: store a single constant insert id + vector_info[vector_idx] = + make_uniq(allocator, vector_idx * STANDARD_VECTOR_SIZE, transaction.transaction_id); } else { // part of a vector is encapsulated: append to that part - optional_ptr new_info; if (!vector_info[vector_idx]) { // first time appending to this vector: create new info - auto insert_info = make_uniq(allocator, vector_idx * STANDARD_VECTOR_SIZE); - new_info = insert_info.get(); - vector_info[vector_idx] = std::move(insert_info); - } else if (vector_info[vector_idx]->type == ChunkInfoType::VECTOR_INFO) { - // use existing vector - new_info = &vector_info[vector_idx]->Cast(); - } else { - throw InternalException("Error in RowVersionManager::AppendVersionInfo - expected either a " - "ChunkVectorInfo or no version info"); + vector_info[vector_idx] = make_uniq(allocator, vector_idx * STANDARD_VECTOR_SIZE); } - new_info->Append(vector_start, vector_end, transaction.transaction_id); + vector_info[vector_idx]->Append(vector_start, vector_end, transaction.transaction_id); } } } @@ -167,29 +157,55 @@ ChunkVectorInfo &RowVersionManager::GetVectorInfo(idx_t vector_idx) { if (!vector_info[vector_idx]) { // no info yet: create it vector_info[vector_idx] = make_uniq(allocator, vector_idx * STANDARD_VECTOR_SIZE); - } else if (vector_info[vector_idx]->type == ChunkInfoType::CONSTANT_INFO) { - auto &constant = vector_info[vector_idx]->Cast(); - // info exists but it's a constant info: convert to a vector info - auto new_info = make_uniq(allocator, vector_idx * STANDARD_VECTOR_SIZE, constant.insert_id); - vector_info[vector_idx] = std::move(new_info); } - D_ASSERT(vector_info[vector_idx]->type == ChunkInfoType::VECTOR_INFO); - return vector_info[vector_idx]->Cast(); + return *vector_info[vector_idx]; } idx_t RowVersionManager::DeleteRows(idx_t vector_idx, transaction_t transaction_id, row_t rows[], idx_t count) { lock_guard lock(version_lock); + needs_compression_check = true; return GetVectorInfo(vector_idx).Delete(transaction_id, rows, count); } void RowVersionManager::CommitDelete(idx_t vector_idx, transaction_t commit_id, const DeleteInfo &info) { lock_guard lock(version_lock); + needs_compression_check = true; if (!uncheckpointed_delete_commit.IsValid() || commit_id > uncheckpointed_delete_commit.GetIndex()) { uncheckpointed_delete_commit = commit_id; } GetVectorInfo(vector_idx).CommitDelete(commit_id, info); } +void RowVersionManager::CompressVersionIds(transaction_t lowest_active_start) { + lock_guard lock(version_lock); + if (!needs_compression_check) { + // no version ids were modified since the last pass, and the last pass left nothing + // that could still become compressible - nothing to do +#ifdef DEBUG + // a cleared manager-level check implies every vector's check is disarmed - verify the + // per-vector claims that the skipped pass relies on + for (auto &info : vector_info) { + if (info) { + D_ASSERT(!info->RecheckCompression()); + info->VerifyCachedCompressionState(); + } + } +#endif + return; + } + bool pending = false; + for (auto &info : vector_info) { + if (info && info->CompressVersionIds(lowest_active_start) == VersionCompressionResult::PENDING) { + // some ids can still compress once the lowest active start advances - check again next pass + pending = true; + } + } + needs_compression_check = pending; + // compression frees the per-row id segments - release any buffers that are now empty + // (Free keeps the last buffer with free space alive to prevent buffer creation fluctuation) + allocator.RemoveEmptyBuffers(); +} + vector RowVersionManager::Checkpoint(RowGroupWriter &writer) { lock_guard lock(version_lock); auto &manager = *writer.GetMetadataManager(); @@ -201,8 +217,8 @@ vector RowVersionManager::Checkpoint(RowGroupWriter &writer) { // return the current set of pointers return storage_pointers; } - // first count how many ChunkInfo's we need to deserialize - vector>> to_serialize; + // first count how many chunk infos we need to serialize + vector>> to_serialize; for (idx_t vector_idx = 0; vector_idx < vector_info.size(); vector_idx++) { auto chunk_info = vector_info[vector_idx].get(); if (!chunk_info) { @@ -255,7 +271,13 @@ shared_ptr RowVersionManager::Deserialize(MetaBlockPointer de } version_info->FillVectorInfo(vector_index); - version_info->vector_info[vector_index] = ChunkInfo::Read(version_info->GetAllocator(), source); + auto info = ChunkVectorInfo::Read(version_info->GetAllocator(), source); + if (info && info->RecheckCompression()) { + // with the current storage format deserialized ids are always settled, but Read + // derives this from the deserialized content - follow its verdict + version_info->needs_compression_check = true; + } + version_info->vector_info[vector_index] = std::move(info); } version_info->uncheckpointed_delete_commit = optional_idx(); return version_info;