【问题标题】:MySQL query performance with subselects - table with millions of rows带有子选择的 MySQL 查询性能 - 具有数百万行的表
【发布时间】:2012-08-18 19:05:29
【问题描述】:

我知道有很多关于 sql 查询性能改进的问题,但我无法使用这些问题的答案来提高我的查询性能(够了)。

因为我想要比 rsync 和 fslint 更灵活的东西,所以我编写了一个小型 java 工具,它可以遍历文件树并将路径和校验和存储在 mysql 数据库中。

您会在此处找到我的表结构: http://code.google.com/p/directory-scanner/source/browse/trunk/sql/create_table.sql - 起初我只有一张桌子,但后来我想如果我将多余的很长的目录路径字符串移动到一个单独的地方并使其成为 1:n 关系,我可以节省很多空间

我已经定义了这两个索引:

CREATE INDEX files_sha1 ON files (sha1);
CREATE INDEX files_size ON files (size);

现在困扰我的查询是: http://code.google.com/p/directory-scanner/source/browse/trunk/sql/reporingQueries.sql

其中最糟糕的是最后一个,它很有可能总是返回一个空集(sha1 冲突和错误地插入了多个文件):

SELECT 
    d.path, 
    d.id, 
    f.filename, 
    f.id, 
    f.size, 
    f.scandate, 
    f.sha1, 
    f.lastmodified 
FROM files f 
INNER JOIN directories d 
    ON d.id = f.dir_id 
WHERE EXISTS ( /* same sha1 but different size */ 
    SELECT ff.id 
    FROM files ff 
    WHERE ff.sha1 = f.sha1 
    AND ff.size <> f.size 
) 
OR EXISTS ( /* files with same name and path but different id */ 
    SELECT ff2.id 
    FROM files ff2 
    INNER JOIN directories dd2 
        ON dd2.id = ff2.dir_id 
    WHERE ff2.id <> f.id 
    AND ff2.filename = f.filename 
    AND dd2.path = d.path 
) 
ORDER BY f.sha1

只要我只有 20k 行(在创建索引之后),它在不到一秒的时间内就运行得很好,但是现在我有 750k 行,它会运行几个小时,而 mysql 完全用光了我的一个 cpu核心。

这个查询的解释给出了这个结果:

id ; select_type ; table ; type ; possible_keys ; key ; key_len ; ref ; rows ; filtered ; Extra
1 ; PRIMARY ; d ; ALL ; PRIMARY ; NULL ; NULL ; NULL ; 56855 ; 100.0 ; Using temporary; Using filesort
1 ; PRIMARY ; f ; ref ; dir_id ; dir_id ; 4 ; files.d.id ; 13 ; 100.0 ; Using where
3 ; DEPENDENT SUBQUERY ; dd2 ; ALL ; PRIMARY ; NULL ; NULL ; NULL ; 56855 ; 100.0 ; Using where
3 ; DEPENDENT SUBQUERY ; ff2 ; ref ; dir_id ; dir_id ; 4 ; files.dd2.id ; 13 ; 100.0 ; Using where
2 ; DEPENDENT SUBQUERY ; ff ; ref ; files_sha1 ; files_sha1 ; 23 ; files.f.sha1 ; 1 ; 100.0 ; Using where

我的其他查询对于 750k 行也不快,但至少在 15 分钟或类似的时间内完成(但是,我希望它们也能处理数百万行..)

更新:感谢 radashk 的评论,但您建议的索引似乎是由 mysql 自动创建的 -->

"Table","Non_unique","Key_name","Seq_in_index","Column_name","Collation","Cardinality","Sub_part","Packed","Null","Index_type","Comment","Index_comment"
"files","0","PRIMARY","1","id","A","698397","NULL","NULL",,"BTREE",,
"files","1","dir_id","1","dir_id","A","53722","NULL","NULL",,"BTREE",,
"files","1","scanDir_id","1","scanDir_id","A","16","NULL","NULL","YES","BTREE",,
"files","1","files_sha1","1","sha1","A","698397","NULL","NULL","YES","BTREE",,
"files","1","files_size","1","size","A","174599","NULL","NULL",,"BTREE",,

