mysql 表很少,一个大表上的子查询执行缓慢

Posted

技术标签:

【中文标题】mysql 表很少,一个大表上的子查询执行缓慢【英文标题】:mysql with few tables, subquery on one large table performs slow 【发布时间】:2020-10-15 09:01:11 【问题描述】:

我们在 mysql 数据库上查询时遇到性能缓慢的问题,我们不确定查询是否错误,或者 mysql 或服务器不够好。

带有子查询的查询返回一些项目详细信息(3 个字段)和最近拍摄的在线相机照片的文件名。

信息 表“项目”包含 40 条记录。 表“相机”包含大约 40 条记录(1 个项目,可能有多个相机) 表“cameraimages”包含大约 250000(250000)条记录。 (一台相机可以有数千张图像) 引擎是 InnoDb 数据库大小约为 100Mb 尚未添加任何索引。

版本号mysql 8.0.15

这是查询

SELECT
    pj.title,
    pj.description,
    pj.city,
    (SELECT cmi.filename 
       FROM cameras cm
       LEFT JOIN cameraimages cmi ON cmi.cameraId = cm.id
      WHERE cm.projectId = pj.id
      ORDER BY cmi.dateRecording DESC 
      LIMIT 0,1) as latestfilename
FROM
    projects pj

返回此数据需要 40-50 秒。 这对网页来说太长了,但我认为根本不需要那么长时间。 我们在另一台服务器上测试了相同的查询,以进行比较。相同的数据,相同的查询。 这需要 25 秒。

我的问题是:

    此查询是否为“重/差”,如果是,哪个查询应该执行得更好? 有没有办法或者我应该检查什么来找出为什么这个查询在旧的/其他服务器上运行得更好?

希望有人能给点建议。 谢谢!

附加信息

CREATE TABLE `cameras` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `guid` varchar(50) DEFAULT NULL,
  `title` varchar(50) DEFAULT NULL,
  `longitude` double DEFAULT NULL,
  `latitude` double DEFAULT NULL,
  `status` smallint(6) DEFAULT NULL,
  `cameraUid` varchar(20) DEFAULT NULL,
  `cameraFriendlyName` varchar(50) DEFAULT NULL,
  `projectId` int(11) DEFAULT NULL,
  `dateCreated` datetime DEFAULT NULL,
  `dateModified` datetime DEFAULT NULL,
  `address` varchar(100) DEFAULT NULL,
  `city` varchar(50) DEFAULT NULL,
  `createArchive` smallint(6) DEFAULT '0',
  `createDaily` smallint(6) DEFAULT '1',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=88 DEFAULT CHARSET=latin1

cameraId,dateRecording 列是唯一的。 一台相机同时拍摄照片。

【问题讨论】:

请添加您的MySql版本。 您可以在问题中包含show create table cameras 吗? (cameraId, dateRecording) 是唯一的吗? 请阅读此***.com/tags/query-performance/info 和edit 您的问题以提供更多信息。 你能发布使用任何项目 ID 的单个子查询的解释吗? EXPLAIN SELECT cmi.filename .... 另外,您应该尝试将 LEFT JOIN 替换为 INNER JOIN,因为 LEFT JOIN 会阻止一些优化 【参考方案1】:

您正在使用所谓的依赖子查询。这很慢。

我猜cameraimages.id 是您的相机图像文件的主键。这是一个猜测。您没有在问题中提供足够的信息来肯定地回答。

我还猜想cameraimages 中的dateRecording 值与您的自动递增主键id 值的顺序相同。也就是说,我猜您在捕获每张图像时向该表插入一条记录。

让我们分解一下。

您想要每个项目的最新图像的id。你怎么能得到那个?编写一个子查询来检索每个项目的最大、最新的id

                         SELECT cm.projectId,
                                MAX(cmi.id) imageId
                           FROM cameras cm
                           JOIN cameraimages cmi ON cmi.cameraId = cm.id
                          GROUP BY cm.projectId

该子查询完成了搜索大表的繁重工作。它只执行一次,而不是针对每个项目,因此不会花费很长时间。

然后将该子查询放入您的查询中以检索您需要的列。

 SELECT 
       pj.title,
       pj.description,
       pj.city,
       cmi.filename latestfilename
  FROM projects pj
  JOIN (
                         SELECT cm.projectId,
                                MAX(cmi.id) imageId
                           FROM cameras cm
                           JOIN cameraimages cmi ON cmi.cameraId = cm.id
                          GROUP BY cm.projectId
       ) latest ON pj.id = latest.projectId
  JOIN cameraimages cmi ON cmi.imageId = latest.imageId

这有一系列 JOIN 构成从 projectslatest 子查询并从那里到 cameraimages 的链。

这取决于cameraimages.id 值是否按时间顺序排列。如果它们不是按照更精细的查询的顺序,仍然可以完成。

【讨论】:

谢谢!这个查询已经运行得更快了,不到 2 秒。伟大的。唯一的问题是 max(cmi.id) 不必是最新的图像,正如您已经提到的。大多数时候是,但 dateRecording 包含比 id 更重要的 datetime 值。因此,如果我们决定或需要从昨天导入额外的图像,则 (max) id 将返回昨天的图像。我看不出我应该如何更改您的查询以实现这一目标。我可以获得 max(dateRecording) 但不返回文件名值? 对。没有按时间顺序排列的id 值会更难。现在正在努力。【参考方案2】:

索引:

cm:   INDEX(projectId, id)
cmi:  INDEX(cameraId, dateRecording, filename)
cmi:  INDEX(cameraId, id)

【讨论】:

谢谢。 o.Jones 在之前的帖子中建议的查询已经具有很大的优势。这些索引是否还能带来更多好处? @Midiman7472 - 可能。其中之一是专门针对他推荐的查询公式量身定制的。【参考方案3】:

cameraimages.id 值不是按时间顺序排列时,我们需要使用最新的dateRecording 值。

这将需要一系列子查询。因此,与其嵌套它们,不如使用 MySQL 8+ Common Table Expressions。这是一个很大的查询。

WITH 
ProjectCameraImage AS (
     /* a virtual version of the cameraimages table including projectId */
     SELECT cmi.id, cmi.dateRecording, cm.projectId, cm.cameraId 
       FROM cameras cm
       JOIN cameraimages cmi ON cm.id = cmi.cameraId
),
LatestDate AS (
     /* the latest date for each entry in ProjectCameraImage */
     /* Notice how this uses MAX rather than ORDER BY ... DESC LIMIT 1 */
     SELECT projectId, cameraId, 
            MAX(dateRecording) dateRecording
       FROM ProjectCameraImage
      GROUP BY projectId, cameraId
),
ProjectCameraLatest AS (
      /* the cameraimage.id values for the latest images in ProjectCameraImage */
      SELECT ProjectCameraImage.id, 
             ProjectCameraImage.projectId, 
             ProjectCameraImage.cameraId,
             ProjectCameraImage.dateRecording
        FROM ProjectCameraImage 
        JOIN LatestDate
                 ON ProjectCameraImage.projectId = LatestDate.projectId
                AND ProjectCameraImage.cameraId = LatestDate.cameraId
                AND ProjectCameraImage.dateRecording = LatestDate.dateRecording
),
LatestProjectDate AS (
       /* the latest data for each entry in ProjectCameraLatest */
       SELECT projectId, 
              MAX(dateRecording) dateRecording
         FROM ProjectCameraLatest
        GROUP BY projectId
),
ProjectLatest AS (
        /* the cameraimage.id values for the latest images in ProjectCameraLatest */
        SELECT ProjectCameraLatest.id,
               ProjectCameraLatest.projectId
          FROM ProjectCameraLatest
          JOIN LatestProjectDate 
                ON ProjectCameraLatest.projectId = LatestProjectDate.projectId
               AND ProjectCameraLatest.dateRecording = LatestProjectDate.dateRecording
)
/* the main query */
SELECT pj.title,
       pj.description,
       pj.city,
       cmi.filename latestfilename
  FROM projects pj
  JOIN ProjectLatest ON pj.id = ProjectLatest.projectId
  JOIN cameraimages cmi ON ProjectLatest.id = cmi.id;

这很重要,因为我们必须经历两个不同的循环来找到具有最大 dateRecordingcameraimages.id 值。

编辑 就搜索表而言,繁重的工作发生在第二个公用表表达式 (CTE) 中,即 LatestDate。我建议为您的cameraimages 表添加一个索引,如下所示,以增强它。

CREATE INDEX cmi_cameraid_daterec 
          ON cameraimages (cameraId, dateRecording DESC);

该复合索引应允许cameraId 随机访问,然后快速访问最新日期。请注意,它也应该有助于 ProjectCameraLatest CTE。

您可以通过将主查询中的最后一个 SELECT 更改为 SELECT * FROM LatestDate; 来测试其性能。并查看它是否/如何使用索引尝试using EXPLAIN or EXPLAIN ANALYZE:使用EXPLAIN SELECT * FROM LatestDate; 作为主要查询。

如果您在使用和不使用索引的情况下运行 EXPLAIN,您可能会学到一些有关索引的有用信息。

【讨论】:

哇。美丽的选择方式!我必须将 'ProjectCameraImage.dateRecording' 列添加到 'ProjectCameraLatest' 表中才能使其正常工作,但效果却很好。返回数据需要 10 秒而不是 2 秒。因此,尽管我喜欢(并从这种方法中学到)并且它确实返回了我们想要的东西,但这并不是一种更快的方法。我正在考虑诸如选择 dateRecording 列值作为字符串 unixtimestamp 之类的事情,这使我们有可能获得 max(dateRecordingConverted) ?也可能很耗时..不知道吗?非常感谢您的帮助。 在进行 MAX 或其他聚合之前,不要将日期转换为文本字符串,甚至是数字字符串。 DMBS 软件在日期算术方面几乎具有神奇的效率,您可能无法超越它。你有没有为你的表添加任何索引?这是您的下一个优化步骤。 好的,清楚。还没有索引。我上周在 cameraId 上添加了一个,但这减慢了速度。 Rick James 的帖子中提到的那些还好吗?添加一个额外的列以将 dateRecording 存储为 unixtimestamp 或字符串然后使用 MAX 选择是否是一个想法? 请查看我对答案的编辑。 @RickJames 的第二个索引没问题,但我在这里投射查询的方式不需要 filename 列。他的第三个索引对他的第二个索引是多余的(所有索引都包括表的 PK)。他的第一个索引很好,但是您的 cameras 表很小。史诗般的问题!阅读 use-the-index-luke.com 了解索引。

以上是关于mysql 表很少,一个大表上的子查询执行缓慢的主要内容,如果未能解决你的问题,请参考以下文章

大表上的慢 MySQL SELECT

如何在 Zend 中使用表上的子查询执行查询并获取 Rowset 对象作为结果?

SUM GROUP BY与多个表上的子查询

大表上的第一次查询调用速度非常慢

如何使用具有多个 GROUP BY、子查询和 WHERE IN 在大表上的查询来优化查询?

在 3 个大表上使用内连接优化 SQL 查询