Perf: Status-Counts der PM-Uebersicht via UNION statt OR-GROUP-BY

Die nach Status gruppierte Zaehlung lief ueber accessibleBy() (user_id OR
company_id IN sub). Der OR-Ausdruck zwingt MySQL zu Full-Scans (EXPLAIN:
companies type=ALL, press_releases Full-Index-Scan ueber 124k Zeilen) und
war mit ~500-950ms der groesste Einzelposten der Seite.

Neue PressRelease::accessibleStatusCountsFor(User, ?Closure): kombiniert
Autor- und Firmen-Zweig als indexfreundliche UNION-Subquery (per PK
dedupliziert) und gruppiert darueber. Im Benchmark ~2,5x schneller bei
identischer Verteilung. Der Firmen-Filter der Uebersicht wird als geteilte
Closure auf Counts UND Liste angewandt (garantiert deckungsgleiche Filterung).

Gesamtmessung /admin/me/press-releases (User 14016, 26.772 Firmen / 20.830
PMs): Query-Pfad ~2850ms -> ~740ms warm; hydratisierte Company-Models 24.

Tests: accessibleStatusCountsFor zaehlt Autor+Firma je einmal (UNION-Dedup)
und wendet Zusatz-Constraints auf beide Zweige an.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-30 13:55:13 +00:00
parent 6a406bf577
commit 7d6b97f3fb
3 changed files with 99 additions and 14 deletions

View file

@ -111,26 +111,32 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class
public function with(): array
{
$context = app(CustomerCompanyContext::class);
$selectedCompanyId = $context->selectedCompanyId(auth()->user());
$user = auth()->user();
$selectedCompanyId = $context->selectedCompanyId($user);
// Firmen-Scope: eigene PMs (Autor) ODER PMs der zugeordneten Firmen.
$base = PressRelease::accessibleBy(auth()->user())
->when($selectedCompanyId !== null, fn($q) => $q->where('company_id', $selectedCompanyId))
->when($selectedCompanyId === null && $this->companyFilter === 'assigned', fn($q) => $q->whereNotNull('company_id'))
->when($selectedCompanyId === null && $this->companyFilter === 'unassigned', fn($q) => $q->whereNull('company_id'));
// Firmen-Scope der Übersicht: auf eine gewählte Firma einschränken bzw.
// (im globalen Kontext) auf zugeordnete/unzugeordnete PMs. Wird sowohl
// auf die Status-Counts als auch die Liste angewandt, damit beide
// deckungsgleich filtern.
$applyCompanyFilter = function ($query) use ($selectedCompanyId): void {
$query->when($selectedCompanyId !== null, fn($q) => $q->where('company_id', $selectedCompanyId))
->when($selectedCompanyId === null && $this->companyFilter === 'assigned', fn($q) => $q->whereNotNull('company_id'))
->when($selectedCompanyId === null && $this->companyFilter === 'unassigned', fn($q) => $q->whereNull('company_id'));
};
$statusCountsRaw = (clone $base)->toBase()->selectRaw('status, COUNT(*) as aggregate')->groupBy('status')->pluck('aggregate', 'status');
$statusCountsRaw = PressRelease::accessibleStatusCountsFor($user, $applyCompanyFilter);
$statusCounts = [
'all' => (int) $statusCountsRaw->sum(),
PressReleaseStatus::Published->value => (int) ($statusCountsRaw[PressReleaseStatus::Published->value] ?? 0),
PressReleaseStatus::Draft->value => (int) ($statusCountsRaw[PressReleaseStatus::Draft->value] ?? 0),
PressReleaseStatus::Review->value => (int) ($statusCountsRaw[PressReleaseStatus::Review->value] ?? 0),
PressReleaseStatus::Rejected->value => (int) ($statusCountsRaw[PressReleaseStatus::Rejected->value] ?? 0),
PressReleaseStatus::Archived->value => (int) ($statusCountsRaw[PressReleaseStatus::Archived->value] ?? 0),
'all' => array_sum($statusCountsRaw),
PressReleaseStatus::Published->value => $statusCountsRaw[PressReleaseStatus::Published->value] ?? 0,
PressReleaseStatus::Draft->value => $statusCountsRaw[PressReleaseStatus::Draft->value] ?? 0,
PressReleaseStatus::Review->value => $statusCountsRaw[PressReleaseStatus::Review->value] ?? 0,
PressReleaseStatus::Rejected->value => $statusCountsRaw[PressReleaseStatus::Rejected->value] ?? 0,
PressReleaseStatus::Archived->value => $statusCountsRaw[PressReleaseStatus::Archived->value] ?? 0,
];
$prs = (clone $base)
$prs = PressRelease::accessibleBy($user)
->tap($applyCompanyFilter)
->with('company:id,name')
->when(filled($this->search), function ($q): void {
$term = $this->search;