2

Ich habe eine ziemlich gemeinsame (zumindest wie ich denke) Datenbankstruktur: Es gibt Nachrichten (News(id, source_id)), jede Nachricht hat eine Quelle (Source(id, url)). Quellen werden über TopicSource(source_id, topic_id) zu Themen zusammengefasst (Topic(id, title)). Zusätzlich gibt es Benutzer (User(id, name)), die Nachrichten über NewsRead(news_id, user_id) als gelesen markieren können. Hier ist ein Diagramm, Dinge zu klären: db diagramZählen ungelesene Nachrichten in einer großen Tabelle

Ich mag Zahl ungelesener Nachrichten in Themen für bestimmte Benutzer. Das Problem ist News Tabelle ist eine große (10^6 - 10^7 Zeilen). Glücklicherweise muss ich nicht wissen genau zählen, es ist in Ordnung zu stoppen, nach einem Schwellenwert zu zählen, der diesen Schwellenwert als einen gezählten Wert zurückgibt.

Nach this answer für ein ein Thema Ich kam mit einer folgenden Abfrage auf:

SELECT t.topic_id, count(1) as unread_count 
FROM (
SELECT 1, topic_id 
FROM news n 
    JOIN topic_source t ON n.source_id = t.source_id 
    -- join news_read to filter already read news 
    LEFT JOIN news_read r 
    ON (n.id = r.news_id AND r.user_id = 1) 
WHERE t.topic_id = 3 AND r.user_id IS NULL 
LIMIT 10 -- Threshold 
) t GROUP BY t.topic_id; 

(query plan 1). Diese Abfrage dauert etwa 50 ms bei der Test-DB, was akzeptabel ist.

Jetzt möchten Sie ungelesene Anzahl für mehrere Themen auswählen. Ich habe versucht, so zu wählen:

SELECT 
    t.topic_id, 
    (SELECT count(1) 
    FROM (SELECT 1 FROM news n 
      JOIN topic_source tt ON n.source_id = tt.source_id 
      LEFT JOIN news_read r 
      ON (n.id = r.news_id AND r.user_id = 1) 
      WHERE tt.topic_id = t.topic_id AND r.user_id IS NULL 
      LIMIT 10 -- Threshold 
     ) t) AS unread_count 
FROM topic_source t WHERE t.topic_id IN (1, 2) GROUP BY t.topic_id; 

(query plan 2). Aber aus dem mir unbekannten Grund dauert es etwa 1,5 s auf Testdaten, während die Summe der einzelnen Abfragen etwa 0,2-0,3 s betragen sollte.

Ich vermisse hier eindeutig etwas. Gibt es einen Fehler in der zweiten Abfrage? Gibt es einen besseren (schnelleren) Weg, eine Anzahl ungelesener Nachrichten auszuwählen?

Zusätzliche Informationen:

Tischgrößen:

News - 10^6 - 10^7 
User - 10^3 
Source - 10^4 
Topic - 10^3 
TopicSource - 10^5 
NewsRead - 10^6 

UPD: Abfragepläne zeigen deutlich, verwirrte ich zweite Abfrage nach oben. Irgendwelche Hinweise werden geschätzt.

UPD2: habe ich versucht, diese Abfrage mit seitlich verbinden, die einfach soll zuerst laufen (die schnellste) Abfrage für jeden topic_id:

SELECT 
    id, 
    count(*) 
FROM topic t 
    LEFT JOIN LATERAL (
    SELECT ts.topic_id 
    FROM news n 
     LEFT JOIN news_read r 
     ON (n.id = r.news_id AND r.user_id = 1) 
     JOIN topic_source ts ON n.source_id = ts.source_id 
    WHERE ts.topic_id = t.id AND r.user_id IS NULL 
    LIMIT 10 
) p ON TRUE 
WHERE t.id IN (4, 10, 12, 16) 
GROUP BY t.id; 

(query plan 3). Aber es scheint, dass Pg Planer eine andere Meinung zu diesem Thema hat - es läuft sehr langsam Seq-Scans und Hash-Joins statt Index-Scans und Merge-Joins.

+0

