Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ file. This project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Fixed
- Reduce PostgreSQL temp-file I/O for the `/trades` endpoint when filtered by account, offer, or liquidity pool. The account-filter query pattern was the dominant contributor to slow-query time and replication lag on pubnet. The subquery UNION now uses `UNION ALL` (the two branches are disjoint by protocol invariant — an account cannot match its own offer, a trade matches two distinct offers, and an LP trade has the pool on exactly one side) and each branch now applies the outer `LIMIT` so only the top rows per branch are materialized rather than the entire filtered set.

### DB Schema Migration
- Add composite index `htrd_by_base_account_op_order` on `history_trades(base_account_id, history_operation_id, "order")` and `htrd_by_counter_account_op_order` on `(counter_account_id, history_operation_id, "order")` to give the planner a density-independent, spill-free plan for account-filtered trade queries. At pubnet scale each index build takes multiple hours; the migration uses `CREATE INDEX CONCURRENTLY` to avoid blocking writes. The existing single-column indexes `htrd_by_base_account` and `htrd_by_counter_account` are retained until follow-up observation confirms the composites cover all usage.

## 26.0.0

**This release adds support for Protocol 26**
Expand Down
12 changes: 9 additions & 3 deletions internal/db2/history/trade.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,8 @@ func createTradesSQL(page db2.PageQuery, oldestLedger int32, query historyTrades
secondSelect = sql.Where("htrd.counter_liquidity_pool_id = ?", query.poolID)
}

firstSelect = appendOrdering(firstSelect, oldestLedger, op, idx, page.Order)
secondSelect = appendOrdering(secondSelect, oldestLedger, op, idx, page.Order)
firstSelect = appendOrdering(firstSelect, oldestLedger, op, idx, page.Order).Limit(page.Limit)
secondSelect = appendOrdering(secondSelect, oldestLedger, op, idx, page.Order).Limit(page.Limit)
firstSQL, firstArgs, err := firstSelect.ToSql()
if err != nil {
return "", nil, errors.Wrap(err, "error building a firstSelect query")
Expand All @@ -215,7 +215,13 @@ func createTradesSQL(page db2.PageQuery, oldestLedger int32, query historyTrades
return "", nil, errors.Wrap(err, "error building a secondSelect query")
}

rawSQL := fmt.Sprintf("(%s) UNION (%s) ", firstSQL, secondSQL)
// UNION ALL is safe here: the two branches filter on base_/counter_ columns of the same
// field (account, offer, or liquidity_pool), and stellar-core's invariants guarantee those
// columns never hold the same non-null value in a single trade row — an account cannot
// match its own offer, a trade matches two distinct offers, and an LP trade has one side
// as the pool and the other as an account (so exactly one of base_/counter_liquidity_pool_id
// is non-null). This lets PostgreSQL skip the dedup sort UNION would otherwise force.
rawSQL := fmt.Sprintf("(%s) UNION ALL (%s) ", firstSQL, secondSQL)
args := append(firstArgs, secondArgs...)
// Order the final UNION:
switch page.Order {
Expand Down
29 changes: 29 additions & 0 deletions internal/db2/history/trade_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package history

import (
"strings"
"testing"

"github.com/stellar/go-stellar-sdk/toid"
Expand Down Expand Up @@ -41,6 +42,34 @@ func filterByAccount(trades []Trade, account string) []Trade {
return result
}

// TestCreateTradesSQLUsesUnionAll guards against a regression where the account,
// offer, or liquidity-pool filter variant's subquery UNION is switched back to
// plain UNION (which forces a server-side dedup sort and was the dominant
// temp-file I/O source on pubnet). The two branches are disjoint by protocol
// invariant, so UNION ALL is semantically equivalent and significantly cheaper.
func TestCreateTradesSQLUsesUnionAll(t *testing.T) {
cases := []struct {
name string
query historyTradesQuery
}{
{"account", historyTradesQuery{accountID: 1, tradeType: AllTrades, orderPreserved: true}},
{"offer", historyTradesQuery{offerID: 1, tradeType: AllTrades, orderPreserved: true}},
{"pool", historyTradesQuery{poolID: 1, tradeType: AllTrades, orderPreserved: true}},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
sql, _, err := createTradesSQL(descPQ, 0, c.query)
if err != nil {
t.Fatalf("createTradesSQL failed: %v", err)
}
if !strings.Contains(sql, "UNION ALL") {
t.Errorf("generated SQL must use UNION ALL (not UNION) for the %s filter; got: %s", c.name, sql)
}
})
}
}

func TestSelectTrades(t *testing.T) {
tt := test.Start(t)
defer tt.Finish()
Expand Down
Loading
Loading