Как я могу далее оптимизировать полученный запрос таблицы, который работает лучше, чем эквивалент, к которому ПРИСОЕДИНЯЮТСЯ?

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

Как это:

for (Type type : types) {
    boolean flag=false;
    for (Type t : types2) {
        if (some condition) {
            // Do something and break...
            flag=true;
            break; // Breaks out of the inner loop
        }
    }
    if(flag)
        break;
}
21
задан Glorfindel 31 March 2019 в 19:59
поделиться

4 ответа

Well, I found a solution. It took a lot of experimentation, and I think a good bit of blind luck, but here it is:

CREATE TABLE magic ENGINE=MEMORY
SELECT
  s.shop_id AS shop_id,
  s.id AS shift_id,
  st.dow AS dow,
  st.start AS start,
  st.end AS end,
  su.user_id AS manager_id
FROM shifts s
JOIN shift_times st ON s.id = st.shift_id
JOIN shifts_users su ON s.id = su.shift_id
JOIN shift_positions sp ON su.shift_position_id = sp.id AND sp.level = 1

ALTER TABLE magic ADD INDEX (shop_id, dow);

CREATE TABLE tickets_extra ENGINE=MyISAM
SELECT 
  t.id AS ticket_id,
  (
    SELECT m.manager_id
    FROM magic m
    WHERE DAYOFWEEK(t.created) = m.dow
    AND TIME(t.created) BETWEEN m.start AND m.end
    AND m.shop_id = t.shop_id
  ) AS manager_created,
  (
    SELECT m.manager_id
    FROM magic m
    WHERE DAYOFWEEK(t.resolved) = m.dow
    AND TIME(t.resolved) BETWEEN m.start AND m.end
    AND m.shop_id = t.shop_id
  ) AS manager_resolved
FROM tickets t;
DROP TABLE magic;

Lengthy Explanation

Now, I'll explain why this works, and my relative though process and steps to get here.

First, I knew the query I was trying was suffering because of the huge derived table, and the subsequent JOINs onto this. I was taking my well-indexed tickets table and joining all the shift_times data onto it, then letting MySQL chew on that while it attempts to join the shifts and shift_positions table. This derived behemoth would be up to a 2 million row unindexed mess.

Now, I knew this was happening. The reason I was going down this road though was because the "proper" way to do this, using strictly JOINs was taking an even longer amount of time. This is due to the nasty bit of chaos required to determine who the manager of a given shift is. I have to join down to shift_times to find out what the correct shift even is, while simultaneously joining down to shift_positions to figure out the user's level. I don't think the MySQL optimizer handles this very well, and ends up creating a HUGE monstrosity of a temporary table of the joins, then filtering out what doesn't apply.

So, as the derived table seemed to be the "way to go" I stubbornly persisted in this for a while. I tried punting it down into a JOIN clause, no improvement. I tried creating a temporary table with the derived table in it, but again it was too slow as the temp table was unindexed.

I came to realize that I had to handle this calculation of shift, times, positions sanely. I thought, maybe a VIEW would be the way to go. What if I created a VIEW that contained this information: (shop_id, shift_id, dow, start, end, manager_id). Then, I would simply have to join the tickets table by shop_id and the whole DAYOFWEEK/TIME calculation, and I'd be in business. Of course, I failed to remember that MySQL handles VIEWs rather assily. It doesn't materialize them at all, it simply runs the query you would have used to get the view for you. So by joining tickets onto this, I was essentially running my original query - no improvement.

So, instead of a VIEW I decided to use a TEMPORARY TABLE. This worked well if I only fetched one of the managers (created or resolved) at a time, but it was still pretty slow. Also, I found out that with MySQL you can't refer to the same table twice in the same query (I would have to join my temporary table twice to be able to differentiate between manager_created and manager_resolved). This is a big WTF, as I can do it as long as I don't specify "TEMPORARY" - this is where the CREATE TABLE magic ENGINE=MEMORY came into play.

With this pseudo temporary table in hand, I tried my JOIN for just manager_created again. It performed well, but still rather slow. Yet, when I JOINed again to get manager_resolved in the same query the query time ticked back up into the stratosphere. Looking at the EXPLAIN showed the full table scan of tickets (rows ~2mln), as expected, and the JOINs onto the magic table at ~2,087 each. Again, I seemed to be running into fail.

I now began to think about how to avoid the JOINs altogether and that's when I found some obscure ancient message board post where someone suggested using subselects (can't find the link in my history). This is what led to the second SELECT query shown above (the tickets_extra creation one). In the case of selecting just a single manager field, it performed well, but again with both it was crap. I looked at the EXPLAIN and saw this:

*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: t
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 173825
        Extra: 
*************************** 2. row ***************************
           id: 3
  select_type: DEPENDENT SUBQUERY
        table: m
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2037
        Extra: Using where
*************************** 3. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: m
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2037
        Extra: Using where
3 rows in set (0.00 sec)

Ack, the dreaded DEPENDENT SUBQUERY. It's often suggested to avoid these, as MySQL will usually execute them in an outside-in fashion, executing the inner query for every row of the outer. I ignored this, and wondered: "Well... what if I just indexed this stupid magic table?". Thus, the ADD index (shop_id, dow) was born.

Check this out:

mysql> CREATE TABLE magic ENGINE=MEMORY
<snip>
Query OK, 3220 rows affected (0.40 sec)

mysql> ALTER TABLE magic ADD INDEX (shop_id, dow);
Query OK, 3220 rows affected (0.02 sec)

mysql> CREATE TABLE tickets_extra ENGINE=MyISAM
<snip>
Query OK, 1933769 rows affected (24.18 sec)

mysql> drop table magic;
Query OK, 0 rows affected (0.00 sec)

Now THAT'S what I'm talkin' about!

Conclusion

This is definitely the first time I've created a non-TEMPORARY table on the fly, and INDEXed it on the fly, simply to do a single query efficiently. I guess I always assumed that adding an index on the fly is a prohibitively expensive operation. (Adding an index on my tickets table of 2mln rows can take over an hour). Yet, for a mere 3,000 rows this is a cakewalk.

Don't be afraid of DEPENDENT SUBQUERIES, creating TEMPORARY tables that really aren't, indexing on the fly, or aliens. They can all be good things in the right situation.

Thanks for all the help StackOverflow. :-D

13
ответ дан 29 November 2019 в 22:04
поделиться

Это позволит вам иметь доступ только для чтения на время внесения изменений:

create table_new (new schema);
insert into table_new select * from table order by primary_key_column;
rename table to table_old;
rename table_new to table;
-- recreate triggers if necessary

При вставке данных в таблицы InnoDB очень важно, чтобы вы делали это в порядок первичного ключа (в противном случае с большими наборами данных это на несколько порядков медленнее).

0
ответ дан 29 November 2019 в 22:04
поделиться

Вы должны были использовать Postgres, смеется. Такой простой запрос не должен занимать больше нескольких десятков секунд, если у вас достаточно ОЗУ, чтобы избежать перегрузки диска.

В любом случае.

=> Проблема в SELECT или INSERT?

(запустите SELECT отдельно на тестовом сервере и укажите время).

=> Ваш запрос связан с диском или CPU?

Запустить это на тестовом сервере и проверьте вывод vmstat. Такой простой запрос не должен занимать больше нескольких десятков секунд, если у вас достаточно ОЗУ, чтобы избежать перегрузки диска.

В любом случае.

=> Проблема в SELECT или INSERT?

(запустите только SELECT на тестовом сервере и укажите время).

=> Ваш запрос связан с диском или CPU?

Запустить это на тестовом сервере и проверьте вывод vmstat. Такой простой запрос не должен занимать больше нескольких десятков секунд, если у вас достаточно ОЗУ, чтобы избежать перегрузки диска.

В любом случае.

=> Проблема в SELECT или INSERT?

(запустите только SELECT на тестовом сервере и укажите время).

=> Ваш запрос связан с диском или CPU?

Запустить это на тестовом сервере и проверьте вывод vmstat. Если это связано с процессором, пропустите это. Если он привязан к диску, проверьте размер рабочего набора (т.е. размер вашей базы данных). Если рабочий набор меньше вашей оперативной памяти, он не должен быть привязан к диску. Вы можете принудительно загрузить таблицу в кеш ОС перед выполнением запроса, запустив фиктивный выбор, например, SELECT sum (some column) FROM table. Это может быть полезно, если запрос выбирает много строк в случайном порядке из таблицы, которая не кэшируется в ОЗУ ... вы запускаете последовательное сканирование таблицы, которое загружает ее в кеш, тогда произвольный доступ выполняется намного быстрее. С помощью некоторых уловок вы также можете кэшировать индексы (или просто поместить каталог базы данных в> / dev / null, смеется).

Конечно, добавление дополнительной оперативной памяти может помочь (но вам нужно проверить, убивает ли запрос диск или сначала процессор). Или скажите MySQL использовать больше вашей оперативной памяти в конфигурации (key_buffer и т. Д.).

Если вы делаете миллионы случайных поисков жесткого диска, вы находитесь в состоянии PAIN.

=> ОК, теперь запрос

ПЕРВЫЙ, АНАЛИЗИРУЙ ваши таблицы.

LEFT JOIN shift_positions ON su.shift_position_id = shift_positions.id WHERE shift_positions.level = 1

ПОЧЕМУ вы оставили JOIN, а затем добавили WHERE? LEFT не имеет смысла. Если в shift_positions нет строки, LEFT JOIN сгенерирует NULL, а WHERE отклонит его.

Решение: используйте JOIN вместо LEFT JOIN и переместите (level = 1) в условие JOIN ON ().

Пока вы занимаетесь этим, также избавьтесь от другого LEFT JOIN (замените на JOIN), если вас действительно не интересуют все эти NULL? (Полагаю, что нет).

Теперь вы, вероятно, можете избавиться от подзапроса.

Далее.

ГДЕ ВРЕМЯ (t.created) МЕЖДУ shift_times.start И shift_times.end)

Это не индексируется, потому что у вас есть функция TIME () в условии (используйте Postgres, смеется). Давайте посмотрим на это:

JOIN shift_times ON (shifts.id = shift_times.shift_id И shift_times.dow = DAYOFWEEK (t.created) AND TIME (t.created) BETWEEN shift_times.start AND shift_times.end)

В идеале вы хотели бы иметь многоколоночный индекс для shift_times (shift_id, DAYOFWEEK (t.created), TIME (t.created)), поэтому это JOIN могут быть проиндексированы.

Решение: добавьте столбцы «день», «время» в shift_times, содержащие DAYOFWEEK (t.created), TIME (t.created), заполненные правильными значениями с помощью триггера, срабатывающего при INSERT или UPDATE.

Теперь создайте многоколоночный индекс для (shift_id, day, time)

2
ответ дан 29 November 2019 в 22:04
поделиться

О BETWEEN

SELECT * FROM a WHERE a.column BETWEEN x AND y 
  • индексируется и соответствует поиску диапазона по индексу a.column (если он у вас есть)
  • на 100% эквивалентен a.column> = x AND a.column <= y

Хотя этот:

SELECT * FROM a WHERE somevalue BETWEEN a.column1 AND a.column2
  • на 100% эквивалентен somevalue> = a.column1 И somevalue <= a.column2
  • сильно отличается от первого выше
  • не индексируется поиском по диапазону (диапазона нет, у вас здесь 2 столбца)
  • обычно приводит к ужасной производительности запроса

Я думаю, что в дискуссии по поводу «между» выше возникла путаница .

OP имеет первый тип, так что не беспокойтесь.

0
ответ дан 29 November 2019 в 22:04
поделиться
Другие вопросы по тегам:

Похожие вопросы: