Skip to content

feat: split INSERT .. SELECT * FROM unnest(..) - #1988

Draft
jkaczman wants to merge 2 commits into
mainfrom
jk-unnest-insert-split
Draft

jkaczman wants to merge 2 commits into
mainfrom
jk-unnest-insert-split

Conversation

@jkaczman

@jkaczman jkaczman commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

This PR adds functionality to split INSERT INTO <sharded table> .. SELECT * FROM unnest($1, ..) by how the rows from unnest resolve to Shards if split_inserts is set to rewrite

E.g., if we have a statement such as

INSERT INTO sharded (id, value) SELECT * FROM unnest($1::pg_catalog._int8, $2::pg_catalog._text)

with $1 = {1,2,3,4} and $2 = {a,b,c,d}, Postgres inserts the rows (1, a), (2, b), (3, c) and (4, d).

If we get one of these INSERTs, we split the INSERT by shard, as follows:

  1. we decode the arrays from the Bind (in text or binary format) using the element types from the casts (e.g., int8 from pg_catalog._int8).
  2. then, we route all of the rows one at a time with the unnest(..) arguments matched to the INSERT columns by position
  3. finally, we send the original INSERT to all shards that the router found. To make this work correctly, we cut the arrays from the Bind down to the rows that belong on the shard. E.g., if ids 1 and 3 belong on Shard A, Shard A will get $1 = {1,3} and $2 = {a,c}, and Shard B will get $1 = {2,4} and $2 = {b,d}.

Otherwise notable:

  • If the arrays have different lengths, unnest(..) will pad the shorter arrays with NULLs so we use the length of the longest array as the number of rows and send NULLs for the missing elements.
  • We only split SELECT * FROM unnest(..) where all arguments are parameters with a cast to an array type that we can decode (e.g., _bool, _int2, _int4, _int8...) without any clauses (e.g., LIMIT) present.
  • Future performance work: currently, We route the rows one at a time (one Router::query call per row), which is slow for large arrays. We could compute the shards of the rows from the array of the sharding key directly instead.

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Metal bench (arm64) failed. 76 of 475 measures outside bounds.

suite throughput cpu/txn user cycles/txn user instr/txn kernel cycles/txn kernel instr/txn ctx/txn
pooler 18.51 (-0.1% ± 0.6%) 17,720 (-0.2% ± 0.1%) 31,992 (-0.0% ± 0.2%) 29,385 (-0.1% ± 1.1%) 51,306 (+0.0% ± 0.4%)
lb 19.2 (-0.1% ± 1.1%) 18,305 (-0.3% ± 0.1%) 32,933 (+0.1% ± 0.03%) 30,495 (-0.0% ± 1.9%) 52,460 (+0.0% ± 0.2%)
sharding-w2 32.04 (-0.9% ± 1.3%) 42,955 (-0.8% ± 1.2%) 97,398 (+0.1% ± 0.00%) 40,360 (-0.8% ± 2.4%) 53,656 (-0.0% ± 0.2%) 1.547 (+0.0% ± 0.2%)
sharding-w8 249,354 (+1.9% ± 2.2%) 28.64 (-2.2% ± 2.7%) 44,022 (-1.4% ± 0.9%) 52,918 (+0.1% ± 0.06%) 35,075 (-3.3% ± 5.2%) 42,371 (+0.0% ± 0.06%) 0.1714 (+2.7% ± 2.8%)
Counters per txn
suite frontend stall l1i refill backend stall
pooler 7,007 (-0.9% ± 0.4%) 2,538 (-1.3% ± 0.2%)
lb 7,305 (-1.3% ± 0.7%) 2,593 (-1.2% ± 0.3%)
sharding-w2 16,768 (-1.9% ± 2.9%) 3,950 (-1.8% ± 2.3%)
sharding-w8 19,570 (-3.4% ± 1.8%) 4,013 (-2.2% ± 0.9%)
benchmark measure result baseline change bound
extended_cold::aggregate alloc_bytes 1,990 1,966 +1.22% ± 0.00% +1%
extended_cold::begin alloc_bytes 654 630 +3.81% ± 0.00% +1%
extended_cold::cte alloc_bytes 1,731 1,707 +1.41% ± 0.00% +1%
extended_cold::delete_all_shards alloc_bytes 876 852 +2.82% ± 0.00% +1%
extended_cold::delete_by_key alloc_bytes 1,101 1,077 +2.23% ± 0.00% +1%
extended_cold::distinct alloc_bytes 1,099 1,075 +2.23% ± 0.00% +1%
extended_cold::group_by alloc_bytes 1,819 1,795 +1.34% ± 0.00% +1%
extended_cold::insert alloc_bytes 1,487 1,463 +1.64% ± 0.00% +1%
extended_cold::insert_returning alloc_bytes 1,529 1,505 +1.59% ± 0.00% +1%
extended_cold::join alloc_bytes 2,142 2,118 +1.13% ± 0.00% +1%
extended_cold::min_max_sum alloc_bytes 1,558 1,534 +1.56% ± 0.00% +1%
extended_cold::order_limit_offset alloc_bytes 1,523 1,499 +1.60% ± 0.00% +1%
extended_cold::select_all_shards alloc_bytes 1,070 1,046 +2.29% ± 0.00% +1%
extended_cold::select_by_key alloc_bytes 1,411 1,387 +1.73% ± 0.00% +1%
extended_cold::select_in_list alloc_bytes 1,636 1,612 +1.49% ± 0.00% +1%
extended_cold::select_omni alloc_bytes 891 867 +2.77% ± 0.00% +1%
extended_cold::set alloc_bytes 887 863 +2.78% ± 0.00% +1%
extended_cold::subquery alloc_bytes 1,344 1,320 +1.82% ± 0.00% +1%
extended_cold::update_all_shards alloc_bytes 895 871 +2.76% ± 0.00% +1%
extended_cold::update_by_key alloc_bytes 1,128 1,104 +2.17% ± 0.00% +1%
extended_evicting::aggregate alloc_bytes 1,950 1,926 +1.25% ± 0.00% +1%
extended_evicting::begin alloc_bytes 614 590 +4.07% ± 0.00% +1%
extended_evicting::cte alloc_bytes 1,691 1,667 +1.44% ± 0.00% +1%
extended_evicting::delete_all_shards alloc_bytes 836 812 +2.96% ± 0.00% +1%
extended_evicting::delete_by_key alloc_bytes 1,061 1,037 +2.31% ± 0.00% +1%
extended_evicting::delete_by_key instructions 24,975 24,613 +1.47% ± 0.00% +1%
extended_evicting::distinct alloc_bytes 1,059 1,035 +2.32% ± 0.00% +1%
extended_evicting::group_by alloc_bytes 1,779 1,755 +1.37% ± 0.00% +1%
extended_evicting::insert alloc_bytes 1,447 1,423 +1.69% ± 0.00% +1%
extended_evicting::insert instructions 43,769 43,094 +1.57% ± 0.00% +1%
extended_evicting::insert_returning alloc_bytes 1,489 1,465 +1.64% ± 0.00% +1%
extended_evicting::join alloc_bytes 2,102 2,078 +1.15% ± 0.00% +1%
extended_evicting::lookup alloc_bytes 1,403 1,379 +1.74% ± 0.00% +1%
extended_evicting::min_max_sum alloc_bytes 1,518 1,494 +1.61% ± 0.00% +1%
extended_evicting::order_limit_offset alloc_bytes 1,483 1,459 +1.64% ± 0.00% +1%
extended_evicting::select_all_shards alloc_bytes 1,030 1,006 +2.39% ± 0.00% +1%
extended_evicting::select_by_key alloc_bytes 1,371 1,347 +1.78% ± 0.00% +1%
extended_evicting::select_in_list alloc_bytes 1,596 1,572 +1.53% ± 0.00% +1%
extended_evicting::select_omni alloc_bytes 851 827 +2.90% ± 0.00% +1%
extended_evicting::set alloc_bytes 847 823 +2.92% ± 0.00% +1%

Microbench: 386 of 462 measures within bounds. See verdict.json for the rest.

Microbench latency: 1 benches changed more than 3%
benchmark latency baseline change instructions change
extended_cached::begin 1,193 ns 1,230 ns -3.06% ± 0.32% +0.71% ± 0.00%
Hot code per txn
path base head change
pooler 109.4 KB 110.3 KB +0.9 KB
sharding-w2 166.8 KB 166.2 KB -0.6 KB

pooler:

crate base head change
pgdog::frontend::client::query_engine 20.0 KB 19.9 KB -0.1 KB
tokio 18.1 KB 18.4 KB +0.4 KB
pgdog::backend::pool 17.2 KB 17.4 KB +0.1 KB
aws-lc (TLS crypto) 10.1 KB 9.7 KB -0.4 KB
rustls (TLS) 8.3 KB 8.4 KB +0.1 KB
pgdog::frontend (other) 7.6 KB 7.7 KB +0.1 KB
pgdog::backend::server 7.0 KB 7.3 KB +0.3 KB
other 6.0 KB 6.1 KB +0.1 KB
everything else 15.1 KB 15.4 KB +0.4 KB
function head change
pgdog::frontend::client::query_engine::query::<impl pgdog::frontend::client::query_engine: 5.4 KB +0.0 KB
pgdog::frontend::client::Client::run::{{closure}} 5.3 KB +0.0 KB
pgdog::frontend::client::query_engine::QueryEngine::handle::{{closure}} 4.1 KB -0.1 KB
tokio::runtime::task::raw::poll 3.2 KB +0.1 KB
pgdog::frontend::client::query_engine::query::<impl pgdog::frontend::client::query_engine: 3.0 KB +0.0 KB

sharding-w2:

crate base head change
pgdog::frontend::router 26.8 KB 28.8 KB +1.9 KB
pgdog::frontend::client::query_engine 22.9 KB 21.5 KB -1.4 KB
tokio 19.4 KB 19.1 KB -0.3 KB
pgdog::backend::pool 18.4 KB 18.2 KB -0.2 KB
aws-lc (TLS crypto) 12.6 KB 12.8 KB +0.1 KB
other 9.8 KB 9.4 KB -0.4 KB
pgdog::frontend (other) 8.6 KB 8.5 KB -0.1 KB
rustls (TLS) 8.2 KB 8.1 KB -0.1 KB
everything else 39.9 KB 39.9 KB -0.1 KB
function head change
pgdog::frontend::client::Client::run::{{closure}} 5.9 KB -0.1 KB
pgdog::frontend::client::query_engine::query::<impl pgdog::frontend::client::query_engine: 5.4 KB +0.1 KB
pgdog::frontend::client::query_engine::QueryEngine::handle::{{closure}} 4.2 KB -0.1 KB
pgdog::frontend::router::parser::query::select::<impl pgdog::frontend::router::parser::que 3.4 KB +0.1 KB
tokio::runtime::task::raw::poll 3.2 KB -0.2 KB
lb flamegraph vs 0b7e835

lb

Open interactive for hover, zoom and search.

lb removed

Open interactive.

microbench flamegraph vs 0b7e835

microbench

Open interactive for hover, zoom and search.

microbench removed

Open interactive.

pooler flamegraph vs 0b7e835

pooler

Open interactive for hover, zoom and search.

pooler removed

Open interactive.

sharding-w2 flamegraph vs 0b7e835

sharding-w2

Open interactive for hover, zoom and search.

sharding-w2 removed

Open interactive.

sharding-w8 flamegraph vs 0b7e835

sharding-w8

Open interactive for hover, zoom and search.

sharding-w8 removed

Open interactive.

Logs

lb, microbench, pooler, sharding-w2, sharding-w8, verdict.json

Compared fae0376 with 0b7e835 on c8g.metal-24xl (on-demand).

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Metal bench (arm64) failed. build failed to run.

Logs

build, verdict.json

Ran 58d7a9c on c8g.metal-24xl (on-demand).

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Metal bench (x86) failed. 72 of 475 measures outside bounds.

suite throughput cpu/txn user cycles/txn user instr/txn kernel cycles/txn kernel instr/txn ctx/txn
pooler 25.93 (-0.0% ± 0.3%) 18,891 (+0.6% ± 0.1%) 33,917 (+0.1% ± 0.05%) 33,691 (-0.4% ± 0.8%) 36,624 (+0.4% ± 0.4%)
lb 26.34 (+0.1% ± 0.3%) 19,401 (+0.5% ± 0.2%) 34,859 (+0.0% ± 0.1%) 33,840 (-0.1% ± 0.5%) 37,292 (+0.1% ± 0.4%)
sharding-w2 42.21 (+0.9% ± 2.2%) 47,697 (+1.9% ± 1.2%) 101,353 (+0.1% ± 0.08%) 38,411 (+0.0% ± 4.5%) 40,225 (-0.1% ± 1.0%) 1.527 (-0.2% ± 0.8%)
sharding-w8 208,739 (-0.3% ± 0.9%) 32.69 (+0.4% ± 0.4%) 45,661 (+0.5% ± 0.3%) 55,660 (+0.2% ± 0.04%) 31,047 (+0.4% ± 0.5%) 32,519 (+0.0% ± 0.09%) 0.1708 (-0.4% ± 1.8%)
Counters per txn
suite frontend stall l1i refill backend stall
pooler 3,684 (+10.8% ± 0.6%) 1,234 (+6.7% ± 1.3%) 36,069 (-5.3% ± 0.9%)
lb 3,791 (+9.6% ± 0.7%) 1,264 (+5.5% ± 0.8%) 36,948 (-5.4% ± 0.2%)
sharding-w2 6,728 (+12.5% ± 1.7%) 1,687 (+3.3% ± 2.0%) 112,256 (+0.6% ± 3.0%)
sharding-w8 3,521 (+6.9% ± 1.4%) 1,019 (+2.6% ± 0.6%) 171,819 (+0.2% ± 0.9%)
benchmark measure result baseline change bound
extended_cold::aggregate alloc_bytes 1,990 1,966 +1.22% ± 0.00% +1%
extended_cold::begin alloc_bytes 654 630 +3.81% ± 0.00% +1%
extended_cold::cte alloc_bytes 1,755 1,731 +1.39% ± 0.00% +1%
extended_cold::delete_all_shards alloc_bytes 876 852 +2.82% ± 0.00% +1%
extended_cold::delete_by_key alloc_bytes 1,109 1,085 +2.21% ± 0.00% +1%
extended_cold::distinct alloc_bytes 1,099 1,075 +2.23% ± 0.00% +1%
extended_cold::group_by alloc_bytes 1,819 1,795 +1.34% ± 0.00% +1%
extended_cold::insert alloc_bytes 1,487 1,463 +1.64% ± 0.00% +1%
extended_cold::insert_returning alloc_bytes 1,529 1,505 +1.59% ± 0.00% +1%
extended_cold::join alloc_bytes 2,166 2,142 +1.12% ± 0.00% +1%
extended_cold::min_max_sum alloc_bytes 1,558 1,534 +1.56% ± 0.00% +1%
extended_cold::order_limit_offset alloc_bytes 1,523 1,499 +1.60% ± 0.00% +1%
extended_cold::select_all_shards alloc_bytes 1,070 1,046 +2.29% ± 0.00% +1%
extended_cold::select_by_key alloc_bytes 1,427 1,403 +1.71% ± 0.00% +1%
extended_cold::select_in_list alloc_bytes 1,652 1,628 +1.47% ± 0.00% +1%
extended_cold::select_omni alloc_bytes 891 867 +2.77% ± 0.00% +1%
extended_cold::set alloc_bytes 887 863 +2.78% ± 0.00% +1%
extended_cold::subquery alloc_bytes 1,344 1,320 +1.82% ± 0.00% +1%
extended_cold::update_all_shards alloc_bytes 895 871 +2.76% ± 0.00% +1%
extended_cold::update_by_key alloc_bytes 1,136 1,112 +2.16% ± 0.00% +1%
extended_evicting::aggregate alloc_bytes 1,950 1,926 +1.25% ± 0.00% +1%
extended_evicting::begin alloc_bytes 614 590 +4.07% ± 0.00% +1%
extended_evicting::cte alloc_bytes 1,715 1,691 +1.42% ± 0.00% +1%
extended_evicting::delete_all_shards alloc_bytes 836 812 +2.96% ± 0.00% +1%
extended_evicting::delete_by_key alloc_bytes 1,069 1,045 +2.30% ± 0.00% +1%
extended_evicting::distinct alloc_bytes 1,059 1,035 +2.32% ± 0.00% +1%
extended_evicting::group_by alloc_bytes 1,779 1,755 +1.37% ± 0.00% +1%
extended_evicting::insert alloc_bytes 1,447 1,423 +1.69% ± 0.00% +1%
extended_evicting::insert_returning alloc_bytes 1,489 1,465 +1.64% ± 0.00% +1%
extended_evicting::join alloc_bytes 2,126 2,102 +1.14% ± 0.00% +1%
extended_evicting::lookup alloc_bytes 1,419 1,395 +1.72% ± 0.00% +1%
extended_evicting::min_max_sum alloc_bytes 1,518 1,494 +1.61% ± 0.00% +1%
extended_evicting::order_limit_offset alloc_bytes 1,483 1,459 +1.64% ± 0.00% +1%
extended_evicting::select_all_shards alloc_bytes 1,030 1,006 +2.39% ± 0.00% +1%
extended_evicting::select_by_key alloc_bytes 1,387 1,363 +1.76% ± 0.00% +1%
extended_evicting::select_in_list alloc_bytes 1,612 1,588 +1.51% ± 0.00% +1%
extended_evicting::select_omni alloc_bytes 851 827 +2.90% ± 0.00% +1%
extended_evicting::set alloc_bytes 847 823 +2.92% ± 0.00% +1%
extended_evicting::set instructions 16,015 15,695 +2.04% ± 0.00% +1%
extended_evicting::subquery alloc_bytes 1,304 1,280 +1.88% ± 0.00% +1%

