Skip to content

CREATE TABLE AS into a columnar table fails when the source plan is parallel, which is the default at scale #387

Description

@ChronicallyJD

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions