【问题标题】:Is there a way to force a MySQL select query to resolve the having clause before the where clause?有没有办法强制 MySQL 选择查询在 where 子句之前解析有子句?
【发布时间】:2020-04-20 21:43:25
【问题描述】:

我有一个有点复杂的 MySQL Select 查询,它用几个左/外连接连接多个表,HAVING 子句中的一些 COUNT 和表达式,以及 WHERE 子句中的几个表达式,每次加载 web 时都会调用一次-我的网络应用程序中的页面,并再次使用网页中的 AJAX 以每 15 秒自动翻阅数据库中的数据。根据用户分页的方式,来自网页中表格的值(用于向后分页的第一行或用于正向分页的最后一行)在 AJAX 调用中发送以加载上一页/下一页。查询表达式是使用 php HereDoc 创建的,它使用变量替换来设置控制 WHERE 和 HAVING 子句的值,并首先按投票排序返回数据,然后是标题和艺术家姓名,如下所示:

WHERE ...
  AND ( ( `tracks`.`title`,  `artists`.`name` ) > ( ${title}, ${name} ) )
--   This line is not included in the initial query when the page is first created/loaded, but is
--   used in the AJAX called.

ORDER BY
  -- Total_Songs_Votes, Track_Sales, Track_Votes, Track_Listens, Tracks_Title,     Artists_Name
  3 DESC,               5 DESC,      4 DESC,      6 DESC,        `tracks`.`title`, `artists`.`name`

HAVING COUNT( `track_votes`.`id` )   <= ${votes}
   AND COUNT( `track_sales`.`id` )   <= ${sales}
   AND COUNT( `track_listens`.`id` ) <= ${listens}

选择输出列是:

SELECT `artists`.`name`                AS artists_name,
       `tracks`.`title`                AS tracks_title,
       COUNT( `track_votes`.`id` ) +
         COUNT( `track_sales`.`id` ) +
         COUNT( `track_listens`.`id` ) AS 'total_song_votes', -- Order By column 3
       COUNT( `track_votes`.`id` )     AS 'track_votes',      -- Order By column 4
       COUNT( `track_sales`.`id` )     AS 'track_sales',      -- Order By column 5
       COUNT( `track_listens`.`id` )   AS 'track_listens'     -- Order By column 6

问题在于,为了控制查询返回的数据,上面的计数由于 COUNT 需要使用 HAVING 子句,但这是在 WHERE 子句之后执行的。因此,不是从某些 total_song_votes、track_votes、track_sales 和 track_listens 的项目中“挑选”,然后是 WHERE 子句中的歌曲名和艺术家名,而是首先使用不能使用投票的 WHERE 子句,所以只有有标题和艺术家的名字,所以在使用的 HAVING 子句之前消除了太多行。

如何强制查询在 WHERE 子句中执行 HAVING 子句过滤?

选择查询的输出类似于以下内容:

<table border=1>
<tr>
<td bgcolor=silver class='medium'>artists_name</td>
<td bgcolor=silver class='medium'>tracks_title</td>
<td bgcolor=silver class='medium'>total_song_votes</td>
<td bgcolor=silver class='medium'>track_votes</td>
<td bgcolor=silver class='medium'>track_sales</td>
<td bgcolor=silver class='medium'>track_listens</td>
</tr>

<tr>
<td class='normal' valign='top'>DHF</td>
<td class='normal' valign='top'>T Song</td>
<td class='normal' valign='top'>4</td>
<td class='normal' valign='top'>2</td>
<td class='normal' valign='top'>2</td>
<td class='normal' valign='top'>0</td>
</tr>

<tr>
<td class='normal' valign='top'>DHF</td>
<td class='normal' valign='top'>H Song</td>
<td class='normal' valign='top'>2</td>
<td class='normal' valign='top'>1</td>
<td class='normal' valign='top'>1</td>
<td class='normal' valign='top'>0</td>
</tr>

<tr>
<td class='normal' valign='top'>DHF</td>
<td class='normal' valign='top'>A Song 2</td>
<td class='normal' valign='top'>1</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>1</td>
<td class='normal' valign='top'>0</td>
</tr>

<tr>
<td class='normal' valign='top'>DHF</td>
<td class='normal' valign='top'>A Song 1</td>
<td class='normal' valign='top'>1</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>1</td>
<td class='normal' valign='top'>0</td>
</tr>

<tr>
<td class='normal' valign='top'>DB</td>
<td class='normal' valign='top'>killer song</td>
<td class='normal' valign='top'>1</td>
<td class='normal' valign='top'>1</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>0</td>
</tr>

