Skip to content

Commit

Permalink
/db/history: update the claimable balance SQL query (#4398)
Browse files Browse the repository at this point in the history
* #4382: refined the placement of LIMIT clause on claimant balance query by claimant id per profiling results.
  • Loading branch information
sreuland authored May 25, 2022
1 parent e0b4d32 commit da94a37
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 7 deletions.
23 changes: 17 additions & 6 deletions services/horizon/internal/db2/history/claimable_balances.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,25 @@ func (cbq ClaimableBalancesQuery) Cursor() (int64, string, error) {
// indexes.
func (cbq ClaimableBalancesQuery) ApplyCursor(sql sq.SelectBuilder) (sq.SelectBuilder, error) {
p := cbq.PageQuery
hasPagedLimit := false
l, r, err := cbq.Cursor()
if err != nil {
return sql, err
}
if l > 0 && r != "" {
hasPagedLimit = true
}

switch p.Order {
case db2.OrderAscending:
if l > 0 && r != "" {
if hasPagedLimit {
sql = sql.
Where(sq.Expr("(cb.last_modified_ledger, cb.id) > (?, ?)", l, r))

}
sql = sql.OrderBy("cb.last_modified_ledger asc, cb.id asc")
case db2.OrderDescending:
if l > 0 && r != "" {
if hasPagedLimit {
sql = sql.
Where(sq.Expr("(cb.last_modified_ledger, cb.id) < (?, ?)", l, r))
}
Expand Down Expand Up @@ -197,11 +202,17 @@ func (q *Q) FindClaimableBalanceByID(ctx context.Context, balanceID string) (Cla
// GetClaimableBalances finds all claimable balances where accountID is one of the claimants
func (q *Q) GetClaimableBalances(ctx context.Context, query ClaimableBalancesQuery) ([]ClaimableBalance, error) {
sql, err := query.ApplyCursor(selectClaimableBalances)
// we need to use WITH syntax and correct LIMIT placement to force the query planner to use the right
// indexes, otherwise when the limit is small, it will use an index scan
// which will be very slow once we have millions of records
limitClausePlacement := "LIMIT ?) select " + claimableBalancesSelectStatement + " from cb"

if err != nil {
return nil, errors.Wrap(err, "could not apply query to page")
}

if query.Asset != nil {
// when search by asset, profiling has shown best performance to have the LIMIT on inner query
sql = sql.Where("cb.asset = ?", query.Asset)
}

Expand All @@ -212,15 +223,15 @@ func (q *Q) GetClaimableBalances(ctx context.Context, query ClaimableBalancesQue
if query.Claimant != nil {
sql = sql.
Where(`cb.claimants @> '[{"destination": "` + query.Claimant.Address() + `"}]'`)
// when search by claimant, profiling has shown the LIMIT should be on the outer query to
// hint appropriate indexes for best performance
limitClausePlacement = ") select " + claimableBalancesSelectStatement + " from cb LIMIT ?"
}

// we need to use WITH syntax to force the query planner to use the right
// indexes, otherwise when the limit is small, it will use an index scan
// which will be very slow once we have millions of records
sql = sql.
Prefix("WITH cb AS (").
Suffix(
"LIMIT ?) select "+claimableBalancesSelectStatement+" from cb",
limitClausePlacement,
query.PageQuery.Limit,
)

Expand Down
21 changes: 20 additions & 1 deletion services/horizon/internal/db2/history/claimable_balances_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package history

import (
"fmt"
"testing"

"github.com/guregu/null"
Expand Down Expand Up @@ -114,7 +115,7 @@ func TestFindClaimableBalancesByDestination(t *testing.T) {
},
},
Asset: asset,
LastModifiedLedger: 123,
LastModifiedLedger: 300,
Amount: 10,
}

Expand All @@ -126,6 +127,7 @@ func TestFindClaimableBalancesByDestination(t *testing.T) {
Claimant: xdr.MustAddressPtr(dest1),
}

// this validates the cb query with claimant parameter
cbs, err := q.GetClaimableBalances(tt.Ctx, query)
tt.Assert.NoError(err)
tt.Assert.Len(cbs, 2)
Expand All @@ -134,11 +136,28 @@ func TestFindClaimableBalancesByDestination(t *testing.T) {
tt.Assert.Equal(dest1, cb.Claimants[0].Destination)
}

// this validates the cb query with different claimant parameter
query.Claimant = xdr.MustAddressPtr(dest2)
cbs, err = q.GetClaimableBalances(tt.Ctx, query)
tt.Assert.NoError(err)
tt.Assert.Len(cbs, 1)
tt.Assert.Equal(dest2, cbs[0].Claimants[1].Destination)

// this validates the cb query with claimant and cb.id/ledger cursor parameters
query.PageQuery = db2.MustPageQuery(fmt.Sprintf("%v-%s", 150, cbs[0].BalanceID), false, "", 10)
query.Claimant = xdr.MustAddressPtr(dest1)
cbs, err = q.GetClaimableBalances(tt.Ctx, query)
tt.Assert.NoError(err)
tt.Assert.Len(cbs, 1)
tt.Assert.Equal(dest2, cbs[0].Claimants[1].Destination)

// this validates the cb query with no claimant parameter,
// should still produce working sql, as it triggers different LIMIT position in sql.
query.PageQuery = db2.MustPageQuery("", false, "", 1)
query.Claimant = nil
cbs, err = q.GetClaimableBalances(tt.Ctx, query)
tt.Assert.NoError(err)
tt.Assert.Len(cbs, 1)
}

func TestUpdateClaimableBalance(t *testing.T) {
Expand Down

0 comments on commit da94a37

Please sign in to comment.