Perf: customer press-releases-Liste ueber UNION statt OR-Paginate
Die Uebersicht /admin/me/press-releases war auch fuer leere Konten ~2s: der OR-Filter in scopeAccessibleBy zwang die Paginator-count(*) zum Full-Scan (EXPLAIN type=ALL, ~174k Zeilen) und den Zeilen-Fetch bei 0 Treffern zum vollen created_at-Index-Scan (~1s). Neue PressRelease::accessiblePageFor() nutzt dieselbe index-freundliche UNION wie accessibleStatusCountsFor(): schmale UNION liefert die Seiten-IDs ueber user_id-/company_id-Index, danach nur 25 Modelle hydratisiert; Total via schnellem UNION-count. Messung: leer ~2000ms -> 167ms, groesstes Konto (20.830 PMs) ~740ms -> 468ms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a25355ede3
commit
17b586f136
3 changed files with 164 additions and 11 deletions
|
|
@ -19,6 +19,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
|
@ -287,6 +289,79 @@ class PressRelease extends Model
|
|||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Seitenweise, für $user zugängliche PMs – über dieselbe index-freundliche
|
||||
* UNION wie {@see accessibleStatusCountsFor()} statt des OR-Filters aus
|
||||
* {@see scopeAccessibleBy()}.
|
||||
*
|
||||
* Warum nicht `accessibleBy()->paginate()`? Der OR-Ausdruck entartet bei
|
||||
* beiden Extremen: bei leeren/sparsamen Treffermengen scannt der Zeilen-
|
||||
* Fetch den created_at-Index vollständig (~1 s für 0 Treffer), und der
|
||||
* Paginator-`count(*)` macht ohnehin einen Full-Scan (~1 s, EXPLAIN
|
||||
* type=ALL). Stattdessen: schmale UNION (nur Sortierspalten) liefert die
|
||||
* Seiten-IDs über die user_id-/company_id-Indizes, danach werden nur die
|
||||
* 25 Modelle der Seite hydratisiert. Ergebnis: ~100 ms für normale, <500 ms
|
||||
* für die größten Konten (Benchmark 02.07.).
|
||||
*
|
||||
* @param (Closure(Builder<self>): mixed)|null $constrain Filter, die auf
|
||||
* beide UNION-Zweige angewandt werden (Firmen-/Status-/Portal-/Suchfilter)
|
||||
* @return LengthAwarePaginator<self>
|
||||
*/
|
||||
public static function accessiblePageFor(
|
||||
User $user,
|
||||
?Closure $constrain = null,
|
||||
string $sortColumn = 'created_at',
|
||||
string $sortDir = 'desc',
|
||||
int $perPage = 25,
|
||||
): LengthAwarePaginator {
|
||||
$sortColumn = in_array($sortColumn, ['created_at', 'title', 'status'], true) ? $sortColumn : 'created_at';
|
||||
$sortDir = $sortDir === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
$branch = function (Closure $where) use ($constrain): Builder {
|
||||
$query = static::query()->withoutGlobalScopes()
|
||||
->select('id', 'created_at', 'title', 'status');
|
||||
$where($query);
|
||||
|
||||
if ($constrain !== null) {
|
||||
$constrain($query);
|
||||
}
|
||||
|
||||
return $query;
|
||||
};
|
||||
|
||||
$authored = $branch(fn (Builder $q) => $q->where('user_id', $user->id));
|
||||
$viaCompany = $branch(fn (Builder $q) => $q->whereIn(
|
||||
'company_id',
|
||||
Company::accessibleBy($user)->select('companies.id'),
|
||||
));
|
||||
|
||||
$accessible = DB::query()->fromSub($authored->union($viaCompany), 'accessible_press_releases');
|
||||
|
||||
$page = Paginator::resolveCurrentPage();
|
||||
$total = (clone $accessible)->count();
|
||||
|
||||
$pageIds = $accessible
|
||||
->orderBy($sortColumn, $sortDir)
|
||||
->orderBy('id', $sortDir)
|
||||
->forPage($page, $perPage)
|
||||
->pluck('id');
|
||||
|
||||
$models = static::query()->withoutGlobalScopes()
|
||||
->whereKey($pageIds)
|
||||
->with('company:id,name')
|
||||
->orderBy($sortColumn, $sortDir)
|
||||
->orderBy('id', $sortDir)
|
||||
->get();
|
||||
|
||||
return new LengthAwarePaginator(
|
||||
$models,
|
||||
$total,
|
||||
$perPage,
|
||||
$page,
|
||||
['path' => Paginator::resolveCurrentPath(), 'pageName' => 'page'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bewusst enger als {@see scopeAccessibleBy()}: Der API-Zugriff meint nur
|
||||
* die eigene Autorschaft (user_id), nicht den breiteren Firmenzugriff.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue