Obter contagens incrementais de um valor agregado em uma tabela unida

10

Eu tenho duas tabelas em um banco de dados MySQL 5.7.22: postse reasons. Cada linha de postagem possui e pertence a muitas linhas de motivo. Cada razão tem um peso associado a ela e, portanto, cada postagem possui um peso agregado total associado a ela.

Para cada incremento de 10 pontos de peso (ou seja, para 0, 10, 20, 30, etc.), quero obter uma contagem de postagens que tenham um peso total menor ou igual a esse incremento. Eu esperaria que os resultados parecessem algo assim:

 weight | post_count
--------+------------
      0 | 0
     10 | 5
     20 | 12
     30 | 18
    ... | ...
    280 | 20918
    290 | 21102
    ... | ...
   1250 | 118005
   1260 | 118039
   1270 | 118040

Os pesos totais são aproximadamente distribuídos normalmente, com alguns valores muito baixos e alguns valores muito altos (o máximo é atualmente 1277), mas a maioria no meio. Existem pouco menos de 120.000 linhas postse cerca de 120 polegadas reasons. Cada post tem em média 5 ou 6 razões.

As partes relevantes das tabelas são assim:

CREATE TABLE `posts` (
  id BIGINT PRIMARY KEY
);

CREATE TABLE `reasons` (
  id BIGINT PRIMARY KEY,
  weight INT(11) NOT NULL
);

CREATE TABLE `posts_reasons` (
  post_id BIGINT NOT NULL,
  reason_id BIGINT NOT NULL,
  CONSTRAINT fk_posts_reasons_posts (post_id) REFERENCES posts(id),
  CONSTRAINT fk_posts_reasons_reasons (reason_id) REFERENCES reasons(id)
);

Até agora, tentei soltar o ID da postagem e o peso total em uma visualização e associá-la a ela mesma para obter uma contagem agregada:

CREATE VIEW `post_weights` AS (
    SELECT 
        posts.id,
        SUM(reasons.weight) AS reason_weight
    FROM posts
    INNER JOIN posts_reasons ON posts.id = posts_reasons.post_id
    INNER JOIN reasons ON posts_reasons.reason_id = reasons.id
    GROUP BY posts.id
);

SELECT
    FLOOR(p1.reason_weight / 10) AS weight,
    COUNT(DISTINCT p2.id) AS cumulative
FROM post_weights AS p1
INNER JOIN post_weights AS p2 ON FLOOR(p2.reason_weight / 10) <= FLOOR(p1.reason_weight / 10)
GROUP BY FLOOR(p1.reason_weight / 10)
ORDER BY FLOOR(p1.reason_weight / 10) ASC;

Isso é, no entanto, inusitavelmente lento - deixei que funcionasse por 15 minutos sem terminar, o que não posso fazer na produção.

Existe uma maneira mais eficiente de fazer isso?

Caso você esteja interessado em testar o conjunto de dados inteiro, ele pode ser baixado aqui . O arquivo tem cerca de 60 MB e se expande para cerca de 250 MB. Como alternativa, existem 12.000 linhas em uma essência do GitHub aqui .

ArtOfCode
fonte

Respostas:

8

Usar funções ou expressões em condições JOIN geralmente é uma má idéia, digo normalmente porque alguns otimizadores podem lidar com isso razoavelmente bem e utilizar índices de qualquer maneira. Eu sugeriria criar uma tabela para os pesos. Algo como:

CREATE TABLE weights
( weight int not null primary key 
);

INSERT INTO weights (weight) VALUES (0),(10),(20),...(1270);

Verifique se você tem índices em posts_reasons:

CREATE UNIQUE INDEX ... ON posts_reasons (reason_id, post_id);

Uma consulta como:

SELECT w.weight
     , COUNT(1) as post_count
FROM weights w
JOIN ( SELECT pr.post_id, SUM(r.weight) as sum_weight     
       FROM reasons r
       JOIN posts_reasons pr
             ON r.id = pr.reason_id
       GROUP BY pr.post_id
     ) as x
    ON w.weight > x.sum_weight
GROUP BY w.weight;

Minha máquina em casa provavelmente tem de 5 a 6 anos, possui uma CPU Intel (R) Core (i) i5-3470 a 3,20 GHz e 8 Gb de ram.

uname -a Linux dustbite 4.16.6-302.fc28.x86_64 # 1 SMP Wed May 2 00:07:06 UTC 2018 x86_64 x86_64 x86_64 GNU / Linux

Eu testei contra:

https://drive.google.com/open?id=1q3HZXW_qIZ01gU-Krms7qMJW3GCsOUP5

MariaDB [test3]> select @@version;
+-----------------+
| @@version       |
+-----------------+
| 10.2.14-MariaDB |
+-----------------+
1 row in set (0.00 sec)


SELECT w.weight
     , COUNT(1) as post_count
FROM weights w
JOIN ( SELECT pr.post_id, SUM(r.weight) as sum_weight     
       FROM reasons r
       JOIN posts_reasons pr
             ON r.id = pr.reason_id
       GROUP BY pr.post_id
     ) as x
    ON w.weight > x.sum_weight
GROUP BY w.weight;

+--------+------------+
| weight | post_count |
+--------+------------+
|      0 |          1 |
|     10 |       2591 |
|     20 |       4264 |
|     30 |       4386 |
|     40 |       5415 |
|     50 |       7499 |
[...]   
|   1270 |     119283 |
|   1320 |     119286 |
|   1330 |     119286 |
[...]
|   2590 |     119286 |
+--------+------------+
256 rows in set (9.89 sec)

Se o desempenho for crítico e mais nada ajudar, você poderá criar uma tabela de resumo para:

SELECT pr.post_id, SUM(r.weight) as sum_weight     
FROM reasons r
JOIN posts_reasons pr
    ON r.id = pr.reason_id
GROUP BY pr.post_id

Você pode manter essa tabela por meio de gatilhos

Como há uma certa quantidade de trabalho que precisa ser feita para cada peso em pesos, pode ser benéfico limitar esta tabela.

    ON w.weight > x.sum_weight 
WHERE w.weight <= (select MAX(sum_weights) 
                   from (SELECT SUM(weight) as sum_weights 
                   FROM reasons r        
                   JOIN posts_reasons pr
                       ON r.id = pr.reason_id 
                   GROUP BY pr.post_id) a
                  ) 
GROUP BY w.weight

Como eu tinha muitas linhas desnecessárias na minha tabela de pesos (máximo 2590), a restrição acima reduziu o tempo de execução de 9 para 4 segundos.

Lennart
fonte
Esclarecimento: parece que ele está contando razões com um peso menor que w.weight- está certo? Estou procurando contar postagens com um peso total (soma dos pesos das linhas de razão associadas) de lte w.weight.
ArtOfCode
Ah desculpa. Vou reescrever a consulta
Lennart
Isso me deixou no resto do caminho, então obrigada! Só precisava selecionar a post_weightsexibição existente que eu já criei em vez de reasons.
ArtOfCode 16/05
@ArtOfCode, eu acertei na consulta revisada? BTW, obrigado por uma excelente pergunta. Clara, concisa e com muitos dados de amostra. Bravo
Lennart
7

No MySQL, variáveis ​​podem ser usadas em consultas para serem calculadas a partir de valores em colunas e para expressão em colunas novas e calculadas. Nesse caso, o uso de uma variável resulta em uma consulta eficiente:

SELECT
  weight,
  @cumulative := @cumulative + post_count AS post_count
FROM
  (SELECT @cumulative := 0) AS x,
  (
    SELECT
      FLOOR(reason_weight / 10) * 10 AS weight,
      COUNT(*)                       AS post_count
    FROM
      (
        SELECT 
          p.id,
          SUM(r.weight) AS reason_weight
        FROM
          posts AS p
          INNER JOIN posts_reasons AS pr ON p.id = pr.post_id
          INNER JOIN reasons AS r ON pr.reason_id = r.id
        GROUP BY
          p.id
      ) AS d
    GROUP BY
      FLOOR(reason_weight / 10)
    ORDER BY
      FLOOR(reason_weight / 10) ASC
  ) AS derived
;

A dtabela derivada é realmente sua post_weightsvisão. Portanto, se você planeja manter a visualização, poderá usá-la em vez da tabela derivada:

SELECT
  weight,
  @cumulative := @cumulative + post_count AS post_count
FROM
  (SELECT @cumulative := 0),
  (
    SELECT
      FLOOR(reason_weight / 10) * 10 AS weight,
      COUNT(*)                       AS post_count
    FROM
      post_weights
    GROUP BY
      FLOOR(reason_weight / 10)
    ORDER BY
      FLOOR(reason_weight / 10) ASC
  ) AS derived
;

Uma demonstração desta solução, que usa uma edição concisa da versão reduzida da sua instalação, pode ser encontrada e reproduzida no SQL Fiddle .

Andriy M
fonte
Eu tentei sua consulta com o conjunto de dados completo. Não sei por que (a consulta parece boa para mim), mas o MariaDB reclama ERROR 1055 (42000): 'd.reason_weight' isn't in GROUP BYse ONLY_FULL_GROUP_BYestá em @@ sql_mode. Desativando, notei que sua consulta é mais lenta que a minha na primeira vez em que é executada (~ 11 segundos). Depois que os dados são armazenados em cache, fica mais rápido (~ 1 segundo). Minha consulta é executada em cerca de 4 segundos todas as vezes.
Lennart
11
@Lennart: Isso é porque não é a consulta real. Corrigi-o no violino, mas esqueci de atualizar a resposta. Atualizando agora, obrigado pelo aviso.
Andriy M
@Lennart: Quanto ao desempenho, posso ter um equívoco sobre esse tipo de consulta. Eu pensei que deveria funcionar eficientemente porque os cálculos seriam completos em uma passagem sobre a mesa. Talvez não seja necessariamente o caso de tabelas derivadas, em particular aquelas que usam agregação. Receio não ter uma instalação adequada do MySQL nem conhecimento suficiente para analisar mais profundamente.
Andriy M
@Andriy_M, parece ser um bug na minha versão do MariaDB. Não gosta, GROUP BY FLOOR(reason_weight / 10)mas aceita GROUP BY reason_weight. Quanto ao desempenho, certamente não sou especialista também no MySQL, foi apenas uma observação na minha máquina de baixa qualidade. Como eu executei minha consulta primeiro, todos os dados já deveriam ter sido armazenados em cache, então não sei por que foi mais lento na primeira vez em que foi executado.
Lennart