2009-04-18 17 views
0

Je travaille sur un système de reporting qui permet à l'utilisateur d'interroger arbitrairement un ensemble de tables de faits, contraignant sur plusieurs tables de dimension pour chaque table de faits. J'ai écrit une classe de générateur de requête qui assemble automatiquement toutes les jointures et sous-requêtes correctes en fonction des paramètres de contrainte, et tout fonctionne comme prévu. Mais j'ai l'impression de ne pas générer les requêtes les plus efficaces. Sur un ensemble de tables contenant quelques millions d'enregistrements, ces requêtes prennent environ 10 secondes à s'exécuter, et j'aimerais les réduire à moins d'une seconde. J'ai le sentiment que, si je pouvais me débarrasser des sous-requêtes, le résultat serait beaucoup plus efficace. Plutôt que de vous montrer mon schéma actuel (ce qui est beaucoup plus compliqué), je vais vous montrer un exemple analogue qui illustre le point sans avoir à expliquer l'ensemble de mon application et de mon modèle de données. Imaginez que j'ai une base de données d'informations sur les concerts, avec des artistes et des lieux. Les utilisateurs peuvent arbitrairement étiqueter les artistes et les sites. Donc, le schéma ressemble à ceci:Requêtes de création de rapports: meilleur moyen de rejoindre plusieurs tableaux de faits?

concert 
    id 
    artist_id 
    venue_id 
    date 

artist 
    id 
    name 

venue 
    id 
    name 

tag 
    id 
    name 

artist_tag 
    artist_id 
    tag_id 

venue_tag 
    venue_id 
    tag_id 

Assez simple. Maintenant, disons que je veux interroger la base de données pour tous les concerts qui se déroulent dans un mois d'aujourd'hui, pour tous les artistes avec des tags 'techno' et 'trombone', en concert avec 'cheap-beer' et 'great-mosh' étiquette de -pits.

La meilleure question que je suis en mesure de trouver ressemble à ceci:

SELECT 
    concert.id AS concert_id, 
    concert.date AS concert_date, 
    artist.id AS artist_id, 
    artist.name AS artist_name, 
    venue.id AS venue_id, 
    venue.name AS venue_name, 
FROM 
    concert 
INNER JOIN (
    artist ON artist.id = concert.artist_id 
) INNER JOIN (
    venue ON venue.id = concert.venue_id 
) 
WHERE (
    artist.id IN (
    SELECT artist_id 
    FROM artist_tag 
    INNER JOIN tag AS a on (
     a.id = artist_tag.tag_id 
     AND 
     a.name = 'techno' 
    ) INNER JOIN tag AS b on (
     b.id = artist_tag.tag_id 
     AND 
     b.name = 'trombone' 
    ) 
) 
    AND 
    venue.id IN (
    SELECT venue_id 
    FROM venue_tag 
    INNER JOIN tag AS a on (
     a.id = venue_tag.tag_id 
     AND 
     a.name = 'cheap-beer' 
    ) INNER JOIN tag AS b on (
     b.id = venue_tag.tag_id 
     AND 
     b.name = 'great-mosh-pits' 
    ) 
) 
    AND 
    concert.date BETWEEN NOW() AND (NOW() + INTERVAL 1 MONTH) 
) 

La requête fonctionne, mais je vraiment n'aime pas avoir ces multiples sous-requêtes. Si je pouvais accomplir la même logique en utilisant simplement la logique JOIN, j'ai le sentiment que la performance s'améliorerait considérablement.

Dans un monde parfait, j'utiliserais un vrai serveur OLAP. Mais mes clients vont se déployer sur MySQL ou MSSQL ou Postgres, et je ne peux pas garantir qu'un moteur compatible OLAP sera disponible. Je suis donc bloqué en utilisant un SGBDR ordinaire avec un schéma en étoile. Ne vous méprenez pas trop sur les détails de cet exemple (ma vraie application n'a rien à voir avec la musique, mais elle a plusieurs tables de faits avec une relation analogue à celles que j'ai montrées ici). Dans ce modèle, les tables 'artist_tag' et 'lieu_tag' fonctionnent comme des tables de faits, et tout le reste est une dimension.

Il est important de noter, dans cet exemple, que les requêtes sont beaucoup plus simples à écrire si j'autorise uniquement l'utilisateur à contraindre par rapport à une seule valeur artist_tag ou lieu_tag. Cela devient vraiment compliqué quand j'autorise les requêtes à inclure la logique ET, nécessitant plusieurs balises distinctes. Donc, ma question est la suivante: quelles sont les meilleures techniques que vous connaissez pour écrire des requêtes efficaces contre plusieurs tables de faits?

+0

je pense que le nœud de la question est vraiment ici la nature AND de la requête, plutôt que les "tables de faits multiples". (Bien qu'ils se complètent mutuellement.) La réponse que je donne ci-dessous résout cela en exécutant le composant AND de la requête dans une clause HAVING, au lieu de nécessiter plusieurs fois les jointures aux mêmes tables de faits. – MatBailie

+0

Il est temps de marquer comme résolu/fermé/... :) –

Répondre

1

Dénormaliser le modèle. Inclure le nom du tag dans les tables des lieux et des artistes. De cette façon, vous évitez une relation de plusieurs à plusieurs et vous avez un schéma en étoile simple. En appliquant cette dénormalisation, la clause where peut uniquement vérifier ce champ tag_name supplémentaire dans les deux tables (artiste et lieu).

+0

Mais si je dénormalise, comment autoriser un artiste ou un lieu à avoir plusieurs tags? La chose est, je ne peux vraiment pas éliminer la relation many-to-many sans paralyser totalement le modèle. – benjismith

+1

Vous aurez plusieurs enregistrements pour le même artiste, mais avec des tags différents. Dans les entrepôts de données, il est habituel d'avoir des données dénormalisées pour améliorer les performances des requêtes. C'est l'une des raisons pour lesquelles les jobs ETL (Extract-Transform-Load data) sont utilisés: pour convertir un modèle relationnel normalisé en un modèle spécifique d'entrepôt de données (modèle dimensionnel ou étoile). –

+0

D'accord, sur un couple d'hypothèses. Cela peut entraîner une augmentation considérable de la taille des données, l'espace disponible est-il disponible? (Allez, les disques sont bon marché ...) Avec des données modifiables, rafraîchir les données dénormalisées est coûteux en termes de cpu, etc. Les données sont-elles relativement statiques, et/ou peuvent-elles être ETL pendant la nuit, etc.? Si oui, une telle dénormalisation (au format de fichier plat, par exemple) peut être extrêmement bénéfique pour le reporting. – MatBailie

2

Mon approche est un peu plus générique, en mettant les paramètres de filtre dans les tables, puis en utilisant GROUP BY, HAVING et COUNT pour filtrer les résultats.J'ai utilisé cette approche de base plusieurs fois pour une recherche très sophistiquée et cela fonctionne très bien (pour moi grin).

Je ne joins pas d'abord sur les tables de dimension Artist et Venue. J'obtiendrais les résultats en tant qu'ID (juste besoin de artist_tag et de lieu_tag) puis joindrais les résultats sur les tables de l'artiste et du lieu pour obtenir ces valeurs de dimension. (En fait, la recherche de l'ID entité est dans une sous requête, puis dans une requête externe obtenir la dimension que vous avez besoin des valeurs. Garder séparées devrait améliorer les choses ...)

DECLARE @artist_filter TABLE (
    tag_id INT 
) 

DECLARE @venue_filter TABLE (
    tag_id INT 
) 

INSERT INTO @artist_filter 
SELECT id FROM tag 
WHERE name IN ('techno','trombone') 

INSERT INTO @venue_filter 
SELECT id FROM tag 
WHERE name IN ('cheap-beer','great-most-pits') 


SELECT 
    concert.id AS concert_id, 
    concert.date AS concert_date, 
    artist.id AS artist_id, 
    venue.id AS venue_id 
FROM 
    concert 
INNER JOIN 
    artist_tag 
    ON artist_tag.artist_id = concert.artist_id 
INNER JOIN 
    @artist_filter AS [artist_filter] 
    ON [artist_filter].tag_id = artist_tag.id 
INNER JOIN 
    venue_tag 
    ON venue_tag.venue_id = concert.venue_id 
INNER JOIN 
    @venue_filter AS [venue_filter] 
    ON [venue_filter].tag_id = venue_tag.id 
WHERE 
    concert.date BETWEEN NOW() AND (NOW() + INTERVAL 1 MONTH) 
GROUP BY 
    concert.id, 
    concert.date, 
    artist_tag.artist_id, 
    venue_tag.id 
HAVING 
    COUNT(DISTINCT [artist_filter].id) = (SELECT COUNT(*) FROM @artist_filter) 
    AND 
    COUNT(DISTINCT [venue_filter].id) = (SELECT COUNT(*) FROM @venue_filter) 

(je suis sur un netbook et la souffrance pour elle, donc je vais laisser la requête externe obtenir les noms d'artistes et lieu des tables d'artiste et lieu sourire)

EDIT
note:

Une autre option serait de filtrer l'artis Tables t_tag et lieu_tag dans les sous-requêtes/tables dérivées. Que cela en vaille la peine dépend de l'influence de la jointure sur la table de concert. Mon hypothèse ici est qu'il y a BEAUCOUP d'artistes et de lieux, mais une fois filtrée sur la table de concert (elle-même filtrée par les dates), le nombre d'artistes/lieux diminue considérablement.

De même, il existe souvent un besoin/désir de traiter le cas où NO_tags_articles et/ou les_tags_table sont spécifiés. Par expérience, il est préférable de traiter cela par programme. Autrement dit, utilisez des instructions IF et des requêtes spécialement adaptées à ces cas. Une seule requête SQL peut être écrite pour la gérer, mais elle est beaucoup plus lente que l'alternative programmatique. De même, écrire des requêtes similaires à plusieurs reprises peut sembler désordonné et dégrader la maintenabilité, mais l'augmentation de la complexité doit faire en sorte qu'une requête unique soit souvent plus difficile à maintenir.

EDIT

Une autre disposition similaire pourrait être ...
- concert Filtrer par artiste sub_query/derived_table
- Filtrer les résultats par lieu comme sub_query/derived_table
- Joignez-vous à des résultats sur les tables de dimension obtenir les noms, etc

(filtrage cascadé)

SELECT 
    <blah> 
FROM 
    (
    SELECT 
     <blah> 
    FROM 
     (
     SELECT 
      <blah> 
     FROM 
      concert 
     INNER JOIN 
      artist_tag 
     INNER JOIN 
      artist_filter 
     WHERE 
     GROUP BY 
     HAVING 
    ) 
    INNER JOIN 
     venue_tag 
    INNER JOIN 
     venue_filter 
    GROUP BY 
    HAVING 
) 
INNER JOIN 
    artist 
INNER JOIN 
    venue 

En cascadant le filtrage, chaque filtrage suivant a un jeu réduit sur lequel il doit travailler. Cela PEUT réduire le travail effectué par la section GROUP BY-HAVING de la requête. Pour deux niveaux de filtrage, j'imagine qu'il est peu probable que cela soit dramatique.

L'original peut être encore plus performant car il bénéficie d'un filtrage supplémentaire d'une manière différente. Dans votre exemple:
- Il peut y avoir de nombreux artistes dans la plage de dates, mais peu qui répondent au moins un critère
- Il peut y avoir de nombreux endroits dans la plage de dates, mais peu qui répondent au moins un critère
- Avant GROUP BY, cependant, tous les concerts sont éliminés où ...
---> l'artiste (s) ne répond à aucun des critères
---> et/ou le lieu ne répond à aucun des critères

Lorsque vous effectuez une recherche selon plusieurs critères, ce filtrage se dégrade. Là où les lieux et/ou les artistes partagent beaucoup de tags, le filtrage se dégrade également.

Alors, quand devrais-je utiliser l'original, ou quand devrais-je utiliser la version en cascade?
- Original: Peu de critères de recherche et lieux/artistes sont dis-semblables les uns des autres
- Cascadé: Beaucoup de critères de recherche ou lieux/artistes ont tendance à être similaires

+0

Je n'ai pas utilisé la table "tag_artist_user" car elle n'a pas eu d'impact sur les résultats dans votre exemple – MatBailie

+0

Oups. La table "tag_artist_user" était un artefact d'un brouillon précédent de la requête. Juste édité le post original pour l'enlever. – benjismith

+0

J'aime l'approche de l'utilisation des tables de filtrage, mais n'utilisant pas de variables de table. Vous n'avez aucun index sur ceux-ci. Il est possible d'avoir un index sur une variable de table mais pour des raisons équitables il n'y a pas de statistiques. Votre solution est également spécifique à SQL Server. Si vous utilisez une variable de table, SQL Server génère un plan d'exécution qui suppose que la variable de table a une seule ligne (en raison de l'absence de statistiques). Cela peut fonctionner correctement s'il n'y a pas beaucoup de lignes dans la variable table, mais quand il y en a plus, les performances vont baisser. – Davos

0

Cette situation n'est pas techniquement plusieurs tables de faits. Vous avez un beaucoup à beaucoup de relations entre les lieux & étiquettes ainsi que des artistes & étiquettes.

Je pense que MatBailie fournit quelques exemples intéressants ci-dessus, mais je pense que cela peut être beaucoup plus simple si vous gérez les paramètres de votre application d'une manière utile. En dehors de la requête générée par l'utilisateur sur la table de faits, vous avez besoin de deux requêtes statiques pour fournir les options de paramètre à l'utilisateur en premier lieu. L'une d'entre elles est une liste de balises appropriées à Venue, l'autre concerne les balises appropriées à l'artiste.

Lieu: balises appropriées

SELECT DISTINCT tag_id, tag.name as VenueTagName 
FROM venue_tag 
INNER JOIN tag 
ON venue_tag.tag_id = tag.id 

Artiste balises appropriées:

SELECT DISTINCT tag_id, tag.name as ArtistTagName 
FROM artist_tag 
INNER JOIN tag 
ON artist_tag.tag_id = tag.id 

Ces deux requêtes conduisent une liste déroulante ou d'autres contrôles paramètres de sélection. Dans un système de rapport, vous devriez essayer d'éviter de transmettre des variables de chaîne. Dans votre application, vous présentez le nom de la chaîne à l'utilisateur, mais transmettez l'ID entier à la base de données.

par exemple. Lorsque l'utilisateur choisit les balises, vous prenez les valeurs tag.id et leur fournir à votre requête (où j'ai le (1,2) et le bit (100,200) ci-dessous):

SELECT 
    concert.id AS concert_id, 
    concert.date AS concert_date, 
    artist.id AS artist_id, 
    artist.name AS artist_name, 
    venue.id AS venue_id, 
    venue.name AS venue_name, 
FROM 
concert 
INNER JOIN artist 
    ON artist.id = concert.artist_id 
INNER JOIN artist_tag 
    ON artist.id = artist_tag.artist_id 
INNER JOIN venue 
    ON venue.id = concert.venue_id 
INNER JOIN venue_tag 
    ON venue.id = venue_tag.venue_id 
WHERE venue_tag.tag_id in (1,2) -- Assumes that the IDs 1 and 2 map to "cheap-beer" and "great-mosh-pits) 
AND artist_tag.tag_id in (100,200) -- Assumes that the IDs 100 and 200 map to "techno" and "trombone") Sounds like a wild night of drunken moshing to brass band techno! 
AND concert.date BETWEEN NOW() AND (NOW() + INTERVAL 1 MONTH) 
+0

Notez que 'WHERE lieu_tag.tag_id dans (1,2)' ne répond pas aux exigences de l'OP. Cela donne des lieux avec de la «bière bon marché» ou des «grands moshpits», mais l'OP veut des lieux qui ont de la «bière bon marché» et des «grands moshpits». Cela implique de vérifier plusieurs rangées * (une rangée ayant 'cheap-beer' et une rangée ayant' great-moshpits' et demandant ensuite que les deux rangées doivent exister pour le même lieu) *. En outre, SQL est notoirement pauvre aux listes de paramétrage. Et si le PO exige de la «bière bon marché» et du «grand-moshpits» ET «entrée libre»? Cette réponse ne fournit pas une approche généralisée pour prouver les attributs «n». – MatBailie

+0

@MatBailie Oui, je vois que vous avez raison, je n'avais pas considéré l'exigence ET pour les balises. Mon exemple traite uniquement l'exemple OR. Je pense que mon point sur la gestion des paramètres est toujours valide, mais je vois pourquoi vous comparez les comptes d'étiquettes dans la clause HAVING dans votre premier exemple, qui est en effet généralisé de façon +1. – Davos