UPDATE2:感谢 Eugen Rieck!我认为您的答案可以很好地替代此查询,因为无论如何它很可能会返回一个空集,我只会选择数据来显示用户,以便稍后在另一个查询中描述问题。 为了让我真的很高兴,如果有人也可以看看我的其他查询,那就太好了:D

UPDATE3:Justin Swanhart 的回答启发了我采用以下解决方案:与其让查询检查无意中多次插入的目录和文件,不如创建像这样的唯一约束:

ALTER TABLE directories ADD CONSTRAINT uc_dir_path UNIQUE (path);
ALTER TABLE files ADD CONSTRAINT uc_files UNIQUE(dir_id, filename);

但是,我想知道这会对插入语句的性能产生多大的负面影响,有人可以对此发表评论吗?

更新4:

ALTER TABLE directories ADD CONSTRAINT uc_dir_path UNIQUE (path);

不起作用,因为它太长了..

ERROR 1071 (42000): Specified key was too long; max key length is 767 bytes

更新5:

好的,这是我将用于替换我在最初问题中引用的查询的解决方案:

对于第一部分,寻找 sha1 碰撞,我将使用这个:

SELECT sha1
FROM files
GROUP BY sha1
HAVING COUNT(*)>1
AND MIN(size)<>MAX(size)

如果它返回任何内容,我将使用另一个查询 WHERE sha1 = ?

选择详细信息

我猜这个查询会运行得最好,定义了这个索引:

CREATE INDEX sha1_size ON files (sha1, size);

为了验证不存在重复的目录,我将使用它,因为他不允许约束(参见上面的 UPDATE4):

SELECT path
FROM directories
GROUP BY path
HAVING COUNT(*)>1

对于重复的文件,我将尝试创建此约束:

CREATE UNIQUE INDEX filename_dir ON files (filename, dir_id);

这运行得非常快(15 到 20 秒),我不需要在它之前创建其他索引以使其更快。错误消息还包含我需要向用户显示问题的详细信息(无论如何这不太可能,因为我在插入之前检查了这些内容)

现在只需要在更短的时间内再执行 5 个查询;)感谢 Eugen 和 Justin 迄今为止的大力帮助!

UPDATE6:好吧,自从上次有人回复以来已经有几天了,我只会接受贾斯汀的回答,因为那是对我帮助最大的回答。我将我从你们那里学到的东西整合到我的应用程序中,并在此处发布了 0.0.4 版本:http://code.google.com/p/directory-scanner/downloads/detail?name=directory-scanner-0.0.4-jar-with-dependencies.jar

【问题讨论】:

  • 索引你连接表的ID!文件.id,目录.id
  • 我会将唯一索引放在(文件名,dir_id)上。对于 FOREIGN KEY 约束,您已经在 (dir_id) 上有一个索引,并且您可能希望按文件名查找条目,如果索引是 (dir_id, filename) 则这是不可能的
  • 啊,好的,现在我明白你的意思了。好吧。但是,在添加了该约束之后,他似乎不想让我再次删除它 --> ALTER TABLE files DROP INDEX uc_files; ERROR 1553 (HY000): Cannot drop index 'uc_files': needed in a foreign key constraint --> 可能他在创建此唯一索引时自动删除了 dir_id 上的索引..

标签: mysql sql indexing query-performance


【解决方案1】:

虽然我无法在不构建表格和填充的情况下进行验证,但我会尝试类似

-- This checks the SHA1 collisions
SELECT
  MIN(id) AS id,
FROM files
GROUP BY sha1
HAVING COUNT(*)>1
AND MIN(size)<>MAX(size)

-- This checks for directory duplicates
SELECT
  MIN(path) AS path
FROM directories
GROUP BY path
HAVING COUNT(*)>1

-- This checks for file duplicates
SELECT
  MIN(f.id) AS id
FROM files AS f
INNER JOIN files AS ff 
   ON f.dir_id=ff.dir_id
   AND f.filename=ff.filename
GROUP BY f.id
HAVING COUNT(*)>1

一个接一个地跑。

编辑

第三次查询是虚假的 - 抱歉

