【问题标题】:Select distinct faster than group by选择不同的比分组更快
【发布时间】:2013-10-14 16:57:39
【问题描述】:

我想运行以下搜索:

schema->resultset('Entity')->search({ 
        -or => { "me.user_id" => $user_id, 'set_to_user.user_id' => $user_id } 
    }, {
        'distinct' => 1,
        'join' => {'entity_to_set' => {'entity_set' => 'set_to_user'}},
        'order_by' => {'-desc' => 'modified'},
        'page' => 1,'rows' => 100
    });

在具有如下所示表的数据库上。

CREATE TABLE entity (
  id varchar(500) NOT NULL,
  user_id varchar(100) NOT NULL,
  modified timestamp NOT NULL,
  PRIMARY KEY (id, user_id),
  FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE
);

CREATE TABLE entity_to_set (
  set_id varchar(100) NOT NULL,
  user_id varchar(100) NOT NULL,
  entity_id varchar(500) NOT NULL,
  PRIMARY KEY (set_id, user_id, entity_id),
  FOREIGN KEY (entity_id, user_id) REFERENCES entity(id, user_id) ON DELETE CASCADE ON UPDATE CASCADE,
  FOREIGN KEY (set_id) REFERENCES entity_set(id) ON DELETE CASCADE ON UPDATE CASCADE
);

CREATE TABLE entity_set (
  id varchar(100) NOT NULL,
  PRIMARY KEY (id)
);

CREATE TABLE set_to_user (
  set_id varchar(100) NOT NULL,
  user_id varchar(100) NOT NULL,
  PRIMARY KEY (set_id, user_id),
  FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE,
  FOREIGN KEY (set_id) REFERENCES entity_set(id) ON DELETE CASCADE ON UPDATE CASCADE
);

CREATE TABLE user (
  id varchar(100) NOT NULL,
  PRIMARY KEY (id)
);

我有大约 6000 个entity、6000 个entity_to_set、10 个entity_set 和 50 个set_to_user

现在,这个查询需要一些时间(一两秒),这很不幸。当只对实体表进行查询时,包括ORDER BY,结果几乎是即时的。作为调试的第一步,我发现了 DBIC 代码变成的实际 SQL 查询:

SELECT me.id, me.user_id, me.modified FROM entity me
LEFT JOIN entity_to_set entity_to_set ON ( entity_to_set.entity_id = me.id AND entity_to_set.user_id = me.user_id ) 
LEFT JOIN entity_set entity_set ON entity_set.id = entity_to_set.set_id 
LEFT JOIN set_to_user set_to_user ON set_to_user.set_id = entity_set.id 
WHERE ( ( set_to_user.user_id = 'Craigy' OR me.user_id = 'Craigy' ) ) 
GROUP BY me.id, me.user_id, me.modified ORDER BY modified DESC LIMIT 100;

这是EXPLAIN QUERY PLAN的结果

0|0|0|SCAN TABLE entity AS me USING INDEX sqlite_autoindex_entity_1 (~1000000 rows)
0|1|1|SEARCH TABLE entity_to_set AS entity_to_set USING COVERING INDEX entity_to_set_idx_cover (entity_id=? AND user_id=?) (~9 rows)
0|2|2|SEARCH TABLE entity_set AS entity_set USING COVERING INDEX sqlite_autoindex_entity_set_1 (id=?) (~1 rows)
0|3|3|SEARCH TABLE set_to_user AS set_to_user USING COVERING INDEX sqlite_autoindex_set_to_user_1 (set_id=?) (~5 rows)
0|0|0|USE TEMP B-TREE FOR ORDER BY

entity_to_set_idx_cover 在哪里

CREATE INDEX entity_to_set_idx_cover ON entity_to_set (entity_id, user_id, set_id);

现在,问题是用于排序的 b-tree,而不是在我不进行连接时使用的索引。

我注意到 DBIx::Class 将 'distinct' => 1 转换为 GROUP BY 语句 (I believe the documentation says they are equivalent here)。我删除了GROUP BY 语句并改用SELECT DISTINCT,并使用以下查询

SELECT DISTINCT me.id, me.user_id, me.modified FROM entity me
LEFT JOIN entity_to_set entity_to_set ON ( entity_to_set.entity_id = me.id AND entity_to_set.user_id = me.user_id ) 
LEFT JOIN entity_set entity_set ON entity_set.id = entity_to_set.set_id 
LEFT JOIN set_to_user set_to_user ON set_to_user.set_id = entity_set.id 
WHERE ( ( set_to_user.user_id = 'Craigy' OR me.user_id = 'Craigy' ) ) 
ORDER BY modified DESC LIMIT 100;

我相信这会产生相同的结果。此查询的EXPLAIN QUERY PLAN

0|0|0|SCAN TABLE entity AS me USING COVERING INDEX entity_sort_modified_user_id (~1000000 rows)
0|1|1|SEARCH TABLE entity_to_set AS entity_to_set USING COVERING INDEX entity_to_set_idx_cover (entity_id=? AND user_id=?) (~9 rows)
0|2|2|SEARCH TABLE entity_set AS entity_set USING COVERING INDEX sqlite_autoindex_entity_set_1 (id=?) (~1 rows)
0|3|3|SEARCH TABLE set_to_user AS set_to_user USING COVERING INDEX sqlite_autoindex_set_to_user_1 (set_id=?) (~5 rows)

其中entity_sort_modified_user_id 是使用创建的索引

CREATE INDEX entity_sort_modified_user_id ON entity (modified, user_id, id);

这几乎是即时运行的(没有 b-tree)。

编辑:为了证明当ORDER BY 以升序排列时问题仍然存在,以及索引对这些查询的影响,这里是对相同表的类似查询。前两个查询分别使用SELECT DISTINCTGROUP BY没有索引,后两个查询和索引相同。

sqlite> EXPLAIN QUERY PLAN SELECT DISTINCT me.id, me.user_id, me.modified FROM entity me LEFT JOIN entity_to_set entity_to_set ON ( entity_to_set.entity_id = me.id AND entity_to_set.user_id = me.user_id ) LEFT JOIN entity_set entity_set ON entity_set.id = entity_to_set.set_id WHERE ( me.user_id = 'Craigy' AND entity_set.id = 'SetID' ) ORDER BY modified LIMIT 100;
0|0|0|SCAN TABLE entity AS me (~100000 rows)
0|1|1|SEARCH TABLE entity_to_set AS entity_to_set USING AUTOMATIC COVERING INDEX (entity_id=? AND user_id=?) (~7 rows)
0|2|2|SEARCH TABLE entity_set AS entity_set USING COVERING INDEX sqlite_autoindex_entity_set_1 (id=?) (~1 rows)
0|0|0|USE TEMP B-TREE FOR DISTINCT
0|0|0|USE TEMP B-TREE FOR ORDER BY
sqlite> EXPLAIN QUERY PLAN SELECT me.id, me.user_id, me.modified FROM entity me LEFT JOIN entity_to_set entity_to_set ON ( entity_to_set.entity_id = me.id AND entity_to_set.user_id = me.user_id ) LEFT JOIN entity_set entity_set ON entity_set.id = entity_to_set.set_id WHERE ( me.user_id = 'Craigy' AND entity_set.id = 'SetID' ) GROUP BY me.id, me.user_id, me.modified ORDER BY modified LIMIT 100;
0|0|0|SCAN TABLE entity AS me USING INDEX sqlite_autoindex_entity_1 (~100000 rows)
0|1|1|SEARCH TABLE entity_to_set AS entity_to_set USING AUTOMATIC COVERING INDEX (entity_id=? AND user_id=?) (~7 rows)
0|2|2|SEARCH TABLE entity_set AS entity_set USING COVERING INDEX sqlite_autoindex_entity_set_1 (id=?) (~1 rows)
0|0|0|USE TEMP B-TREE FOR ORDER BY
sqlite> CREATE INDEX entity_idx_user_id_modified_id ON entity (user_id, modified, id);
sqlite> EXPLAIN QUERY PLAN SELECT DISTINCT me.id, me.user_id, me.modified FROM entity me LEFT JOIN entity_to_set entity_to_set ON ( entity_to_set.entity_id = me.id AND entity_to_set.user_id = me.user_id ) LEFT JOIN entity_set entity_set ON entity_set.id = entity_to_set.set_id WHERE ( me.user_id = 'Craigy' AND entity_set.id = 'SetID' ) ORDER BY modified LIMIT 100;
0|0|0|SEARCH TABLE entity AS me USING COVERING INDEX entity_idx_user_id_modified_id (user_id=?) (~10 rows)
0|1|1|SEARCH TABLE entity_to_set AS entity_to_set USING AUTOMATIC COVERING INDEX (entity_id=? AND user_id=?) (~7 rows)
0|2|2|SEARCH TABLE entity_set AS entity_set USING COVERING INDEX sqlite_autoindex_entity_set_1 (id=?) (~1 rows)
sqlite> EXPLAIN QUERY PLAN SELECT me.id, me.user_id, me.modified FROM entity me LEFT JOIN entity_to_set entity_to_set ON ( entity_to_set.entity_id = me.id AND entity_to_set.user_id = me.user_id ) LEFT JOIN entity_set entity_set ON entity_set.id = entity_to_set.set_id WHERE ( me.user_id = 'Craigy' AND entity_set.id = 'SetID' ) GROUP BY me.id, me.user_id, me.modified ORDER BY modified LIMIT 100;
0|0|0|SEARCH TABLE entity AS me USING COVERING INDEX entity_idx_user_id_modified_id (user_id=?) (~10 rows)
0|1|1|SEARCH TABLE entity_to_set AS entity_to_set USING AUTOMATIC COVERING INDEX (entity_id=? AND user_id=?) (~7 rows)
0|2|2|SEARCH TABLE entity_set AS entity_set USING COVERING INDEX sqlite_autoindex_entity_set_1 (id=?) (~1 rows)
0|0|0|USE TEMP B-TREE FOR GROUP BY
0|0|0|USE TEMP B-TREE FOR ORDER BY

我的问题是:如何修复我的 DBIx::Class 代码,使其性能与SELECT DISTINCT 查询一样好。或者如何添加索引以使其正常工作?还是需要其他类型的修复?

【问题讨论】:

  • 我认为在 sqlite 方面可以做的事情并不多: GROUP BY 是按升序处理的,但您的 ORDER BY 是按降序处理的。即使您将索引创建为 DESC(可能在最新版本的 sqlite 中),您仍然可以获得临时 b-tree。请注意,当您拥有... GROUP BY me.modified, me.user_id, me.id ORDER BY me.modified, me.user_id, me.id ASC LIMIT 100 时它会消失,但在您使用 DESC 时不会。所以我想说你必须解决 SQL 生成方面的主题。
  • (在sqlite邮件列表中讨论here
  • @Fabian 感谢您的关注!我添加了一个使用升序ORDER BY 的示例,它显示了相同的问题。它还显示了索引具有的效果(它删除了SELECT DISTINCT 查询中的B-TREE,并在GROUP BY 查询中添加了另一个B-TREE)。
  • GROUP BY 和 ORDER BY 子句需要匹配,即 GROUP BY me.modified , me.user_id, me.id ORDER BY me.modified, me.user_id, me.id LIMIT 100;然后额外的 b-tree 就消失了。
  • 好吧,你是对的,但它仍然留下USE TEMP B-TREE FOR GROUP BY

标签: perl sqlite dbix-class query-performance


【解决方案1】:

注意:这不是这个问题的完整答案。它只展示了在按升序顺序排序时如何避免临时b-tree。当需要按降序排序时,AFAIK 目前(版本 3.8.1)无法(不调整 sqlite)避免 GROUP BY 版本的临​​时 b-tree。

使用问题中的表定义和索引:

sqlite> select sqlite_version();
sqlite_version()
----------------
3.8.1

当 (a) 您按升序排序并且 (b) GROUP BY 子句逐列匹配 ORDER BY 子句时,您的查询将在没有临时 b 树的情况下运行。

除了 GROUP BY 和 ORDER BY 子句之外,查询不变:

/* table definitions as shown in the question */
sqlite> CREATE INDEX entity_to_set_idx_cover ON entity_to_set (entity_id, user_id, set_id);
sqlite> CREATE INDEX entity_sort_modified_user_id ON entity (modified, user_id, id);

sqlite> EXPLAIN QUERY PLAN
   ...> SELECT  me.id, me.user_id, me.modified FROM entity me
   ...> LEFT JOIN entity_to_set entity_to_set ON ( entity_to_set.entity_id = me.id AND entity_to_set.user_id = me.user_id )
   ...> LEFT JOIN entity_set entity_set ON entity_set.id = entity_to_set.set_id
   ...> LEFT JOIN set_to_user set_to_user ON set_to_user.set_id = entity_set.id
   ...> WHERE ( ( set_to_user.user_id = 'Craigy' OR me.user_id = 'Craigy' ) )
   ...> GROUP BY me.modified,  me.user_id, me.id
   ...> ORDER BY me.modified,  me.user_id, me.id ASC LIMIT 100;

selectid    order       from        detail
----------  ----------  ----------  -------------------------------------------------------------------------
0           0           0           SCAN TABLE entity AS me USING COVERING INDEX entity_sort_modified_user_id
0           1           1           SEARCH TABLE entity_to_set AS entity_to_set USING COVERING INDEX entity_t
0           2           2           SEARCH TABLE entity_set AS entity_set USING COVERING INDEX sqlite_autoind
0           3           3           SEARCH TABLE set_to_user AS set_to_user USING COVERING INDEX sqlite_autoi

但是,当您按降序排序时,您会得到一个临时 b-tree:

   ...> ...
   ...> GROUP BY me.modified,  me.user_id, me.id
   ...> ORDER BY me.modified,  me.user_id, me.id DESC LIMIT 100;
selectid    order       from        detail
----------  ----------  ----------  -------------------------------------------------------------------------
0           0           0           SCAN TABLE entity AS me USING COVERING INDEX entity_sort_modified_user_id
0           1           1           SEARCH TABLE entity_to_set AS entity_to_set USING COVERING INDEX entity_t
0           2           2           SEARCH TABLE entity_set AS entity_set USING COVERING INDEX sqlite_autoind
0           3           3           SEARCH TABLE set_to_user AS set_to_user USING COVERING INDEX sqlite_autoi
0           0           0           USE TEMP B-TREE FOR ORDER BY

原因是 sqlite(直到当前版本 3.8.1)不承认它可以按降序进行分组。因此,您将始终获得单独的步骤。这是无法避免的,即使索引也被声明为 DESC。请参阅关于此的 sqlite mailing list 的讨论。

结论 如果您希望查询在没有临时 b 树的情况下按 DESC 排序,则必须调整 SQL 生成以使用 DISTINCT。

【讨论】:

    猜你喜欢
    • 2019-05-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多