【问题标题】:Remove redundant SQL code删除多余的 SQL 代码
【发布时间】:2010-05-10 00:43:35
【问题描述】:

代码

以下代码针对大量数据计算线性回归的斜率和截距。然后它将方程y = mx + b 应用于相同的结果集以计算每一行的回归线的值。

如何连接两个查询,以便在不执行两次WHERE 子句的情况下计算数据及其斜率/截距?

问题的一般形式是:

SELECT a.group, func(a.group, avg_avg)
FROM a
    (SELECT AVG(field1_avg) as avg_avg
     FROM (SELECT a.group, AVG(field1) as field1_avg
           FROM a
           WHERE (SOME_CONDITION)
           GROUP BY a.group) as several_lines -- potentially
    ) as one_line -- always
WHERE (SOME_CONDITION)
GROUP BY a.group -- again, potentially several lines

我让SOME_CONDITION 执行了两次。如下所示(更新为 STRAIGHT_JOIN 优化):

SELECT STRAIGHT_JOIN
  AVG(D.AMOUNT) as AMOUNT,
  Y.YEAR * ymxb.SLOPE + ymxb.INTERCEPT as REGRESSION_LINE,
  Y.YEAR as YEAR,
  MAKEDATE(Y.YEAR,1) as AMOUNT_DATE,
  ymxb.SLOPE,
  ymxb.INTERCEPT,
  ymxb.CORRELATION,
  ymxb.MEASUREMENTS
FROM
  CITY C,
  STATION S,
  STATION_DISTRICT SD,
  YEAR_REF Y,
  MONTH_REF M,
  DAILY D,
  (SELECT
    SUM(MEASUREMENTS) as MEASUREMENTS,

    ((sum(t.YEAR) * sum(t.AMOUNT)) - (count(1) * sum(t.YEAR * t.AMOUNT))) /
    (power(sum(t.YEAR), 2) - count(1) * sum(power(t.YEAR, 2))) as SLOPE,

    ((sum( t.YEAR ) * sum( t.YEAR * t.AMOUNT )) -
    (sum( t.AMOUNT ) * sum(power(t.YEAR, 2)))) /
    (power(sum(t.YEAR), 2) - count(1) * sum(power(t.YEAR, 2))) as INTERCEPT,

    ((avg(t.AMOUNT * t.YEAR)) - avg(t.AMOUNT) * avg(t.YEAR)) /
    (stddev( t.AMOUNT ) * stddev( t.YEAR )) as CORRELATION
  FROM (
    SELECT STRAIGHT_JOIN
      COUNT(1) as MEASUREMENTS,
      AVG(D.AMOUNT) as AMOUNT,
      Y.YEAR as YEAR
    FROM
      CITY C,
      STATION S,
      STATION_DISTRICT SD,
      YEAR_REF Y,
      MONTH_REF M,
      DAILY D
    WHERE
      -- For a specific city ...
      --
      $X{ IN, C.ID, CityCode } AND

      -- Find all the stations within a specific unit radius ...
      --
      6371.009 *
      SQRT(
        POW(RADIANS(C.LATITUDE_DECIMAL - S.LATITUDE_DECIMAL), 2) +
        (COS(RADIANS(C.LATITUDE_DECIMAL + S.LATITUDE_DECIMAL) / 2) *
         POW(RADIANS(C.LONGITUDE_DECIMAL - S.LONGITUDE_DECIMAL), 2)) ) <= $P{Radius} AND

      SD.ID = S.STATION_DISTRICT_ID AND

      -- Gather all known years for that station ...
      --
      Y.STATION_DISTRICT_ID = SD.ID AND

      -- The data before 1900 is shaky; insufficient after 2009.
      --
      Y.YEAR BETWEEN 1900 AND 2009 AND

      -- Filtered by all known months ...
      --
      M.YEAR_REF_ID = Y.ID AND

      -- Whittled down by category ...
      --
      M.CATEGORY_ID = $P{CategoryCode} AND

      -- Into the valid daily climate data.
      --
      M.ID = D.MONTH_REF_ID AND
      D.DAILY_FLAG_ID <> 'M'
    GROUP BY
      Y.YEAR
  ) t
) ymxb
WHERE
  -- For a specific city ...
  --
  $X{ IN, C.ID, CityCode } AND

  -- Find all the stations within a specific unit radius ...
  --
  6371.009 *
  SQRT(
    POW(RADIANS(C.LATITUDE_DECIMAL - S.LATITUDE_DECIMAL), 2) +
    (COS(RADIANS(C.LATITUDE_DECIMAL + S.LATITUDE_DECIMAL) / 2) *
     POW(RADIANS(C.LONGITUDE_DECIMAL - S.LONGITUDE_DECIMAL), 2)) ) <= $P{Radius} AND

  SD.ID = S.STATION_DISTRICT_ID AND

  -- Gather all known years for that station ...
  --
  Y.STATION_DISTRICT_ID = SD.ID AND

  -- The data before 1900 is shaky; insufficient after 2009.
  --
  Y.YEAR BETWEEN 1900 AND 2009 AND

  -- Filtered by all known months ...
  --
  M.YEAR_REF_ID = Y.ID AND

  -- Whittled down by category ...
  --
  M.CATEGORY_ID = $P{CategoryCode} AND

  -- Into the valid daily climate data.
  --
  M.ID = D.MONTH_REF_ID AND
  D.DAILY_FLAG_ID <> 'M'
