Skip to content

Commit 8d5c33f

Browse files
Recompress some chunks on VACUUM FULL
When columns with default values are added to already compressed hypertable, chunks have missing attributes (atthasmissing). VACUUM FULL rewrites tables and materializes these attributes. Detect affected compressed chunks during VACUUM FULL and automatically recompress them to ensure compressed data reflects the new attribute. Fixes #8091
1 parent 399a82d commit 8d5c33f

File tree

5 files changed

+163
-16
lines changed

5 files changed

+163
-16
lines changed

.unreleased/pr_9024

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixes: #9024 Recompress some chunks on VACUUM FULL

src/process_utility.c

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -987,10 +987,64 @@ foreach_chunk_multitransaction(Oid relid, MemoryContext mctx, mt_process_chunk_t
987987

988988
typedef struct VacuumCtx
989989
{
990+
bool is_vacuumfull;
990991
VacuumRelation *ht_vacuum_rel;
991992
List *chunk_rels;
993+
List *recompress_chunk_oids;
992994
} VacuumCtx;
993995

996+
static bool
997+
chunk_has_missing_attrs(Chunk *chunk)
998+
{
999+
bool has_missing_attrs = false;
1000+
Relation ht_rel = relation_open(chunk->hypertable_relid, AccessShareLock);
1001+
Relation chunk_rel = relation_open(chunk->table_id, AccessShareLock);
1002+
TupleDesc tupdesc = RelationGetDescr(chunk_rel);
1003+
1004+
for (int i = 0; i < tupdesc->natts; i++)
1005+
{
1006+
Form_pg_attribute att = TupleDescAttr(tupdesc, i);
1007+
1008+
if (att->atthasmissing)
1009+
{
1010+
has_missing_attrs = true;
1011+
break;
1012+
}
1013+
}
1014+
1015+
relation_close(chunk_rel, AccessShareLock);
1016+
relation_close(ht_rel, AccessShareLock);
1017+
return has_missing_attrs;
1018+
}
1019+
1020+
static void
1021+
check_chunk_needs_recompression(Oid chunk_relid, VacuumCtx *ctx)
1022+
{
1023+
/* Only VACUUM FULL does a complete table rewrite */
1024+
if (!ctx->is_vacuumfull)
1025+
return;
1026+
1027+
Chunk *chunk = ts_chunk_get_by_relid(chunk_relid, false);
1028+
if (!chunk)
1029+
return;
1030+
1031+
/*
1032+
* VACUUM FULL will materialize missing attributes. Recompress chunks to propagate
1033+
* these changes to compressed data.
1034+
*/
1035+
if (chunk_has_missing_attrs(chunk))
1036+
{
1037+
/*
1038+
* Frozen chunks are not allowed to add columns with defaults
1039+
*/
1040+
Ensure(!ts_chunk_is_frozen(chunk),
1041+
"chunk \"%s.%s\" was altered unsafely after it was frozen",
1042+
NameStr(chunk->fd.schema_name),
1043+
NameStr(chunk->fd.table_name));
1044+
ctx->recompress_chunk_oids = lappend_oid(ctx->recompress_chunk_oids, chunk_relid);
1045+
}
1046+
}
1047+
9941048
/* Adds a chunk to the list of tables to be vacuumed */
9951049
static void
9961050
add_chunk_to_vacuum(Hypertable *ht, Oid chunk_relid, void *arg)
@@ -1018,6 +1072,8 @@ add_chunk_to_vacuum(Hypertable *ht, Oid chunk_relid, void *arg)
10181072
ctx->chunk_rels = lappend(ctx->chunk_rels, chunk_vacuum_rel);
10191073
}
10201074
}
1075+
1076+
check_chunk_needs_recompression(chunk_relid, ctx);
10211077
}
10221078

10231079
/*
@@ -1026,7 +1082,7 @@ add_chunk_to_vacuum(Hypertable *ht, Oid chunk_relid, void *arg)
10261082
* from vacuum.c.
10271083
*/
10281084
static List *
1029-
ts_get_all_vacuum_rels(bool is_vacuumcmd)
1085+
ts_get_all_vacuum_rels(bool is_vacuumcmd, VacuumCtx *ctx)
10301086
{
10311087
List *vacrels = NIL;
10321088
Relation pgclass;
@@ -1066,6 +1122,8 @@ ts_get_all_vacuum_rels(bool is_vacuumcmd)
10661122
* about failure to open one of these relations later.
10671123
*/
10681124
vacrels = lappend(vacrels, makeVacuumRelation(NULL, relid, NIL));
1125+
1126+
check_chunk_needs_recompression(relid, ctx);
10691127
}
10701128

