Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
23 changes: 23 additions & 0 deletions internal/db2/schema/bindata.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- +migrate Up notransaction

CREATE INDEX CONCURRENTLY IF NOT EXISTS htrd_by_base_account_op_order
ON history_trades (base_account_id, history_operation_id, "order");

CREATE INDEX CONCURRENTLY IF NOT EXISTS htrd_by_counter_account_op_order
ON history_trades (counter_account_id, history_operation_id, "order");

-- +migrate Down notransaction

DROP INDEX IF EXISTS htrd_by_counter_account_op_order;
DROP INDEX IF EXISTS htrd_by_base_account_op_order;
Loading