2009-11-22 8 views
3

La requête suivante est assez simple. Il sélectionne les 20 derniers enregistrements d'une table de messages pour une utilisation dans un scénario de pagination. La première fois que cette requête est exécutée, cela prend de 15 à 30 secondes. Les exécutions suivantes prennent moins d'une seconde (je suppose que la mise en cache est impliquée). J'essaie de déterminer pourquoi la première fois prend tellement de temps.La requête simple prend 15-30 secondes

est ici la requête:

SELECT DISTINCT ID,List,`From`,Subject, UNIX_TIMESTAMP(MsgDate) AS FmtDate 
FROM messages 
WHERE List='general' 
ORDER BY MsgDate 
LIMIT 17290,20; 

version MySQL: 4.0.26-log

Voici le tableau:

messages CREATE TABLE `messages` (
    `ID` int(10) unsigned NOT NULL auto_increment, 
    `List` varchar(10) NOT NULL default '', 
    `MessageId` varchar(128) NOT NULL default '', 
    `From` varchar(128) NOT NULL default '', 
    `Subject` varchar(128) NOT NULL default '', 
    `MsgDate` datetime NOT NULL default '0000-00-00 00:00:00', 
    `TextBody` longtext NOT NULL, 
    `HtmlBody` longtext NOT NULL, 
    `Headers` text NOT NULL, 
    `UserID` int(10) unsigned default NULL, 
    PRIMARY KEY (`ID`), 
    UNIQUE KEY `List` (`List`,`MsgDate`,`MessageId`), 
    KEY `From` (`From`), 
    KEY `UserID` (`UserID`,`List`,`MsgDate`), 
    KEY `MsgDate` (`MsgDate`), 
    KEY `ListOnly` (`List`) 
) TYPE=MyISAM ROW_FORMAT=DYNAMIC 

Voici le expliquer:

table type possible_keys key  key_len ref  rows Extra 
------ ------ ------------- -------- ------- ------ ------ -------------------------------------------- 
m  ref  List,ListOnly ListOnly 10  const 18002 Using where; Using temporary; Using filesort 

Pourquoi est-il en utilisant un fichier de fichiers quand je avoir des index sur toutes les colonnes pertinentes? J'ai ajouté l'index ListOnly juste pour voir si cela aiderait. J'avais initialement pensé que l'index de la liste gérerait à la fois la sélection de la liste et le tri sur MsgDate, mais ce n'était pas le cas. Maintenant que j'ai ajouté l'index ListOnly, c'est celui qu'il utilise, mais il fait toujours un fichier sur MsgDate, ce qui, je le soupçonne, prend tellement de temps.

J'essayé d'utiliser FORCE INDEX comme suit:

SELECT DISTINCT ID,List,`From`,Subject, UNIX_TIMESTAMP(MsgDate) AS FmtDate 
FROM messages 
FORCE INDEX (List) 
WHERE List='general' 
ORDER BY MsgDate 
LIMIT 17290,20; 

Cela ne semble forcer MySQL à utiliser l'index, mais il n'accélère pas la requête du tout.

est ici l'expliquer pour cette requête:

table type possible_keys key  key_len ref  rows Extra      
------ ------ ------------- ------ ------- ------ ------ ---------------------------- 
m  ref  List   List 10  const 18002 Using where; Using temporary 

MISES À JOUR:

J'ai enlevé DISTINCT de la requête. Cela n'a pas aidé la performance du tout.

J'ai supprimé l'appel UNIX_TIMESTAMP. Cela n'a pas non plus affecté les performances.

J'ai fait un cas particulier dans mon code PHP afin que si je perçois l'utilisateur regarde la dernière page de résultats, ajouter une clause WHERE qui ne renvoie que les 7 derniers jours de résultats:

SELECT m.ID,List,From,Subject,MsgDate 
FROM messages 
WHERE MsgDate>='2009-11-15' 
ORDER BY MsgDate DESC 
LIMIT 20 

C'est beaucoup plus rapide. Cependant, dès que je navigue vers une autre page de résultats, il doit utiliser l'ancien SQL et prend beaucoup de temps à s'exécuter. Je ne peux pas penser à un moyen pratique et réaliste de le faire pour toutes les pages. De plus, faire ce cas particulier rend mon code PHP plus complexe.

Etrangement, la première exécution de la requête d'origine prend beaucoup de temps. Les exécutions ultérieures de la même requête ou d'une requête présentant une page de résultats différente (c'est-à-dire, uniquement les modifications de la clause LIMIT) sont très rapides. La requête ralentit à nouveau si elle n'a pas été exécutée pendant environ 5 minutes.

SOLUTION:

La meilleure solution que je suis venu avec Jason est basé sur Orendorff et l'idée de Juliette. Tout d'abord, je détermine si la page en cours est plus proche du début ou de la fin du nombre total de pages.Si c'est plus proche de la fin, j'utilise ORDER BY MsgDate DESC, applique une limite appropriée, puis inverse l'ordre des enregistrements retournés.

Cela permet de récupérer des pages proches du début ou de la fin du jeu de résultats beaucoup plus rapidement (la première fois prend 4-5 secondes au lieu de 15-30). Si l'utilisateur souhaite naviguer vers une page proche du milieu (actuellement autour de la 430ème page), la vitesse peut redescendre. Mais ce serait un cas rare.

Alors qu'il ne semble pas y avoir de solution parfaite, c'est beaucoup mieux que dans la plupart des cas.

Merci, Jason et Juliette.

+0

Bon point, shylent. Je vais essayer sans DISTINCT. – elmonty

+0

La suppression de DISTINCT n'a pas du tout amélioré les performances. – elmonty

+0

Est-ce que "LIMIT 17290,20" ne correspond pas à la 865ème page de votre requête? Les utilisateurs naviguent-ils vraiment aussi loin dans l'ensemble de données? – Juliet

Répondre

3

Au lieu de ORDER BY MsgDate LIMIT 17290,20, essayez ORDER BY MsgDate DESC LIMIT 20.

Bien sûr, les résultats apparaîtront dans l'ordre inverse, mais cela devrait être facile à gérer.

EDIT: Vos valeurs de MessageId augmentent-elles toujours avec le temps? Sont-ils uniques?

Si oui, je ferais un indice:

UNIQUE KEY `ListMsgId` (`List`, `MessageId`) 

et requête basée sur le message Ids plutôt que la date possible.

-- Most recent messages (in reverse order) 
SELECT * FROM messages 
WHERE List = 'general' 
ORDER BY MessageId DESC 
LIMIT 20 

-- Previous page (in reverse order) 
SELECT * FROM messages 
WHERE List = 'general' AND MessageId < '15885830' 
ORDER BY MessageId DESC 
LIMIT 20 

-- Next page 
SELECT * FROM messages 
WHERE List = 'general' AND MessageId > '15885829' 
ORDER BY MessageId 
LIMIT 20 

Je pense que vous payez aussi pour avoir varchar colonnes où serait beaucoup plus vite un type int. Par exemple, List pourrait à la place être un ListId qui pointe vers une entrée dans une table distincte. Vous pourriez vouloir l'essayer dans une base de données de test pour voir si c'est vraiment vrai; Je ne suis pas un expert MySQL.

+0

J'utilise la LIMIT parce que l'utilisateur peut revenir sur les 20 messages précédents (ou les 20 précédents, etc.). – elmonty

+0

J'ai essayé mais ça ne m'aide pas du tout. C'est probablement dû au fait que LIMIT a les mêmes problèmes, qu'un offset soit utilisé ou non. – elmonty

+0

Wow. C'est plutôt bizarre. La forme la plus simple possible de cette requête serait 'SELECT * FROM messages WHERE List = 'general' ORDER BY MsgDate' que je m'attendrais à être super rapide puisque vous avez un index sur (List, MsgDate, MessageId). –

1

Quelle version de mon code SQL utilisez-vous? Certaines versions plus anciennes utilisaient la clause LIMIT comme filtre de post-traitement (ce qui signifie obtenir tous les enregistrements demandés au serveur, mais afficher uniquement les 20 que vous avez demandés).

Vous pouvez voir à partir de votre explication, 18002 lignes reviennent, même si vous n'en voyez que 20. Existe-t-il un moyen d'ajuster vos critères de sélection pour identifier les 20 lignes que vous voulez retourner, plutôt que d'avoir 18 000 lignes en arrière et n'en montrer que 20 ???

+0

4.0.26: certainement une ancienne version de MySQL que je voudrais utiliser aujourd'hui. – bobince

+0

Cela a du sens, votre première requête renvoie 18002 lignes, même si elle ne vous en montre que 20. (Votre explication montre cela). Il semble que d'autres personnes ont proposé des solutions dans la même ligne, essayez de récupérer quelques lignes via la clause WHERE plutôt que de se fier à l'option LIMIT. Si vous recherchez les enregistrements les plus récents, vous pouvez saisir le msgdate max puis l'utiliser et peut-être la date précédente dans votre clause where. Même s'il s'agit de deux requêtes, l'une pour obtenir la date maximale et l'autre pour les données, elle devrait être plus rapide. Bonne chance – Sparky

+0

J'ai fait un cas particulier dans mon code PHP de sorte que si je détecte que l'utilisateur regarde la dernière page de résultats, j'ajoute une clause WHERE qui renvoie seulement les 7 derniers jours de résultats: SELECT m.ID, List, 'From', Subject, MsgDate FROM messages m WHERE MsgDate> = '2009-11-15' ORDER BY MsgDate DESC LIMIT 20 C'est beaucoup plus rapide. Cependant, dès que je navigue vers une autre page de résultats, il utilise l'ancien SQL et prend une éternité à revenir. Je ne peux pas penser à un moyen pratique et réaliste de le faire pour toutes les pages. – elmonty

2

Vous pouvez supprimer la clé ListOnly. L'index composé List contient déjà toutes les informations qu'il contient.

Votre EXPLAIN pour la requête List -indexée semble beaucoup mieux, manquant le filesort. Vous pouvez obtenir de meilleures performances réelles en échangeant la commande comme suggéré par Jason, et en perdant peut-être l'appel UNIX_TIMESTAMP (vous pouvez le faire dans la couche application, ou simplement utiliser les horodatages Unix stockés comme INTEGER dans le schéma).

+0

La perte de UNIX_TIMESTAMP affecte-t-elle réellement les performances? – elmonty

+0

Ne sais pas vraiment, ne peut que le tester et voir. Je ne l'espère pas, mais souvent en utilisant des colonnes calculées, MySQL peut utiliser des tables temporaires là où cela ne serait pas nécessaire. Personnellement, je n'utilise que des horodatages unix pour les dates, car les types de dates natifs ont tendance à provoquer des incompatibilités de couche d'accès entre les SGBD et offrent relativement peu d'utilité sur les horodatages. – bobince

+0

J'ai essayé de supprimer UNIX_TIMESTAMP. Cela n'a pas aidé la performance du tout. – elmonty