mysql 很少 tables,对一个大的 table 的子查询执行缓慢

mysql with few tables, subquery on one large table performs slow

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

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

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

版本号mysql8.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

return 此数据需要 40-50 秒。 这是一个网页的渴望,但我认为它应该不会花那么长时间。 我们在另一台服务器上测试了相同的查询,以进行比较。相同的数据,相同的查询。 这需要 25 秒。

我的问题是:

  1. 这个查询是 'heavy/bad' 吗?如果是,哪个查询应该执行得更好?
  2. 有没有办法,或者我应该检查什么,来找出为什么这个查询在 older/other 服务器上运行得更好?

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

附加信息

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 是唯一的。 一台相机当时正在拍照。

您正在使用 so-called 依赖子查询。太慢了。

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

我还猜测 cameraimages 中的 dateRecording 值与您的自动递增主键 id 值的顺序相同。也就是说,我猜你在拍摄每张图像时向 table 插入了一条记录。

我们来分解一下。

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

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

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

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

 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 值按时间顺序排列。如果它们不按更详细的查询顺序排列,仍然可以完成。

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

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

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 值。

编辑 在搜索您的 table 方面,繁重的工作发生在第二个常见的 table 表达式 (CTE) 中,该表达式称为LatestDate。我建议按如下方式为您的 cameraimages table 添加一个索引以提升它。

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; 作为主查询。

如果您 运行 使用和不使用索引进行解释,您可能会学到一些关于索引的有用知识。

索引:

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