CREATE TABLE ... USING pgcolumnar AS SELECT ... fails whenever the planner picks a
parallel plan for the source query:
ERROR: cannot update SecondarySnapshot during a parallel operation
I hit this doing something ordinary, building a clustered copy of the TSBS table on the
bench to measure the question docs/benchmarks.md leaves open.
It fails by default, and only at scale
Nothing is set here. Stock cluster, stock session:
=# EXPLAIN (COSTS OFF) CREATE TABLE zz USING pgcolumnar AS SELECT * FROM cpu_pgc ORDER BY hostname, time;
Gather Merge
Workers Planned: 6
-> Sort
-> Parallel Custom Scan (ColumnarScan) on cpu_pgc
Small sources get a serial plan and work. Large ones get a parallel plan and fail. So
the failure arrives exactly when someone bulk loads a real table, which is the main way
a columnar table gets created from existing data.
Minimal repro
Zeroing the parallel costs makes a small table go parallel, so this runs in seconds:
CREATE TABLE src AS SELECT g AS id, 'h'||(g%97) AS host FROM generate_series(1,200000) g;
SET parallel_setup_cost=0; SET parallel_tuple_cost=0;
SET min_parallel_table_scan_size=0; SET max_parallel_workers_per_gather=2;
CREATE TABLE t USING pgcolumnar AS SELECT * FROM src ORDER BY host;
ERROR: cannot update SecondarySnapshot during a parallel operation
What is and is not affected
I checked the plan of each case rather than trusting the result, because a green run
proves nothing if the plan was never parallel.
| case |
plan |
result |
| CTAS into heap, same forced costs |
Gather Merge, 2 workers |
ok |
| CTAS into pgcolumnar |
Gather Merge, 2 workers |
ERROR |
CTAS into pgcolumnar, max_parallel_workers_per_gather=0 |
serial |
ok |
CTAS into pgcolumnar, WITH NO DATA, then INSERT ... SELECT |
serial insert |
ok |
plain parallel SELECT from a columnar table |
Gather |
ok |
| parallel CTAS reading a columnar table into a heap |
Gather Merge |
ok |
The first two rows are the finding. Same plan, same costs, heap works and columnar does
not, so it is the target and not the query.
One row I want to be careful about. INSERT INTO <columnar> SELECT ... ORDER BY
also succeeds, but when I checked its plan it had no Gather at all:
Insert on tgt_p
-> Sort
-> Seq Scan on src_p
So INSERT ... SELECT is untested here, not proven safe. I could not get PG18 to
build a parallel plan under it for a columnar target.
Where it comes from
backtrace_functions='GetLatestSnapshot', frames resolved with addr2line:
columnar_tuple_insert src/columnar_tableam.c:638
ColumnarWriteRow src/columnar_write_state.c:794
columnar_flush_row_group src/columnar_write_state.c:1369
ColumnarInsertNativeStorageRow src/columnar_metadata.c:1543
Line 1543 is the re-check after taking the storage-row creation advisory lock:
PushActiveSnapshot(GetLatestSnapshot());
snapshot = ColumnarCatalogSnapshot(GetActiveSnapshot());
GetLatestSnapshot() raises this exact error under IsInParallelMode(). The comment
above it explains why the fresh snapshot is needed, and that reasoning looks right to
me: the loser of a cross-transaction first-write race has to see the winner's committed
row or it fails on storage_pkey. The problem is only that the call is unconditional,
and CTAS runs the whole executor inside parallel mode even though the leader does the
writing.
Thoughts on a fix, not a proposal
I have not written one, and I would rather not guess at the concurrency reasoning in
that function.
Worth noting though that in the CTAS case the relation is created by the same statement,
so no other session can see it, and the race the fresh snapshot defends against cannot
happen. There is also already precedent for making this conditional: the
columnar_bulk_parallel_writer fast path directly above does exactly that for
pgcolumnar.parallel_copy.
Happy to take it if you want, but you know that lock's history better than I do.
Environment
PostgreSQL 18, pgcolumnar 1.0-dev at author/main, on the bench. I have not checked
whether 15 through 17 behave the same.
Workaround
SET max_parallel_workers_per_gather = 0 for the load, or create the table empty and
INSERT ... SELECT into it.
CREATE TABLE ... USING pgcolumnar AS SELECT ...fails whenever the planner picks aparallel plan for the source query:
I hit this doing something ordinary, building a clustered copy of the TSBS table on the
bench to measure the question docs/benchmarks.md leaves open.
It fails by default, and only at scale
Nothing is set here. Stock cluster, stock session:
Small sources get a serial plan and work. Large ones get a parallel plan and fail. So
the failure arrives exactly when someone bulk loads a real table, which is the main way
a columnar table gets created from existing data.
Minimal repro
Zeroing the parallel costs makes a small table go parallel, so this runs in seconds:
What is and is not affected
I checked the plan of each case rather than trusting the result, because a green run
proves nothing if the plan was never parallel.
Gather Merge, 2 workersGather Merge, 2 workersmax_parallel_workers_per_gather=0WITH NO DATA, thenINSERT ... SELECTSELECTfrom a columnar tableGatherGather MergeThe first two rows are the finding. Same plan, same costs, heap works and columnar does
not, so it is the target and not the query.
One row I want to be careful about.
INSERT INTO <columnar> SELECT ... ORDER BYalso succeeds, but when I checked its plan it had no
Gatherat all:So
INSERT ... SELECTis untested here, not proven safe. I could not get PG18 tobuild a parallel plan under it for a columnar target.
Where it comes from
backtrace_functions='GetLatestSnapshot', frames resolved withaddr2line:Line 1543 is the re-check after taking the storage-row creation advisory lock:
GetLatestSnapshot()raises this exact error underIsInParallelMode(). The commentabove it explains why the fresh snapshot is needed, and that reasoning looks right to
me: the loser of a cross-transaction first-write race has to see the winner's committed
row or it fails on
storage_pkey. The problem is only that the call is unconditional,and CTAS runs the whole executor inside parallel mode even though the leader does the
writing.
Thoughts on a fix, not a proposal
I have not written one, and I would rather not guess at the concurrency reasoning in
that function.
Worth noting though that in the CTAS case the relation is created by the same statement,
so no other session can see it, and the race the fresh snapshot defends against cannot
happen. There is also already precedent for making this conditional: the
columnar_bulk_parallel_writerfast path directly above does exactly that forpgcolumnar.parallel_copy.Happy to take it if you want, but you know that lock's history better than I do.
Environment
PostgreSQL 18,
pgcolumnar 1.0-devatauthor/main, on the bench. I have not checkedwhether 15 through 17 behave the same.
Workaround
SET max_parallel_workers_per_gather = 0for the load, or create the table empty andINSERT ... SELECTinto it.