Работа с представлением MariaDB, которое генерирует много вычисляемых статистических данных. Как перейти к вычисляемой таблице? - PullRequest
2 голосов
/ 30 октября 2019

В настоящее время у меня есть база данных MariaDB, которая ежедневно заполняется различными продуктами (около 800), а также получает обновления цен на эти продукты.

Я создал представление о ценах / продуктах. таблица, которая генерирует статистические данные, такие как среднее значение, среднее значение и режим за последние 7, 15 и 30 дней, и вычисляет разницу от сегодняшней цены до средних значений за 7, 15 и 30 дней.

Проблема заключается в том, чтовсякий раз, когда я запускаю это представление, генерация данных занимает почти 50 секунд. Я видел некоторые комментарии о переходе на расчетную таблицу, в которой вычисления будут обновляться при вводе новых данных в таблицу, однако я довольно скептически отношусь к этому, поскольку я вставляю около 1000 ценовых точек в одну конкретнуювремя суток, которое повлияет на все расчеты на столе. Является ли вычисленная таблица чем-то, что обновляет только те строки, которые были обновлены, или она будет пересчитывать все? Я беспокоюсь о накладных расходах, которые это может вызвать (память не является проблемой для сервера).

Я вставил таблицы продуктов и цен и представление в DBFiddle, здесь: https://dbfiddle.uk/?rdbms=mariadb_10.2&fiddle=4cf594a85f950bed34f64d800601baa9

Расчеты можно увидеть для кода продукта 22141

Просто, чтобы дать представление, вот некоторые из расчетов, выполненных представлением (также доступно на скрипке):

        ROUND((((SELECT preconormal
        FROM precos
        WHERE codigowine = vinhos.codigowine
            AND timestamp >= CURRENT_DATE - INTERVAL 9 HOUR) / (SELECT AVG(preconormal)
        FROM precos
        WHERE codigowine = vinhos.codigowine
            AND timestamp >= CURRENT_DATE - INTERVAL 7 DAY) - 1) * 100), 2) as dif_7_dias,
        ROUND((((SELECT preconormal
        FROM precos
        WHERE codigowine = vinhos.codigowine
            AND timestamp >= CURRENT_DATE - INTERVAL 9 HOUR) / (SELECT AVG(preconormal)
        FROM precos
        WHERE codigowine = vinhos.codigowine
            AND timestamp >= CURRENT_DATE - INTERVAL 15 DAY) - 1) * 100), 2) as dif_15_dias,
        ROUND((((SELECT preconormal
        FROM precos
        WHERE codigowine = vinhos.codigowine
            AND timestamp >= CURRENT_DATE - INTERVAL 9 HOUR) / (SELECT AVG(preconormal)
        FROM precos
        WHERE codigowine = vinhos.codigowine
            AND timestamp >= CURRENT_DATE - INTERVAL 30 DAY) - 1) * 100), 2) as dif_30_dias

Если перейти на вычисляемую таблицу, есть ли оптимальный способ сделать это?

Ответы [ 4 ]

2 голосов
/ 31 октября 2019

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

В вашем запросе есть 15-20 встроенных подзапросов, которые все обращаются к одной и той же зависимойтаблицу (насколько я читал) и делать совокупные вычисления для одного и того же столбца precos(preconormal) (min, max, avg, наиболее часто встречающееся значение). Каждый показатель вычисляется несколько раз в диапазоне дат, который варьируется от 9 часов до 1 месяца назад. Итак:

SELECT 
    codigowine, 
    nomevinho, 
    DATE(timestamp) AS data_adc,
    -- ...

    /* Medidas estatísticas para 7 dias - min, max, media e moda  */
    ROUND(
        (
            SELECT MIN(preconormal)
            FROM precos
            WHERE 
                codigowine = vinhos.codigowine
                AND timestamp >= CURRENT_DATE - INTERVAL 7 DAY
        ), 
        2
    ) AS min_7_dias,
    ROUND(
        (
            SELECT MAX(preconormal)
            FROM precos
            WHERE 
                codigowine = vinhos.codigowine
                AND timestamp >= CURRENT_DATE - INTERVAL 7 DAY
        ), 
        2
    ) AS max_7_dias,

    -- ... and so on ...

FROM vinhos

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

select 
    codigowine,
    min(preconormal) min_30d 
    max(preconormal) max_30d,
    avg(preconormal) avg_30d,
    min(case when timestamp >= current_date - interval 15 day) min_15d,
    max(case when timestamp >= current_date - interval 15 day) max_15d,
    avg(case when timestamp >= current_date - interval 15 day) avg_15d,
    min(case when timestamp >= current_date - interval 7  day) min_07d,
    max(case when timestamp >= current_date - interval 7  day) max_07d,
    avg(case when timestamp >= current_date - interval 7  day) avg_07d
