Как рассчитывать sql на дополнительную таблицу внутри хранимой процедуры sql? - PullRequest
3 голосов
/ 19 июля 2009

У меня есть сохраненный процесс (называемый sprocGetArticles), который возвращает список статей из таблицы статей. Этот сохраненный процесс не имеет параметров.

Пользователи могут оставлять комментарии к каждой статье, и я сохраняю эти комментарии в таблице комментариев, связанной с идентификатором статьи.

Можно ли как-нибудь подсчитать количество комментариев для каждого articleid в возвращенном списке из хранимой процедуры sprocGetArticles, поэтому мне нужно сделать только один вызов в базу данных?

Моя проблема в том, что мне нужен идентификатор статьи для подсчета, который я не могу объявить.

Это лучший подход в любом случае?

Ответы [ 7 ]

5 голосов
/ 19 июля 2009

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

SELECT a.*, (
  SELECT COUNT(*)
  FROM Comments c
  WHERE c.article_id = a.article_id) AS CountComments
  FROM Articles a;

Обратите внимание, что подсчет комментариев каждый раз может быть довольно дорогим, лучше сохранить счет как свойство Article.

2 голосов
/ 19 июля 2009

Может быть, я что-то упустил, но что со всеми подзапросами и встроенными представлениями? Почему бы просто не сделать прямое левое соединение, например ::100100

  SELECT a.ArticleId
       , a.ArticleName
       , (other a columns)
       , COUNT(*)
    FROM Articles a
         LEFT JOIN Comments c
                ON c.ArticleId = a.ArticleId
GROUP BY a.ArticleId
       , a.ArticleName
       , (other a columns);
2 голосов
/ 19 июля 2009

Ну, не зная, что вы выбираете и свою общую схему (и предполагая, что вы хотя бы используете SQL Server 2005:

WITH CommentCounts AS
(
   SELECT COUNT(*) CommentCount, ac.ArticleID
   FROM Articles a
   INNER JOIN ArticleComments ac
      ON ac.ArticleID = a.ID
   GROUP BY ac.ArticleID
)

SELECT a.*,
       c.CommentCount
FROM Articles a
INNER JOIN CommentCounts c
   ON a.ID = c.ArticleID

Это общее табличное выражение или CTE. Подробнее о них можно прочитать здесь: http://msdn.microsoft.com/en-us/library/ms190766.aspx

1 голос
/ 19 июля 2009

Один вариант, о котором никто еще не упомянул, - это вычисляемый столбец в вашей таблице статей, который будет подсчитывать количество комментариев. В целом это намного быстрее, чем вычисление количества комментариев каждый раз, и если вам действительно нужно часто запрашивать это число, это может сэкономить вам много времени на обработку!

В SQL Server 2005 и более поздних версиях в этом случае вы могли бы создать небольшую хранимую функцию для подсчета количества комментариев для каждой статьи, а затем добавить ее в качестве вычисляемого столбца в таблицу статей. Затем вы можете использовать это как обычный столбец и поверить мне - это намного быстрее, чем использовать подзапросы все время!

CREATE FUNCTION dbo.CountComments(@ArticleID INT)
RETURNS INT 
WITH SCHEMABINDING
AS BEGIN
    DECLARE @ArticleCommentCount INT

    SELECT @ArticleCommentCount = COUNT(*)
    FROM dbo.ArticleComments
    WHERE ArticleID = @ArticleID

    RETURN @ArticleCommentCount
END
GO

Добавьте это в таблицу статей в виде столбца:

ALTER TABLE dbo.Articles
    ADD CommentCount AS dbo.CountComments(ArticleID)

и с тех пор просто используйте его как обычный столбец:

SELECT ArticleID, ArticleTitle, ArticlePostDate, CommentCount 
FROM dbo.Articles

Чтобы сделать его еще быстрее, вы можете добавить этот столбец как постоянный столбец к своей таблице, и тогда он будет действительно потрясающим! : -)

ALTER TABLE dbo.Articles
    ADD CommentCount AS dbo.CountComments(ArticleID) PERSISTED

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

