Оптимизировать запрос, который группирует результаты по полю из объединенной таблицы

0

У меня очень простой запрос, который должен группировать результаты по полю из объединенной таблицы:

SELECT SQL_NO_CACHE p.name, COUNT(1) FROM ycs_sales s
INNER JOIN ycs_products p ON s.id = p.sales_id 
WHERE s.dtm BETWEEN '2018-02-16 00:00:00' AND  '2018-02-22 23:59:59'
GROUP BY p.name

Таблица ycs_products - фактически sales_products, перечисляет продукты в каждой продаже. Я хочу видеть долю каждого проданного продукта в течение определенного периода времени.

Текущая скорость запроса составляет 2 секунды, что слишком много для взаимодействия с пользователем. Мне нужно, чтобы этот запрос выполнялся быстро. Есть ли способ избавиться от Using temporary без денормализации?

Порядок присоединения критически важен, в обеих таблицах много данных, и ограничение количества записей по дате является неоспоримым предварительным условием.

здесь приведен результат объяснения

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: s
         type: range
possible_keys: PRIMARY,dtm
          key: dtm
      key_len: 6
          ref: NULL
         rows: 1164728
        Extra: Using where; Using index; Using temporary; Using filesort
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: p
         type: ref
possible_keys: sales_id
          key: sales_id
      key_len: 5
          ref: test.s.id
         rows: 1
        Extra: 
2 rows in set (0.00 sec)

и то же самое в json

EXPLAIN: {
  "query_block": {
    "select_id": 1,
    "filesort": {
      "sort_key": "p.'name'",
      "temporary_table": {
        "table": {
          "table_name": "s",
          "access_type": "range",
          "possible_keys": ["PRIMARY", "dtm"],
          "key": "dtm",
          "key_length": "6",
          "used_key_parts": ["dtm"],
          "rows": 1164728,
          "filtered": 100,
          "attached_condition": "s.dtm between '2018-02-16 00:00:00' and '2018-02-22 23:59:59'",
          "using_index": true
        },
        "table": {
          "table_name": "p",
          "access_type": "ref",
          "possible_keys": ["sales_id"],
          "key": "sales_id",
          "key_length": "5",
          "used_key_parts": ["sales_id"],
          "ref": ["test.s.id"],
          "rows": 1,
          "filtered": 100
        }
      }
    }
  }
}

а также создавать таблицы, хотя я считаю это ненужным

    CREATE TABLE 'ycs_sales' (
      'id' int(11) NOT NULL AUTO_INCREMENT,
      'dtm' datetime DEFAULT NULL,
      PRIMARY KEY ('id'),
      KEY 'dtm' ('dtm')
    ) ENGINE=InnoDB AUTO_INCREMENT=2332802 DEFAULT CHARSET=latin1
    CREATE TABLE 'ycs_products' (
      'id' int(11) NOT NULL AUTO_INCREMENT,
      'sales_id' int(11) DEFAULT NULL,
      'name' varchar(255) DEFAULT NULL,
      PRIMARY KEY ('id'),
      KEY 'sales_id' ('sales_id')
    ) ENGINE=InnoDB AUTO_INCREMENT=2332802 DEFAULT CHARSET=latin1

А также PHP-код для репликации тестовой среды

#$pdo->query("set global innodb_flush_log_at_trx_commit = 2");
$pdo->query("create table ycs_sales (id int auto_increment primary key, dtm datetime)");
$stmt = $pdo->prepare("insert into ycs_sales values (null, ?)");
foreach (range(mktime(0,0,0,2,1,2018), mktime(0,0,0,2,28,2018)) as $stamp){
    $stmt->execute([date("Y-m-d", $stamp)]);
}
$max_id = $pdo->lastInsertId();
$pdo->query("alter table ycs_sales add key(dtm)");

$pdo->query("create table ycs_products (id int auto_increment primary key, sales_id int, name varchar(255))");
$stmt = $pdo->prepare("insert into ycs_products values (null, ?, ?)");
$products = ['food', 'drink', 'vape'];
foreach (range(1, $max_id) as $id){
    $stmt->execute([$id, $products[rand(0,2)]]);
}
$pdo->query("alter table ycs_products add key(sales_id)");
  • 0
    Если бы у меня была такая проблема, я бы обратился к вам за помощью, отсюда и мое отчаяние. Кроме того, я не понимаю, почему вы называете это «объединенной» таблицей. Порядок таблиц в этом случае, безусловно, произвольный!?!
  • 0
    Я называю это присоединенным, потому что оно объединено, поэтому я не могу добавить поле группировки в индекс. Если вы измените порядок, он будет присоединен еще только с противоположной стороны.
Показать ещё 5 комментариев
Теги:
join
group-by
query-optimization

2 ответа

1

Проблема в том, что группировка по name заставляет вас потерять информацию sales_id, поэтому MySQL вынужден использовать временную таблицу.

Хотя это не самый чистый из решений, и один из моих менее избранных подход, вы можете добавить новый индекс, как на name и на столбцы sales_id, например:

ALTER TABLE 'yourdb'.'ycs_products' 
ADD INDEX 'name_sales_id_idx' ('name' ASC, 'sales_id' ASC);

и заставить запрос использовать этот индекс с force index или use index:

SELECT SQL_NO_CACHE p.name, COUNT(1) FROM ycs_sales s
INNER JOIN ycs_products p use index(name_sales_id_idx) ON s.id = p.sales_id 
WHERE s.dtm BETWEEN '2018-02-16 00:00:00' AND  '2018-02-22 23:59:59'
GROUP BY p.name;

Мое выполнение сообщило только "используя где: используя индекс" в таблице p и "используя where" в таблице s.

Во всяком случае, я настоятельно рекомендую вам задуматься о своей схеме, потому что, возможно, вы можете найти лучший дизайн для этих двух таблиц. С другой стороны, если это не критическая часть вашего приложения, вы можете иметь дело с "принудительным" индексом.

РЕДАКТИРОВАТЬ

Поскольку совершенно ясно, что проблема в дизайне, я предлагаю использовать отношения как "многие ко многим". Если у вас есть возможность проверить его в тестовой среде, вот что я буду делать:

1) Создайте временную таблицу, чтобы сохранить имя и идентификатор продукта:

create temporary table tmp_prods
select min(id) id, name
from ycs_products
group by name;

2) Начиная с временной таблицы, присоединитесь к таблице продаж, чтобы создать замену для ycs_product:

create table ycs_products_new
select * from tmp_prods;

ALTER TABLE 'poc'.'ycs_products_new' 
CHANGE COLUMN 'id' 'id' INT(11) NOT NULL ,
ADD PRIMARY KEY ('id');

3) Создайте таблицу соединений:

CREATE TABLE 'prod_sale' (
'prod_id' INT(11) NOT NULL,
'sale_id' INT(11) NOT NULL,
PRIMARY KEY ('prod_id', 'sale_id'),
INDEX 'sale_fk_idx' ('sale_id' ASC),
CONSTRAINT 'prod_fk'
  FOREIGN KEY ('prod_id')
  REFERENCES ycs_products_new ('id')
  ON DELETE NO ACTION
  ON UPDATE NO ACTION,
CONSTRAINT 'sale_fk'
  FOREIGN KEY ('sale_id')
  REFERENCES ycs_sales ('id')
  ON DELETE NO ACTION
  ON UPDATE NO ACTION);

и заполнить его существующими значениями:

insert into prod_sale (prod_id, sale_id)
select tmp_prods.id, sales_id from ycs_sales s
inner join ycs_products p
on p.sales_id=s.id
inner join tmp_prods on tmp_prods.name=p.name;

Наконец, запрос соединения:

select name, count(name) from ycs_products_new p
inner join prod_sale ps on ps.prod_id=p.id
inner join ycs_sales s on s.id=ps.sale_id 
WHERE s.dtm BETWEEN '2018-02-16 00:00:00' AND  '2018-02-22 23:59:59'
group by p.id;

Пожалуйста, обратите внимание, что группа находится на первичном ключе, а не на имени.

Объяснить вывод:

explain select name, count(name) from ycs_products_new p inner join prod_sale ps on ps.prod_id=p.id inner join ycs_sales s on s.id=ps.sale_id  WHERE s.dtm BETWEEN '2018-02-16 00:00:00' AND  '2018-02-22 23:59:59' group by p.id;
+------+-------------+-------+--------+---------------------+---------+---------+-----------------+------+-------------+
| id   | select_type | table | type   | possible_keys       | key     | key_len | ref             | rows | Extra       |
+------+-------------+-------+--------+---------------------+---------+---------+-----------------+------+-------------+
|    1 | SIMPLE      | p     | index  | PRIMARY             | PRIMARY | 4       | NULL            |    3 |             |
|    1 | SIMPLE      | ps    | ref    | PRIMARY,sale_fk_idx | PRIMARY | 4       | test.p.id       |    1 | Using index |
|    1 | SIMPLE      | s     | eq_ref | PRIMARY,dtm         | PRIMARY | 4       | test.ps.sale_id |    1 | Using where |
+------+-------------+-------+--------+---------------------+---------+---------+-----------------+------+-------------+
  • 0
    Спасибо, что нашли время и выполнили весь код, я искренне ценю это. К сожалению, это меняет порядок соединения, и, как я уже сказал в своем комментарии, не зная даты для продукта, он должен выбрать все строки в таблице продуктов, число которых слишком велико. Хотя использование временных действительно ушло, но количество проверенных строк увеличилось до 2 мил, а время запроса - до 1 минуты (с 2 секунд).
  • 0
    что вы имеете в виду под "отменить порядок соединения"?
Показать ещё 15 комментариев
0

Зачем ycs_products id для ycs_products? Похоже, что sales_id должно быть PRIMARY KEY из этой таблицы?

Если это возможно, это устраняет проблему производительности, избавляясь от проблем, вызванных сенапе.

Если вместо этого есть несколько строк для каждого sales_id, то изменение вторичного индекса на это поможет:

INDEX(sales_id, name)

Еще одна вещь, которую нужно проверить, - innodb_buffer_pool_size. Это должно быть около 70% доступной оперативной памяти. Это улучшит точность данных и индексов.

Есть ли в этой неделе 1,1 миллиона строк?

Ещё вопросы

Сообщество Overcoder
Наверх
Меню