Skip to content

Commit 3fda4b0

Browse files
VojtechVitekclaude
andcommitted
perf(table): reuse RowScanner across Iter rows
Iter created a fresh scany RowScanner per row via pgxscan.API.ScanRow, recomputing the column-to-field mapping on every iteration. Create one RowScanner before the loop and reuse it so the reflection work happens once per query instead of once per row. Benchmarked via BenchmarkTableIter (in-memory fake pgx.Rows), count=8: rows=1000 time 394.8µs -> 177.5µs (-55%) B/op 406.6Ki -> 63.2Ki (-84%) allocs 6002 -> 1008 (-83%, ~6/row -> ~1/row) All deltas p=0.000. GetAll/List/pagination already reuse a single RowScanner internally, so this only affects the Iter streaming path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 33be518 commit 3fda4b0

1 file changed

Lines changed: 7 additions & 1 deletion

File tree

table.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,12 +565,18 @@ func (t *Table[T, P, I]) Iter(ctx context.Context, where sq.Sqlizer, orderBy []s
565565
}
566566

567567
// iterRows yields records scanned from rows, closing rows when iteration ends.
568+
//
569+
// A single RowScanner is created once and reused across every row: it caches
570+
// the column-to-field mapping after the first Scan, so the reflection work is
571+
// done once per query rather than once per row (which pgxscan.API.ScanRow
572+
// would do by allocating a fresh RowScanner each call).
568573
func (t *Table[T, P, I]) iterRows(rows pgx.Rows) iter.Seq2[P, error] {
569574
return func(yield func(P, error) bool) {
570575
defer rows.Close()
576+
rs := t.Query.Scan.NewRowScanner(rows)
571577
for rows.Next() {
572578
var record T
573-
if err := t.Query.Scan.ScanRow(&record, rows); err != nil {
579+
if err := rs.Scan(&record); err != nil {
574580
yield(nil, err)
575581
return
576582
}

0 commit comments

Comments
 (0)