Organization and mail datagrids: closure columns query the database once per row

Author: alex-frolovCreated Sep 8, 2026Updated Sep 10, 2026
LabelsBugFix ProposedOptimizationQA Passed

Bug report

Issue Description

Two datagrid columns query the database inside their closure, which the grid runs once per rendered row. Both listings therefore cost two extra queries per row — up to 100 on a page of 50, which the toolbar offers (DataGrid::$perPageOptions).

1. OrganizationDataGrid, column persons_count (packages/Webkul/Admin/src/DataGrids/Contact/OrganizationDataGrid.php:71)

php
'closure' => function ($row) {
    $personsCount = $this->personRepository->findWhere(['organization_id' => $row->id])->count();

    return $personsCount;
},

One SELECT * FROM persons WHERE organization_id = ? per row — and because Person::$with = 'organization' (Person.php:35), each of those also re-selects the organization it came from. Two queries per row, to produce a number the database can return as one column.

2. EmailDataGrid, column tags (packages/Webkul/Admin/src/DataGrids/Mail/EmailDataGrid.php:109-110)

php
'closure' => function ($row) {
    if ($email = app(EmailRepository::class)->find($row->id)) {
        return $email->tags;
    }

    return '--';
},

The row was just selected by the grid's own query; the closure selects it again by primary key, then lazy-loads its tags.

Worth noting: the grid already has that value. prepareQueryBuilder() selects 'tags.name as tags' (line 35) through the email_tags / tags joins on lines 39-40, so $row->tags is populated before the closure runs. The closure discards it and re-fetches.

Measured (MySQL 8.0, queries per grid request):

rows on the page organizations mail
5 14 14
10 23 23
20 43 43

Preconditions

  1. Krayin master @ 2c12091 (Core::KRAYIN_VERSION 2.2.6), also present in v2.2.5.
  2. PHP 8.3.33, Laravel 12, MySQL 8.0.

Steps to reproduce

  1. Create 20 organizations, each with one person; and 20 inbox emails, each with a tag.
  2. Count the queries each listing request makes — Laravel Debugbar's Queries tab on the grid XHR, or from a feature test:
php
DB::enableQueryLog();

$this->actingAs(User::find(1), 'user')
     ->getJson('/admin/contacts/organizations?pagination[per_page]=20',
               ['X-Requested-With' => 'XMLHttpRequest']);

count(DB::getQueryLog());
  1. Repeat with per_page=10 and compare: the difference is two queries per row. Same for /admin/mail/inbox?pagination[per_page]=20.

Expected result

  • A listing costs a constant number of queries regardless of the page size.

For persons_count, counting in the query the grid already runs, e.g. in prepareQueryBuilder():

php
$queryBuilder->selectSub(
    DB::table('persons')->selectRaw('count(*)')->whereColumn('persons.organization_id', 'organizations.id'),
    'persons_count'
);

and dropping the closure — the column index is already persons_count.

For the mail grid, the closure can go entirely: $row->tags already carries the joined value. The filterable_options block on the same column is unaffected.

Actual result

Both listings issue two extra queries for every row displayed. On the largest page size the toolbar offers that is about 100 queries spent on data the grid either already has (mail) or could get as one subquery (organizations).


How this was found

Found with query-guard — a PHPUnit extension that traces the SQL a test run produces, fingerprints every query by call site and shape, and flags a call site when the same-shaped query runs 3+ times with differing bound values inside one test (batched IN (...) lookups are excluded, since those are the cure rather than the disease).

The Pest suite could not surface this on its own, since these screens are covered by the Playwright suite rather than by PHPUnit. So the numbers above come from driving the two grid endpoints through Laravel's HTTP kernel — real route, middleware, controller and datagrid — with the page size varied.

You do not need that tool to check any of this: the reproduction above uses nothing but DB::enableQueryLog(), and Laravel Debugbar's Queries tab shows the same counts in the browser.