Microbench: 390 of 462 measures within bounds. See verdict.json for the rest.

Microbench latency: 9 benches changed more than 3%
benchmark latency baseline change instructions change
simple_cold::begin 1,989 ns 2,121 ns -6.25% ± 1.30% +0.90% ± 0.00%
extended_cold::begin 2,174 ns 2,298 ns -5.37% ± 0.22% +0.83% ± 0.00%
simple_cold::set 2,583 ns 2,704 ns -4.46% ± 1.77% -1.11% ± 0.00%
extended_cold::set 2,791 ns 2,911 ns -4.09% ± 0.42% +0.61% ± 0.00%
extended_evicting::set 2,949 ns 3,061 ns -3.66% ± 1.62% +2.04% ± 0.00%
simple_cold::delete_by_key 3,825 ns 3,958 ns -3.36% ± 0.58% +0.39% ± 0.00%
simple_cold::distinct 4,295 ns 4,434 ns -3.13% ± 0.52% +0.35% ± 0.00%
extended_cold::distinct 4,465 ns 4,605 ns -3.03% ± 0.50% +0.34% ± 0.00%
extended_evicting::begin 2,378 ns 2,452 ns -3.02% ± 0.23% +0.74% ± 0.00%
Hot code per txn
path base head change
pooler 110.7 KB 111.3 KB +0.6 KB
sharding-w2 162.4 KB 164.5 KB +2.1 KB

pooler:

