【问题标题】:Trouble optimizing MySQL query优化 MySQL 查询时遇到问题
【发布时间】:2014-06-13 23:03:52
【问题描述】:

我正在处理以下查询,但不确定如何进行进一步优化:

SELECT u.id AS userId, firstName, profilePhotoId, preferredActivityId, preferredSubActivityId, availabilityType,
       3959 * ACOS(COS(radians(requestingUserLat)) * COS(radians(u.latitude)) * COS(radians(u.longitude) - radians(requestingUserLon)) + SIN(radians(requestingUserLat)) * SIN(radians(u.latitude))) AS distanceInMiles
  FROM users u
 WHERE u.id IN (
        SELECT uu.id
          FROM users uu
         WHERE uu.latitude      between lat1    and lat2 -- MySQL 5.7 supports Point data type, but it is not indexed in innoDB. We store latitude and longitude as DOUBLE for now
           AND uu.longitude     between lon1    and lon2
           AND uu.dateOfBirth   between maxAge  and minAge -- dates are in millis, therefore maxAge will have a smaller value than minAge and so it needs to go first
     )
   AND IF(gender       is null, TRUE, u.gender = gender)
   AND IF(activityType is null, TRUE, u.preferredActivityType = activityType)
   AND u.accountState = 'A'
   AND u.id != userId
HAVING distanceInMiles < searchRadius ORDER BY distanceInMiles LIMIT pagingStart, pagingLength;


CREATE INDEX `findMatches` ON `users` (`latitude` ASC, `longitude` ASC, `dateOfBirth` ASC) USING BTREE;


这里的想法是有一个内部查询,使用上面指定的覆盖索引,根据用户位置和年龄来识别符合条件的行。在具有几百万行的表中,无需全表扫描即可将它们缩小到几千行。然后针对更细粒度的条件(例如性别、可用性等)对生成的行进行测试 - 这一次对缩减的结果集进行全面扫描是不可避免的。

这“几乎”按预期运行,EXPLAIN 表明内部查询确实使用了覆盖索引的完整键长度(3 列),然后外部查询通过 PK 查找返回的行.


问题:
搜索范围在几百英里内时性能令人满意,但是当我到达一千英里时,由于指定边界内的用户数量增加,它开始下降。如果搜索范围保持不变,但用户数量增加了几个订单,问题也会变得明显。以下是我目前发现的问题:

  1. MySQL 目前不支持内部查询中的LIMIT,因此内部查询将返回所有符合条件的userIDs(即数千个),即使外部查询随后会将它们限制为十几个。李>
  2. 启用optimizer_trace 并查看幕后工作表明只有我的覆盖索引的列latitude 用作range。我不确定为什么会这样,特别是因为 EXPLAIN 建议使用完整的索引键长度。


问题:
我如何解决上面的(1)和(2)?在有人建议对 lat 和 long 使用空间数据类型之前,请注意 the latest InnoDB engine (MySQL v5.7) does not support spatial indexes, just spatial data types

【问题讨论】:

    标签: mysql sql indexing query-optimization


    【解决方案1】:

    您可以将查询简化为:

    SELECT u.id AS userId, firstName, profilePhotoId, preferredActivityId, preferredSubActivityId, availabilityType,
           3959 * ACOS(COS(radians(requestingUserLat)) * COS(radians(u.latitude)) * COS(radians(u.longitude) - radians(requestingUserLon)) + SIN(radians(requestingUserLat)) * SIN(radians(u.latitude))) AS distanceInMiles
      FROM users u
       WHERE u.latitude between lat1 and lat2
        AND u.longitude between lon1 and lon2
        AND u.dateOfBirth between maxAge and minAge
        AND IF(gender is null, TRUE, u.gender = gender)
        AND IF(activityType is null, TRUE, u.preferredActivityType = activityType)
        AND u.accountState = 'A'
        AND u.id != userId
    HAVING distanceInMiles < searchRadius
    ORDER BY distanceInMiles
    LIMIT pagingStart, pagingLength;
    

    然后在where 子句中为all 列创建索引,您可以使用索引中列的顺序,从具有较少不同值的列开始(如性别、状态)

    【讨论】:

    • I tried that,不幸的是它不起作用。优化器会忽略低基数的列,缩短键长度,最终完全忽略索引并恢复为全表扫描。
    • @DTs 尝试使用索引提示,或更改列顺序
    【解决方案2】:

    我认为其他答案已经涵盖了这一点。使用索引中的数据进行查询和使用索引中的数据寻找正确的解决方案是有区别的。后者是索引的最有效使用。前者是有帮助的,但效率只是不读数据页。

    我认为您可以使用exists 而不是in 来改进您的查询。这应该允许在外部级别进行过滤以提高查询的性能:

    SELECT u.id AS userId, firstName, profilePhotoId, preferredActivityId, preferredSubActivityId, availabilityType,
           3959 * ACOS(COS(radians(requestingUserLat)) * COS(radians(u.latitude)) * COS(radians(u.longitude) - radians(requestingUserLon)) + SIN(radians(requestingUserLat)) * SIN(radians(u.latitude))) AS distanceInMiles
    FROM users u
    WHERE EXISTS (SELECT 1
                  FROM users uu
                  WHERE uu.latitude      between lat1    and lat2  AND
                        uu.longitude     between lon1    and lon2 AND
                        uu.dateOfBirth   between maxAge  and minAge  AND
                        uu.id = u.id
                 ) AND
         IF(gender       is null, TRUE, u.gender = gender) AND
         IF(activityType is null, TRUE, u.preferredActivityType = activityType) AND
         u.accountState = 'A' AND
         u.id <> userId
    HAVING distanceInMiles < searchRadius
    ORDER BY distanceInMiles LIMIT pagingStart, pagingLength;
    

    请注意,表达式IF(gender is null, TRUE, u.gender = gender) 相当荒谬,因为它的计算结果始终为真。如果您有一个名为gender 的变量,则不会在此表达式中使用它。 gender 将根据 MySQL 范围规则进行解释,并成为表中的列。您应该始终使用诸如 var_p_ 之类的前缀或其他东西来区分参数与表中的列。

    编辑:

    我应该提到索引需要包含id 作为与exists 一起使用的第一列。

    【讨论】:

    • 好主意,但没有得到回报,EXISTS 内的条件 uu.id = u.id 使优化器选择基于 PK 的完整扫描,而不是我的覆盖索引。查询现在需要 1.5 秒而不是 0.3 秒。如果我使用ignore index (primary),则需要 14 秒。
    • 感谢带有 _var 前缀参数的指针。表达式IF(gender is null, TRUE, u.gender = gender) 似乎工作正常,它确实返回了正确的结果。如果未指定性别(即参数为空),它仅评估为真。这样我就不会在查询中引入任何条件。如果指定了性别,则需要满足条件u.gender = var_gender
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-11
    • 1970-01-01
    • 2022-01-09
    • 2016-05-08
    • 1970-01-01
    相关资源
    最近更新 更多