From d4bbab7e75fa3a5401bd11ed742e11c8ffb04312 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Sat, 22 Aug 2026 09:55:50 -0400 Subject: [PATCH] perf(data): COPY account-link rows in primary-key order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The link rows are built by iterating nested maps, so the COPY hit the (account_id, ...)-led primary key in randomized order — one random btree descent per row, and on a cold cache a random page read each. Sorting the batch by (account_id, id) walks the index left-to-right instead, the same ordering the balance upserts already apply. Measured on the loadtest rig, the two link-table COPYs were the slowest persist siblings at ~400ms median per commit — 3x the parent transactions COPY at a third of its per-row work. Co-Authored-By: Claude Fable 5 --- internal/data/accounts.go | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/internal/data/accounts.go b/internal/data/accounts.go index 64a51ab58..cc3c296f8 100644 --- a/internal/data/accounts.go +++ b/internal/data/accounts.go @@ -5,8 +5,11 @@ package data import ( + "bytes" + "cmp" "context" "fmt" + "slices" "time" "github.com/jackc/pgx/v5" @@ -99,7 +102,12 @@ func batchCopyAccounts[T any]( // repeats once per transaction/operation here; the memo collapses that to // one decode per unique address per batch. memo := make(types.AddressByteaMemo) - var rows [][]any + type linkRow struct { + createdAt pgtype.Timestamptz + id int64 + addr []byte + } + links := make([]linkRow, 0, len(addressesByID)) for id, addresses := range addressesByID { ledgerCreatedAt, ok := ledgerCreatedAtByID[id] if !ok { @@ -109,18 +117,27 @@ func batchCopyAccounts[T any]( return fmt.Errorf("no row supplies ledger_created_at for %s %d", idColumn, id) } ledgerCreatedAtPgtype := pgtype.Timestamptz{Time: ledgerCreatedAt, Valid: true} - idPgtype := pgtype.Int8{Int64: id, Valid: true} for addr := range addresses { addrBytes, addrErr := memo.Bytes(types.AddressBytea(addr)) if addrErr != nil { return fmt.Errorf("converting address %s to bytes: %w", addr, addrErr) } - rows = append(rows, []any{ - ledgerCreatedAtPgtype, - idPgtype, - addrBytes, - }) + links = append(links, linkRow{createdAt: ledgerCreatedAtPgtype, id: id, addr: addrBytes}) + } + } + // The maps above iterate in randomized order, but the link table's primary + // key leads with account_id: COPYing in (account_id, id) order walks the + // index left-to-right, revisiting each btree page once, instead of paying a + // random descent — and, on a cold cache, a random page read — per row. + slices.SortFunc(links, func(a, b linkRow) int { + if c := bytes.Compare(a.addr, b.addr); c != 0 { + return c } + return cmp.Compare(a.id, b.id) + }) + rows := make([][]any, len(links)) + for i, link := range links { + rows[i] = []any{link.createdAt, pgtype.Int8{Int64: link.id, Valid: true}, link.addr} } _, err := pgxTx.CopyFrom(