【问题标题】:Optimizing mySQL query to scale better优化 mySQL 查询以更好地扩展
【发布时间】:2021-12-11 21:02:02
【问题描述】:

我想查询数据库以检索名称列表(名称列表由用户在 python 中提供)。我查找这些姓名的数据的标准如下:结果应该按照用户提供的姓名列表的顺序出现,所以如果我说 ...WHERE name = "Bob" OR name = "Alice" 我希望 Bob 的结果首先出现,然后是 Alice 的结果.第二个标准是,如果搜索一个名字两次,那么结果也应该包含两次,所以我想写下...WHERE name = 'Bob' OR name = 'Bob',这样结果也包含两次 Bob 的行。

我想出了以下查询:

SELECT * FROM
   (SELECT *, 1 order_position FROM table WHERE name = 'Alice'
    UNION ALL
    SELECT *, 2 order_position FROM table WHERE name = 'Bob'
    UNION ALL
    SELECT *, 3 order_position FROM table WHERE name = 'Charlie'
    UNION ALL 
    SELECT *, 4 order_position FROM table WHERE name = 'Dan'
   ) r ORDER BY order_position

这个查询效果很好,但是当用户提交数百个名字并且有数百个 UNION ALL 部分时,查询变得非常慢。有没有办法在保持前面提到的两个条件的同时提高查询的性能?

【问题讨论】:

标签: mysql sql performance query-optimization


【解决方案1】:

如果name 被编入索引,以下将是“快速”的:

WHERE name IN             ('Alice', 'Bob', 'Charlie', 'Dan')
ORDER BY FIND_IN_SET(name, 'Alice,Bob,Charlie,Dan')

注意 Where 和 Order 之间的语法差异。

以下可能会慢一些,因为它不能使用任何索引,但编码更简单:

WHERE    FIND_IN_SET(name, 'Alice,Bob,Charlie,Dan')
ORDER BY FIND_IN_SET(name, 'Alice,Bob,Charlie,Dan')

请注意FIND_IN_SET 中的限制,即项目中不能使用逗号。

CASEFIND_IN_SET() 在任何情况下都不会使用索引。 (参见“sargable”)

重复

例如,如果有多个“Bobs”,那么每个“Bobs”的效果都与上述完全相同:

name IN ('Alice', 'Bob', 'Charlie', 'Bob', 'Dan')
FIND_IN_SET(name, 'Alice,Bob,Charlie,Bob,Dan')

也就是说,所有 Bob 将在所有 Charlie 之前列在输出中。此外,没有单独的行被列出两次。

【讨论】:

  • 你错过了这个:如果搜索一个名字两次,那么结果也应该包含两次
  • @forpas - 我的建议恰好是这样做的。 (我为此添加了一个明确的注释。)
  • 所有的 Bob 都将在所有 Charlies 之前列在输出中 是,但不是两次。
【解决方案2】:

您必须以某种方式使用每个名称的order_position 构造名称列表。
您可以在使用UNION ALL 保留重复名称的查询中执行此操作,如下所示:

SELECT 'Alice' name, 1 order_position UNION ALL
SELECT 'Bob', 2 UNION ALL
SELECT 'Charlie', 3 UNION ALL
SELECT 'Dan', 4 UNION ALL
SELECT 'Alice', 1 UNION ALL
SELECT 'Bob', 2 UNION ALL
...............................

那么你所要做的就是将它加入到表中:

SELECT t.* 
FROM tablename t
INNER JOIN (
  SELECT 'Alice' name, 1 order_position UNION ALL
  SELECT 'Bob', 2 UNION ALL
  SELECT 'Charlie', 3 UNION ALL
  SELECT 'Dan', 4 UNION ALL
  SELECT 'Alice', 1 UNION ALL
  SELECT 'Bob', 2 UNION ALL
  ...............................
) n ON n.name = t.name
ORDER BY n.order_position;

在 MySql 8.0+ 中,您可以使用 CTE:

WITH cte(name, order_position) AS (VALUES 
  ROW('Alice', 1), ROW('Bob', 2), ROW('Charlie', 3), 
  ROW('Dan', 4), ROW('Alice', 1), ROW('Bob', 2),
  ...................................................
)
SELECT t.* 
FROM tablename t INNER JOIN cte c 
ON c.name = t.name
ORDER BY c.order_position;

【讨论】:

    【解决方案3】:
    SELECT *, CASE name WHEN 'Alice'   THEN 1
                        WHEN 'Bob'     THEN 2
                        WHEN 'Charlie' THEN 3
                        WHEN 'Dan'     THEN 4
                        END AS order_position 
    FROM table 
    WHERE name IN ('Alice', 'Bob', 'Charlie', 'Dan')
    ORDER BY order_position;
    

    或没有附加列:

    SELECT *
    FROM table 
    WHERE name IN ('Alice', 'Bob', 'Charlie', 'Dan')
    ORDER BY CASE name WHEN 'Alice'   THEN 1
                       WHEN 'Bob'     THEN 2
                       WHEN 'Charlie' THEN 3
                       WHEN 'Dan'     THEN 4
                       END;
    

    PS。对于这个名称,设置ORDER BY name 就足够了。


    这如何处理重复某些结果的要求? – 威廉·伦泽马

    如果需要重复,则必须将列表转换为行集。

    SELECT table.*
    FROM table
    JOIN ( SELECT 1 pos, 'Alice'   name UNION ALL
           SELECT 2    , 'Bob'          UNION ALL
           SELECT 3    , 'Charlie'      UNION ALL
           SELECT 4    , 'Bob'          UNION ALL
           SELECT 5    , 'Charlie' ) names USING (name)
    ORDER BY names.pos
    

    【讨论】:

    • 这如何处理重复某些结果的要求?
    • @WillemRenzema 如果需要重复,则必须将列表转换为行集...等等...完成。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-06-18
    • 1970-01-01
    • 2014-09-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多