American Express's disputes team pulls a cardholder's disputed high-value transactions for case review. The query below is used to build that view, but on the production card_transactions table, which holds billions of rows, it's showing up as one of the slowest queries in the nightly performance report.
CREATE TABLE card_transactions (
transaction_id BIGINT PRIMARY KEY,
cardholder_id BIGINT,
card_number_masked TEXT, -- e.g. '**** **** **** 4471'
merchant_id INT,
amount NUMERIC(10,2),
txn_time TIMESTAMP,
status TEXT
);
CREATE TABLE disputes (
dispute_id BIGINT PRIMARY KEY,
transaction_id BIGINT REFERENCES card_transactions(transaction_id),
filed_at TIMESTAMP,
reason TEXT
);
CREATE INDEX idx_txn_cardholder ON card_transactions(cardholder_id);
SELECT t.transaction_id, t.amount, t.txn_time, t.merchant_id
FROM card_transactions t
WHERE t.cardholder_id = 88213471
AND EXTRACT(YEAR FROM t.txn_time) = 2024
AND t.amount > 500
AND t.card_number_masked LIKE '%4471'
AND (SELECT COUNT(*) FROM disputes d WHERE d.transaction_id = t.transaction_id) > 0
ORDER BY t.amount DESC
OFFSET 5000 LIMIT 50;
Explain the execution plan of this query and identify potential bottlenecks.
| Column | Type | Description |
|---|---|---|
| transaction_idPK | BIGINT | Unique transaction identifier |
| cardholder_id | BIGINT | Cardholder owning the transaction |
| card_number_masked | TEXT | Masked card number used for display and filtering |
| merchant_id | INT | Merchant identifier |
| amount | NUMERIC(10,2) | Transaction amount |
| txn_time | TIMESTAMP | Transaction timestamp |
| status | TEXT | Transaction status |
| Column | Type | Description |
|---|---|---|
| dispute_idPK | BIGINT | Unique dispute identifier |
| transaction_id | BIGINT | Referenced transaction identifier |
| filed_at | TIMESTAMP | When the dispute was filed |
| reason | TEXT | Dispute reason |