from precos
where timestamp >= current_date - interval 30 day
group by codigowine

Для производительности вам нужен индекс для (codigowine, timestamp, preconormal).

Затем вы можете объединить его с исходной таблицей:

select
    v.nomevinho, 
    date(v.timestamp) data_adc,
    p.*
from vinhos v
inner join (
    select 
        codigowine,
        min(preconormal) min_30d 
        max(preconormal) max_30d,
        avg(preconormal) avg_30d,
        min(case when timestamp >= current_date - interval 15 day then preconormal end) min_15d,
        max(case when timestamp >= current_date - interval 15 day then preconormal end) max_15d,
        avg(case when timestamp >= current_date - interval 15 day then preconormal end) avg_15d,
        min(case when timestamp >= current_date - interval 7  day then preconormal end) min_07d,
        max(case when timestamp >= current_date - interval 7  day then preconormal end) max_07d,
        avg(case when timestamp >= current_date - interval 7  day then preconormal end) avg_07d
    from precos
    where timestamp >= current_date - interval 30 day
    group by codigowine         
) p on p.codigowine = v.codigowine

Это должен быть разумный базовый запрос для построения. Чтобы получить другие вычисленные значения (наиболее часто встречающееся значение за период, последнее значение), вы можете добавить дополнительные объединения или использовать встроенные запросы.

Для завершения: вот еще одна версия базового запроса, которая объединяет после присоединения. В зависимости от того, как ваши данные распределяются по двум таблицам, это может быть или не быть более эффективным (и не будет эквивалентным, если в таблице vinhos есть дубликаты *1021*):

select
    v.nomevinho, 
    date(v.timestamp) data_adc,
    p.codigowine,
    date(v.timestamp) data_adc,
    min(p.preconormal) min_30d 
    max(p.preconormal) max_30d,
    avg(p.preconormal) avg_30d,
    min(case when p.timestamp >= current_date - interval 15 day then p.preconormal end) min_15d,
    max(case when p.timestamp >= current_date - interval 15 day then p.preconormal end) max_15d,
    avg(case when p.timestamp >= current_date - interval 15 day then p.preconormal end) avg_15d,
    min(case when p.timestamp >= current_date - interval 7  day then p.preconormal end) min_07d,
    max(case when p.timestamp >= current_date - interval 7  day then p.preconormal end) max_07d,
    avg(case when p.timestamp >= current_date - interval 7  day then p.preconormal end) avg_07d
from vinhos v
inner join precos p
    on  p.codigowine = v.codigowine
    and p.timestamp >= current_date - interval 30 day
group by v.codigowine, v.nomevinho
2 голосов
/ 31 октября 2019

Это целая куча коррелированных подзапросов, требующих соответствующей индексации.

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

Я бы не стал запускать несколько SELECT для одной и той же таблицы, чтобы получить последние 7 дней, последние 15дней, последние 30 дней, а затем повторяя это, чтобы получить AVG, повторяя это, чтобы получить MAX, и снова, чтобы получить MIN.

Вместо этого я бы склонялся к использованию условного агрегирования, чтобы получить всю статистику AVG, MAX, MIN, для всех периодов времени 30 дней, 15 дней и 7 дней, за один проход по таблице.


... пауза, чтобы заметить, что представления могут быть проблематичнымидля исполнения;предикаты из внешнего запроса могут не помещаться в запрос представления. Мы не видим, что делает все определение представления, но я подозреваю, что мы можем материализовать большой набор.


Рассмотрим такой запрос:

SELECT ...
     , ROUND( ( n.mal / a.avg_07_day - 1)*100 ,2)     AS dif_7_dias
     , ROUND( ( n.mal / a.avg_15_day - 1)*100 ,2)     AS dif_15_dias
     , ROUND( ( n.mal / a.avg_30_day - 1)*100 ,2)     AS dif_30_dias
     , ...
  FROM vinhos
  LEFT
  JOIN ( SELECT h.codigowine
              , AVG(IF( h.timestamp >= CURRENT_DATE + INTERVAL -30 DAY, h.preconormal, NULL)) AS avg_30_day
              , MAX(IF( h.timestamp >= CURRENT_DATE + INTERVAL -30 DAY, h.preconormal, NULL)) AS max_30_day
              , MIN(IF( h.timestamp >= CURRENT_DATE + INTERVAL -30 DAY, h.preconormal, NULL)) AS min_30_day
              , AVG(IF( h.timestamp >= CURRENT_DATE + INTERVAL -15 DAY, h.preconormal, NULL)) AS avg_15_day 
              , MAX(IF( h.timestamp >= CURRENT_DATE + INTERVAL -15 DAY, h.preconormal, NULL)) AS max_15_day 
              , MIN(IF( h.timestamp >= CURRENT_DATE + INTERVAL -15 DAY, h.preconormal, NULL)) AS min_15_day 
              , AVG(IF( h.timestamp >= CURRENT_DATE + INTERVAL  -7 DAY, h.preconormal, NULL)) AS avg_07_day
              , MAX(IF( h.timestamp >= CURRENT_DATE + INTERVAL  -7 DAY, h.preconormal, NULL)) AS max_07_day
              , MIN(IF( h.timestamp >= CURRENT_DATE + INTERVAL  -7 DAY, h.preconormal, NULL)) AS min_07_day
           FROM precos h
          GROUP
             BY h.codigowine
         HAVING h.codigowine IS NOT NULL
       ) a
    ON a.codigowine = vinhos.codigowine

  LEFT
  JOIN ( SELECT s.codigowine
              , MAX(s.precnormal) AS mal
              , MIN(s.precnormal) AS mil
           FROM precos s
          WHERE s.timestamp >= CURRENT_DATE - INTERVAL 9 HOUR
          GROUP 
             BY s.codigowine
         HAVING s.codigowine IS NOT NULL
       ) n
    ON n.codigowine = vinhos.codigowine

