【问题标题】:select rows as well as a total count in one query in mysql在mysql中的一个查询中选择行以及总数
【发布时间】:2012-09-20 15:42:14
【问题描述】:

假设我有一个表 t,表 t 有 15000 个条目

假设查询

SELECT * FROM t WHERE t.nid <1000

返回 1000 行

但是我只想要前 10 行,所以我做了一个 LIMIT

SELECT * FROM t WHERE t.nid <1000 LIMIT 10

是否可以构造一个查询,其中除了返回上面的 LIMIT 子句的 10 行信息之外,它还返回满足 WHERE 子句中设置的条件的行的总数,因此除了返回上面的 10 行,它也返回 1000,因为总共有 1000 行满足 WHERE 子句......并且都在单个查询中返回

【问题讨论】:

  • 请提及您的 MYSQL 版本。

标签: php mysql sql select


【解决方案1】:

你可以试试SQL_CALC_FOUND_ROWS,它可以在不再次运行语句的情况下获得总记录数。

SELECT SQL_CALC_FOUND_ROWS * FROM t WHERE t.nid <1000 LIMIT 10;   -- get records
SELECT FOUND_ROWS();                -- get count

参考:http://dev.mysql.com/doc/refman/5.0/en/information-functions.html

【讨论】:

  • SQL_CALC_FOUND_ROWS 查询修饰符和随附的 FOUND_ROWS() 函数自 MySQL 8.0.17 起已弃用,并将在未来的 MySQL 版本中删除。
【解决方案2】:

首选解决方案

首先,found_rows() 函数不可移植(它是 MySQL 扩展)并且将被删除。正如用户 @Zveddochka 指出的那样,它已经在 MySQL 8.0.17 中被弃用。

但更重要的是,事实证明如果您使用正确的索引,那么运行两个查询实际上会更快SQL_CALC_FOUND_ROWS 指令是通过产生额外恢复成本的“虚拟扫描”实现的。当查询未编入索引时,此成本将与 COUNT() 相同,因此运行两个查询将成本翻倍 - 即,使用 SQL_CALC_FOUND_ROWS 将使运行速度提高 50%。

但是当查询正确编入索引时会发生什么? The guys at Percona checked it out。事实证明,不仅COUNT() 非常快,因为它只访问元数据和索引,而且没有SQL_CALC_FOUND_ROWS 的查询更快,因为它不会产生任何额外的成本; 两个查询组合的成本低于增强型单个查询的成本

SQL_CALC_FOUND_ROWS 的结果如下:对于每个 b 值 执行非缓存需要 20-100 秒,预热后需要 2-5 秒。这样的 差异可以通过此查询所需的 I/O 来解释 – mysql 访问该查询可能产生的所有 10k 行,没有 LIMIT 子句。

结果如下:运行此查询需要 0.01-0.11 秒 第一次和所有连续运行的 0.00-0.02 秒。

所以,正如我们所见,SELECT+COUNT 的总时间(0.00-0.15 秒)很多 少于原始查询的执行时间(2-100 秒)。让我们来一个 看看解释...

那么,该怎么办?

// Run two queries ensuring they satisfy exactly the same conditions

$field1 = "Field1, Field2, blah blah blah";
$field2 = "COUNT(*) AS rows";

$where  = "Field5 = 'X' AND Field6 = 'Y' AND blah blah";

$cntQuery = "SELECT {$field2} FROM {$joins} WHERE {$where}";
$rowQuery = "SELECT {$field1} FROM {$joins} WHERE {$where} LIMIT {$limit}";

现在第一个查询返回计数,第二个查询返回实际数据。

旧答案(仅对非索引表有用)

不要这样做。如果您发现答案的这一部分比上面的部分更适合您,这几乎肯定是一个信号,表明您的设置中的其他东西不是最佳的 - 很可能您没有正确使用索引,或者您需要更新您的MySQL 服务器,或运行数据库分析/优化以更新基数统计信息

你可以,但我认为这会成为性能杀手。

您最好的选择是使用SQL_CALC_FOUND_ROWS MySQL 扩展并发出第二个查询以使用 FOUND_ROWS() 恢复全部行数。

 SELECT SQL_CALC_FOUND_ROWS * FROM t WHERE t.nid <1000 LIMIT 10;
 SELECT FOUND_ROWS();

参见例如http://www.arraystudio.com/as-workshop/mysql-get-total-number-of-rows-when-using-limit.html

或者您可以简单地运行没有LIMIT 子句的完整查询,并且只检索前十行。然后您可以根据需要使用一个查询,还可以通过mysql_num_rows() 获取行数。这并不理想,但对于大多数查询来说也不是那么灾难性。

如果你最后这样做,关闭查询并释放其资源时要非常小心:我发现检索不到完整结果集并忘记释放 rs 句柄是 @ 987654323@.

