Как вы запрашиваете комментарии в стиле stackoverflow? - PullRequest
3 голосов
/ 17 декабря 2009

Я видел этот вопрос на мета: https://meta.stackexchange.com/questions/33101/how-does-so-query-comments

Я хотел установить порядок и задать вопрос надлежащим техническим способом.

Скажем, у меня есть 2 таблицы:

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

Comments
 id 
 body 
 is_deleted
 post_id 
 upvotes 
 date 

Примечание : Я думаю, что так настроена схема для SO, ответы имеют parent_id, который является вопросом, вопросы там имеют нулевое значение. Вопросы и ответы хранятся в одной таблице.

Как мне вытащить стиль стека в виде комментария очень эффективным способом с минимальными циклами?

Правила:

  1. Один запрос должен вытащить все комментарии, необходимые для отображения страницы с несколькими сообщениями
  2. Необходимо получить только 5 комментариев за ответ, с префом для голосов
  3. Требуется предоставить достаточно информации, чтобы сообщить пользователю, что есть еще комментарии, кроме 5, которые есть. (и фактическое количество - например, еще 2 комментария)
  4. Сортировка комментариев очень сложна, как вы можете видеть в комментариях к этому вопросу. Правила состоят в том, чтобы отображать комментарии по дате, ОДНАКО , если у комментария есть повышенное голосование, это должно быть льготным режимом и отображаться в нижней части списка. (это неприятно трудно выразить в sql)

Если какие-то денормализации делают вещи лучше, что они? Какие показатели являются критическими?

Ответы [ 3 ]

4 голосов
/ 17 декабря 2009

Я бы не стал фильтровать комментарии с помощью SQL (что может вас удивить, потому что я сторонник SQL). Просто извлеките их все, отсортированные по CommentId, и отфильтруйте их в коде приложения.

На самом деле довольно редко встречается более пяти комментариев к данному сообщению, поэтому их необходимо отфильтровать. В октябрьском дампе данных StackOverflow 78% постов содержат ноль или один комментарий, а 97% имеют пять или меньше комментариев. Только 20 сообщений имеют> = 50 комментариев, и только два сообщения имеют более 100 комментариев.

Таким образом, написание сложного SQL для такой фильтрации увеличит сложность при запросе всех сообщений. Я за то, чтобы использовать умный SQL, когда это уместно, но это было бы глупо и глупо.

Вы могли бы сделать это так:

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;

Но это дает вам избыточные копии столбцов q и a, что важно, поскольку эти столбцы содержат текстовые двоичные объекты. Дополнительные затраты на копирование избыточного текста из РСУБД в приложение становятся значительными.

Так что, вероятно, лучше , а не сделать это в двух запросах. Вместо этого, учитывая, что клиент просматривает Вопрос с PostId = 1234, сделайте следующее:

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);

А затем сортировать их в коде приложения, собирать их по ссылкам и отфильтровывать дополнительные комментарии помимо пяти самых интересных для каждой записи.


Я проверил эти два запроса к базе данных MySQL 5.1, загруженной с дампом данных StackOverflow с октября. Первый запрос занимает около 50 секунд. Второй запрос почти мгновенный (после того, как я предварительно кэшировал индексы для таблиц Posts и Comments).

Суть в том, что настаивать на извлечении всех данных, которые вам нужны, используя один SQL-запрос, является искусственным требованием (вероятно, основанным на неправильном представлении о том, что двусторонняя обработка запроса к СУБД - это издержки, которые должны быть сведены к минимуму при любой ценой). Часто один запрос является менее эффективным решением. Вы пытаетесь написать весь код своего приложения в одной функции? : -)

1 голос
/ 17 декабря 2009

реальный вопрос не в запросе, а в схеме, особенно в кластеризованных индексах. Требования к порядку комментариев неоднозначны, как вы их определили (только 5 за ответ или нет?). Я интерпретировал требования как «вытащить 5 комментариев на пост (ответ или вопрос») и отдать предпочтение тем, кто проголосовал выше, а затем новым. Я знаю, что это не так, как комментарии SO, но вы должны более точно определить ваши требования.

Вот мой запрос:

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

В моих тестовых таблицах около 14 тыс. Сообщений и 67 тыс. Комментариев, запрос получает сообщения в 7 мс:

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.

Вот схема, с которой я тестировал:

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

и вот генерация моих тестовых данных:

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
1 голос
/ 17 декабря 2009

Использование:

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

Несколько вещей, о которых нужно знать:

  1. StackOverflow отображает первые 5 комментариев, независимо от того, были ли они проголосованы или нет. Последующие комментарии, за которые проголосовали, немедленно отображаются
  2. Вы не можете разместить до 5 комментариев на пост, не выделяя SELECT для каждого поста. Добавление TOP 5 к тому, что я написал, вернет только первые пять строк на основе оператора ORDER BY
...