Рассмотримзапрос встроенного просмотра a.

Обратите внимание, что мы можем запустить этот SELECT отдельно и получить возвращенный набор результатов, как если бы мы возвращали результат из таблицы. Мы ожидаем, что это сделает одиночный проход через ссылочную таблицу. Могут существовать некоторые предикаты (условия в предложении WHERE), которые будут фильтровать нашу строку или позволят нам лучше использовать индекс. Как написано в настоящее время, запрос может использовать индекс с начальным столбцом codigowine, чтобы избежать (потенциально дорогой) операции «Использование файловой сортировки» для удовлетворения GROUP BY.


I'mнемного смущен запросами - ИНТЕРВАЛ 9 ЧАС. Мне кажется, что эти подзапросы могут потенциально возвращать более одной строки. Там нет предложения LIMIT (и нет ORDER BY) ... но похоже, что мы ожидаем одно значение (скалярное), учитывая операцию деления.

Без понимания того, чего мы пытаемся достичьНе зная спецификации, я обернул свою путаницу и поместил ее в другое встроенное представление n ... не то, что мы хотим сделать, а просто, чтобы проиллюстрировать (снова) встроенное представление, возвращающее набор результатов. Какие бы значения мы ни пытались получить из - подзапроса INTERVAL 9 HOUR, я думаю, что мы можем вернуть их и в виде набора.


С учетом всего сказанного мы можем теперь обойтина ответ на заданный вопрос: добавление «вычисляемой таблицы».

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