【讨论】:

  • 感谢您添加更新。可悲的是,Stack Overflow 上很少见,臭名昭著的“游戏化”规则从不鼓励让旧答案保持最新,我害怕在这个旧问题中找到所有过时的答案。
  • @YourCommonSense 不幸的是,很难跟踪一个人的答案。如果可能的话,我会尝试在任何人 cmet 或赞成/反对票时进行更新。也许他们可以在合适的停留时间后为“第二轮”更新提供奖金?
  • 我过去一直采用这种方法,但我从未想过性能。我刚刚进行了一个测试,COUNT(*)SQL_CALC_FOUND_ROWS 快了大约 15%。如果我是你,我会完全删除“旧”答案。
【解决方案3】:

听起来像你想要的FOUND_ROWS()

SELECT SQL_CALC_FOUND_ROWS * FROM t WHERE t.nid <1000 LIMIT 10;
SELECT FOUND_ROWS();

【讨论】:

    【解决方案4】:

    "是否可以构造一个查询,其中除了返回上面的 LIMIT 子句的 10 行信息之外,它还返回满足 WHERE 子句中设置的条件的行的总数"

    是的,可以通过使用窗口函数,即COUNT(*) OVER()(MySQL 8.0+),在单个查询中完成这两项操作:

    SELECT t.*, COUNT(*) OVER() AS cnt 
    FROM t 
    WHERE t.nid <1000 
    LIMIT 10;  
    

    db<>fiddle demo


    旁注:

    LIMIT 没有明确的ORDER BY 是不确定的。它可能会在多次运行之间返回不同的结果。

    【讨论】:

    • 它不是像 SQL_CALC_FOUND_ROWS 一样有效地工作,即无限制地遍历所有结果集,这可能是巨大的吗?
    • @YourCommonSense Logical query processing,LIMIT 在 WHERE、SELECT、ORDER BY 之后应用,因此在此总行数是已知的。无论如何,如果有疑问,我建议比较两种方法的执行计划。
    • 虽然有可能,但这种方法在我的本地机器上比单独的 COUNT(*) 查询慢 20 倍左右。
    【解决方案5】:

    有很多事情需要讨论。

    • 没有ORDER BYLIMIT 有点不可预测,因此有点毫无意义。

    • 但如果您添加ORDER BY,它可能需要查找所有行,对它们进行排序,然后只提供您想要的 10 个。

    • 或者,ORDER BY 可以由INDEX 充分处理。

    如果变成 2 个查询(根据 8.0.17 之后的需要),您的特定查询将是

    SELECT * FROM t WHERE t.nid < 1000 LIMIT 10;
    SELECT COUNT(*) FROM t WHERE t.nid < 1000;
    

    请注意,每个人都将从INDEX(nid) 中受益。第一个将从索引的 BTree 中挑选 10 个项目,然后在数据的 BTree 中查找它们——每个项目只涉及 10 行。第二个将扫描 INDEX 直到它达到 1000,并且不接触数据 BTree。

    如果您按照建议添加 ORDER BY,那么,第一个查询

    SELECT * FROM t WHERE t.nid < 1000 ORDER BY t.nid LIMIT 10;
    

    将与上述相同。但是

    SELECT * FROM t WHERE t.nid < 1000 ORDER BY t.abcd LIMIT 10;
    

    需要扫描很多行,而且速度很慢。并且可能使用临时表和文件排序。 (查看EXPLAIN 了解详细信息。)INDEX(nid, abcd) 会有所帮助,但只有一点点。

    还有其他变种,比如索引什么时候可以“覆盖”。

    “一次查询”的目标是什么?

    • 速度? -- 如上所述,还有其他更相关的因素。

    • 一致性? -- 您可能需要一个事务来避免,例如,从第一个查询中获取 N 行,并从 COUNT 中获取较小的数字。

       BEGIN;
       SELECT * ...
       SELECT COUNT(*) ...
       COMMIT;
      
    • 单个命令? -- 考虑一个结合了 2 个语句的存储过程。或者

       SELECT * FROM t WHERE t.nid < 1000 LIMIT 10
       UNION ALL
       SELECT COUNT(*) FROM t WHERE t.nid < 1000;
      

    但这会变得很棘手,因为列数不同,因此需要一些杂凑才能使第二个查询具有相同的列数。另一个变体涉及GROUP BY WITH ROLLUP。 (但它可能更难伪造。)

    Lukasz 的答案看起来很有希望。但是,它提供了一个额外的列(这可能很好),它的性能需要测试。如果您使用的是 8.0 并且他们的回答对您很有效,请接受该回答。

    【讨论】:

      【解决方案6】:

      Count(*) 时间复杂度为 O(1),所以可以使用子查询

      SELECT *, (SELECT COUNT(*) FROM t WHERE t.nid <1000) AS cnt
      FROM t 
      WHERE t.nid <1000 
      LIMIT 10
      

      【讨论】:

      • 首先应该是Select Count(*) FROM t WHERE t.nid &lt;1000。其次,在同一个查询中这样做没有多大意义。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-25
      • 2014-11-09
      • 2014-12-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多