Transactions endpoint /transactions Performance Improvement
#2452
Reference in New Issue
Block a user
Delete Branch "transactions-perf-improvement"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Overview
This PR implements comprehensive performance optimizations for the API endpoints, focusing on eliminating N+1 queries, improving database query efficiency, and enhancing response times for transaction-related operations.
Performance Issues Addressed
1. N+1 Query Elimination
Problem: Transfer associations were causing N+1 queries when accessing transaction data through the API. This was initially raised here and also validated using the Prosopite Gem
Solution:
Transfer#to_accountandTransfer#from_accountmethodswith_transfer_detailsin the Transaction model2. Query Optimization in API Controller
Problem: API endpoints were not efficiently loading all necessary associations.
Solution: Updated includes to load all required associations in a single query.
Before:
After:
Performance Improvements
Query Count Reduction
Response Time Improvement
🧊 Before Optimization
⚡ After Optimization
Comparison Summary
Query Count Validation
API Endpoint Testing
/api/v1/transactionsendpoint with various filtersAdditional Optimizations
I'm also considering adding indexes to the
TransfersandEntriestables to speed up association lookups that were also introducing significant latency to the endpointThanks for taking a deep dive into this!
Left a few comments, and I have a few additional questions:
kindcolumn toTransactionwhich should identify transfers and was hoping to move away from "reaching" through the transaction to get transfer/account/entry details. I'm wondering if there's a nicer way to pass these details through as-needed rather than the complex preloading this requires.@@ -15,11 +15,8 @@ class TransactionsController < ApplicationControllerIs this necessary? Might be missing something, but this would just move the query loading from the view to the controller; but not reduce the number of queries / load time.
@@ -94,3 +75,3 @@endall.size# arbitrary cutoff date to avoid expensive sync operationsMoving the
privatekeyword up here will cause all the other methods to be inaccessible, so we'll want to move this.@@ -27,0 +30,4 @@{ inflow_transaction: { entry: :account } },{ outflow_transaction: { entry: :account } })}Is this being used anywhere?
@@ -27,0 +30,4 @@{ inflow_transaction: { entry: :account } },{ outflow_transaction: { entry: :account } })}Yeah, this is used in the transactions controller
#indexmethod@@ -94,3 +75,3 @@endall.size# arbitrary cutoff date to avoid expensive sync operationsActually,
invalidate_cachesis an internal callback method in this case.Methods defined inside the
class << selfblock aren't affected by the instance-level private keyword. They maintain their own visibility scope.@@ -15,11 +15,8 @@ class TransactionsController < ApplicationControllerYou're right, I was thinking it could avoid overfetching since we might only need the account references from the transfer association in the views, not all the transfer attributes.
I'll switch back to using includes for a cleaner approach.
I followed the instructions from the benchmarking setup PR, which I believe uses the
derailed_benchmarkgem. I had written a simple custom benchmark myself, but once I saw the existing rake task, I went with that instead.I ran the
demo_data:defaultrake task to populate the maybe_production database, and here’s a snapshot of the resulting table sizes:/transactionsendpoint. I was using the Prosopite gem, which flagged several repeated queries. Here’s an excerpt from the logs: from the main branchIn total, I saw repeated SELECT calls for transactions, entries, and accounts, all triggered through nested partials in the transactions/index view.
Skylight also highlighted this endpoint as a hot spot, and Prosopite helped confirm the source as N+1 queries:

Pull request closed