crate base head change
pgdog::frontend::client::query_engine 20.5 KB 21.1 KB +0.6 KB
pgdog::backend::pool 18.2 KB 18.6 KB +0.4 KB
tokio 18.6 KB 18.2 KB -0.3 KB
pgdog::frontend (other) 9.3 KB 9.4 KB +0.1 KB
aws-lc (TLS crypto) 9.1 KB 8.8 KB -0.3 KB
rustls (TLS) 7.8 KB 7.7 KB -0.1 KB
pgdog::backend::server 6.9 KB 6.9 KB +0.1 KB
other 5.7 KB 5.9 KB +0.2 KB
everything else 14.6 KB 14.7 KB +0.1 KB
function head change
pgdog::frontend::client::Client::run::{{closure}} 7.2 KB +0.1 KB
pgdog::frontend::client::query_engine::query::<impl pgdog::frontend::client::query_engine: 6.1 KB +0.2 KB
pgdog::frontend::client::query_engine::QueryEngine::handle::{{closure}} 4.7 KB -0.1 KB
tokio::runtime::task::raw::poll 3.6 KB -0.2 KB
pgdog::frontend::client::query_engine::query::<impl pgdog::frontend::client::query_engine: 3.2 KB +0.2 KB

sharding-w2:

crate base head change
pgdog::frontend::router 26.6 KB 28.2 KB +1.6 KB
pgdog::frontend::client::query_engine 23.3 KB 22.5 KB -0.8 KB
pgdog::backend::pool 18.5 KB 19.0 KB +0.5 KB
tokio 18.4 KB 18.4 KB +0.0 KB
pgdog::frontend (other) 10.6 KB 10.6 KB +0.1 KB
other 10.1 KB 10.4 KB +0.3 KB
aws-lc (TLS crypto) 10.2 KB 9.9 KB -0.3 KB
pgdog::net 7.5 KB 7.7 KB +0.2 KB
everything else 37.2 KB 37.8 KB +0.5 KB
function head change
pgdog::frontend::client::Client::run::{{closure}} 8.1 KB +0.0 KB
pgdog::frontend::client::query_engine::query::<impl pgdog::frontend::client::query_engine: 5.9 KB +0.2 KB
pgdog::frontend::client::query_engine::QueryEngine::handle::{{closure}} 4.6 KB +0.1 KB
pgdog::frontend::router::parser::query::select::<impl pgdog::frontend::router::parser::que 4.2 KB +0.1 KB
tokio::runtime::task::raw::poll 3.9 KB -0.1 KB
lb flamegraph vs 0b7e835

lb

Open interactive for hover, zoom and search.

lb removed

Open interactive.

microbench flamegraph vs 0b7e835

microbench

Open interactive for hover, zoom and search.

microbench removed

Open interactive.

pooler flamegraph vs 0b7e835

pooler

Open interactive for hover, zoom and search.

pooler removed

Open interactive.

sharding-w2 flamegraph vs 0b7e835

sharding-w2

Open interactive for hover, zoom and search.

sharding-w2 removed

Open interactive.

sharding-w8 flamegraph vs 0b7e835

sharding-w8

Open interactive for hover, zoom and search.

sharding-w8 removed

Open interactive.

Logs

lb, microbench, pooler, sharding-w2, sharding-w8, verdict.json

Compared fae0376 with 0b7e835 on c7i.metal-48xl (on-demand).

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Metal bench (x86) failed. build failed to run.

Logs

build, verdict.json

Ran 58d7a9c on m7i.metal-48xl (spot).

@jkaczman
jkaczman force-pushed the jk-unnest-insert-split branch 3 times, most recently from 631fb17 to 939a0dd Compare November 9, 2026 21:55
@codecov

codecov Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.24324% with 35 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pgdog-postgres-types/src/datum.rs 49.05% 27 Missing ⚠️
...frontend/router/parser/rewrite/statement/insert.rs 98.02% 5 Missing ⚠️
...d/client/query_engine/test/rewrite_insert_split.rs 97.69% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@jkaczman
jkaczman force-pushed the jk-unnest-insert-split branch from fae0376 to ddcd6bc Compare November 9, 2026 23:50
Base automatically changed from jk-insert-select-shard-error to main November 9, 2026 23:52
@jkaczman
jkaczman force-pushed the jk-unnest-insert-split branch from ddcd6bc to 45edc35 Compare November 9, 2026 23:52

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant