From bd22c82edaa05e1d87d73e8ac0e43391a94163a3 Mon Sep 17 00:00:00 2001 From: Andrey Rachitskiy Date: Sat, 22 Aug 2026 02:19:08 +0500 Subject: [PATCH] Pack short varlenas when flattening expanded objects into tuples heap_form_tuple already converts ordinary 4-byte-header varlenas to the 1-byte short header when the value is small enough and the attribute is packable. The expanded-object path flattened via EOH_flatten_into and wrote the flattener's mandatory 4-byte-header result directly, so small arrays (and other expanded types) inserted from PL/pgSQL kept a 4-byte header and wasted three bytes per value compared with a plain INSERT of the same datum. After flattening, apply the same short-header conversion when the flat size fits. Flatten into a temporary palloc buffer first because flatten_into expects a maxaligned destination while short packing does not. Do not pfree it, since CurrentMemoryContext may be a bump allocator. The plpgsql regress forces a generic plan so the parameter remains an expanded object through to heap_form_tuple. A custom plan flattens via datumCopy during planning and would not exercise the EXTERNAL_EXPANDED arms. Author: Andrey Rachitskiy Reported-by: anliuan <17801022106@163.com> Discussion: https://www.postgresql.org/message-id/19636-c44abe40ca7e2c4d@postgresql.org --- src/backend/access/common/heaptuple.c | 61 ++++++++++++++++++++++----- src/test/regress/expected/plpgsql.out | 28 ++++++++++++ src/test/regress/sql/plpgsql.sql | 18 ++++++++ 3 files changed, 97 insertions(+), 10 deletions(-) diff --git a/src/backend/access/common/heaptuple.c b/src/backend/access/common/heaptuple.c index f30346469ed..d1f5a09a286 100644 --- a/src/backend/access/common/heaptuple.c +++ b/src/backend/access/common/heaptuple.c @@ -87,6 +87,16 @@ #define COMPACT_ATTR_IS_PACKABLE(att) \ ((att)->attlen == -1 && (att)->attispackable) +/* + * Size rule matching VARATT_CAN_MAKE_SHORT, for use with EOH_get_flat_size() + * before flattening. VARATT_CAN_MAKE_SHORT needs a 4-byte-header (4B_U) + * varlena. An expanded-object toast pointer is not one. + */ +#define VARATT_SIZE_CAN_MAKE_SHORT(len) \ + ((len) - VARHDRSZ + VARHDRSZ_SHORT <= VARATT_SHORT_MAX) +#define VARATT_SHORT_SIZE_FROM_4B(len) \ + ((len) - VARHDRSZ + VARHDRSZ_SHORT) + /* * Setup for caching pass-by-ref missing attributes in a way that survives * tupleDesc destruction. @@ -248,11 +258,20 @@ heap_compute_data_size(TupleDesc tupleDesc, VARATT_IS_EXTERNAL_EXPANDED(DatumGetPointer(val))) { /* - * we want to flatten the expanded value so that the constructed - * tuple doesn't depend on it + * Expanded objects will be flattened into a 4-byte-header + * varlena. If that fits a short header, account for packing + * (no alignment), matching fill_val. */ - data_length = att_nominal_alignby(data_length, atti->attalignby); - data_length += EOH_get_flat_size(DatumGetEOHP(val)); + Size flat_size = EOH_get_flat_size(DatumGetEOHP(val)); + + if (COMPACT_ATTR_IS_PACKABLE(atti) && + VARATT_SIZE_CAN_MAKE_SHORT(flat_size)) + data_length += VARATT_SHORT_SIZE_FROM_4B(flat_size); + else + { + data_length = att_nominal_alignby(data_length, atti->attalignby); + data_length += flat_size; + } } else { @@ -329,14 +348,36 @@ fill_val(CompactAttribute *att, if (VARATT_IS_EXTERNAL_EXPANDED(val)) { /* - * we want to flatten the expanded value so that the - * constructed tuple doesn't depend on it + * Flatten so the tuple doesn't depend on the expanded + * object. Flatteners produce a 4-byte-header varlena. + * Convert to short header when possible. */ ExpandedObjectHeader *eoh = DatumGetEOHP(datum); - - data = (char *) att_nominal_alignby(data, att->attalignby); - data_length = EOH_get_flat_size(eoh); - EOH_flatten_into(eoh, data, data_length); + Size flat_size = EOH_get_flat_size(eoh); + + if (COMPACT_ATTR_IS_PACKABLE(att) && + VARATT_SIZE_CAN_MAKE_SHORT(flat_size)) + { + char *tmp; + + /* + * Flatten into a temp buffer: EOH_flatten_into needs a + * maxaligned destination, short packing does not. + * Do not pfree(tmp); CurrentMemoryContext may be a bump + * allocator. + */ + tmp = palloc(flat_size); + EOH_flatten_into(eoh, tmp, flat_size); + data_length = VARATT_CONVERTED_SHORT_SIZE(tmp); + SET_VARSIZE_SHORT(data, data_length); + memcpy(data + 1, VARDATA(tmp), data_length - 1); + } + else + { + data = (char *) att_nominal_alignby(data, att->attalignby); + data_length = flat_size; + EOH_flatten_into(eoh, data, data_length); + } } else { diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out index b37b2abaf80..69fbbc87ac4 100644 --- a/src/test/regress/expected/plpgsql.out +++ b/src/test/regress/expected/plpgsql.out @@ -5161,6 +5161,32 @@ begin raise notice 'a = %', a; end$$; NOTICE: a = {1,2,3} +-- Inserting an expanded array should use the same short-varlena packing as a +-- plain INSERT of the equivalent flat value. +set plan_cache_mode = force_generic_plan; +create temp table expanded_short_pack(a text[]); +insert into expanded_short_pack values ('{aaaaa}'); +create function insert_expanded_short_pack(racl text[]) returns void as $$ +begin + insert into expanded_short_pack values (racl); +end; +$$ language plpgsql; +select insert_expanded_short_pack('{aaaaa}'); + insert_expanded_short_pack +---------------------------- + +(1 row) + +select pg_column_size(a) from expanded_short_pack; + pg_column_size +---------------- + 33 + 33 +(2 rows) + +drop function insert_expanded_short_pack(text[]); +drop table expanded_short_pack; +reset plan_cache_mode; -- -- Test access to call stack -- diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql index ae6b67e3e22..cadb8398375 100644 --- a/src/test/regress/sql/plpgsql.sql +++ b/src/test/regress/sql/plpgsql.sql @@ -4160,6 +4160,22 @@ begin raise notice 'a = %', a; end$$; +-- Inserting an expanded array should use the same short-varlena packing as a +-- plain INSERT of the equivalent flat value. +set plan_cache_mode = force_generic_plan; +create temp table expanded_short_pack(a text[]); +insert into expanded_short_pack values ('{aaaaa}'); +create function insert_expanded_short_pack(racl text[]) returns void as $$ +begin + insert into expanded_short_pack values (racl); +end; +$$ language plpgsql; +select insert_expanded_short_pack('{aaaaa}'); +select pg_column_size(a) from expanded_short_pack; +drop function insert_expanded_short_pack(text[]); +drop table expanded_short_pack; +reset plan_cache_mode; + -- -- Test access to call stack -- 2.53.0