Evolutia top N useri

Plecand de la data fiecarui post al unui user:

A, 2020-01-01
A, 2020-01-01
B, 2020-01-02
C, 2020-01-02
B, 2020-01-03

Se doreste obtinerea unei statistici cu top N useri dupa numarul total de posts (de la inceput) la finalul fiecarei zi.

2021-01-01, A, 2
2021-01-02, A, 2
2021-01-02, B, 1
2021-01-02, C, 1
2021-01-03, A, 2
2021-01-03, B, 2
2021-01-03, C, 1

Intr-un limbaj obisnuit se rezolva usor cu un hashtable si cate o resortare in fiecare zi. Cum ati rezolva asta insa in SQL?

Sursa mare de date de test: https://archive.org/download/stackexchange/stackoverflow.com-Posts.7z (17.2G xml arhivat)

1 Like

inchiriezi un procesor calumea la server si ii dai un count(*)

Cred ca seamana cu ce am facut eu pe aici

1 Like

Ce ai facut acolo ar fi raspunsul din ultima zi. Se complica cand vine vorba de raspunsul pentru fiecare zi.

Așa?

CREATE TABLE posts (
  user_name INT,
  post_date DATE
);

INSERT INTO
	posts
VALUES
    (1, '2020-01-01'),
    (1, '2020-01-01'),
    (2, '2020-01-02'),
    (3, '2020-01-02'),
    (2, '2020-01-03');

Evident, într-o bază de date reală probabil ar fi tabele separate pentru useri dar am simplificat pentru că nu ăsta e scopul.

Și acum query-ul:

WITH accumulate_users AS (
  SELECT DISTINCT
    p1.user_name AS user_name,
    p2.post_date AS post_date
  FROM
    posts p1
    INNER JOIN posts p2 ON (TRUE)
  WHERE
    (
      SELECT
        COUNT(*)
      FROM
        posts tmp
      WHERE
        tmp.user_name = p1.user_name
        AND tmp.post_date <= p2.post_date
    ) > 0
  ORDER BY
    p2.post_date, p1.user_name
),
get_counts AS (
  SELECT
    CASE
      WHEN t.user_name = 1 THEN 'A'
      WHEN t.user_name = 2 THEN 'B'
      WHEN t.user_name = 3 THEN 'C'
      ELSE 'Unkown'
    END user_name,
    t.post_date AS date_posted,
    (
      SELECT
        COUNT(*)
      FROM
        posts p
      WHERE
        p.post_date <= t.post_date
        AND p.user_name = t.user_name
    ) AS number_of_posts
  FROM
    accumulate_users t
  ORDER BY
    t.post_date ASC, number_of_posts DESC
)
SELECT * FROM get_counts;

Cu output-ul:

user_name date_posted number_of_posts
A 2020-01-01 2
A 2020-01-02 2
B 2020-01-02 1
C 2020-01-02 1
A 2020-01-03 2
B 2020-01-03 2
C 2020-01-03 1

Demo.

Nu știu dacă e cel mai eficient mod, probabil că nu, dar măcar pe exemplul tău îți dă rezultatul dorit.

2 Likes

Folosind self JOIN


SELECT DISTINCT
a.post_date as a_post_date, 
b.user_code as b_user_code, 
(
    SELECT SUM(ppd.day_posts) 
    FROM ( 
       SELECT post_date, user_code, COUNT(*) as day_posts 
       FROM posts
       GROUP BY post_date, user_code
    ) as ppd 
    WHERE post_date <= a.post_date AND user_code = b.user_code
  ) as total_posts
FROM posts a
JOIN posts b

GROUP BY a.post_date, b.user_code, total_posts
HAVING total_posts IS NOT NULL

2 Likes

Mersi amandurora pentru sugestii.

Planurile rezultate insa au fost \mathcal{O}(N^2), ceea ce e prea mult si pentru setul mediu de test de doar 200k randuri.

Exista oare vreun window function sau ceva similar care sa poata calcula rezultatul cu o complexitate mai buna?

Nu cred ca exista din simplul fapt ca daca vrei ca sa ai mai multe rezultate in final, decat sunt in tabelul sursa, e imposibil sa faci asta fara un self join.
Pornind doar de la un SELECT O(n), vei obtine un numar maxim de rezultate cu cel care este in tabelul sursa. Asta daca nu faci nici o filtrare/limitare/grupare.

Ce poti face in schimb, este sa ai un tabel/view separat in care sa introduci aceste date in formatul de care ai nevoie, astfel incat sa le poti selecta cu o complexitate de O(n).
Poti tine up-to-date acel view/tabel folosind functionalitate de TRIGGER de care dispunea baza de date.

Păi rezultatul cuprinde aproximativ numărul de useri ori numărul de dăți în care a fost postat ceva, deci nu prea văd cum ar arăta ceva eficient, având în vedere câte rezultate trebuiesc afișate. Mai degrabă aș vedea niște optimizări la nivel de aplicație, adică să se evite o astfel de statistică cât mai mult. De exemplu pentru a calcula pentru o singură dată se poate face mult mai eficient. Și poate e necesar doar pentru un user, nu toți neapărat. Și poate se poate face cumva astfel încât rezultatele să fie încărcate on-demand folosind queries mai eficiente, nu încărcat totul dinainte. Altfel, nu văd un astfel de query scalând prea bine.

Asa arata rezultatul pentru date de intrare mai mici (Stackexchange DBA) https://danistorage.blob.core.windows.net/public/dba-stackexchange.mp4

Urmeaza zilele astea sa-l generez si pentru Stackoverflow.

1 Like

Arata bine. Pana la urma ce solutie ai folosit? Una cu O(n)?

Din cod cu un hashtable dupa utilizator si resortat dupa fiecare zi imi ia cam 3 secunde pentru dba stackexchange (cam 200k postari).

Din SQL n-a mers din pacate intr-un timp decent.

Animatia pentru Stack Overflow https://danistorage.blob.core.windows.net/public/stackoverflow.mp4

Setul de date a inclus 54741615 postari scrise de 16279655 utilizatori

3 Likes