CREATE TABLE calc_stats_n_days
( codigowine <datatype> PRIMARY KEY
, avg_30_day  DOUBLE
, max_30_day  DOUBLE      
, min_30_day  DOUBLE
, avg_15_day  DOUBLE
, ...

Для начальной совокупности ...

INSERT INTO calc_stats_n_days 
( codigowine, avg_30_day, maxg_30_day, min_30_day, avg_15_day, ... )
         SELECT h.codigowine
              , AVG(IF( h.timestamp >= CURRENT_DATE + INTERVAL -30 DAY, h.preconormal, NULL)) AS avg_30_day
              , MAX(IF( h.timestamp >= CURRENT_DATE + INTERVAL -30 DAY, h.preconormal, NULL)) AS max_30_day
              , MIN(IF( h.timestamp >= CURRENT_DATE + INTERVAL -30 DAY, h.preconormal, NULL)) AS min_30_day
              , AVG(IF( h.timestamp >= CURRENT_DATE + INTERVAL -15 DAY, h.preconormal, NULL)) AS avg_15_day 
              , ...

Для текущей синхронизации, я бы, вероятно,создайте временную таблицу, заполните ее тем же запросом, а затем выполните синхронизацию между временной таблицей и целевой таблицей. Может быть INSERT ... ON DUPLICATE KEY и DELETE анти-объединение (для удаления старых строк).

2 голосов
/ 30 октября 2019

«Расчетная таблица» не является функцией MySQL / MariaDB. Итак, я предполагаю, что вы имеете в виду еще одну таблицу, полученную из ваших необработанных данных, которую вы используете, когда вам нужна эта статистика.

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

Всегда ли вам нужно выбирать все строки в вашем представлении, или вы можете иногда делать SELECT columns FROM view WHERE something = 'constant'; 'Это важнопотому что методы оптимизации различаются для случая со всеми строками и случая с несколькими строками.

Как вы можете эффективно решить эту проблему?

  1. Вы можете оптимизировать запрос, используемый для определения вашего представления, и сделать его быстрее. Скорее всего, это хороший подход.

  2. MariaDB имеет тип столбца, известный как постоянный вычисляемый столбец. Они вычисляются, когда строки вставлены или обновлены. Тогда они доступны для быстрого ознакомления. Но у них есть ограничения;они не могут быть определены с помощью подзапросов.

  3. Вы можете определить СОБЫТИЕ (запланированное задание SQL) для выполнения следующих действий.

    • Создать новый, пустой,«вычисленная» таблица с именем, например tbl_new.
    • Используйте свое (медленное) представление для вставки нужных ей строк.
    • Переверните таблицы, чтобы новая заменила текущую. и ты оставляешь пару старших. Это даст вам краткое окно, где tbl не существует.
      • УДАЛИТЬ СТОЛ, ЕСЛИ СУЩЕСТВУЕТ tbl_old_2;
      • ПЕРЕИМЕНОВАТЬ СТОЛ tbl_old TO tbl_old_2, tbl TO tbl_old, tbl_new TO tbl;
1 голос
/ 31 октября 2019

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

Определение режима - это приложение для поиска подробной записи для экстремального значения в наборе данных. Если вы используете это как подзапрос

    WITH freq AS (
            SELECT COUNT(*) freq,
                   ROUND(preconormal, 2) preconormal,
                   codigowine
              FROM precos
              WHERE timestamp >= CURRENT_DATE - INTERVAL 7 DAY
              GROUP BY  ROUND(preconormal, 2), codigowine
        ),
        most AS (
           SELECT MAX(freq) freq,
                  codigowine
             FROM freq
            GROUP BY codigowine
       ),
       mode AS (
         SELECT GROUP_CONCAT(preconormal ORDER BY preconormal DESC) modeps,
                freq.codigowine
           FROM freq
           JOIN most ON freq.freq = most.freq
          GROUP BY freq.codigowine
       )
       SELECT * FROM mode

Вы можете найти наиболее частую цену для каждого элемента. Первый CTE, freq, получает цены и их частоты.

Второй CTE, most, находит частоту наиболее частой цены (или цен).

ТретийCTE, mode, извлекает наиболее часто встречающиеся цены из freq, используя JOIN. Он также использует GROUP_CONCAT (), потому что возможно иметь более одного режима - самая частая цена.

Для вашей статистики вы можете сделать это:

WITH s7 AS (
  SELECT ROUND(MIN(preconormal), 2) minp,
         ROUND(AVG(preconormal), 2) meanp,
         ROUND(MAX(preconormal), 2) maxp,
         codigowine
    FROM precos
   WHERE timestamp >= CURRENT_DATE - INTERVAL 7 DAY
   GROUP BY codigowine
),
s15 AS (
  SELECT ROUND(MIN(preconormal), 2) minp,
         ROUND(AVG(preconormal), 2) meanp,
         ROUND(MAX(preconormal), 2) maxp,
         codigowine
    FROM precos
   WHERE timestamp >= CURRENT_DATE - INTERVAL 15 DAY
   GROUP BY codigowine
),
s30 AS (
  SELECT ROUND(MIN(preconormal), 2) minp,
         ROUND(AVG(preconormal), 2) meanp,
         ROUND(MAX(preconormal), 2) maxp,
         codigowine
    FROM precos
   WHERE timestamp >= CURRENT_DATE - INTERVAL 30 DAY
   GROUP BY codigowine
),
m7 AS (
   WITH freq AS (
         SELECT COUNT(*) freq,
                ROUND(preconormal, 2) preconormal,
                codigowine
           FROM precos
           WHERE timestamp >= CURRENT_DATE - INTERVAL 7 DAY
           GROUP BY  ROUND(preconormal, 2), codigowine
     ),
     most AS (
        SELECT MAX(freq) freq,
               codigowine
          FROM freq
         GROUP BY codigowine
    ),
    mode AS (
      SELECT GROUP_CONCAT(preconormal ORDER BY preconormal DESC) modeps,
             freq.codigowine
        FROM freq
        JOIN most ON freq.freq = most.freq
       GROUP BY freq.codigowine
    )
    SELECT * FROM mode
)
SELECT v.codigowine, v.nomevinho, DATE(timestamp) AS data_adc,
       s7.minp min_7_dias, s7.maxp max_7_dias,  s7.meanp media_7_dias, m7.modeps moda_7_dias,
       s15.minp min_15_dias, s15.maxp max_15_dias,  s15.meanp media_15_dias, 
       s30.minp min_30_dias, s30.maxp max_30_dias,  s30.meanp media_30_dias
  FROM vinhos v
  LEFT JOIN s7 ON v.codigowine = s7.codigowine
  LEFT JOIN m7 ON v.codigowine = m7.codigowine
  LEFT JOIN s15 ON v.codigowine = s15.codigowine
  LEFT JOIN s30 ON v.codigowine = s30.codigowine

Я оставлю это длявам делать режимы на 15 и 30 дней.

Это вполне запрос. Тебе лучше надеяться, что следующий парень, который поработает над этим, не проклянет твое имя. : -)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...