Очень рекомендую! Эта функция часто пропускается в SQL Server.

Марк

1 голос
/ 19 июля 2009

Следующее будет работать на SQL Server 2005+ или Oracle 9i +:

WITH COMMENT_COUNT AS (
      SELECT ac.article_id
             COUNT(ac.*) 'numComments'
        FROM ARTICLE_COMMENTS ac
    GROUP BY ac.article_id)
SELECT t.description,
       cc.numComments
  FROM ARTICLES t
  JOIN COMMENT_COUNT cc ON cc.article_id = t.article_id

SQL Server называет это выражением Common Table (CTE); Oracle называет это факторингом подзапроса.

Альтернатива:

SELECT t.description,
       cc.numComments
  FROM ARTICLES t
  JOIN (SELECT ac.article_id
               COUNT(ac.*) 'numComments'
          FROM ARTICLE_COMMENTS ac
      GROUP BY ac.article_id) cc ON cc.article_id = t.article_id

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

0 голосов
/ 21 июля 2009

Стив - я выполнил все упражнение с вашей настройкой на моем локальном компьютере (настольный ПК, без сервера), и я провел сравнение выборок несколько раз - сначала выбор с помощью функции сначала, затем другой сначала, затем просто один из них, чтобы получить числа только для этого выбора, другой другой.

 SELECT h.keycol
       , h.name1
       , COUNT(*)
    FROM smb_header h
       , smb_detail d 
   WHERE h.keycol between 5000 and 10000
     AND h.keycol = d.keycol 
GROUP BY h.keycol, h.name1

 SELECT h.keycol
       , h.name1
       , CommentCount
    FROM smb_header h
   WHERE h.keycol between 5000 and 10000

Это сводится к следующему: я получаю 25% для выбора с функцией, 75% для выбора с объединением. Тот, у кого эта функция, в 3 раза быстрее.

output of actual SQL Server 2008 execution plan

У меня есть стандартная рабочая станция Dell Desktop, Vista Business x64 с пакетом обновления 1, 4 ГБ ОЗУ, SQL Server 2008 Developer Edition.

GUESS: Я не знаю достаточно о внутренностях SQL Server, чтобы действительно знал это, но как насчет этой мысли: когда у вас есть вычисляемый столбец, как в этом случае SQL Server должен фактически выйти и посчитать количество дочерних записей. Что делать, если SQL Server будет кэшировать эти результаты и использовать их повторно, если один и тот же «ключ» подсчитывается снова и снова и снова. Вместо того, чтобы действительно выходить и подсчитывать их снова (как это, вероятно, потребуется в случае использования JOIN или коррелированного подзапроса), SQL-сервер может избавиться от подсчета того же набора дочерних записей x-раз и вместо этого просто вернуть обратно кешированный счет. Это звучит выполнимо / разумно ??

Марк

0 голосов
/ 21 июля 2009

Относительно использования вычисляемых столбцов, упомянутых в ответе, я хотел подтвердить утверждения о том, что использование вычисляемого столбца приведет к повышению производительности (для меня это не имело смысла, но я не гуру SQL Server). Результаты, которые я получил, показали, что использование вычисляемого столбца действительно медленнее - намного медленнее, чем простая группировка или подзапрос. Я провел тест на экземпляре SQL Server, установленном на моем собственном ПК - вот методология и результаты:

CREATE TABLE smb_header (keycol INTEGER NOT NULL
                        , name1 VARCHAR2(255)
                        , name2 VARCHAR2(255));

INSERT INTO smb_header
  VALUES (1
        , 'This is column 1'
        , 'This is column 2'
         );

INSERT INTO smb_header
   SELECT (SELECT MAX(keycol)
             FROM smb_header
          ) + keycol
        , name1
        , name2
     FROM smb_header;
REM (repeat 20 times to generate ~1 million rows)

ALTER TABLE smb_header ADD PRIMARY KEY (keycol);

CREATE TABLE smb_detail (keycol INTEGER
                        , commentno INTEGER
                        , commenttext VARCHAR2(255));