Ich frage mich, wie [das] (https://paste.ofcode.org/TMhZbxCGqiSgc3ijhzZwfX) Abfrage auf Ihre Daten ergehen würde. Ich habe versucht, ähnliche Volumes von Beispieldaten wie Ihre zu erstellen, aber da die Verteilungen so unterschiedlich sind, erhalte ich sogar für Ihre ursprünglichen Abfragen sehr unterschiedliche Ergebnisse. Zum Beispiel dauert die Multi-Topic-Abfrage nur ~ 19ms (am besten von vielen). –

+0

@ IljaEverilä, danke für den Vorschlag! Diese Abfrage benötigt ca. 3,5 Sekunden für meine Daten. Ich denke, unsere Distributionen sind weit weg. [Erklärung ist hier] (https://explain.depesz.com/s/q740). Plötzlich scheint es, dass mehrere "UNION ALL" sehr schnell sind. Ich werde meinen Beitrag nach kurzer Recherche aktualisieren. – 9dogs

+0

Ich bemerkte, dass ich die LIMIT 10 von der innersten Unterabfrage vergessen habe. Das ist es, was Sie bekommen, wenn Sie nur einige der vielen Versuche kopieren. Vielleicht wäre es damit etwas schneller gelaufen. –

Antwort

0

Nach einiger Benchmarking ich endlich auf einfache UNION ALL Abfrage gestoppt haben, und es ist zehnmal schneller als seitliche auf meine Daten kommen:

SELECT 
    p.topic_id, 
    count(*) 
FROM (
     SELECT * 
     FROM (
       SELECT fs.topic_id 
       FROM news n 
       LEFT JOIN news_read r 
        ON (n.id = r.news_id AND r.user_id = 1) 
       JOIN topic_source fs ON n.source_id = fs.source_id 
       WHERE fs.topic_id = 4 AND r.user_id IS NULL 
       LIMIT 100 
      ) t1 
     UNION ALL 
     SELECT * 
     FROM (
       SELECT fs.topic_id 
       FROM news n 
       LEFT JOIN news_read r 
        ON (n.id = r.news_id AND r.user_id = 1) 
       JOIN topic_source fs ON n.source_id = fs.source_id 
       WHERE fs.topic_id = 10 AND r.user_id IS NULL 
       LIMIT 100 
      ) t1 
     UNION ALL 
     SELECT * 
     FROM (
       SELECT fs.topic_id 
       FROM news n 
       LEFT JOIN news_read r 
        ON (n.id = r.news_id AND r.user_id = 1) 
       JOIN topic_source fs ON n.source_id = fs.source_id 
       WHERE fs.topic_id = 12 AND r.user_id IS NULL 
       LIMIT 100 
      ) t1 
     UNION ALL 
     SELECT * 
     FROM (
       SELECT fs.topic_id 
       FROM news n 
       LEFT JOIN news_read r 
        ON (n.id = r.news_id AND r.user_id = 1) 
       JOIN topic_source fs ON n.source_id = fs.source_id 
       WHERE fs.topic_id = 16 AND r.user_id IS NULL 
       LIMIT 100 
      ) t1 
    ) p 
GROUP BY p.topic_id; 

(execute plan)

Intuition ist hier, dass durch Spezifizieren Sie topic_id`s explizit. Geben Sie dem Pg-Planer genügend Informationen, um einen effektiven Plan zu erstellen.

Von SQLAlchemy Sicht ist es ziemlich einfach:

# topic_ids, user_id are defined elsewhere, e.g. 
# topic_ids = [4, 10, 12, 16] 
# user_id = 1 
for topic_id in topic_ids: 
    topic_query = (
     db.session.query(News.id, TopicSource.topic_id) 
     .join(TopicSource, TopicSource.source_id == News.source_id) 
     # LEFT JOIN NewsRead table to filter only unreads 
     # (where News.user_id IS NULL) 
     .outerjoin(NewsRead, 
        and_(NewsRead.news_id == News.id, 
         NewsRead.user_id == user_id)) 
     .filter(TopicSource.topic_id == topic_id, 
       NewsRead.user_id.is_(None)) 
     .limit(100)) 
    topic_queries.append(topic_query) 
# Unite queries with UNION ALL 
union_query = topic_queries[0].union_all(*topic_queries[1:]) 
# Groups query by `topic_id` and count unreads 
counts = (union_query 
      # Using `with_entities(func.count())` to avoid 
      # a subquery. See link below for info: 
      # https://gist.github.com/hest/8798884 
      .with_entities(TopicSource.topic_id.label('topic_id'), 
         func.count().label('unread_count')) 
      .group_by(TopicSource.topic_id)) 
result = counts.all() 
Verwandte Themen