lnd/sqldb/sqlc/queries/invoices.sql
ziggie e02f77ec9f
invoices+sqldb/sqlc: replace offset-based pagination with cursor-based
The invoice filter queries (FetchPendingInvoices,
FilterInvoicesBySettleIndex, FilterInvoicesByAddIndex,
FilterInvoicesForward, FilterInvoicesReverse) all used LIMIT+OFFSET for
internal pagination. This causes SQLite to build an ephemeral temp
B-tree for every page to implement the OFFSET skip, making each
successive page O(offset+limit). On nodes with large invoice histories
this compounds into a significant CPU cost — profiling showed
FilterInvoicesReverse consuming 53% of total CPU, with _sqlite3BtreeInsert
and _balance_nonroot (2.4s combined) appearing inside the SELECT due to
the temp B-tree being built and rebalanced to skip rows.

Replace the OFFSET loop (queryWithLimit) with cursor-based pagination
across all four callers in sql_store.go:

- FetchPendingInvoices: add id_cursor param, advance cursor to
  last_id + 1 each page.
- InvoicesSettledSince: add id_cursor param alongside the existing
  settle_index lower bound, advance cursor to last_id + 1 each page.
- InvoicesAddedSince: cursor starts at idx+1, advances to last_id+1.
- QueryInvoices: forward cursor starts at IndexOffset+1 and advances
  by +1; reverse cursor starts at IndexOffset-1 (or MaxInt64) and
  advances by -1. Inclusive SQL bounds (>= / <=) are preserved so
  query semantics and all existing callers are unchanged.

The queryWithLimit helper is removed as it has no remaining callers.

Each page now performs a single PK seek + forward scan of exactly
page_size rows with no temp sort structure, matching the cursor-based
pattern already used by the payments filter queries.
2026-04-01 10:55:42 +02:00

221 lines
6.8 KiB
SQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- name: InsertInvoice :one
INSERT INTO invoices (
hash, preimage, memo, amount_msat, cltv_delta, expiry, payment_addr,
payment_request, payment_request_hash, state, amount_paid_msat, is_amp,
is_hodl, is_keysend, created_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15
) RETURNING id;
-- name: InsertMigratedInvoice :one
INSERT INTO invoices (
hash, preimage, settle_index, settled_at, memo, amount_msat, cltv_delta,
expiry, payment_addr, payment_request, payment_request_hash, state,
amount_paid_msat, is_amp, is_hodl, is_keysend, created_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17
) RETURNING id;
-- name: InsertInvoiceFeature :exec
INSERT INTO invoice_features (
invoice_id, feature
) VALUES (
$1, $2
);
-- name: GetInvoiceFeatures :many
SELECT *
FROM invoice_features
WHERE invoice_id = $1;
-- name: GetInvoiceByHash :one
SELECT i.*
FROM invoices i
WHERE i.hash = $1;
-- name: GetInvoiceByAddr :one
SELECT i.*
FROM invoices i
WHERE i.payment_addr = $1;
-- name: GetInvoiceBySetID :many
-- TODO(ziggie): This query can only return one invoice if the set_id is
-- the primary key of amp_sub_invoices table.
SELECT i.*
FROM invoices i
INNER JOIN amp_sub_invoices a
ON i.id = a.invoice_id AND a.set_id = $1;
-- name: FetchPendingInvoices :many
-- FetchPendingInvoices returns all invoices in a pending state (open or
-- accepted). The invoices_state_idx index on the state column makes this a
-- fast index scan rather than a full table scan. id_cursor is an exclusive
-- lower bound on the primary key used for cursor-based pagination; the caller
-- must supply 0 when starting from the beginning.
SELECT
invoices.*
FROM invoices
WHERE state IN (0, 3) -- 0 = ContractOpen, 3 = ContractAccepted
AND id > @id_cursor
ORDER BY id ASC
LIMIT @num_limit;
-- name: FilterInvoicesBySettleIndex :many
-- FilterInvoicesBySettleIndex returns settled invoices whose settle_index is
-- greater than or equal to the given value, ordered by id. The caller must
-- always supply a concrete lower bound so the invoices_settle_index_idx index
-- can be used. id_cursor is an exclusive lower bound on the primary key used
-- for cursor-based pagination; the caller must supply 0 when starting from
-- the beginning.
SELECT
invoices.*
FROM invoices
WHERE settle_index >= @settle_index_get
AND id > @id_cursor
ORDER BY id ASC
LIMIT @num_limit;
-- name: FilterInvoicesByAddIndex :many
-- FilterInvoicesByAddIndex returns invoices whose add_index (primary key id)
-- is greater than or equal to the given value, ordered by id. Because id is
-- the primary key, this is always an efficient range scan on the clustered
-- index. For cursor-based pagination the caller advances add_index_get to
-- last_returned_id + 1 on each subsequent page.
SELECT
invoices.*
FROM invoices
WHERE id >= @add_index_get
ORDER BY id ASC
LIMIT @num_limit;
-- name: FilterInvoicesForward :many
-- FilterInvoicesForward returns invoices in ascending id order. All parameters
-- are non-nullable so the planner always sees plain range predicates and can
-- use the primary-key index. For cursor-based pagination the caller advances
-- add_index_get to last_returned_id + 1 on each subsequent page. The caller
-- is responsible for supplying Go-side defaults when a filter is not needed:
-- add_index_get → 1 (first valid invoice id)
-- created_after → time.Unix(0, 0).UTC() (epoch before any invoice)
-- created_before → time.Date(9999, …) (far future no upper cap)
-- pending_only → false (include all states)
SELECT
invoices.*
FROM invoices
WHERE id >= @add_index_get
AND (NOT @pending_only OR state IN (0, 3)) -- 0 = ContractOpen, 3 = ContractAccepted
AND created_at >= @created_after
AND created_at < @created_before
ORDER BY id ASC
LIMIT @num_limit;
-- name: FilterInvoicesReverse :many
-- FilterInvoicesReverse is the descending counterpart of FilterInvoicesForward.
-- It returns invoices in descending id order. For cursor-based pagination the
-- caller advances add_index_let to last_returned_id - 1 on each subsequent
-- page; pass math.MaxInt64 to start from the most recent invoice. See
-- FilterInvoicesForward for the expected Go-side defaults.
SELECT
invoices.*
FROM invoices
WHERE id <= @add_index_let
AND (NOT @pending_only OR state IN (0, 3)) -- 0 = ContractOpen, 3 = ContractAccepted
AND created_at >= @created_after
AND created_at < @created_before
ORDER BY id DESC
LIMIT @num_limit;
-- name: UpdateInvoiceState :execresult
UPDATE invoices
SET state = $2,
preimage = COALESCE(preimage, $3),
settle_index = COALESCE(settle_index, $4),
settled_at = COALESCE(settled_at, $5)
WHERE id = $1;
-- name: UpdateInvoiceAmountPaid :execresult
UPDATE invoices
SET amount_paid_msat = $2
WHERE id = $1;
-- name: NextInvoiceSettleIndex :one
UPDATE invoice_sequences SET current_value = current_value + 1
WHERE name = 'settle_index'
RETURNING current_value;
-- name: DeleteInvoice :execresult
DELETE
FROM invoices
WHERE (
id = sqlc.narg('add_index') OR
sqlc.narg('add_index') IS NULL
) AND (
hash = sqlc.narg('hash') OR
sqlc.narg('hash') IS NULL
) AND (
settle_index = sqlc.narg('settle_index') OR
sqlc.narg('settle_index') IS NULL
) AND (
payment_addr = sqlc.narg('payment_addr') OR
sqlc.narg('payment_addr') IS NULL
);
-- name: DeleteCanceledInvoices :execresult
DELETE
FROM invoices
WHERE state = 2;
-- name: InsertInvoiceHTLC :one
INSERT INTO invoice_htlcs (
htlc_id, chan_id, amount_msat, total_mpp_msat, accept_height, accept_time,
expiry_height, state, resolve_time, invoice_id
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
) RETURNING id;
-- name: GetInvoiceHTLCs :many
SELECT *
FROM invoice_htlcs
WHERE invoice_id = $1;
-- name: UpdateInvoiceHTLC :exec
UPDATE invoice_htlcs
SET state=$4, resolve_time=$5
WHERE htlc_id = $1 AND chan_id = $2 AND invoice_id = $3;
-- name: UpdateInvoiceHTLCs :exec
UPDATE invoice_htlcs
SET state=$2, resolve_time=$3
WHERE invoice_id = $1 AND resolve_time IS NULL;
-- name: InsertInvoiceHTLCCustomRecord :exec
INSERT INTO invoice_htlc_custom_records (
key, value, htlc_id
) VALUES (
$1, $2, $3
);
-- name: GetInvoiceHTLCCustomRecords :many
SELECT ihcr.htlc_id, key, value
FROM invoice_htlcs ih JOIN invoice_htlc_custom_records ihcr ON ih.id=ihcr.htlc_id
WHERE ih.invoice_id = $1;
-- name: InsertKVInvoiceKeyAndAddIndex :exec
INSERT INTO invoice_payment_hashes (
id, add_index
) VALUES (
$1, $2
);
-- name: SetKVInvoicePaymentHash :exec
UPDATE invoice_payment_hashes
SET hash = $2
WHERE id = $1;
-- name: GetKVInvoicePaymentHashByAddIndex :one
SELECT hash
FROM invoice_payment_hashes
WHERE add_index = $1;
-- name: ClearKVInvoiceHashIndex :exec
DELETE FROM invoice_payment_hashes;