Bon, j'ai trouvé une solution. Il a fallu beaucoup d'expérimentation, et je pense un peu de chance aveugle, mais la voici :
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;
Longue explication
Maintenant, je vais vous expliquer pourquoi cela fonctionne, et mon processus relatif et les étapes pour y arriver.
Tout d'abord, je savais que la requête que j'essayais souffrait à cause de l'énorme table dérivée et des JOIN suivants. Je prenais ma table de tickets bien indexée et j'y joignais toutes les données shift_times, puis je laissais MySQL mâcher cela pendant qu'il tentait de joindre la table shifts et shift_positions. Ce mastodonte dérivé représenterait jusqu'à 2 millions de lignes non indexées.
Maintenant, je savais que cela se passait. La raison pour laquelle j'empruntais cette voie était que la "bonne" façon de le faire, en utilisant strictement les JOIN, prenait encore plus de temps. Cela est dû au chaos nécessaire pour déterminer qui est le responsable d'un quart de travail donné. Je dois me joindre à shift_times pour savoir quel est le décalage correct, tout en me joignant simultanément à shift_positions pour déterminer le niveau de l'utilisateur. Je ne pense pas que l'optimiseur MySQL gère cela très bien, et finit par créer une ÉNORME monstruosité d'une table temporaire des jointures, puis filtre ce qui ne s'applique pas.
Ainsi, comme la table dérivée semblait être la "voie à suivre", j'ai obstinément persisté pendant un certain temps. J'ai essayé de le mettre dans une clause JOIN, aucune amélioration. J'ai essayé de créer une table temporaire avec la table dérivée, mais encore une fois, c'était trop lent car la table temporaire n'était pas indexée.
J'en suis venu à réaliser que je devais gérer ce calcul d'équipe, d'heures, de postes avec sagesse. J'ai pensé que peut-être une VUE serait la voie à suivre. Et si je créais une VIEW contenant ces informations :(shop_id, shift_id, dow, start, end, manager_id). Ensuite, je n'aurais qu'à joindre la table des tickets par shop_id et l'ensemble du calcul DAYOFWEEK/TIME, et je serais en affaires. Bien sûr, j'ai omis de me souvenir que MySQL gère les VIEWs plutôt assily. Il ne les matérialise pas du tout, il exécute simplement la requête que vous auriez utilisée pour obtenir la vue pour vous. Donc, en joignant des tickets à cela, j'exécutais essentiellement ma requête d'origine - aucune amélioration.
Ainsi, au lieu d'une VUE, j'ai décidé d'utiliser une TABLE TEMPORAIRE. Cela fonctionnait bien si je ne récupérais qu'un des gestionnaires (créé ou résolu) à la fois, mais c'était encore assez lent. De plus, j'ai découvert qu'avec MySQL, vous ne pouvez pas faire référence à la même table deux fois dans la même requête (je devrais joindre ma table temporaire deux fois pour pouvoir faire la différence entre manager_created et manager_resolved). C'est un gros WTF, car je peux le faire tant que je ne spécifie pas "TEMPORARY" - c'est là que la magie CREATE TABLE ENGINE=MEMORY est entrée en jeu.
Avec cette pseudo table temporaire en main, j'ai essayé à nouveau mon JOIN pour juste manager_created. Il a bien fonctionné, mais encore assez lent. Pourtant, lorsque je me suis joint à nouveau pour obtenir manager_resolved dans la même requête, le temps de requête a reculé dans la stratosphère. L'examen de l'EXPLAIN a montré l'analyse complète de la table des tickets (lignes ~ 2 mln), comme prévu, et les JOIN sur la table magique à ~ 2 087 chacun. Encore une fois, j'ai semblé courir vers l'échec.
J'ai maintenant commencé à réfléchir à la façon d'éviter complètement les JOIN et c'est à ce moment-là que j'ai trouvé un ancien message obscur sur le forum où quelqu'un a suggéré d'utiliser des sous-sélections (je ne trouve pas le lien dans mon historique). C'est ce qui a conduit à la deuxième requête SELECT présentée ci-dessus (celle de la création de tickets_extra). Dans le cas de la sélection d'un seul champ de gestionnaire, cela a bien fonctionné, mais encore une fois avec les deux, c'était de la merde. J'ai regardé le EXPLAIN et j'ai vu ceci :
*************************** 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, la sous-requête dépendante redoutée. Il est souvent suggéré de les éviter, car MySQL les exécutera généralement de l'extérieur vers l'intérieur, en exécutant la requête interne pour chaque ligne de l'externe. J'ai ignoré cela et je me suis demandé:"Eh bien ... et si j'indexais simplement cette stupide table magique?". Ainsi, l'index ADD (shop_id, dow) est né.
Regardez ceci :
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)
Maintenant C'EST de quoi je parle !
Conclusion
C'est certainement la première fois que je crée une table non TEMPORAIRE à la volée et que je l'INDEXE à la volée, simplement pour effectuer une seule requête efficacement. Je suppose que j'ai toujours supposé que l'ajout d'un index à la volée était une opération d'un coût prohibitif. (Ajouter un index sur ma table de tickets de 2 millions de lignes peut prendre plus d'une heure). Pourtant, pour seulement 3 000 lignes, c'est du gâteau.
N'ayez pas peur des SOUS-REQUETES DEPENDANTES, de la création de tables TEMPORAIRES qui ne le sont pas vraiment, de l'indexation à la volée ou des extraterrestres. Ils peuvent tous être de bonnes choses dans la bonne situation.
Merci pour toute l'aide StackOverflow. :-D