GROUP BY
  Y.YEAR

问题

如何每次查询只执行一次重复位,而不是两次?重复代码:

  $X{ IN, C.ID, CityCode } AND
  6371.009 *
  SQRT(
    POW(RADIANS(C.LATITUDE_DECIMAL - S.LATITUDE_DECIMAL), 2) +
    (COS(RADIANS(C.LATITUDE_DECIMAL + S.LATITUDE_DECIMAL) / 2) *
     POW(RADIANS(C.LONGITUDE_DECIMAL - S.LONGITUDE_DECIMAL), 2)) ) <= $P{Radius} AND
  SD.ID = S.STATION_DISTRICT_ID AND
  Y.STATION_DISTRICT_ID = SD.ID AND
  Y.YEAR BETWEEN 1900 AND 2009 AND
  M.YEAR_REF_ID = Y.ID AND
  M.CATEGORY_ID = $P{CategoryCode} AND
  M.ID = D.MONTH_REF_ID AND
  D.DAILY_FLAG_ID <> 'M'
GROUP BY
  Y.YEAR

更新 1

使用变量和拆分查询似乎允许缓存启动,因为它现在在 3.5 秒内运行,而它曾经在 7 秒内运行。不过,如果有任何方法可以删除重复的代码,我会感谢您的帮助。

更新 2

上面的代码不能在 JasperReports 中运行,而 VIEW 虽然是一种可能的修复方法,但可能效率极低(因为 WHERE 子句是参数化的)。

更新 3

使用 Unreason 提出的勾股公式与收敛经络验证距离:

  6371.009 *
  SQRT(
    POW(RADIANS(C.LATITUDE_DECIMAL - S.LATITUDE_DECIMAL), 2) +
    (COS(RADIANS(C.LATITUDE_DECIMAL + S.LATITUDE_DECIMAL) / 2) *
    POW(RADIANS(C.LONGITUDE_DECIMAL - S.LONGITUDE_DECIMAL), 2)) )

(这与问题无关,但其他人是否想知道......)

更新 4

如图所示,代码在 JasperReports 中运行,针对 MySQL 数据库运行。 JasperReports 不允许变量或多个查询。

更新 5

我正在寻找一种可以干净利落地执行的解决方案。 ;-) 我已经编写了一些部分有效的解决方案,但遗憾的是,MySQL 不理解部分正确。请参阅与 Unreason 的讨论,以获得几乎可行的答案。

更新 6

我也许可以重用第一个 WHERE 子句中的变量并将它们与第二个子句进行比较(从而消除 一些 重复 - 对 $P{} 值的检查),但我会真的很喜欢消除重复。

更新 7

比较 YEAR 子句(如上一次更新中的假设)以消除重复的 BETWEEN 不起作用。

相关

How to eliminate duplicate calculation in SQL?

谢谢!

