0001-Truncate-trailing-null-attributes-from-heap-rows-to-.patch
application/octet-stream
Filename: 0001-Truncate-trailing-null-attributes-from-heap-rows-to-.patch
Type: application/octet-stream
Part: 0
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch 0001
Subject: Truncate trailing null attributes from heap rows to reduce the size of the row bitmap.
| File | + | − |
|---|---|---|
| src/backend/access/common/heaptuple.c | 44 | 20 |
| src/backend/access/common/indextuple.c | 9 | 1 |
| src/backend/access/heap/tuptoaster.c | 14 | 6 |
| src/include/access/htup.h | 1 | 1 |
| src/test/regress/expected/trailing_nulls.out | 210 | 0 |
| src/test/regress/parallel_schedule | 1 | 1 |
| src/test/regress/sql/trailing_nulls.sql | 166 | 0 |
From 319a81beafad444cf352040942123f0aed1dc392 Mon Sep 17 00:00:00 2001
From: Jamie Martin <jameison.martin@yahoo.com>
Date: Wed, 11 Apr 2012 18:12:42 -0700
Subject: [PATCH] Truncate trailing null attributes from heap rows to reduce the size of the row bitmap.
---
src/backend/access/common/heaptuple.c | 64 ++++++---
src/backend/access/common/indextuple.c | 10 +-
src/backend/access/heap/tuptoaster.c | 20 ++-
src/include/access/htup.h | 2 +-
src/test/regress/expected/trailing_nulls.out | 210 ++++++++++++++++++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/trailing_nulls.sql | 166 ++++++++++++++++++++
7 files changed, 445 insertions(+), 29 deletions(-)
create mode 100644 src/test/regress/expected/trailing_nulls.out
create mode 100644 src/test/regress/sql/trailing_nulls.sql
diff --git a/src/backend/access/common/heaptuple.c b/src/backend/access/common/heaptuple.c
index 034dfe5..8975a12 100644
--- a/src/backend/access/common/heaptuple.c
+++ b/src/backend/access/common/heaptuple.c
@@ -133,22 +133,24 @@ void
heap_fill_tuple(TupleDesc tupleDesc,
Datum *values, bool *isnull,
char *data, Size data_size,
- uint16 *infomask, bits8 *bit)
+ uint16 *infomask, bits8 *bit,
+ int numberOfAttributes)
{
bits8 *bitP;
int bitmask;
int i;
- int numberOfAttributes = tupleDesc->natts;
Form_pg_attribute *att = tupleDesc->attrs;
#ifdef USE_ASSERT_CHECKING
char *start = data;
#endif
+ *infomask &= ~(HEAP_HASNULL | HEAP_HASVARWIDTH | HEAP_HASEXTERNAL);
if (bit != NULL)
{
bitP = &bit[-1];
bitmask = HIGHBIT;
+ *infomask |= HEAP_HASNULL;
}
else
{
@@ -157,7 +159,6 @@ heap_fill_tuple(TupleDesc tupleDesc,
bitmask = 0;
}
- *infomask &= ~(HEAP_HASNULL | HEAP_HASVARWIDTH | HEAP_HASEXTERNAL);
for (i = 0; i < numberOfAttributes; i++)
{
@@ -176,7 +177,6 @@ heap_fill_tuple(TupleDesc tupleDesc,
if (isnull[i])
{
- *infomask |= HEAP_HASNULL;
continue;
}
@@ -636,14 +636,20 @@ heap_form_tuple(TupleDesc tupleDescriptor,
int hoff;
bool hasnull = false;
Form_pg_attribute *att = tupleDescriptor->attrs;
- int numberOfAttributes = tupleDescriptor->natts;
+ int maxNumberOfAttributes = tupleDescriptor->natts;
int i;
+ int lastNonNullAttribute = -1;
+ int numberOfAttributes; /* The actual number of attributes for this particular row.
+ * trailing nulls aren't stored so this may be smaller than
+ * maxNumberOfAttributes
+ */
+
- if (numberOfAttributes > MaxTupleAttributeNumber)
+ if (maxNumberOfAttributes > MaxTupleAttributeNumber)
ereport(ERROR,
(errcode(ERRCODE_TOO_MANY_COLUMNS),
errmsg("number of columns (%d) exceeds limit (%d)",
- numberOfAttributes, MaxTupleAttributeNumber)));
+ maxNumberOfAttributes, MaxTupleAttributeNumber)));
/*
* Check for nulls and embedded tuples; expand any toasted attributes in
@@ -656,20 +662,26 @@ heap_form_tuple(TupleDesc tupleDescriptor,
* if an attribute is already toasted, it must have been sent to disk
* already and so cannot contain toasted attributes.
*/
- for (i = 0; i < numberOfAttributes; i++)
+ for (i = 0; i < maxNumberOfAttributes; i++)
{
if (isnull[i])
hasnull = true;
- else if (att[i]->attlen == -1 &&
+ else
+ {
+ lastNonNullAttribute = i;
+
+ if (att[i]->attlen == -1 &&
att[i]->attalign == 'd' &&
att[i]->attndims == 0 &&
!VARATT_IS_EXTENDED(DatumGetPointer(values[i])))
- {
- values[i] = toast_flatten_tuple_attribute(values[i],
+ {
+ values[i] = toast_flatten_tuple_attribute(values[i],
att[i]->atttypid,
att[i]->atttypmod);
+ }
}
}
+ numberOfAttributes = lastNonNullAttribute + 1;
/*
* Determine total space needed
@@ -719,7 +731,8 @@ heap_form_tuple(TupleDesc tupleDescriptor,
(char *) td + hoff,
data_len,
&td->t_infomask,
- (hasnull ? td->t_bits : NULL));
+ (hasnull ? td->t_bits : NULL),
+ numberOfAttributes);
return tuple;
}
@@ -1390,14 +1403,19 @@ heap_form_minimal_tuple(TupleDesc tupleDescriptor,
int hoff;
bool hasnull = false;
Form_pg_attribute *att = tupleDescriptor->attrs;
- int numberOfAttributes = tupleDescriptor->natts;
+ int maxNumberOfAttributes = tupleDescriptor->natts;
int i;
+ int lastNonNullAttribute = -1;
+ int numberOfAttributes; /* The actual number of attributes for this particular row.
+ * trailing nulls aren't stored so this may be smaller than
+ * maxNumberOfAttributes
+ */
- if (numberOfAttributes > MaxTupleAttributeNumber)
+ if (maxNumberOfAttributes > MaxTupleAttributeNumber)
ereport(ERROR,
(errcode(ERRCODE_TOO_MANY_COLUMNS),
errmsg("number of columns (%d) exceeds limit (%d)",
- numberOfAttributes, MaxTupleAttributeNumber)));
+ maxNumberOfAttributes, MaxTupleAttributeNumber)));
/*
* Check for nulls and embedded tuples; expand any toasted attributes in
@@ -1410,20 +1428,25 @@ heap_form_minimal_tuple(TupleDesc tupleDescriptor,
* if an attribute is already toasted, it must have been sent to disk
* already and so cannot contain toasted attributes.
*/
- for (i = 0; i < numberOfAttributes; i++)
+ for (i = 0; i < maxNumberOfAttributes; i++)
{
if (isnull[i])
hasnull = true;
- else if (att[i]->attlen == -1 &&
+ else
+ {
+ lastNonNullAttribute = i;
+ if (att[i]->attlen == -1 &&
att[i]->attalign == 'd' &&
att[i]->attndims == 0 &&
!VARATT_IS_EXTENDED(values[i]))
- {
- values[i] = toast_flatten_tuple_attribute(values[i],
+ {
+ values[i] = toast_flatten_tuple_attribute(values[i],
att[i]->atttypid,
att[i]->atttypmod);
+ }
}
}
+ numberOfAttributes = lastNonNullAttribute + 1;
/*
* Determine total space needed
@@ -1463,7 +1486,8 @@ heap_form_minimal_tuple(TupleDesc tupleDescriptor,
(char *) tuple + hoff,
data_len,
&tuple->t_infomask,
- (hasnull ? tuple->t_bits : NULL));
+ (hasnull ? tuple->t_bits : NULL),
+ numberOfAttributes);
return tuple;
}
diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c
index 76c76e9..a924863 100644
--- a/src/backend/access/common/indextuple.c
+++ b/src/backend/access/common/indextuple.c
@@ -29,6 +29,13 @@
/* ----------------
* index_form_tuple
* ----------------
+ *
+ * Note that as opposed to heap rows we don't bother truncating off trailing
+ * null attributes. This is because the maximum number of attributes in an
+ * index (32) is well bounded and there isn't much to be gained by trying to
+ * reduce the bitmap that tracks null attributes since we don't expect index
+ * keys have a lot of trailing nulls. Also, the logic for tracking the null
+ * bitmap is hard-wired at 4 bytes (see IndexAttributeBitMapData).
*/
IndexTuple
index_form_tuple(TupleDesc tupleDescriptor,
@@ -139,7 +146,8 @@ index_form_tuple(TupleDesc tupleDescriptor,
(char *) tp + hoff,
data_size,
&tupmask,
- (hasnull ? (bits8 *) tp + sizeof(IndexTupleData) : NULL));
+ (hasnull ? (bits8 *) tp + sizeof(IndexTupleData) : NULL),
+ numberOfAttributes);
#ifdef TOAST_INDEX_HACK
for (i = 0; i < numberOfAttributes; i++)
diff --git a/src/backend/access/heap/tuptoaster.c b/src/backend/access/heap/tuptoaster.c
index 28b5a20..6935a3c 100644
--- a/src/backend/access/heap/tuptoaster.c
+++ b/src/backend/access/heap/tuptoaster.c
@@ -425,6 +425,7 @@ toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
bool need_free = false;
bool need_delold = false;
bool has_nulls = false;
+ int lastNonNullValOffset = -1;
Size maxDataLen;
Size hoff;
@@ -534,6 +535,10 @@ toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
has_nulls = true;
continue;
}
+ else
+ {
+ lastNonNullValOffset = i;
+ }
/*
* Now look at varlena attributes
@@ -593,8 +598,8 @@ toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
/* compute header overhead --- this should match heap_form_tuple() */
hoff = offsetof(HeapTupleHeaderData, t_bits);
- if (has_nulls)
- hoff += BITMAPLEN(numAttrs);
+ if (has_nulls && lastNonNullValOffset > -1)
+ hoff += BITMAPLEN(lastNonNullValOffset+1);
if (newtup->t_data->t_infomask & HEAP_HASOID)
hoff += sizeof(Oid);
hoff = MAXALIGN(hoff);
@@ -867,6 +872,7 @@ toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
int32 new_header_len;
int32 new_data_len;
int32 new_tuple_len;
+ numAttrs = lastNonNullValOffset + 1; /* only store up to the last non-null attribute */
/*
* Calculate the new size of the tuple.
@@ -879,7 +885,7 @@ toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
* whether there needs to be one at all.
*/
new_header_len = offsetof(HeapTupleHeaderData, t_bits);
- if (has_nulls)
+ if (has_nulls && numAttrs > 0)
new_header_len += BITMAPLEN(numAttrs);
if (olddata->t_infomask & HEAP_HASOID)
new_header_len += sizeof(Oid);
@@ -914,7 +920,8 @@ toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
(char *) new_data + new_header_len,
new_data_len,
&(new_data->t_infomask),
- has_nulls ? new_data->t_bits : NULL);
+ has_nulls ? new_data->t_bits : NULL,
+ numAttrs);
}
else
result_tuple = newtup;
@@ -1060,12 +1067,12 @@ toast_flatten_tuple_attribute(Datum value,
return value; /* not a composite type */
att = tupleDesc->attrs;
- numAttrs = tupleDesc->natts;
/*
* Break down the tuple into fields.
*/
olddata = DatumGetHeapTupleHeader(value);
+ numAttrs = HeapTupleHeaderGetNatts(olddata);
Assert(typeId == HeapTupleHeaderGetTypeId(olddata));
Assert(typeMod == HeapTupleHeaderGetTypMod(olddata));
/* Build a temporary HeapTuple control structure */
@@ -1147,7 +1154,8 @@ toast_flatten_tuple_attribute(Datum value,
(char *) new_data + new_header_len,
new_data_len,
&(new_data->t_infomask),
- has_nulls ? new_data->t_bits : NULL);
+ has_nulls ? new_data->t_bits : NULL,
+ numAttrs);
/*
* Free allocated temp values
diff --git a/src/include/access/htup.h b/src/include/access/htup.h
index 6a3778d..565b32b 100644
--- a/src/include/access/htup.h
+++ b/src/include/access/htup.h
@@ -891,7 +891,7 @@ extern Size heap_compute_data_size(TupleDesc tupleDesc,
extern void heap_fill_tuple(TupleDesc tupleDesc,
Datum *values, bool *isnull,
char *data, Size data_size,
- uint16 *infomask, bits8 *bit);
+ uint16 *infomask, bits8 *bit, int lastNonNullAttributeOffset);
extern bool heap_attisnull(HeapTuple tup, int attnum);
extern Datum nocachegetattr(HeapTuple tup, int attnum,
TupleDesc att);
diff --git a/src/test/regress/expected/trailing_nulls.out b/src/test/regress/expected/trailing_nulls.out
new file mode 100644
index 0000000..197e729
--- /dev/null
+++ b/src/test/regress/expected/trailing_nulls.out
@@ -0,0 +1,210 @@
+--
+-- The row bitmap and row length don't track trailing nulls. Verify that this works correctly.
+--
+-- simple case
+CREATE TABLE trailing_null (
+ c1 varchar(10),
+ c2 varchar(10),
+ c3 varchar(10),
+ c4 varchar(10),
+ c5 varchar(10)
+ );
+
+INSERT INTO trailing_null VALUES (null, null, null, null, null);
+INSERT INTO trailing_null VALUES (null, null, null, null, '5');
+INSERT INTO trailing_null VALUES (null, null, null, '4', null);
+INSERT INTO trailing_null VALUES (null, null, '3', null, null);
+INSERT INTO trailing_null VALUES (null, '2', null, null, null);
+INSERT INTO trailing_null VALUES ('1', null, null, null, null);
+SELECT * FROM trailing_null ORDER BY c1, c2, c3, c4, c5;
+ c1 | c2 | c3 | c4 | c5
+----+----+----+----+----
+ 1 | | | |
+ | 2 | | |
+ | | 3 | |
+ | | | 4 |
+ | | | | 5
+ | | | |
+(6 rows)
+
+CREATE INDEX trailing_nulli ON trailing_null (c1, c2, c3, c4, c5);
+INSERT INTO trailing_null VALUES ('1', null, null, null, null);
+SELECT * FROM trailing_null ORDER BY c1, c2, c3, c4, c5;
+ c1 | c2 | c3 | c4 | c5
+----+----+----+----+----
+ 1 | | | |
+ 1 | | | |
+ | 2 | | |
+ | | 3 | |
+ | | | 4 |
+ | | | | 5
+ | | | |
+(7 rows)
+
+DROP TABLE trailing_null;
+-- corner case, no columns
+CREATE TABLE trailing_null ();
+DROP TABLE trailing_null;
+-- toastable
+CREATE TABLE trailing_null (
+ c1 varchar(500),
+ c2 varchar(500),
+ c3 varchar(500),
+ c4 varchar(500),
+ c5 varchar(500),
+ c6 varchar(500),
+ c7 varchar(500),
+ c8 varchar(500),
+ c9 varchar(500),
+ c10 varchar(500),
+ c11 varchar(500),
+ c12 varchar(500),
+ c13 varchar(500),
+ c14 varchar(500),
+ c15 varchar(500),
+ c16 varchar(500),
+ c17 varchar(500)
+ );
+-- toast: easily compressed
+INSERT INTO trailing_null VALUES (
+ rpad('c1', 450, 'x'),
+ rpad('c2', 450, 'x'),
+ rpad('c3', 450, 'x'),
+ rpad('c4', 450, 'x'),
+ rpad('c5', 450, 'x'),
+ rpad('c6', 450, 'x'),
+ rpad('c7', 450, 'x'),
+ rpad('c8', 450, 'x'),
+ rpad('c9', 450, 'x'),
+ rpad('c10', 450, 'x'),
+ rpad('c11', 450, 'x'),
+ rpad('c12', 450, 'x'),
+ rpad('c13', 450, 'x'),
+ rpad('c14', 450, 'x'),
+ rpad('c15', 450, 'x'),
+ rpad('c16', 450, 'x'),
+ rpad('c17', 450, 'x')
+ );
+-- toast: off row storage
+INSERT INTO trailing_null VALUES (
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3)
+);
+-- verify that we have rows in our toast storage
+VACUUM FULL trailing_null;
+VACUUM ANALYZE trailing_null;
+SELECT CAST(reltuples AS INTEGER) FROM pg_class
+ WHERE oid = (SELECT reltoastrelid FROM pg_class where relname = 'trailing_null');
+ reltuples
+-----------
+ 4
+(1 row)
+
+
+-- non-toast, separate codepath
+UPDATE trailing_null SET c17 = null;
+SELECT length(c1), length(c16), c17 FROM trailing_null;
+ length | length | c17
+--------+--------+-----
+ 450 | 450 |
+ 336 | 336 |
+(2 rows)
+
+VACUUM FULL trailing_null;
+VACUUM ANALYZE trailing_null;
+SELECT CAST(reltuples AS INTEGER) FROM pg_class
+ WHERE oid = (SELECT reltoastrelid FROM pg_class where relname = 'trailing_null');
+ reltuples
+-----------
+ 4
+(1 row)
+
+UPDATE trailing_null SET c16 = null;
+SELECT length(c1), c16, c17 FROM trailing_null;
+ length | c16 | c17
+--------+-----+-----
+ 450 | |
+ 336 | |
+(2 rows)
+
+VACUUM FULL trailing_null;
+VACUUM ANALYZE trailing_null;
+SELECT CAST(reltuples AS INTEGER) FROM pg_class
+ WHERE oid = (SELECT reltoastrelid FROM pg_class where relname = 'trailing_null');
+ reltuples
+-----------
+ 4
+(1 row)
+
+UPDATE trailing_null SET
+ c1 = null,
+ c2 = null,
+ c3 = null,
+ c4 = null,
+ c5 = null,
+ c6 = null,
+ c7 = null,
+ c8 = null,
+ c9 = null,
+ c10 = null,
+ c11 = null,
+ c12 = null,
+ c13 = null,
+ c14 = null,
+ c15 = null,
+ c16 = null,
+ c17 = null;
+-- verify that we got rid of our toasted data
+VACUUM FULL trailing_null;
+VACUUM ANALYZE trailing_null;
+SELECT CAST(reltuples AS INTEGER) FROM pg_class
+ WHERE oid = (SELECT reltoastrelid FROM pg_class where relname = 'trailing_null');
+ reltuples
+-----------
+ 4
+(1 row)
+
+
+DROP TABLE trailing_null;
+-- Let's hit the detoasting logic related to composite objects. Borrowed
+-- from rowtypes.sql
+-- The object here is to ensure that toasted references inside
+-- composite values don't cause problems. The large f1 value will
+-- be toasted inside pp, it must still work after being copied to people.
+CREATE TYPE fullname as (first text, last text, extra text);
+CREATE temp TABLE pp (f1 text, c1 varchar(10) null);
+INSERT INTO pp VALUES (repeat('abcdefghijkl', 100000), null);
+CREATE temp TABLE people (fn fullname, c1 varchar(10), c2 varchar(10));
+-- trailing null in our nested object
+INSERT into people SELECT('Jim', f1, null)::fullname, c1, null from pp;
+SELECT (fn).first, length((fn).last), c1, c2 from people;
+ first | length | c1 | c2
+-------+---------+----+----
+ Jim | 1200000 | |
+(1 row)
+
+ALTER TABLE people ADD COLUMN c3 varchar(10);
+SELECT (fn).first, length((fn).last), c1, c2, c3 from people;
+ first | length | c1 | c2 | c3
+-------+---------+----+----+----
+ Jim | 1200000 | | |
+(1 row)
+
+DROP TABLE pp;
+DROP TABLE people;
+DROP TYPE fullname;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8852e0a..9ab329d 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -92,7 +92,7 @@ test: rules
# ----------
# Another group of parallel tests
# ----------
-test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock json
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock json trailing_nulls
# ----------
# Another group of parallel tests
diff --git a/src/test/regress/sql/trailing_nulls.sql b/src/test/regress/sql/trailing_nulls.sql
new file mode 100644
index 0000000..f617257
--- /dev/null
+++ b/src/test/regress/sql/trailing_nulls.sql
@@ -0,0 +1,166 @@
+--
+-- The row bitmap and row length don't track trailing nulls. Verify that this works correctly.
+--
+
+-- simple case
+CREATE TABLE trailing_null (
+ c1 varchar(10),
+ c2 varchar(10),
+ c3 varchar(10),
+ c4 varchar(10),
+ c5 varchar(10)
+ );
+
+INSERT INTO trailing_null VALUES (null, null, null, null, null);
+INSERT INTO trailing_null VALUES (null, null, null, null, '5');
+INSERT INTO trailing_null VALUES (null, null, null, '4', null);
+INSERT INTO trailing_null VALUES (null, null, '3', null, null);
+INSERT INTO trailing_null VALUES (null, '2', null, null, null);
+INSERT INTO trailing_null VALUES ('1', null, null, null, null);
+
+SELECT * FROM trailing_null ORDER BY c1, c2, c3, c4, c5;
+
+CREATE INDEX trailing_nulli ON trailing_null (c1, c2, c3, c4, c5);
+
+INSERT INTO trailing_null VALUES ('1', null, null, null, null);
+
+SELECT * FROM trailing_null ORDER BY c1, c2, c3, c4, c5;
+
+DROP TABLE trailing_null;
+
+-- corner case, no columns
+CREATE TABLE trailing_null ();
+DROP TABLE trailing_null;
+
+
+-- toastable
+CREATE TABLE trailing_null (
+ c1 varchar(500),
+ c2 varchar(500),
+ c3 varchar(500),
+ c4 varchar(500),
+ c5 varchar(500),
+ c6 varchar(500),
+ c7 varchar(500),
+ c8 varchar(500),
+ c9 varchar(500),
+ c10 varchar(500),
+ c11 varchar(500),
+ c12 varchar(500),
+ c13 varchar(500),
+ c14 varchar(500),
+ c15 varchar(500),
+ c16 varchar(500),
+ c17 varchar(500)
+ );
+
+-- toast: easily compressed
+INSERT INTO trailing_null VALUES (
+ rpad('c1', 450, 'x'),
+ rpad('c2', 450, 'x'),
+ rpad('c3', 450, 'x'),
+ rpad('c4', 450, 'x'),
+ rpad('c5', 450, 'x'),
+ rpad('c6', 450, 'x'),
+ rpad('c7', 450, 'x'),
+ rpad('c8', 450, 'x'),
+ rpad('c9', 450, 'x'),
+ rpad('c10', 450, 'x'),
+ rpad('c11', 450, 'x'),
+ rpad('c12', 450, 'x'),
+ rpad('c13', 450, 'x'),
+ rpad('c14', 450, 'x'),
+ rpad('c15', 450, 'x'),
+ rpad('c16', 450, 'x'),
+ rpad('c17', 450, 'x')
+ );
+
+-- toast: off row storage
+INSERT INTO trailing_null VALUES (
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3),
+ repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3)
+);
+
+-- verify that we have rows in our toast storage
+VACUUM FULL trailing_null;
+VACUUM ANALYZE trailing_null;
+SELECT CAST(reltuples AS INTEGER) FROM pg_class
+ WHERE oid = (SELECT reltoastrelid FROM pg_class where relname = 'trailing_null');
+
+-- non-toast, separate codepath
+UPDATE trailing_null SET c17 = null;
+SELECT length(c1), length(c16), c17 FROM trailing_null;
+VACUUM FULL trailing_null;
+VACUUM ANALYZE trailing_null;
+SELECT CAST(reltuples AS INTEGER) FROM pg_class
+ WHERE oid = (SELECT reltoastrelid FROM pg_class where relname = 'trailing_null');
+
+UPDATE trailing_null SET c16 = null;
+SELECT length(c1), c16, c17 FROM trailing_null;
+VACUUM FULL trailing_null;
+VACUUM ANALYZE trailing_null;
+SELECT CAST(reltuples AS INTEGER) FROM pg_class
+ WHERE oid = (SELECT reltoastrelid FROM pg_class where relname = 'trailing_null');
+
+UPDATE trailing_null SET
+ c1 = null,
+ c2 = null,
+ c3 = null,
+ c4 = null,
+ c5 = null,
+ c6 = null,
+ c7 = null,
+ c8 = null,
+ c9 = null,
+ c10 = null,
+ c11 = null,
+ c12 = null,
+ c13 = null,
+ c14 = null,
+ c15 = null,
+ c16 = null,
+ c17 = null;
+
+-- verify that we got rid of our toasted data
+VACUUM FULL trailing_null;
+VACUUM ANALYZE trailing_null;
+SELECT CAST(reltuples AS INTEGER) FROM pg_class
+ WHERE oid = (SELECT reltoastrelid FROM pg_class where relname = 'trailing_null');
+
+DROP TABLE trailing_null;
+
+-- Let's hit the detoasting logic related to composite objects. Borrowed
+-- from rowtypes.sql
+
+-- The object here is to ensure that toasted references inside
+-- composite values don't cause problems. The large f1 value will
+-- be toasted inside pp, it must still work after being copied to people.
+CREATE TYPE fullname as (first text, last text, extra text);
+CREATE temp TABLE pp (f1 text, c1 varchar(10) null);
+INSERT INTO pp VALUES (repeat('abcdefghijkl', 100000), null);
+CREATE temp TABLE people (fn fullname, c1 varchar(10), c2 varchar(10));
+
+-- trailing null in our nested object
+INSERT into people SELECT('Jim', f1, null)::fullname, c1, null from pp;
+SELECT (fn).first, length((fn).last), c1, c2 from people;
+ALTER TABLE people ADD COLUMN c3 varchar(10);
+SELECT (fn).first, length((fn).last), c1, c2, c3 from people;
+
+DROP TABLE pp;
+DROP TABLE people;
+DROP TYPE fullname;
--
1.7.0.4