INSERT INTO smb_detail
   SELECT keycol
        , 1
        , 'A comment that describes this issue'
     FROM smb_header;

ALTER TABLE smb_detail ADD PRIMARY KEY (keycol, commentno);

ALTER TABLE smb_detail ADD FOREIGN KEY (keycol) 
                           REFERENCES smb_header (keycol);

INSERT INTO smb_detail
   SELECT keycol
        , (SELECT MAX(commentno)
             FROM smb_detail sd2
            WHERE sd2.keycol = sd1.keycol
          ) + commentno
        , 'A comment that follows comment number ' 
          + CAST(sd1.commentno AS VARCHAR(32))
     FROM smb_detail sd1
    WHERE MOD(keycol, 31) = 0;

REM repeat 5 times, to create some records that have 64 comments
REM where others have one.

На данный момент в заголовке будет около 1 миллиона строк и по 1 или 64 комментария для каждого.

Теперь я создаю функцию (такую ​​же, как ваша выше, только с именами моих столбцов и таблиц) и вычисляемый столбец:

alter table dbo.smb_header add CommentCountPersist as dbo.CountComments(keycol)

Кстати, PERSISTED не будет работать для этого столбца, как я и подозревал в моих комментариях выше - для SQL Server невозможно или слишком сложно отслеживать, какие строки нужно обновить, если вы ссылаетесь на другие таблицы в своей функции , Использование ключевого слова PERSISTED приводит к ошибке:

Msg 4934, Level 16, State 3, Line 1
Computed column 'CommentCountPersist' in table 'smb_header' cannot be 
persisted because the column does user or system data access.

Это имеет смысл для меня - я не понимаю, как SQL Server мог определить, какие строки необходимо обновить при изменении других строк, для любой функции, которая может быть реализована, без ужасно неэффективного процесса обновления.

Теперь для тестов. Я создаю временную таблицу #holder для вставки строк - я хочу убедиться, что при выполнении моих запросов я обрабатываю весь набор результатов, а не только первые несколько строк, которые появятся в сеточном элементе управления Mgmt Studio.

  SELECT h.keycol
       , h.name1
       , CommentCount
    INTO #holder
    FROM smb_header h
   WHERE h.keycol < 0

Вот результаты моих запросов. Сначала вычисляемый столбец:

  INSERT
    INTO #holder
  SELECT h.keycol
       , h.name1
       , CommentCount
    FROM smb_header h
   WHERE h.keycol between 5000 and 10000

SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.
Table 'Worktable'. Scan count 1, logical reads 10160, physical reads 0, 
                   read-ahead  reads 0, lob logical reads 0, 
                   lob physical reads 0, lob read-ahead reads 0.
Table 'smb_header'. Scan count 1, logical reads 44, 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 = 265 ms,  elapsed time = 458 ms.

(5001 row(s) affected)
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.

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

Теперь версия GROUP BY, вычисляемый столбец:

  INSERT
    INTO #holder
  SELECT h.keycol
       , h.name1
       , COUNT(*)
    FROM smb_header h
       , smb_detail d 
   WHERE h.keycol between 5000 and 10000
     AND h.keycol = d.keycol 
GROUP BY h.keycol, h.name1

SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.
Table 'smb_header'. Scan count 1, logical reads 44, physical reads 0, 
                    read-ahead reads 0, lob logical reads 0, 
                    lob physical reads 0, lob read-ahead reads 0.
Table 'smb_detail'. Scan count 1, logical reads 366, 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 = 15 ms,  elapsed time = 13 ms.

(5001 row(s) affected)
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.

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

Запись запроса с подзапросом в предложении SELECT, как Remus делал выше, дает тот же план и производительность, что и GROUP BY (что и следовало ожидать).

Как видите, вычисляемый столбец работает с версией значительно хуже. Это имеет смысл для меня, так как оптимизатор вынужден вызывать функцию и выполнять подсчет (*) для каждой строки в заголовке вместо использования более сложных методов разрешения двух наборов данных.

Возможно, я здесь что-то не так делаю. Мне было бы интересно, чтобы marc_s внес свой вклад в его выводы.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...