2009-12-16 5 views
3

J'ai vu cette question sur meta: https://meta.stackexchange.com/questions/33101/how-does-so-query-commentsComment interrogez-vous le style de stackoverflow des commentaires?

Je voulais mettre les pendules à l'heure et poser la question d'une manière appropriée.

Dire que j'ai 2 tables:

 
Posts 
id 
content 
parent_id   (null for questions, question_id for answer) 

Comments 
id 
body 
is_deleted 
post_id 
upvotes 
date 

Remarque: Je pense que c'est la façon dont le schéma est pour SO configuration, les réponses ont une parent_id qui est la question, les questions ont là nulle. Les questions et réponses sont stockées dans la même table.

Comment puis-je extraire le style stackoverflow des commentaires d'une manière très efficace avec un minimum d'allers-retours?

Les règles:

  1. Une requête unique devrait se retirer tous les commentaires nécessaires à une page avec plusieurs messages pour rendre
  2. besoins seulement retirer 5 commentaires par réponse, avec pref pour upvotes
  3. Doit fournir suffisamment d'informations pour informer l'utilisateur il y a plus de commentaires au-delà des 5 qui sont là. (et le nombre réel - par exemple 2 autres commentaires)
  4. Le tri est vraiment poilu pour les commentaires, comme vous pouvez le voir sur les commentaires dans cette question. Les règles sont, afficher les commentaires par date, CEPENDANT si un commentaire a un upvote c'est d'obtenir un traitement préférentiel et être affiché aussi bien en bas de la liste. (Ceci est méchant difficile à exprimer en sql)

Si des dénormalisations rendent les choses meilleures, quelles sont-elles? Quels index sont critiques?

+0

@Mark: SO est configuré de sorte qu'une question et une réponse existent dans la même table. –

+0

SO a des questions, des réponses et des commentaires. Quels sont les «messages»? Sont-ils des questions? Réponses? Tous les deux? Comment savoir quels messages appartiennent à quelle question? –

+0

@OMG Poneys, OK Je ne le savais pas. –

Répondre

1

Utilisation:

WITH post_hierarchy AS (
    SELECT p.id, 
     p.content, 
     p.parent_id, 
     1 AS post_level 
    FROM POSTS p 
    WHERE p.parent_id IS NULL 
    UNION ALL 
    SELECT p.id, 
     p.content, 
     p.parent_id, 
     ph.post_level + 1 AS post_level 
    FROM POSTS p 
    JOIN post_hierarchy ph ON ph.id = p.parent_id) 
SELECT ph.id, 
     ph.post_level, 
     c.upvotes, 
     c.body 
    FROM COMMENTS c 
    JOIN post_hierarchy ph ON ph.id = c.post_id 
ORDER BY ph.post_level, c.date 

Couple de choses à connaître:

  1. StackOverflow affiche les 5 premiers commentaires, n'a pas d'importance si elles étaient upvoted ou non. Les commentaires ultérieurs qui ont été upvoted sont immédiatement affichés
  2. Vous ne pouvez pas accepter une limite de 5 commentaires par publication sans avoir affecté un SELECT à chaque publication. Ajout TOP 5 à ce que je n'AFFICHÉES retourner les cinq premières lignes en fonction de la ORDER BY
4

Je ne viendrais pas à filtrer les commentaires en utilisant SQL (qui peut vous surprendre parce que je suis un défenseur SQL). Il suffit de les récupérer triés par CommentId et de les filtrer dans le code de l'application.

En fait, il est assez rare qu'il y ait plus de cinq commentaires pour un post donné, de sorte qu'ils doivent être filtrés. Dans le vidage de données de StackOverflow en octobre, 78% des messages ont zéro ou un commentaire, et 97% ont cinq commentaires ou moins. Seuls 20 articles ont> = 50 commentaires, et seulement deux messages ont plus de 100 commentaires. Écrire un SQL complexe pour effectuer ce type de filtrage augmenterait donc la complexité lors de l'interrogation de tous les messages. Je suis tout à fait pour l'utilisation de SQL intelligent lorsque cela est approprié, mais ce serait penny-sage et livre-fou.

Vous pouvez le faire de cette façon:

SELECT q.PostId, a.PostId, c.CommentId 
FROM Posts q 
LEFT OUTER JOIN Posts a 
    ON (a.ParentId = q.PostId) 
LEFT OUTER JOIN Comments c 
    ON (c.PostId IN (q.PostId, a.PostId)) 
WHERE q.PostId = 1234 
ORDER BY q.PostId, a.PostId, c.CommentId; 

Mais cela vous donne des copies redondantes de q et a colonnes, ce qui est important parce que ces colonnes comprennent blobs de texte. Le coût supplémentaire de la copie du texte redondant du SGBDR vers l'application devient important.

Il est donc probablement préférable de pas le faire dans deux requêtes. Au lieu de cela, étant donné que le client consulte une question avec postID = 1234, procédez comme suit:

