使用生成大量计算统计信息的 MariaDB 视图 - 如何移动到计算表?

Posted

技术标签:

【中文标题】使用生成大量计算统计信息的 MariaDB 视图 - 如何移动到计算表?【英文标题】:Working with a MariaDB view that generates a lot of calculated statistics - How to move to a calculated table? 【发布时间】:2020-02-26 04:41:05 【问题描述】:

我目前有一个 MariaDB 数据库,每天都会填充不同的产品(大约 800 种),并且还会获取这些产品的价格更新。

我在价格/产品表的顶部创建了一个视图,该视图生成了过去 7 天、15 天和 30 天的平均值、平均值和众数等统计数据,并计算了今天价格与 7 天平均值之间的差异、15 天和 30 天。

问题在于,每当我运行此视图时,生成数据需要将近 50 秒。我看到一些关于切换到计算表的 cmets,其中计算将在新数据输入表时更新,但是我对此持怀疑态度,因为我在一个特定的位置插入了大约 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

如果切换到计算表,是否有最佳方法?

【问题讨论】:

您的表上没有索引 他们在现实生活中这样做,以及 codigowine 是价格表中产品的 FK 和产品上的 PK。很抱歉没有添加。 好的。稍后我将优化您的查询,但我无法用小数据测试性能。然后你可以在你的系统上测试它 每种产品每天多少个价格?如果很多,则构建并维护一个汇总表,每天每个产品一行。 【参考方案1】:

“计算表”不是 mysql / MariaDB 功能。所以我猜你的意思是从你的原始数据派生的另一个表,当你需要这些统计数据时使用它。

你说这张桌子“每天都有人......”。你的意思是它是从头开始重新加载的,还是你的意思是增加了 800 行?您所说的“每天”是指在一天中的特定时间,还是在一天中持续进行。

您是否总是必须从视图中选择所有行,或者您有时可以这样做SELECT columns FROM view WHERE something = 'constant';' 这很重要,因为优化技术在全行情况和少行情况之间有所不同。

如何有效地处理这个问题?

    您可以优化用于定义视图的查询,使其更快。这很可能是一个好方法。

    MariaDB 有一种称为持久计算列的列。这些是在插入或更新行时计算的。然后它们可供快速参考。但它们有局限性;它们不能用子查询来定义。

    您可以定义一个 EVENT(计划的 SQL 作业)来执行以下操作。

    创建一个新的空“计算”表,名称类似于tbl_new。 使用您的(慢速)视图插入所需的行。 翻转您的桌子,这样新的桌子就会替换当前的桌子,而您保留几张旧桌子。这将为您提供一个简短的窗口,其中tbl 不存在。 如果存在 tbl_old_2 则删除表; 重命名表 tbl_old 到 tbl_old_2,tbl 到 tbl_old,tbl_new 到 tbl;

【讨论】:

当我的意思是它每天都在填充时,我的意思是添加了 800 行(我只添加了以前不存在的产品)。每天早上 9 点,我都会执行 Python 工作并通过 API 进行分页,检查产品是否不存在并添加它们,以及当天所有产品的价格点。我可以从视图中选择列,但通常在后端,我确实想查找所有列。谢谢!【参考方案2】:

查看您的查询:尝试重构它以消除尽可能多的依赖子查询,而不是加入子查询。消除这些依赖子查询将产生巨大的性能差异。

计算模式是在数据集中查找极值的详细记录的应用程序。如果您将其用作子查询

    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 使用 JOIN 从 freq 中提取最频繁的价格。它还使用 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 天的模式就交给你了。

这是相当的查询。你最好希望下一个工作的人不会诅咒你的名字。 :-)

【讨论】:

对于没有特定 codigowine 的行的情况,我们可能需要到 s7、s15 的外部连接。我们还可以使用条件聚合模式在单个 CTE 中返回 7 天、15 天和 30 天的统计数据。 非常感谢,真是上了一课!通过阅读您(和其他人)的答案,我学到的知识比我在几个学期的课堂上学到的更多。 @O.琼斯只是想说,我从 58 秒下降到 0.0421 秒,实施了所有计算,包括所有模式和每个天组合的平均下降/增加。非常感谢! 您好,感谢您跟进性能提升。太棒了!而且,相关的子查询......不太好......【参考方案3】:

这是一大堆相关子查询,迫切需要适当的索引。

对于查询返回的合理数量的行,相关子查询可以提供合理的性能。但是,如果外部查询返回数千行,则将执行数千次子查询。

我倾向于避免对同一个表运行多个 SELECT,以获取最近 7 天、最近 15 天、最近 30 天,然后重复该操作以获取 AVG,重复该操作以获取 MAX,然后再次获取得到最小值。

相反,我倾向于使用条件聚合,一次性通过表格获得所有时间段 30 天、15 天和 7 天的所有统计信息 AVG、MAX、MIN。


...暂停一下,注意视图可能会影响性能;来自外部查询的谓词可能不会被推送到视图查询中。我们没有看到整个视图定义在做什么,但我怀疑我们可能正在实现一个大集合。


考虑这样的查询:

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


我对 - INTERVAL 9 HOUR 的查询有点困惑。在我看来,这些子查询可能会返回不止一行。没有 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 KEYDELETE 反连接(删除旧行)。

【讨论】:

感谢整个班级!我接受了 O. Jones 的回答,因为它更有效并且是第一个。不过,我真的很感谢你的全部努力! - INTERVAL 9 HOUR 的原因是因为工作在上午 9 点开始,所以我认为在工作没有运行时将昨天的日期作为平均值/模式的一部分是有意义的,否则只有 - INTERVAL 7 DAYS 我本来打算在午夜之后只获得 6 条记录。 O.Jones 的回答使用了相同的基本方法:使用聚合来检索集合,而不是使用相关子查询。 (在 MySQL 8.0 和 Maria 10.something 中添加了 CTE 支持,它在早期版本中不可用;CTE 的好处是它可以被多次引用,并且它将视图查询重新定位到外部查询的主体之外。)建议我们限定 all 列引用,我的偏好是使用短表别名。请注意,我们可以在单个查询中使用 条件聚合 来获取 s7、s15、s30 的结果,而不是三个单独的查询。【参考方案4】:

在考虑其他选项之前,请尝试提高查询效率。从长远来看,这是有益的:即使您最终移至计算表,您仍将利用更高效的刷新查询。

您的查询有 15-20 个内联子查询,它们都针对同一个依赖表(据我阅读)并对同一列进行聚合计算 precos(preconormal)(最小值、最大值、平均值、最常出现的值)。每个指标在从 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 中有重复的codigowine,则不会等效):

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

【讨论】:

谢谢,这提供了非常丰富的信息和不同的方法!通过阅读你(和其他人)的答案,我学到的知识比我在几个学期的课堂上学到的更多。我接受了 O. Jones 的回答,因为它更有效并且是第一个。 欢迎@LucasNeto!很高兴您觉得这对您有用。

以上是关于使用生成大量计算统计信息的 MariaDB 视图 - 如何移动到计算表?的主要内容,如果未能解决你的问题,请参考以下文章

mysql/mariadb怎样生成core文件

mysql/mariadb怎样生成core文件

生成mysql统计信息

解析大量和多次获取以进行统计

MySQL/MariaDB视图

Oracle统计信息