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:
parent
6a406bf577
commit
7d6b97f3fb
3 changed files with 99 additions and 14 deletions
|
|
@ -10,6 +10,7 @@ use App\Enums\PressReleaseStatus;
|
|||
use App\Models\Concerns\HasUniqueSlug;
|
||||
use App\Scopes\PortalScope;
|
||||
use App\Services\PressRelease\PressReleaseHtmlSanitizer;
|
||||
use Closure;
|
||||
use Database\Factories\PressReleaseFactory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -19,6 +20,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class PressRelease extends Model
|
||||
|
|
@ -242,6 +244,49 @@ class PressRelease extends Model
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Nach Status gruppierte Anzahl der für $user zugänglichen PMs.
|
||||
*
|
||||
* Bewusst als UNION zweier indexfreundlicher Zweige (Autor über user_id,
|
||||
* Firmenzugriff über den company_id-Semi-Join) statt des OR-Filters aus
|
||||
* {@see scopeAccessibleBy()}: Der OR-Ausdruck zwingt MySQL bei Power-Usern
|
||||
* zu Full-Scans (companies type=ALL); die UNION-Variante ist im Benchmark
|
||||
* ~2,5x schneller bei identischem Ergebnis. Die UNION dedupliziert über die
|
||||
* PK, sodass eine PM, die beide Zweige erfüllt, nur einmal zählt.
|
||||
*
|
||||
* @param (Closure(Builder<self>): mixed)|null $constrain zusätzliche
|
||||
* Einschränkung, die auf beide Zweige angewandt wird (z. B. der
|
||||
* Firmen-Filter der Übersicht)
|
||||
* @return array<string, int> status-Wert => Anzahl
|
||||
*/
|
||||
public static function accessibleStatusCountsFor(User $user, ?Closure $constrain = null): array
|
||||
{
|
||||
$branch = function (Closure $where) use ($constrain): Builder {
|
||||
$query = static::query()->withoutGlobalScopes()->select('id', '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'),
|
||||
));
|
||||
|
||||
return DB::query()
|
||||
->fromSub($authored->union($viaCompany), 'accessible_press_releases')
|
||||
->selectRaw('status, COUNT(*) as aggregate')
|
||||
->groupBy('status')
|
||||
->pluck('aggregate', 'status')
|
||||
->map(fn ($count): int => (int) $count)
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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