MySQL GROUP BY оптимизация - PullRequest
       7

MySQL GROUP BY оптимизация

0 голосов
/ 26 ноября 2010

Этот вопрос является более конкретной версией предыдущего вопроса, который я задавал

Таблица


CREATE TABLE Test4_ClusterMatches 
(
    `match_index` INT UNSIGNED,
    `cluster_index` INT UNSIGNED, 
    `id` INT NOT NULL AUTO_INCREMENT,
    `tfidf` FLOAT,
    PRIMARY KEY (`cluster_index`,`match_index`,`id`)
);

Запрос, который я хочу выполнить


mysql> explain SELECT `match_index`, SUM(`tfidf`) AS total 
FROM Test4_ClusterMatches WHERE `cluster_index` IN (1,2,3 ... 3000) 
GROUP BY `match_index`;

Проблема с запросом


Он использует временную и файловую сортировку, поэтому его нужно замедлить

+----+-------------+----------------------+-------+---------------+---------+---------+------+-------+-----------------------------------------------------------+
| id | select_type | table                | type  | possible_keys | key     | key_len | ref  | rows  | Extra                                                     |
+----+-------------+----------------------+-------+---------------+---------+---------+------+-------+-----------------------------------------------------------+
|  1 | SIMPLE      | Test4_ClusterMatches | range | PRIMARY       | PRIMARY | 4       | NULL | 51540 | Using where; Using index; Using temporary; Using filesort | 
+----+-------------+----------------------+-------+---------------+---------+---------+------+-------+-----------------------------------------------------------+
При текущей индексации запрос должен будет сначала отсортировать по cluster_index, чтобы устранитьиспользование временных файлов и сортировка файлов, но это дает неверные результаты для суммы (tfidf).Изменение первичного ключа на
PRIMARY KEY (`match_index`,`cluster_index`,`id`)

Не использует сортировку файлов или временные таблицы, но использует 14 932 441 строку, поэтому оно также замедляет

+----+-------------+----------------------+-------+---------------+---------+---------+------+----------+--------------------------+
| id | select_type | table                | type  | possible_keys | key     | key_len | ref  | rows     | Extra                    |
+----+-------------+----------------------+-------+---------------+---------+---------+------+----------+--------------------------+
|  1 | SIMPLE      | Test5_ClusterMatches | index | NULL          | PRIMARY | 16      | NULL | 14932441 | Using where; Using index | 
+----+-------------+----------------------+-------+---------------+---------+---------+------+----------+--------------------------+

Tight Index Scan


Использование строгого сканирования индекса путем запуска поиска только одного индекса

mysql> explain SELECT <code>match_index</code>, SUM(<code>tfidf</code>) AS total
FROM Test4_ClusterMatches WHERE <code>cluster_index</code> =3000 
GROUP BY <code>match_index</code>;
Удаляет временные таблицы и сортировку файлов.
+----+-------------+----------------------+------+---------------+---------+---------+-------+------+--------------------------+
| id | select_type | table                | type | possible_keys | key     | key_len | ref   | rows | Extra                    |
+----+-------------+----------------------+------+---------------+---------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | Test4_ClusterMatches | ref  | PRIMARY       | PRIMARY | 4       | const |   27 | Using where; Using index | 
+----+-------------+----------------------+------+---------------+---------+---------+-------+------+--------------------------+
Я не уверен, что это можно использовать с каким-то волшебным sql-fu, с которым я еще не сталкивался?

Вопрос


Как я могу изменить свой запрос так, чтобы он использовал 3000 cluster_indexes, избегая использования временных и файловых сортировок без необходимости использовать 14 932 441 строк?



Обновление


Использование таблицы

CREATE TABLE Test6_ClusterMatches 
(
  match_index INT UNSIGNED,
  cluster_index INT UNSIGNED, 
  id INT NOT NULL AUTO_INCREMENT,
  tfidf FLOAT,
  PRIMARY KEY (id),
  UNIQUE KEY(cluster_index,match_index)
);

Затем приведенный ниже запрос дает 10 строк в наборе (0,41 с):)

SELECT `match_index`, SUM(`tfidf`) AS total FROM Test6_ClusterMatches WHERE 
`cluster_index` IN (.....)
GROUP BY `match_index` ORDER BY total DESC LIMIT 0,10;

но он использует временную и сортировку файлов

+----+-------------+----------------------+-------+---------------+---------------+---------+------+-------+----------------------------------------------+
| id | select_type | table                | type  | possible_keys | key           | key_len | ref  | rows  | Extra                                        |
+----+-------------+----------------------+-------+---------------+---------------+---------+------+-------+----------------------------------------------+
|  1 | SIMPLE      | Test6_ClusterMatches | range | cluster_index | cluster_index | 5       | NULL | 78663 | Using where; Using temporary; Using filesort | 
+----+-------------+----------------------+-------+---------------+---------------+---------+------+-------+----------------------------------------------+

Интересно, есть ли способ получить его быстрее, исключив использование временных и использование сортировки файлов?

Ответы [ 2 ]

2 голосов
/ 26 ноября 2010

Я быстро посмотрел, и это то, что я придумал - надеюсь, это поможет ...

Таблица SQL

drop table if exists cluster_matches;
create table cluster_matches
(
 cluster_id int unsigned not null,
 match_id int unsigned not null,
 ...
 tfidf float not null default 0,
 primary key (cluster_id, match_id) -- if this isnt unique add id to the end !!
)
engine=innodb;

Данные испытаний

select count(*) from cluster_matches

count(*)
========
17974591

select count(distinct(cluster_id)) from cluster_matches;

count(distinct(cluster_id))
===========================
1000000

select count(distinct(match_id)) from cluster_matches;

count(distinct(match_id))
=========================
6000

explain select
 cm.match_id,
 sum(tfidf) as sum_tfidf,
 count(*) as count_tfidf
from
 cluster_matches cm
where
 cm.cluster_id between 5000 and 10000
group by
 cm.match_id
order by
 sum_tfidf desc limit 10;

id  select_type table   type    possible_keys   key key_len ref rows    Extra
==  =========== =====   ====    =============   === ======= === ====    =====
1   SIMPLE  cm  range   PRIMARY PRIMARY 4       290016  Using where; Using temporary; Using filesort

runtime - 0.067 seconds.

Довольно респектабельное время выполнения 0,067 секунд, но я думаю, что мы можем сделать его лучше.

Хранимая процедура

Вам придется простить меня за то, что я не хочу вводить / передавать список из 5000+ случайных cluster_ids!

call sum_cluster_matches(null,1); -- for testing
call sum_cluster_matches('1,2,3,4,....5000',1);

Основная часть sproc не очень элегантна, но все, что она делает, это разбивает строку csv на отдельные cluster_ids и заполняет временную таблицу.

drop procedure if exists sum_cluster_matches;

delimiter #

create procedure sum_cluster_matches
(
in p_cluster_id_csv varchar(65535),
in p_show_explain tinyint unsigned
)
proc_main:begin

declare v_id varchar(10);
declare v_done tinyint unsigned default 0;
declare v_idx int unsigned default 1;

    create temporary table tmp(cluster_id int unsigned not null primary key);   

    -- not every elegant - split the string into tokens and put into a temp table...

    if p_cluster_id_csv is not null then
        while not v_done do
            set v_id = trim(substring(p_cluster_id_csv, v_idx, 
                if(locate(',', p_cluster_id_csv, v_idx) > 0, 
                        locate(',', p_cluster_id_csv, v_idx) - v_idx, length(p_cluster_id_csv))));

                if length(v_id) > 0 then
                set v_idx = v_idx + length(v_id) + 1;
                        insert ignore into tmp values(v_id);
                else
                set v_done = 1;
                end if;
        end while;
    else
        -- instead of passing in a huge comma separated list of cluster_ids im cheating here to save typing
        insert into tmp select cluster_id from clusters where cluster_id between 5000 and 10000;
        -- end cheat
    end if;

    if p_show_explain then

        select count(*) as count_of_tmp from tmp;

        explain
        select
         cm.match_id,
         sum(tfidf) as sum_tfidf,
         count(*) as count_tfidf
        from
         cluster_matches cm
        inner join tmp on tmp.cluster_id = cm.cluster_id
        group by
         cm.match_id
        order by
         sum_tfidf desc limit 10;
    end if;

    select
     cm.match_id,
     sum(tfidf) as sum_tfidf,
     count(*) as count_tfidf
    from
     cluster_matches cm
    inner join tmp on tmp.cluster_id = cm.cluster_id
    group by
     cm.match_id
    order by
     sum_tfidf desc limit 10;

    drop temporary table if exists tmp;

end proc_main #

delimiter ;

Результаты

call sum_cluster_matches(null,1);

count_of_tmp
============
5001

id  select_type table   type    possible_keys   key key_len ref rows    Extra
==  =========== =====   ====    =============   === ======= === ====    =====
1   SIMPLE  tmp index   PRIMARY PRIMARY 4       5001    Using index; Using temporary; Using filesort
1   SIMPLE  cm  ref PRIMARY PRIMARY 4   vldb_db.tmp.cluster_id  8   

match_id    sum_tfidf   count_tfidf
========    =========   ===========
1618        387         64
1473        387         64
3307        382         64
2495        373         64
1135        373         64
3832        372         57
3203        362         58
5464        358         67
2100        355         60
1634        354         52

runtime 0.028 seconds.

Объясните план и время выполнения значительно улучшилось.

0 голосов
/ 26 ноября 2010

Если значения cluster_index в условии WHERE являются непрерывными, то вместо IN используйте:

WHERE (cluster_index >= 1) and (cluster_index <= 3000)

Если значения не являются непрерывными, то вы можетесоздайте временную таблицу для хранения значений cluster_index с индексом и используйте INNER JOIN для временной таблицы.

...