Perf: accessibleBy() nutzt company_id-Subquery statt materialisiertem whereIn

scopeAccessibleBy() haengte den Firmenzugriff bisher als whereIn() mit allen
via accessibleCompanyIds() materialisierten Firmen-IDs an. Bei Power-Usern
(26k+ Firmen) ist das ein riesiger IN-Block im SQL.

Jetzt: orWhereIn('company_id', Company::accessibleBy($user)->select('id')) —
MySQL loest die Subquery als Semi-Join ueber den company_id-Index. Gleiche
Semantik (Company-Global-Scopes werden mitkompiliert), aber beschraenkte
SQL-Groesse und keine PHP-seitige ID-Liste mehr.

Benchmark (User 14016): ein korreliertes orWhereHas-EXISTS war ~3x langsamer
(1070ms vs 335ms COUNT) — die Subquery-whereIn-Form ist die schnellste.
accessibleCompanyIds() bleibt fuer canAccessCompany() erhalten.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-30 11:30:49 +00:00
parent 38884a53d6
commit 6a406bf577

View file

@ -229,15 +229,16 @@ class PressRelease extends Model
*/
public function scopeAccessibleBy(Builder $query, User $user): Builder
{
// company_id IN (<Subquery>) statt eines materialisierten whereIn() mit
// zehntausenden IDs: MySQL loest das als Semi-Join ueber den company_id-
// Index (im Benchmark ~3x schneller als ein korreliertes orWhereHas-
// EXISTS). Company::accessibleBy() (Owner ODER Pivot-Mitglied) ist die
// Single Source of Truth; als Eloquent-Subquery werden die Company-
// Global-Scopes (PortalScope, SoftDeletes) mitkompiliert.
return $query->withoutGlobalScopes()
->where(function (Builder $q) use ($user): void {
$q->where('user_id', $user->id);
$companyIds = $user->accessibleCompanyIds();
if ($companyIds !== []) {
$q->orWhereIn('company_id', $companyIds);
}
$q->where('user_id', $user->id)
->orWhereIn('company_id', Company::accessibleBy($user)->select('companies.id'));
});
}