【讨论】:

  • 或者,将这些合并在一起以获得一个问题列表(可能有一个额外的列指定故障所在)。
  • 我做 1-by-1 的理由是例如如果您有目录重复,则需要先修复它,然后才能以有意义的方式检查文件重复。
  • 谢谢!那是个好主意!最后一条语句仍然需要 3 分钟才能完成,但比几个小时要好得多;)您介意看看我的其他查询,还是为这些查询创建一个新问题会更好?
  • 您可以通过在 files.filename 上添加索引来加快查询 #3 - 如果有很多文件/目录,我怀疑这很重要。明天我会看看你的其他问题 - 这里是星期六午夜。
  • 第三个查询由 (filename, dir_id) 提供更好的服务。然后整个事情将是一个松散的索引扫描。您还可以从 f.id 中删除 MIN()。您正在对 f.id 进行分组,因此其上的 MIN() 毫无意义。查询二也一样。如果您真的不想通过文件名查询,那么将索引放在 dir_id 上,并创建一个新的唯一索引(dir_id,文件名)。我怀疑您的应用程序偶尔需要按文件名查询,例如搜索。
【解决方案2】:

尝试使用一个 UNION 和两个带有连接的索引良好的查询,而不是子查询。

首先,您需要两个索引(基于您提供的 create_table.sql 中的架构):

ALTER TABLE files add key (sha1, size);
alter table files add key(filename, dir_id);

然后你需要重写查询:

(SELECT 
    d.path, 
    d.id, 
    f.filename, 
    f.id, 
    f.size, 
    f.scandate, 
    f.sha1, 
    f.lastmodified 
FROM files f 
INNER JOIN directories d 
    ON d.id = f.dir_id 
INNER JOIN files files2
    USING(sha1)
WHERE files2.size != f.size)

UNION

(SELECT 
    d.path, 
    d.id, 
    f.filename, 
    f.id, 
    f.size, 
    f.scandate, 
    f.sha1, 
    f.lastmodified 
FROM files f 
INNER JOIN directories d 
    ON d.id = f.dir_id
INNER JOIN files files2
    ON files2.id != f.id
   AND files2.filename = f.filename
INNER JOIN directories d2
   ON files2.dir_id = d2.id
  AND d2.path = d.path)

【讨论】:

  • 感谢您的回答!那些 alter talbe 添加关键语句,这究竟会做什么?这会增加对 sha1+size 的唯一约束吗?我认为这不是一个好主意,它更像是一个 sha1 不应该有不同的大小,但理论上这也可能发生,并且应该是可插入的。
  • 索引不必是唯一的。通过在 (sha1,size) 的组合上创建索引,您允许优化器对文件表进行仅索引访问以进行自连接。第二个索引允许对第二个查询进行仅索引访问。索引的文件名部分用于连接,而 dir_id 已经在索引中,因此它用于目录查找而不访问实际行(仅索引)。
  • 是的,我知道它们不必是唯一的,但是“键”必须是唯一的,不是吗?!我从来不知道这个“添加键”语句,我认为每个表中只能有一个主键和外键,但这些是另一回事。我不会在脑海中将“键”与“索引”联系起来..
  • KEY 和 INDEX 在 MySQL 中是同义词。 ALTER TABLE ADD KEY、ALTER TABLE ADD INDEX 和 CREATE INDEX 都做同样的事情。
  • 哦...好的,感谢您的澄清!我想我需要对此进行分析,但我不想创建很多索引,因为这些可能会对插入和更新性能产生负面影响..
【解决方案3】:

您是否在第二个子查询中采用了一种交叉联接。尝试将第二个子查询更改为:

SELECT ff2.id 
FROM files ff2 
WHERE ff2.id <> f.id 
AND ff2.dir_id  = d.dir_id 
AND ff2.filename = f.filename 

并在files 表上的dir_id, filename 上创建索引。

【讨论】:

  • 好吧,我是故意这样做的,因为也有可能(并且很可能)目录和文件都被插入两次,那么 dir_id 就不一样了,只有 directory.path将是相同的,因此额外的连接。我认为像 Eugen Rieck 建议的那样将问题分成 3 个陈述,并且在确定没有重复目录后才询问是否有重复文件。
猜你喜欢
  • 1970-01-01
  • 2012-07-10
  • 2017-10-01
  • 1970-01-01
  • 2018-12-26
  • 2023-01-05
  • 1970-01-01
  • 2011-04-22
  • 2012-12-26
相关资源
最近更新 更多