web: Startseiten-Feed cachen + Index gegen Filesort der Feed-Query

Der Livewire-Feed lief auf jeder Startseiten-Impression ungecacht; seine
ORDER-BY-published_at-Query kann wegen portal IN (…) keinen bestehenden
Index zur Sortierung nutzen und filesortete ~10-20k Zeilen (live gemessen
2,3 s pro Aufruf — die Startseite blieb dadurch trotz Seitendaten-Cache
bei 6-9 s TTFB). Zwei Maßnahmen:

- Neuer Index (language, status, deleted_at, published_at): Backward
  Index Scan statt Filesort, Query 2 ms statt 2,3 s — greift auch für
  ungecachte Livewire-Interaktionen (Zeitraum-Filter, tiefe Seiten).
- Feed-Seiten 1-3 laufen über WebPageCache (SWR wie die übrigen
  Seitendaten); gecacht werden nur items + total, der Paginator wird pro
  Request neu gebaut (keine Host-URLs des Cache-Erzeugers im Cache).
  Unbekannte timeframe-Werte aus der URL fallen auf 'all' zurück und
  erreichen weder Query noch Cache-Key. Der doppelte Gesamt-COUNT pro
  Request entfällt; der 'Mehr laden'-Zähler nutzt jetzt den gefilterten
  Total des Paginators.

Lokal mit Live-Datenbestand: Startseite kalt 875 ms (vorher 2,5 s),
warm 11 ms (vorher 72 ms).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-20 14:53:09 +00:00
parent bd4ad855ba
commit 3d10cbf709
3 changed files with 151 additions and 15 deletions

View file

@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Index für den öffentlichen PM-Feed (Startseite): Die Feed-Query filtert
* portal IN (…) — dadurch kann kein portal-geführter Index die Sortierung
* `ORDER BY published_at DESC LIMIT n` bedienen und MySQL filesortet
* ~10–20k Treffer pro Aufruf (live gemessen: 2,3 s statt 30 ms). Dieser
* Index führt mit language/status/deleted_at (Gleichheit) und published_at
* (Range + Order) → Index-Order-Scan, der nach n passenden Zeilen stoppt;
* portal wird als Restfilter auf den wenigen gelesenen Zeilen geprüft.
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('press_releases', function (Blueprint $table): void {
if (! $this->hasIndex('press_releases', 'press_releases_feed_order_idx')) {
$table->index(
['language', 'status', 'deleted_at', 'published_at'],
'press_releases_feed_order_idx'
);
}
});
}
public function down(): void
{
Schema::table('press_releases', function (Blueprint $table): void {
if ($this->hasIndex('press_releases', 'press_releases_feed_order_idx')) {
$table->dropIndex('press_releases_feed_order_idx');
}
});
}
private function hasIndex(string $table, string $indexName): bool
{
return in_array($indexName, array_column(Schema::getIndexes($table), 'name'), true);
}
};