【问题讨论】:

  • 您是否询问过查询规划器打算如何执行该查询?真的是重复努力吗?顺便说一句,Y.YEAR BETWEEN 1900 AND 2009 是一个错误吗?
  • 另外,SQRT( POW( C.LATITUDE - S.LATITUDE, 2 ) + POW( C.LONGITUDE - S.LONGITUDE, 2 ) ) &lt; $P{Radius} 定义了一个椭圆...如果你真的想要一个圆,请使用 SQRT( POW( C.LATITUDE - S.LATITUDE, 2 ) + POW( C.LONGITUDE - S.LONGITUDE, 2 ) * COS ( (C.LATITUDE + S.LATITUDE) / 2 ) &lt; $P{Radius}
  • @Andrew:如果返回的数据没有经过回归线计算,那么它会在约 3.5 秒内执行。使用回归,大约需要 7 秒。我的猜测是重复的努力。 ;-) 需要年份条件(1900 年之前的数据不稳定,而 2009 年之后的整年不存在——也不存在;我没有原始数据,也没有收到新数据的更新)。
  • 好的,听起来确实是重复的。好吧,您可以创建一个临时表来缓存 where 条件的结果,或者向现有表添加一列(在事务中,然后故意放弃)。
  • @Andrew:这比我最初想象的还要复杂;我必须使用Haversine 公式来确定距离。 en.wikipedia.org/wiki/Great-circle_distance

标签: sql mysql postgresql ireport code-duplication


【解决方案1】:

您应该能够一次获得所需的一切:

 SELECT
    AVG(D.AMOUNT) as AMOUNT,
    Y.YEAR as YEAR,
    MAKEDATE(Y.YEAR,1) as AMOUNT_DATE,
    Y.YEAR * ymxb.SLOPE + ymxb.INTERCEPT as REGRESSION_LINE,             
    ((avg(AVG(D.AMOUNT) * Y.YEAR)) - avg(AVG(D.AMOUNT)) * avg(Y.YEAR)) /                  
    (stddev( AVG(D.AMOUNT) ) * stddev( Y.YEAR )) as CORRELATION,                     
    ((sum(Y.YEAR) * sum(AVG(D.AMOUNT))) - (count(1) * sum(Y.YEAR * AVG(D.AMOUNT)))) /
    (power(sum(Y.YEAR), 2) - count(1) * sum(power(Y.YEAR, 2))) as SLOPE,   
    ((sum( Y.YEAR ) * sum( Y.YEAR * AVG(D.AMOUNT) )) -
    (sum( AVG(D.AMOUNT) ) * sum(power(Y.YEAR, 2)))) / 
    (power(sum(Y.YEAR), 2) - count(1) * sum(power(Y.YEAR, 2))) as INTERCEPT
 FROM
    CITY C,
    STATION S,
    YEAR_REF Y,
    MONTH_REF M,
    DAILY D
 WHERE
    $X{ IN, C.ID, CityCode } AND
    SQRT(
        POW( C.LATITUDE - S.LATITUDE, 2 ) +
        POW( C.LONGITUDE - S.LONGITUDE, 2 ) ) < $P{Radius} AND
    S.STATION_DISTRICT_ID = Y.STATION_DISTRICT_ID AND
    Y.YEAR BETWEEN 1900 AND 2009 AND
    M.YEAR_REF_ID = Y.ID AND
    M.CATEGORY_ID = $P{CategoryCode} AND
    M.ID = D.MONTH_REF_ID AND
    D.DAILY_FLAG_ID <> 'M'
 GROUP BY
    Y.YEAR

直接从上面的查询中不起作用的事情(它有无意义的组​​合聚合和其他错误);这可能是检查您的公式的好时机

如果你决定做子查询确实简化了公式,那么:

  • 您可以在最内层查询中抓取(您确实抓取)所有必要的数据,并且您不必再重复外部查询中的所有表(只需从 t 中选择相关列,它们已经供您使用)
  • 您不必重复 where 条件

【讨论】:

  • 整个事情是按年份分组的,并且可以直接在该级别上进行平均;时间允许我会回来的。
  • @Dave,抱歉无法早点回复您。你能发布创建表语句和一些测试数据吗?对于 postgres 或 mysql。不必漂亮地格式化它。
【解决方案2】:

这个问题比你的概括要困难一些。我会这样说:

SELECT a.group, func(a.group, avg_avg)
FROM a
    (SELECT AVG(field1_avg) as avg_avg
     FROM (SELECT a.group, AVG(field1) as field1_avg
           FROM a
           WHERE (YOUR_CONDITION)
           GROUP BY a.group) as several_lines -- potentially
    ) as one_line -- always
WHERE (YOUR_CONDITION)
GROUP BY a.group -- again, potentially several lines

您有一个数据子集(受您的条件限制),它被分组并为每个组进行聚合。然后,您将向下聚合合并为单个值,并且您希望将该值的函数再次应用于每个组。显然,在分组子查询的结果可以作为实体引用之前,您不能重用条件。

在 MSSQL 和 Oracle 中,您将使用 WITH 运算符。在 MySQL 中,唯一的选择是使用临时表。我假设您的报告中有超过一年的时间(否则,查询会简单得多)。

UPD:很抱歉,我现在无法发布现成的代码(明天可以发布),但我有一个想法:

您可以使用GROUP_CONCAT 连接子查询中需要输出的数据,并使用FIND_IN_SETSUBSTRING_INDEX 函数将其拆分回外部查询中。外部查询将只加入 YEAR_REF 和聚合结果。

外部查询中的条件将只是WHERE FIND_IN_SET(year, concatenated_years)

UPD

这是使用 GROUP_CONCAT 将所需数据传递给外部 JOIN 的版本。

我的 cmets 以 --newtover: 开头。顺便说一句,1)我不认为 STRAIGHT_JOIN 有什么好处,2)COUNT(*) 在 MySQL 中有特殊的含义,应该在你想计算行数的时候使用。

SELECT STRAIGHT_JOIN
  -- newtover: extract the corresponding amount back
  SUBSTRING_INDEX(SUBSTRING_INDEX(GROUPED_AMOUNTS, '|', @pos),'|', -1) as AMOUNT,
  Y.YEAR * ymxb.SLOPE + ymxb.INTERCEPT as REGRESSION_LINE,
  Y.YEAR as YEAR,
  MAKEDATE(Y.YEAR,1) as AMOUNT_DATE,
  ymxb.SLOPE,
  ymxb.INTERCEPT,
  ymxb.CORRELATION,
  ymxb.MEASUREMENTS
FROM
  -- newtover: list of tables now contains only the subquery, YEAR_REF for grouping and init_vars to define the variable
  YEAR_REF Y,
  (SELECT
    SUM(MEASUREMENTS) as MEASUREMENTS,
    ((sum(t.YEAR) * sum(t.AMOUNT)) - (count(1) * sum(t.YEAR * t.AMOUNT))) /
    (power(sum(t.YEAR), 2) - count(1) * sum(power(t.YEAR, 2))) as SLOPE,
    ((sum( t.YEAR ) * sum( t.YEAR * t.AMOUNT )) -
    (sum( t.AMOUNT ) * sum(power(t.YEAR, 2)))) /
    (power(sum(t.YEAR), 2) - count(1) * sum(power(t.YEAR, 2))) as INTERCEPT,
    ((avg(t.AMOUNT * t.YEAR)) - avg(t.AMOUNT) * avg(t.YEAR)) /
    (stddev( t.AMOUNT ) * stddev( t.YEAR )) as CORRELATION,
    -- newtover: grouped fields for matching years and the corresponding amounts
    GROUP_CONCAT(Y.YEAR) as GROUPED_YEARS,
    GROUP_CONCAT(AMOUNT SEPARATOR '|') as GROUPED_AMOUNTS
  FROM (
    SELECT STRAIGHT_JOIN
      COUNT(1) as MEASUREMENTS,
      AVG(D.AMOUNT) as AMOUNT,
      Y.YEAR as YEAR
    FROM
      CITY C,
      STATION S,
      STATION_DISTRICT SD,
      YEAR_REF Y,
      MONTH_REF M,
      DAILY D
    WHERE
      -- For a specific city ...
      $X{ IN, C.ID, CityCode } AND
      -- Find all the stations within a specific unit radius ...
      6371.009 *
      SQRT(
        POW(RADIANS(C.LATITUDE_DECIMAL - S.LATITUDE_DECIMAL), 2) +
        (COS(RADIANS(C.LATITUDE_DECIMAL + S.LATITUDE_DECIMAL) / 2) *
         POW(RADIANS(C.LONGITUDE_DECIMAL - S.LONGITUDE_DECIMAL), 2)) ) <= $P{Radius} AND
      SD.ID = S.STATION_DISTRICT_ID AND
      -- Gather all known years for that station ...
      Y.STATION_DISTRICT_ID = SD.ID AND
      -- The data before 1900 is shaky; insufficient after 2009.
      Y.YEAR BETWEEN 1900 AND 2009 AND
      -- Filtered by all known months ...
      M.YEAR_REF_ID = Y.ID AND
      -- Whittled down by category ...
      M.CATEGORY_ID = $P{CategoryCode} AND
      -- Into the valid daily climate data.
      M.ID = D.MONTH_REF_ID AND
      D.DAILY_FLAG_ID <> 'M'
    GROUP BY
      Y.YEAR
  ) t
) ymxb,
(SELECT @pos:=NULL) as init_vars
WHERE
    -- newtover: check if the year is in the list and store the index into the variable
    @pos:=CAST(FIND_IN_SET(Y.YEAR, GROUPED_YEARS) as UNSIGNED)
GROUP BY
  Y.YEAR

【讨论】:

  • @Dave Jarvis:我用使用 GROUP_CONCAT 的 MySQL 查询更新了我的答案。检查是否有帮助。
【解决方案3】:

由于问题中的 SQL 被大量挂起(现在只显示相关部分),这是我的新答案

假设:条件真的一样,子查询和外查询之间没有出现棘手的列别名

回答: 您可以删除外部查询中的 where。

SELECT
  /* aggregate data */
  ymxb.*
FROM (
  SELECT
    /* similar aggregate data */
  WHERE
    /* some condition */
  GROUP BY
    YEAR
) ymxb
GROUP BY
  YEAR

这应该会给你同样的结果。

(另请注意,您可以删除内部位置并保留外部位置 - 结果应该相同,但性能可能不同)。

最后,重复 where 子句可能不会对性能产生太大影响 - 与任何 I/O(以及这些条件不会对任何新列进行操作,因此所有 I/O 都已完成)

此外,您的内部查询和外部查询使用相同的 GROUP BY,外部查询从子查询中获取所有数据。

这使得外部查询中的任何聚合函数都是多余的(子查询中的行,即外部查询的来源,已经按年份分组)。

这使得整个子选择变得多余。

【讨论】:

  • 当我删除内部查询时,检索结果需要更长的时间。奇怪。
  • 当我删除我得到的外部查询时,“列'YEAR'不能为空'。我知道,理论上,额外的 WHERE 子句应该不会有很大的影响。在实践中似乎确实如此——可能是因为数据集的大小,或者我没有正确测试速度。
  • @Dave,重新性能:如果你的意思是当你从内部查询中删除 WHERE 时,结果需要更长的时间来检索这实际上是 MySQL 预期的(可能发生的事情是 MySQL 计算所有表中所有行的聚合和表达式,然后删除不需要的内容;保留其中的位置有助于 MySQL 先过滤掉不需要的数据,然后减少工作)
  • @Dave,Re NULL,不确定您从哪里得到 NULL(您是否可能忘记显示查询的更多相关部分?您是仅从 ymbx 中选择还是加入列出更多表为你最初在做什么?那会改变推理/答案)。你的记录集有多少行?我一直在一台非常慢的机器上测试上述声明,但也在相对较小的记录集(~100k)上重复 where 条件可以忽略不计。
  • @Unreason:是的,当内部 WHERE 子句被删除时,查询花费了很长时间,以至于我在它完成之前将其杀死。粘贴在问题中的查询正是我在 iReport 中编辑的内容。当我删除外部 WHERE 时,我必须添加一些变量。 DAILY 表有 2.72 亿条记录,MONTH_REF 900 万,YEAR_REF 大约 165k,STATION 将近 8k。
【解决方案4】:

您可以在您的情况下使用临时表吗?尽管它仍然需要您使用 WHERE 子句两次,但它应该会大大提高您的性能。

DROP TEMPORARY TABLE IF EXISTS TEMP_DATA

CREATE TEMPORARY TABLE TEMP_DATA 
    (SELECT AVG(field1_avg) as avg_avg
     FROM (SELECT a.group, AVG(field1) as field1_avg
           FROM a
           WHERE (SOME_CONDITION)
           GROUP BY a.group)
    )

SELECT t.group, func(t.group, t.avg_avg)
FROM TEMP_DATA AS t
WHERE (SOME_CONDITION)
GROUP BY t.group

希望这会有所帮助! --配音

【讨论】:

  • 只能使用一条 SQL 语句,因为该语句必须由 JasperReports 执行,不允许多条语句作为其查询。
  • 可能有办法绕过这个单一的 SQL 语句限制。我发现了几个使用 JasperReports 调用复杂 SQL 查询的示例。它们并不漂亮,但看起来它们会起作用:google.com/…"
猜你喜欢
  • 2023-03-25
  • 2012-10-02
  • 1970-01-01
  • 1970-01-01
  • 2010-11-13
  • 2016-08-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多