10711129
table_endscan(scan);
@@ -1082,8 +1140,10 @@ process_vacuum(ProcessUtilityArgs *args)
10821140
VacuumStmt *stmt = (VacuumStmt *) args->parsetree;
10831141
bool is_toplevel = (args->context == PROCESS_UTILITY_TOPLEVEL);
10841142
VacuumCtx ctx = {
1143+
.is_vacuumfull = false,
10851144
.ht_vacuum_rel = NULL,
10861145
.chunk_rels = NIL,
1146+
.recompress_chunk_oids = NIL,
10871147
};
10881148
ListCell *lc;
10891149
Hypertable *ht;
@@ -1094,24 +1154,22 @@ process_vacuum(ProcessUtilityArgs *args)
10941154

10951155
is_vacuumcmd = stmt->is_vacuumcmd;
10961156

1097-
#if PG16_GE
1098-
if (is_vacuumcmd)
1157+
/* Look for new option ONLY_DATABASE_STATS and FULL */
1158+
foreach (lc, stmt->options)
10991159
{
1100-
/* Look for new option ONLY_DATABASE_STATS */
1101-
foreach (lc, stmt->options)
1102-
{
1103-
DefElem *opt = (DefElem *) lfirst(lc);
1104-
1105-
/* if "only_database_stats" is defined then don't execute our custom code and return to
1106-
* the postgres execution for the proper validations */
1107-
if (strcmp(opt->defname, "only_database_stats") == 0)
1108-
return DDL_CONTINUE;
1109-
}
1110-
}
1160+
DefElem *opt = (DefElem *) lfirst(lc);
1161+
#if PG16_GE
1162+
/* if "only_database_stats" is defined then don't execute our custom code and return to
1163+
* the postgres execution for the proper validations */
1164+
if (is_vacuumcmd && strcmp(opt->defname, "only_database_stats") == 0)
1165+
return DDL_CONTINUE;
11111166
#endif
1167+
if (strcmp(opt->defname, "full") == 0)
1168+
ctx.is_vacuumfull = defGetBoolean(opt);
1169+
}
11121170

