Skip to content

Der Newsfeed liest eine Seite statt der ganzen posts-Tabelle (v7.231.2) - #1325

Merged
wintermeyer merged 1 commit into
mainfrom
feed-index-and-author-gate
Aug 6, 2026
Merged

Der Newsfeed liest eine Seite statt der ganzen posts-Tabelle (v7.231.2)#1325
wintermeyer merged 1 commit into
mainfrom
feed-index-and-author-gate

Conversation

@wintermeyer

@wintermeyer wintermeyer commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Die Hauptabfrage des Feeds ("meine Posts plus die der Leute, denen ich folge", neueste zuerst) hat bisher bei jedem Aufruf die komplette posts-Tabelle gelesen und per Top-N-Sort auf 21 Zeilen eingedampft.

Gemessen auf einer Kopie der Produktionsdaten, aufgefüllt auf 200.000 Posts:

posts-Tabelle vorher nachher
370 (heute) 5,2 ms 0,44 ms
200.000 76,7 ms 0,53 ms

Wichtiger als der Faktor ist die Form: vorher wuchs die Abfrage linear mit jedem jemals geschriebenen Post (bei 200k: 84.513 Treffer geprüft, 115.487 verworfen), jetzt liest sie 60 Zeilen und hört auf.

Die beiden Indizes (rein additiv, N-1-sicher)

  1. posts_recency_index auf (inserted_at DESC, id DESC), den Sortierschlüssel von Feed, Tag-Timeline und Entdecken-Leiste. Bisher gab es dafür keinen Index: vorhanden waren nur (user_id, inserted_at) und (user_id, published_on), und die ODER-Verknüpfung "eigene Posts oder die meiner Followees" macht die unbrauchbar.

  2. users_hidden_index, partiell auf die vier Spalten, die einen Account verstecken. Moderation.Query.account_hidden/1 fragt nach den versteckten Accounts, users_visible_covering_index deckt genau die Gegenmenge ab und kann dafür nichts tun, also hat Postgres für ein paar hundert IDs die ganze users-Tabelle gescannt — einmal pro Post-Abfrage, fünfmal auf einem einzigen /feed. Das Prädikat sagt bewusst suspended_until IS NOT NULL statt > now(), weil ein Index-Prädikat immutable sein muss.

Dieselbe Frage, billiger gestellt

Vutuv.Moderation.Query hält seit jeher beide Schreibweisen des Gates bereit, und scope_visible/2 hat immer die teure genommen, obwohl die Feed-Abfrage die Autorenzeile ohnehin schon joint. Jetzt entscheidet das benannte Binding: wer seinen Autoren-Join as: :author nennt, bekommt account_hidden_row/1 auf die bereits gelesene Zeile, alle anderen behalten das EXISTS. Welche Posts sichtbar sind, ändert das nicht — nur wie die Frage gestellt wird. Auf /feed und in der Entdecken-Leiste verschwindet der users-Scan damit komplett (5 → 0 pro Seitenaufbau).

Tests

  • Der unreachable-Arm des Gates hatte als einziger der vier keinen Feed-Test; er kam dazu, weil er genau den neuen Pfad absichert.
  • Die Indizes haben einen eigenen Regressionstest (test/vutuv/repo/feed_indexes_test.exs): sie ändern kein Ergebnis, ihr Verlust würde also in keinem anderen Test auffallen — die Seite würde nur mit den Jahren immer langsamer.

Deploy: enthält eine Migration, aber rein additiv (zwei neue Indizes) und damit N-1-sicher. scripts/deploy.sh migriert im Blue/Green-Ablauf ohnehin vor dem Slot-Wechsel, eine Sonderbehandlung braucht es nicht.
Version: 7.231.2
mix precommit: grün (6937 Tests).

Diesen Text hat ein KI-Agent in meinem Namen geschrieben, ungeprüft von mir. Die Arbeit dahinter ist meine, nur das Aufschreiben habe ich delegiert.

Die Hauptabfrage des Feeds ("meine Posts plus die der Leute, denen ich
folge", neueste zuerst) hat bisher bei jedem Aufruf die komplette
posts-Tabelle gelesen und per Top-N-Sort auf 21 Zeilen eingedampft. Auf
einer Kopie der Produktionsdaten, aufgefüllt auf 200.000 Posts, dauerte
das 76,7 ms; danach sind es 0,53 ms. Wichtiger als der Faktor ist die
Form: vorher wuchs die Abfrage linear mit jedem jemals geschriebenen
Post, jetzt bleibt sie flach.

Zwei Ursachen, beide additiv behoben, also N-1-sicher in einem Deploy:

1. Auf den Sortierschlüssel (inserted_at DESC, id DESC) gab es keinen
   Index. Vorhanden waren nur (user_id, inserted_at) und (user_id,
   published_on), und die ODER-Verknüpfung "eigene Posts oder die
   meiner Followees" macht die unbrauchbar. Neu: posts_recency_index,
   den auch die Tag-Timeline und die Entdecken-Leiste nutzen.

2. Das Moderations-Gate fragt über account_hidden/1 nach den
   *versteckten* Accounts (frozen, deactivated, unreachable,
   suspended). Der vorhandene users_visible_covering_index deckt genau
   die Gegenmenge ab und kann dafür nichts tun, also hat Postgres für
   ein paar hundert IDs die ganze users-Tabelle gescannt, einmal pro
   Post-Abfrage: fünfmal auf einem einzigen /feed. Neu ist der partielle
   users_hidden_index; sein Prädikat sagt bewusst `suspended_until IS
   NOT NULL` statt `> now()`, weil ein Index-Prädikat immutable sein
   muss.

Dazu die billigere Schreibweise desselben Gates: Vutuv.Moderation.Query
hält seit jeher beide Varianten bereit, und scope_visible/2 hat immer
die teure genommen, obwohl die Feed-Abfrage die Autorenzeile ohnehin
schon joint. Jetzt entscheidet das benannte Binding: wer seinen
Autoren-Join `as: :author` nennt, bekommt account_hidden_row/1 auf die
bereits gelesene Zeile, alle anderen behalten das EXISTS. Das ändert
nichts daran, welche Posts sichtbar sind, nur wie die Frage gestellt
wird. Auf /feed und in der Entdecken-Leiste verschwindet der
users-Scan damit komplett (5 → 0 pro Seitenaufbau).

Der Test für den unreachable-Arm des Gates fehlte bisher als einziger
der vier und kam dazu, weil er genau den neuen Pfad absichert. Die
Indizes selbst haben einen eigenen Regressionstest: sie ändern kein
Ergebnis, also würde ihr Verlust in keinem anderen Test auffallen, die
Seite würde nur mit den Jahren immer langsamer.

Diesen Text hat ein KI-Agent in meinem Namen geschrieben, ungeprüft von mir. Die Arbeit dahinter ist meine, nur das Aufschreiben habe ich delegiert.
@wintermeyer
wintermeyer merged commit 540d5cf into main Aug 6, 2026
1 check passed
@wintermeyer
wintermeyer deleted the feed-index-and-author-gate branch August 6, 2026 14:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant