Получите инкрементные значения агрегированного значения в объединенной таблице

10

У меня есть две таблицы в базе данных MySQL 5.7.22: postsи reasons. Каждая строка сообщения имеет и принадлежит многим рядам причин. У каждой причины есть вес, связанный с ней, и поэтому у каждого сообщения есть общий агрегированный вес, связанный с ним.

Для каждого увеличения веса на 10 пунктов (т. Е. Для 0, 10, 20, 30 и т. Д.) Я хочу получить количество сообщений, общий вес которых меньше или равен этому приращению. Я ожидаю, что результаты для этого будут выглядеть примерно так:

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

Полные веса примерно нормально распределены, с несколькими очень низкими значениями и несколькими очень высокими значениями (максимальный в настоящее время 1277), но большинство в середине. Есть чуть менее 120000 строк postsи около 120 дюймов reasons. Каждый пост имеет в среднем 5 или 6 причин.

Соответствующие части таблиц выглядят так:

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)
);

До сих пор я пытался сбросить идентификатор сообщения и общий вес в представление, а затем соединить это представление с самим собой, чтобы получить агрегированное число:

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;

Это, однако, необычайно медленно - я позволил ему работать в течение 15 минут без остановки, что я не могу сделать в производстве.

Есть ли более эффективный способ сделать это?

Если вы заинтересованы в тестировании всего набора данных, его можно загрузить здесь . Размер файла составляет около 60 МБ, он увеличивается до 250 МБ. С другой стороны , есть 12000 строк в сущности GitHub здесь .

ArtOfCode
источник

Ответы:

8

Использование функций или выражений в условиях JOIN обычно плохая идея, я говорю обычно, потому что некоторые оптимизаторы могут справиться с этим довольно хорошо и использовать индексы в любом случае. Я бы предложил создать таблицу для весов. Что-то типа:

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

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

Убедитесь, что у вас есть индексы на posts_reasons:

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

Запрос как:

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;

Моей домашней машине, вероятно, 5-6 лет, у нее процессор Intel Core i5-3470 с тактовой частотой 3,20 ГГц и 8 ГБ оперативной памяти.

uname -a Linux dustbite 4.16.6-302.fc28.x86_64 # 1 SMP Ср 2 мая 00:07:06 UTC 2018 x86_64 x86_64 x86_64 GNU / Linux

Я протестировал против:

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)

Если производительность критична и больше ничего не помогает, вы можете создать сводную таблицу для:

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

Вы можете поддерживать эту таблицу с помощью триггеров

Поскольку существует определенный объем работы, который необходимо выполнить для каждого веса в весах, может быть полезно ограничить эту таблицу.

    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

Поскольку в моей таблице весов было много ненужных строк (максимум 2590), указанное выше ограничение сократило время выполнения с 9 до 4 секунд.

Леннарт
источник
Пояснение: Похоже, что он считает причины с весом ниже w.weight- это правильно? Я рассчитываю подсчитывать сообщения с общим весом (суммой весов связанных с ними строк причин) lte w.weight.
ArtOfCode
Ах, прости. Я перепишу запрос
Леннарт
Это дало мне остаток пути, так что спасибо! Просто нужно было выбрать из существующего post_weightsпредставления, которое я уже создал вместо reasons.
ArtOfCode
@ArtOfCode, правильно ли я понял исправленный запрос? Кстати, спасибо за отличный вопрос. Ясно, кратко и с большим количеством образцов данных. Браво
Леннарт
7

В MySQL переменные могут использоваться в запросах как для вычисления по значениям в столбцах, так и для использования в выражении для новых вычисляемых столбцов. В этом случае использование переменной приводит к эффективному запросу:

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
;

Производная dтаблица на самом деле ваше post_weightsмнение. Поэтому, если вы планируете сохранить представление, вы можете использовать его вместо производной таблицы:

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
;

Демонстрацию этого решения, в которой используется сжатая версия сокращенной версии вашей установки, можно найти и поиграть на SQL Fiddle .

Андрей М
источник
Я попробовал ваш запрос с полным набором данных. Я не уверен, почему (запрос выглядит нормально для меня), но MariaDB жалуется, ERROR 1055 (42000): 'd.reason_weight' isn't in GROUP BYесли ONLY_FULL_GROUP_BYнаходится в @@ sql_mode. Отключив его, я заметил, что ваш запрос выполняется медленнее, чем мой в первый раз (~ 11 секунд). Как только данные кэшируются, это происходит быстрее (~ 1 сек). Мой запрос выполняется около 4 секунд каждый раз.
Леннарт
1
@ Леннарт: Это потому, что это не настоящий запрос. Я исправил это в скрипке, но забыл обновить ответ. Обновление сейчас, спасибо за хедз-ап.
Андрей М
@Lennart: Что касается производительности, у меня может быть неправильное представление об этом типе запроса. Я думал, что это должно работать эффективно, потому что вычисления будут завершены за один проход по таблице. Возможно, это не обязательно относится к производным таблицам, в частности к тем, которые используют агрегирование. Боюсь, у меня нет ни правильной установки MySQL, ни достаточного опыта для более глубокого анализа.
Андрей М
@Andriy_M, похоже, это ошибка в моей версии MariaDB. Это не нравится, GROUP BY FLOOR(reason_weight / 10)но принимает GROUP BY reason_weight. Что касается производительности, то я, конечно, не эксперт в том, что касается MySQL, это было просто наблюдение на моей дрянной машине. Поскольку я сначала запустил свой запрос, все данные уже должны были быть кэшированы, поэтому я не знаю, почему он работал медленнее при первом запуске.
Леннарт