<tr>
<td class='normal' valign='top'>DB</td>
<td class='normal' valign='top'>Kills it</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>0</td>
</tr>

<tr>
<td class='normal' valign='top'>DB</td>
<td class='normal' valign='top'>scarry song</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>0</td>
</tr>

<tr>
<td class='normal' valign='top'>TB</td>
<td class='normal' valign='top'>Reggae All Day</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>0</td>
</tr>

<tr>
<td class='normal' valign='top'>TB</td>
<td class='normal' valign='top'>Reggae Just Today</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>0</td>
</tr>

<tr>
<td class='normal' valign='top'>Howard's Band</td>
<td class='normal' valign='top'>test</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>0</td>
<td class='normal' valign='top'>0</td>
</tr>
</table>

所以如果第一页只显示前两行,那么检索接下来两首歌曲的 AJAX 调用将发送 'DHF', 'H Song', 2, 1, 1, 0 回服务器和 WHERE和 HAVING 子句将被更改为包括以下内容:

  WHERE ...
    AND ( ( `tracks`.`title`,  `artists`.`name` ) > ( "H Song", "DHF" ) )

 HAVING COUNT( `track_votes`.`id` )   <= 1
    AND COUNT( `track_sales`.`id` )   <= 1
    AND COUNT( `track_listens`.`id` ) <= 0

所以 DHF 歌曲 A Song 1 和 A Song 2 都会被错误地淘汰。

这里的目标是检索网页上显示的最后一首歌曲之后的下两首歌曲*,使用投票值、歌曲标题和艺术家姓名,需要使用 WHERE 按顺序获取条款。正如我在上面所说的,WHERE 子句首先只考虑歌曲名称和艺术家的姓名,然后将票数较低但标题和名称较高的歌曲错误地排除在外。

  • 我在这里只使用两行,因为更容易显示此示例的数据,在实际页面中,一次显示 10 个项目,但这并不会真正影响任何内容,除了数据之间的中断位置页面。

请注意,我希望从查询中返回大量项目,因此我希望首先让查询正确删除尽可能多的行,然后再使用 php 逻辑删除剩余的行。另请注意,网页通过每 15 秒触发一次 AJAX 自动重新查询数据库,并且同一页面可以显示给多个用户,但不一定所有人都看到相同的数据,因为有些人可能会向后翻页数据或播放一首歌曲,此时不再分页,所以我希望这个查询非常有效,而不是每次运行时都重新查询整个数据库,而是让它在每个用户的最后一个结果之后独立地提取.

跟进

Astax 建议我将查询包含在 SELECT * FROM (...) 查询中,然后在那里进行过滤。

我将 HAVING 子句留在了内部 SELECT 中,以便它首先执行投票过滤,并且不再将任何数据检索到外部 SELECT * FROM ... 然后它也有。我真的不想过多地浏览整个数据集。

其实我把曲目标题和艺术家的名字移到了外层SELECT的WHERE子句中,但是后来发现还是导致了太多行被淘汰:

WHERE ( ( `tracks`.`title`,  `artists`.`name` ) > ( ${title}, ${name} ) )

所以我把这个表达式改成:

WHERE ( ( `tracks`.`title`,  `artists`.`name` ) not in
            ( ( ${title0}, ${name0} ), ... ( ${title9}, ${name9} ) ) )

在将 SQL 查询提交给数据库引擎之前,title0、name0 到 title9、$name9 变量被替换为页面上显示的十组标题和名称。

但问题是,如果超过十首歌曲(一个歌曲网页)的相同投票数相同,那么之前页面中的歌曲将再次显示并且分页将停止工作。

这个问题使得使用投票、歌曲标题和艺术家的名字作为现实世界的解决方案是不够的。我需要其他可以用来跟踪我的分页符的东西,它仍然允许查看服务器数据库中所做的更改,而不会在用户设备或服务器上的临时表方面产生大量开销。

关于如何管理分页并仍然支持我所描述的动态数据的任何想法?

谢谢

【问题讨论】:

  • 问题是HAVING对聚合结果起作用,聚合结果是从应用WHERE后剩下的内容生成的;如果您可以将它们结合起来,您将不再需要使用HAVING。相反,您能否将 HAVING 中的所有内容从 GROUP 中移出并放入专用列?这样您就可以对这些列应用索引,并将它们作为常规过滤器移动到您的 WHERE 语句中。

标签: php mysql pagination where-clause having-clause


【解决方案1】:

HAVING 存在的主要原因是在WHERE 条件和所有聚合之后应用一些过滤器。所以答案是否定的,你不能让它在WHERE之前运行。

但是,您可以:

  • 使用临时表。将一次选择的结果放入临时表中,从其他选择中添加更多数据,然后运行最终查询。
  • 使用嵌套选择,例如SELECT ... FROM (SELECT .... WHERE ... HAVING ....) as t WHERE ... HAVING ...

更新:

您不需要使用条件来实现分页。使用 LIMIT 声明 - 这就是它的用途。

SELECT `artists`.`name`                AS artists_name,
       `tracks`.`title`                AS tracks_title,
       COUNT( `track_votes`.`id` ) +
         COUNT( `track_sales`.`id` ) +
         COUNT( `track_listens`.`id` ) AS 'total_song_votes', -- Order By column 3
       COUNT( `track_votes`.`id` )     AS 'track_votes',      -- Order By column 4
       COUNT( `track_sales`.`id` )     AS 'track_sales',      -- Order By column 5
       COUNT( `track_listens`.`id` )   AS 'track_listens'     -- Order By column 6
LIMIT {$page_size} OFFSET {$page_start}

或者,如果你有一些强烈的理由使用条件,你可以使用嵌套查询:

SELECT * FROM 
   (SELECT `artists`.`name`                AS artists_name,
       `tracks`.`title`                AS tracks_title,
       COUNT( `track_votes`.`id` ) +
         COUNT( `track_sales`.`id` ) +
         COUNT( `track_listens`.`id` ) AS 'total_song_votes', -- Order By column 3
       COUNT( `track_votes`.`id` )     AS 'track_votes',      -- Order By column 4
       COUNT( `track_sales`.`id` )     AS 'track_sales',      -- Order By column 5
       COUNT( `track_listens`.`id` )   AS 'track_listens'     -- Order By column 6
   ) AS t
WHERE (t.track_title, t.artist_name, t.total_song_votes...) > ({$title}, {$name}, {$votes}...)
LIMIT {$page_size}

您仍然可以将WHERE 添加到非分组字段的内部查询中以处理更少的数据。只需使用不严格的条件 - &gt;=&lt;= 而不是 &gt;&lt;。进一步的过滤将在外部选择中完成。

但我再重复一遍 - 检查EXPLAIN 的输出以获取您的查询。很可能你没有通过使用条件而不是限制来保存任何东西。

【讨论】:

  • 感谢 astax 的回答。由于正在使用此查询的大量调用,我想尝试远离太多临时表。至于嵌套选择,这将起作用,并创建一个隐式临时表,但是当我将 SELECT * FROM ( ... ) 放在我的查询周围时,我得到了一个错误。我将扩展问题以涵盖这一点。
  • @HowardBrown 我明白你的意思。基本上你是说因为你不能在WHERE 部分中使用像(a, b) &gt; (c, d) 这样的分组列,所以你已经将它们添加为HAVING a&gt;c AND b&gt;d。但这些不是等价的表达方式。 (a, b) &gt; (c, d) 的等价物是 a&gt;c OR (a=c AND b&gt;=d)。尝试在HAVING 部分的表达式中使用它。
  • 顺便说一句,如果您担心性能,也许您应该考虑对数据库进行非规范化并将这些计数添加到单独的表中,而不是每次都即时计算。还要仔细查看EXPLAIN &lt;your_query&gt;。我认为您不会通过应用HAVING 而不是使用简单的LIMIT 来节省任何东西。无论如何都需要扫描与WHERE 匹配的所有列以计算这些聚合。
  • 对不起,前面的评论有错误。当然,正确的等效表达式应该使用&gt;,而不是&gt;=。这 - a&gt;c OR (a=c AND b&gt;d)
  • atax - 再次感谢。但是,当您说我应该将 (a, b) > (c, d) 表达式从 WHERE 子句移到 HAVING 子句时,我并没有完全关注您,但它不应该是 >=.,它绝对不应该是 >= 因为这会让网页上的第一个或最后一个项目在后续显示,具体取决于分页方向。将 (a, b) > (c, d) 部分添加到 HAVING 子句中,我需要在我的 GROUP BY 子句中包含 a 和 c 列,这不是问题,但是然后您会谈到 'using simple LIMIT' 。 ..
猜你喜欢
  • 2012-02-17
  • 2020-07-05
  • 1970-01-01
  • 2013-09-04
  • 1970-01-01
  • 2011-07-05
  • 1970-01-01
  • 2021-09-13
  • 2014-11-12
相关资源
最近更新 更多