2010-09-07 5 views
3

J'essaye de déterminer la meilleure approche générale pour interroger contre deux tables jointes qui ont beaucoup de données, où chaque table a une colonne dans la clause where. Imaginez un simple schéma w/deux tables:Dans MySQL, comment JOINmer deux très grandes tables qui ont toutes deux des colonnes dans la condition WHERE?

posts 
id (int) 
blog_id (int) 
published_date (datetime) 
title (varchar) 
body (text) 

posts_tags 
post_id (int) 
tag_id (int) 

Avec les indices suivants:

posts: [blog_id, published_date] 
tags: [tag_id, post_id] 

Nous voulons sélectionner les 10 derniers messages sur un blog donné qui ont été marqués par "foo". Pour le bien de cette discussion, supposons que le blog a 10 millions de messages, et 1 million d'entre eux ont été marqués avec "foo". Quel est le moyen le plus efficace pour interroger ces données?

L'approche naïve serait de le faire:

SELECT 
    id, blog_id, published_date, title, body 
FROM 
    posts p 
INNER JOIN 
    posts_tags pt 
    ON pt.post_id = p.id 
WHERE 
    p.blog_id = 1 
    AND pt.tag_id = 1 
ORDER BY 
    p.published_date DESC 
LIMIT 10 

MySQL utilisera nos index, mais encore jusqu'à la fin numériser des millions d'enregistrements. Existe-t-il un moyen plus efficace de récupérer ces données sans dénormaliser le schéma?

+0

Laissez le moteur faire le travail. Fournissez les astuces (index) et consultez le plan d'exécution. Si une analyse complète est effectuée, elle est soit requise (pour les indices donnés - vérifiez vos index de recouvrement), soit le générateur de plan échoue (peut-être pense-t-il qu'un balayage [complet] gagne toujours, auquel cas il peut même être correct). Je ne suis pas un administrateur de bases de données, mais je n'ai jamais été confronté à la nécessité de baser des données dénormalisées (les données dénormalisées transitoires ne sont pas identiques à un schéma relationnel dénormalisé [brisé]). –

+1

Bonne question. La seule solution que j'ai trouvée à ce type de problème est la dénormalisation. – nathan

Répondre

2

Il est fort probable que MySQL utilisera d'abord l'index (blog_id, published_date) pour analyser toutes les lignes satisfaisant à la condition blog_id = 1 en commençant par la ligne avec la plus récente published_date. Pour ce faire, il suffit de parcourir en arrière à travers l'index en partant du bon endroit. Pour chaque ligne, il doit se joindre à la table posts_tags. À ce stade, le tag_id et le post_id sont connus. Il suffit donc de rechercher dans l'index principal si la ligne existe. 10% des lignes ont l'étiquette foo donc en moyenne environ 100 lignes dans le tableau posts devront être vérifiées avant que les 10 premières lignes du jeu de résultats ne soient trouvées.

Je m'attendrais à ce que la requête que vous avez publiée fonctionne assez rapidement si le tag foo est commun. Je ne pense pas qu'il va vérifier des millions de lignes - peut-être quelques centaines, ou quelques milliers si vous êtes malchanceux. Dès qu'il a trouvé 10 lignes correspondantes, il peut s'arrêter sans vérifier d'autres lignes. D'autre part, si vous choisissez une étiquette qui a moins de 10 occurrences, elle sera lente car elle devra analyser toutes les lignes de ce blog.

Avez-vous des mesures de performance indiquant que la requête est particulièrement lente, même lorsque le tag que vous recherchez est souvent utilisé? Pouvez-vous poster la sortie de EXPLAIN pour la requête?

+0

Je suis d'accord; Ne dérangez pas votre modèle de données pour cela jusqu'à ce que vous soyez sûr que vous avez un vrai problème. La requête que vous proposez peut très bien fonctionner. –

+0

Ceci est un cas général d'un problème que j'ai déjà vu, donc je n'ai pas de sortie EXPLAIN. Votre point sur 10% des lignes marquées w/"foo" est bon. Disons que la fréquence de cette étiquette était beaucoup plus faible, comme 0,1%. Dans ce cas, la dénormalisation serait-elle une prochaine étape judicieuse (c'est-à-dire dupliquer blog_id et published_date sur la table posts_tags, avec un index approprié)? Ou y a-t-il un meilleur moyen de sortir ces données de ce schéma? – Newt

0

Si le plan de requête estime que le nombre de lignes jointes est faible, il ne peut pas utiliser l'index. Parce que l'analyse est une opération linéaire, elle fonctionne mieux pour un petit nombre de lignes, tandis que l'utilisation de l'index est plus efficace pour un grand nombre de lignes. Alors que d'autres ont suggéré de regarder le plan de requête pour voir ce qu'il estime pour le nombre de lignes.

Il est possible d'ajouter les conditions blod_id et tag_id aux critères ON, même si cela semble étrange. Je ne suis pas sûr si cela va changer quelque chose, mais j'essaie habituellement avec de telles choses.

Vous pouvez également expérimenter l'inversion de l'ordre des colonnes dans l'index, car cela est important. Un peu comme un annuaire est un index de FirstName, FirstName qui serait très différent d'un répertoire avec un index FirstName, LastName.

Il est difficile de s'asseoir et de dire de manière déterministe ce qui fonctionnera le mieux sans expérimentation.Habituellement, je fais ce genre de choses à travers l'expérimentation et l'analyse comparative. Parfois, je trouve que les résultats sont contraires à ce que je m'attendais sur la base de la documentation, puis j'y approfondis davantage pour réaliser qu'il y a un comportement/une fonctionnalité subtile que je n'ai pas appliqué à la situation particulière.

2

si la performance est primordiale alors denormalise comme suggéré:

table:

create table posts_tags 
(
blog_id int unsigned not null, -- denormalise 
tag_id smallint unsigned not null, 
post_id int unsigned not null, 
primary key(blog_id, tag_id, post_id) -- clustered composite PK 
) 
engine=innodb; 

dénormalisation déclencheur:

delimiter # 

create trigger posts_tags_before_ins_trig before insert on posts_tags 
for each row 
proc_main:begin 

declare b_id int unsigned default 0; 

    select blog_id into b_id from posts where post_id = new.post_id; 

    set new.blog_id = b_id; 

end proc_main # 

delimiter ; 

requête procédure stockée: (supposée posts.post_id était auto_increment PK)

delimiter ; 

drop procedure if exists get_latest_blog_posts_by_tag; 

delimiter # 

create procedure get_latest_blog_posts_by_tag 
(
in p_blog_id int unsigned, 
in p_tag_id smallint unsigned 
) 
proc_main:begin 

    select 
    p.* 
    from 
    posts p 
    inner join 
    (
    select distinct 
     pt.post_id 
    from 
     posts_tags pt 
    where 
     pt.blog_id = p_blog_id and pt.tag_id = p_tag_id 
    order by 
     pt.post_id desc limit 10 
) rp on p.post_id = rp.post_id 
    order by 
    p.post_id desc; 

end proc_main # 

delimiter ; 

call get_latest_blog_posts_by_tag(1,1); 
3

Tous les filtres que vous voulez faire sur une table jointe doivent aller dans la jointure. Techniquement, la clause WHERE ne devrait contenir que des conditions qui nécessitent plus d'une table ou la table primaire. Bien qu'il ne puisse pas accélérer toutes les requêtes, il assure que MySQL optimise la requête correctement.

FROM 
posts p 
INNER JOIN 
posts_tags pt 
ON pt.post_id = p.id 
    AND pt.tag_id = 1 
WHERE 
p.blog_id = 1 
Questions connexes