fix: batch board records fetch to eliminate per-column N+1 queries#82
fix: batch board records fetch to eliminate per-column N+1 queries#82ManukMinasyan wants to merge 1 commit into4.xfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR optimizes database query performance by eliminating N+1 queries when loading board records. Previously, each column triggered a separate query via getBoardRecords(). The new implementation fetches all column records in a single query and partitions them in PHP using getBatchedBoardRecords().
Changes:
- Added
getBatchedBoardRecords()method to fetch all column records in one query - Modified
Board::getViewData()to use the batched approach instead of per-column queries - Implemented in-memory partitioning with per-column limit support
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/Concerns/HasBoardRecords.php | Added new getBatchedBoardRecords() method that fetches all column records in a single query with filtering and partitioning logic |
| src/Board.php | Updated getViewData() to call getBatchedBoardRecords() once instead of calling getBoardRecords() per column |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| $allRecords = (clone $baseQuery) | ||
| ->whereIn($statusField, $columnIds) | ||
| ->orderBy($positionField, 'asc') | ||
| ->orderBy($keyName, 'asc') | ||
| ->get(); |
There was a problem hiding this comment.
The position field ordering is applied unconditionally, but the original getBoardRecords() method (line 90) checks if the position field exists using modelHasColumn() before ordering. This could cause SQL errors if the position field doesn't exist in the table. Add a check similar to the one in getBoardRecords():
if ($positionField && $this->modelHasColumn($baseQuery->getModel(), $positionField)) {
$allRecords = (clone $baseQuery)
->whereIn($statusField, $columnIds)
->orderBy($positionField, 'asc')
->orderBy($keyName, 'asc')
->get();
} else {
$allRecords = (clone $baseQuery)
->whereIn($statusField, $columnIds)
->orderBy($keyName, 'asc')
->get();
}
| $allRecords = (clone $baseQuery) | |
| ->whereIn($statusField, $columnIds) | |
| ->orderBy($positionField, 'asc') | |
| ->orderBy($keyName, 'asc') | |
| ->get(); | |
| if ($positionField && $this->modelHasColumn($baseQuery->getModel(), $positionField)) { | |
| $allRecords = (clone $baseQuery) | |
| ->whereIn($statusField, $columnIds) | |
| ->orderBy($positionField, 'asc') | |
| ->orderBy($keyName, 'asc') | |
| ->get(); | |
| } else { | |
| $allRecords = (clone $baseQuery) | |
| ->whereIn($statusField, $columnIds) | |
| ->orderBy($keyName, 'asc') | |
| ->get(); | |
| } |
|
Closing — the per-column getBoardRecords() approach with LIMIT is correct. The batched method removes the limit and loads all records, which is worse at scale. Not an actual N+1. |
Summary
getBoardRecords()calls inBoard::getViewData()with a singlegetBatchedBoardRecords()queryTest plan