11131171
if (stmt->rels == NIL)
1114-
vacuum_rels = ts_get_all_vacuum_rels(is_vacuumcmd);
1172+
vacuum_rels = ts_get_all_vacuum_rels(is_vacuumcmd, &ctx);
11151173
else
11161174
{
11171175
Cache *hcache = ts_hypertable_cache_pin();
@@ -1147,6 +1205,20 @@ process_vacuum(ProcessUtilityArgs *args)
11471205
{
11481206
PreventCommandDuringRecovery(is_vacuumcmd ? "VACUUM" : "ANALYZE");
11491207

1208+
if (list_length(ctx.recompress_chunk_oids) > 0)
1209+
{
1210+
/* there are chunks that need recompression */
1211+
ListCell *lc;
1212+
foreach (lc, ctx.recompress_chunk_oids)
1213+
{
1214+
/* TODO: needs to handle partial compression */
1215+
Oid chunk_relid = lfirst_oid(lc);
1216+
(void) DirectFunctionCall3(ts_cm_functions->compress_chunk,
1217+
ObjectIdGetDatum(chunk_relid),
1218+
BoolGetDatum(true),
1219+
BoolGetDatum(true)); /* recompress */
1220+
}
1221+
}
11501222
/* ACL permission checks inside vacuum_rel and analyze_rel called by this ExecVacuum */
11511223
ExecVacuum(args->parse_state, stmt, is_toplevel);
11521224
}

tsl/test/expected/vacuum.out

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
-- This file and its contents are licensed under the Timescale License.
2+
-- Please see the included NOTICE for copyright information and
3+
-- LICENSE-TIMESCALE for a copy of the license.
4+
\c :TEST_DBNAME :ROLE_SUPERUSER
5+
-- Test VACUUM FULL with compressed chunks and missing attributes
6+
CREATE TABLE vacuum_missing_test(ts int, c1 int);
7+
SELECT create_hypertable('vacuum_missing_test', 'ts', chunk_time_interval => 1000);
8+
create_hypertable
9+
----------------------------------
10+
(1,public,vacuum_missing_test,t)
11+
12+
INSERT INTO vacuum_missing_test VALUES (0, 1);
13+
ALTER TABLE vacuum_missing_test SET (timescaledb.compress, timescaledb.compress_segmentby = '');
14+
SELECT compress_chunk(show_chunks('vacuum_missing_test'), true);
15+
compress_chunk
16+
----------------------------------------
17+
_timescaledb_internal._hyper_1_1_chunk
18+
19+
ALTER TABLE vacuum_missing_test ADD COLUMN c2 int DEFAULT 7;
20+
VACUUM FULL ANALYZE vacuum_missing_test;
21+
SELECT * FROM vacuum_missing_test;
22+
ts | c1 | c2
23+
----+----+----
24+
0 | 1 | 7
25+
26+
DROP TABLE vacuum_missing_test;
27+
CREATE TABLE vacuum_missing_test(ts int, c1 int);
28+
SELECT create_hypertable('vacuum_missing_test', 'ts', chunk_time_interval => 1000);
29+
create_hypertable
30+
----------------------------------
31+
(3,public,vacuum_missing_test,t)
32+
33+
INSERT INTO vacuum_missing_test VALUES (0, 1);
34+
ALTER TABLE vacuum_missing_test SET (timescaledb.compress, timescaledb.compress_segmentby = '');
35+
SELECT compress_chunk(show_chunks('vacuum_missing_test'), true);
36+
compress_chunk
37+
----------------------------------------
38+
_timescaledb_internal._hyper_3_4_chunk
39+
40+
ALTER TABLE vacuum_missing_test ADD COLUMN c2 int DEFAULT 7;
41+
VACUUM FULL;
42+
SELECT * FROM vacuum_missing_test;
43+
ts | c1 | c2
44+
----+----+----
45+
0 | 1 | 7
46+
47+
DROP TABLE vacuum_missing_test;

tsl/test/sql/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ if(CMAKE_BUILD_TYPE MATCHES Debug)
166166
vector_agg_text.sql
167167
vector_agg_memory.sql
168168
vector_agg_segmentby.sql
169-
vector_agg_uuid.sql)
169+
vector_agg_uuid.sql
170+
vacuum.sql)
170171

171172
list(
172173
APPEND

tsl/test/sql/vacuum.sql

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
-- This file and its contents are licensed under the Timescale License.
2+
-- Please see the included NOTICE for copyright information and
3+
-- LICENSE-TIMESCALE for a copy of the license.
4+
5+
\c :TEST_DBNAME :ROLE_SUPERUSER
6+
7+
-- Test VACUUM FULL with compressed chunks and missing attributes
8+
CREATE TABLE vacuum_missing_test(ts int, c1 int);
9+
SELECT create_hypertable('vacuum_missing_test', 'ts', chunk_time_interval => 1000);
10+
INSERT INTO vacuum_missing_test VALUES (0, 1);
11+
ALTER TABLE vacuum_missing_test SET (timescaledb.compress, timescaledb.compress_segmentby = '');
12+
SELECT compress_chunk(show_chunks('vacuum_missing_test'), true);
13+
ALTER TABLE vacuum_missing_test ADD COLUMN c2 int DEFAULT 7;
14+
VACUUM FULL ANALYZE vacuum_missing_test;
15+
SELECT * FROM vacuum_missing_test;
16+
DROP TABLE vacuum_missing_test;
17+
18+
CREATE TABLE vacuum_missing_test(ts int, c1 int);
19+
SELECT create_hypertable('vacuum_missing_test', 'ts', chunk_time_interval => 1000);
20+
INSERT INTO vacuum_missing_test VALUES (0, 1);
21+
ALTER TABLE vacuum_missing_test SET (timescaledb.compress, timescaledb.compress_segmentby = '');
22+
SELECT compress_chunk(show_chunks('vacuum_missing_test'), true);
23+
ALTER TABLE vacuum_missing_test ADD COLUMN c2 int DEFAULT 7;
24+
VACUUM FULL;
25+
SELECT * FROM vacuum_missing_test;
26+
DROP TABLE vacuum_missing_test;

0 commit comments

Comments
 (0)