SELECT c.PostId, c.Text 
FROM Comments c 
JOIN (SELECT 1234 AS PostId UNION ALL 
    SELECT a.PostId FROM Posts a WHERE a.ParentId = 1234) p 
    ON (c.PostId = p.PostId); 

Et puis les trier dans le code d'application, la collecte par le poste référencé et le filtrage des commentaires supplémentaires au-delà des cinq plus intéressants par poste.


J'ai testé ces deux requêtes sur une base de données MySQL 5.1 chargé avec les données StackOverflow déversent d'Octobre. La première requête prend environ 50 secondes. La deuxième requête est quasiment instantanée (après avoir précalculé les index pour les tables Posts et Comments). En résumé, insister pour extraire toutes les données dont vous avez besoin en utilisant une seule requête SQL est une exigence artificielle (probablement basée sur l'idée fausse que l'aller-retour d'une requête sur un SGBDR est un surcoût qui doit être minimisé à tout prix). Souvent, une seule requête est une solution efficace moins. Essayez-vous d'écrire tout le code de votre application dans une seule fonction? :-)

+0

Je suis d'accord avec vos sentiments ici, mon implémentation serait en fait une légère optimisation, je voudrais stocker comment_count dans la table des messages. côté client, sortez tous les posts pour le rendu, puis passez à un select * à partir des commentaires où post_id dans (id1, id2, id3) - pour tous les posts avec plus de 0 commentaires) cela rend les choses ultra simples et très efficace pour le cas général –

1

La vraie question n'est pas la requête, mais le schéma, spécialement les index clusterisés. Les exigences d'ordonnancement des commentaires sont ambigues telles que vous les avez définies (est-ce seulement 5 par réponse ou non?). J'ai interprété les exigences comme «tirer 5 commentaires par message (répondre ou question) et donner la préférence aux mises à jour, puis aux plus récentes. Je sais que ce n'est pas la façon dont les commentaires sont visibles, mais vous devez définir vos exigences de manière plus précise.

Voici ma question:

declare @postId int; 
set @postId = ?; 

with cteQuestionAndReponses as (
    select post_id 
    from Posts 
    where post_id = @postId 
    union all 
    select post_id 
    from Posts 
    where parent_id = @postId) 
select * from 
cteQuestionAndReponses p 
outer apply (
    select count(*) as CommentsCount 
    from Comments c 
    where is_deleted = 0 
    and c.post_id = p.post_id) as cc 
outer apply (
    select top(5) * 
    from Comments c 
    where is_deleted = 0 
    and p.post_id = c.post_id 
    order by upvotes desc, date desc 
) as c 

J'ai quelques 14k messages et 67K commentaires dans mes tableaux de test, la requête obtient les messages dans 7 ms:

Table 'Comments'. Scan count 12, logical reads 50, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table 'Posts'. Scan count 1, logical reads 5, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 

SQL Server Execution Times: 
    CPU time = 0 ms, elapsed time = 7 ms. 

Voici le schéma I testé avec:

create table Posts (
post_id int identity (1,1) not null 
, content varchar(max) not null 
, parent_id int null -- (null for questions, question_id for answer) 
, constraint fkPostsParent_id 
    foreign key (parent_id) 
    references Posts(post_id) 
, constraint pkPostsId primary key nonclustered (post_id) 
); 
create clustered index cdxPosts on 
    Posts(parent_id, post_id); 
go 

create table Comments (
comment_id int identity(1,1) not null 
, body varchar(max) not null 
, is_deleted bit not null default 0 
, post_id int not null 
, upvotes int not null default 0 
, date datetime not null default getutcdate() 
, constraint pkComments primary key nonclustered (comment_id) 
, constraint fkCommentsPostId 
    foreign key (post_id) 
    references Posts(post_id) 
); 
create clustered index cdxComments on 
    Comments (is_deleted, post_id, upvotes, date, comment_id); 
go 

et voici mon test de génération de données:

insert into Posts (content) 
select 'Lorem Ipsum' 
from master..spt_values; 

insert into Posts (content, parent_id) 
select 'Ipsum Lorem', post_id 
from Posts p 
cross apply (
    select top(checksum(newid(), p.post_id) % 10) Number 
    from master..spt_values) as r 
where parent_id is NULL 

insert into Comments (body, is_deleted, post_id, upvotes, date) 
select 'Sit Amet' 
    -- 5% deleted comments 
    , case when abs(checksum(newid(), p.post_id, r.Number)) % 100 > 95 then 1 else 0 end 
    , p.post_id 
    -- up to 10 upvotes 
    , abs(checksum(newid(), p.post_id, r.Number)) % 10 
    -- up to 1 year old posts 
    , dateadd(minute, -abs(checksum(newid(), p.post_id, r.Number) % 525600), getutcdate()) 
from Posts p 
cross apply (
    select top(abs(checksum(newid(), p.post_id)) % 10) Number 
    from master..spt_values) as r 
Questions connexes