Эффективно извлекать разные столбцы, используя общий коррелированный подзапрос - PullRequest
3 голосов
/ 28 мая 2011

Мне нужно извлечь несколько столбцов из подзапроса, который также требует фильтра WHERE, ссылающегося на столбцы таблицы FROM. У меня есть пара вопросов по этому поводу:

  1. Есть ли другое решение этой проблемы, кроме моего ниже?
  2. Нужно ли вообще другое решение или оно достаточно эффективно?

Пример:

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

Я не могу просто использовать JOIN, потому что мне нужно сначала отфильтровать свой фактический подзапрос (обратите внимание, что я получаю ТОП-1 для «испытуемого», отсортированного по убыванию или дате)

Моя цель - избежать многократного написания (и выполнения) одного и того же подзапроса.

SELECT ExamineeID, LastName, FirstName, Email,
   (SELECT COUNT(examineeTestID)
    FROM exam.ExamineeTest tests
    WHERE E.ExamineeID = ExamineeID AND TestRevisionID = 3 AND TestID = 2) Attempts,
   (SELECT TOP 1 ExamineeTestID
    FROM exam.ExamineeTest T
    WHERE E.ExamineeID = ExamineeID AND TestRevisionID = 3 AND TestID = 2
    ORDER BY Score DESC) bestExamineeTestID,
   (SELECT TOP 1 Score
    FROM exam.ExamineeTest T
    WHERE E.ExamineeID = ExamineeID AND TestRevisionID = 3 AND TestID = 2
    ORDER BY Score DESC) bestScore,
   (SELECT TOP 1 DateDue
    FROM exam.ExamineeTest T
    WHERE E.ExamineeID = ExamineeID AND TestRevisionID = 3 AND TestID = 2
    ORDER BY Score DESC) bestDateDue,
   (SELECT TOP 1 TimeCommitted
    FROM exam.ExamineeTest T
    WHERE E.ExamineeID = ExamineeID AND TestRevisionID = 3 AND TestID = 2
    ORDER BY Score DESC) bestTimeCommitted,
   (SELECT TOP 1 ExamineeTestID
    FROM exam.ExamineeTest T
    WHERE E.ExamineeID = ExamineeID AND TestRevisionID = 3 AND TestID = 2
    ORDER BY DateDue DESC) currentExamineeTestID,
   (SELECT TOP 1 Score
    FROM exam.ExamineeTest T
    WHERE E.ExamineeID = ExamineeID AND TestRevisionID = 3 AND TestID = 2
    ORDER BY DateDue DESC) currentScore,
   (SELECT TOP 1 DateDue
    FROM exam.ExamineeTest T
    WHERE E.ExamineeID = ExamineeID AND TestRevisionID = 3 AND TestID = 2
    ORDER BY DateDue DESC) currentDateDue,
   (SELECT TOP 1 TimeCommitted
    FROM exam.ExamineeTest T
    WHERE E.ExamineeID = ExamineeID AND TestRevisionID = 3 AND TestID = 2
    ORDER BY DateDue DESC) currentTimeCommitted
FROM exam.Examinee E

Ответы [ 4 ]

9 голосов
/ 28 мая 2011

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

Чтобы ответить на ваш первый вопрос, у меня есть несколько способов для вас. Они предполагают SQL 2005 или выше, если не указано иное.

Обратите внимание, что вам не нужны BestExamineeID и CurrentExamineeID, потому что они всегда будут такими же, как ExamineeID, если только не были проведены тесты, и они имеют значение NULL, что по другим столбцам можно определить как NULL.

Вы можете рассматривать OUTER / CROSS APPLY как оператор, который позволяет перемещать коррелированные подзапросы из предложения WHERE в предложение JOIN. Они могут иметь внешнюю ссылку на ранее названную таблицу и могут возвращать более одного столбца. Это позволяет выполнять работу только один раз для каждого логического запроса, а не один раз для каждого столбца.

SELECT
   ExamineeID,
   LastName,
   FirstName,
   Email,
   B.Attempts,
   BestScore = B.Score,
   BestDateDue = B.DateDue,
   BestTimeCommitted = B.TimeCommitted,
   CurrentScore = C.Score,
   CurrentDateDue = C.DateDue,
   CurrentTimeCommitted = C.TimeCommitted
FROM
   exam.Examinee E
   OUTER APPLY ( -- change to CROSS APPLY if you only want examinees who've tested
      SELECT TOP 1
         Score, DateDue, TimeCommitted,
         Attempts = Count(*) OVER ()
      FROM exam.ExamineeTest T
      WHERE
         E.ExamineeID = T.ExamineeID
         AND T.TestRevisionID = 3
         AND T.TestID = 2
      ORDER BY Score DESC
   ) B
   OUTER APPLY ( -- change to CROSS APPLY if you only want examinees who've tested
      SELECT TOP 1
         Score, DateDue, TimeCommitted
      FROM exam.ExamineeTest T
      WHERE
         E.ExamineeID = T.ExamineeID
         AND T.TestRevisionID = 3
         AND T.TestID = 2
      ORDER BY DateDue DESC
   ) C

Вы должны поэкспериментировать, чтобы увидеть, лучше ли мой Count(*) OVER (), чем дополнительный OUTER APPLY, который просто получает счет. Если вы не ограничиваете проверяемого из таблицы exam.Examinee, может быть лучше просто выполнить обычное агрегирование в производной таблице.

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

WITH Data AS (
   SELECT
      *,
      Count(*) OVER (PARTITION BY ExamineeID) Cnt,
      Row_Number() OVER (PARTITION BY ExamineeID ORDER BY Score DESC) ScoreOrder,
      Row_Number() OVER (PARTITION BY ExamineeID ORDER BY DateDue DESC) DueOrder
   FROM
      exam.ExamineeTest
), Vals AS (
   SELECT
      ExamineeID,
      Max(Cnt) Attempts,
      Max(CASE WHEN ScoreOrder = 1 THEN Score ELSE NULL END) BestScore,
      Max(CASE WHEN ScoreOrder = 1 THEN DateDue ELSE NULL END) BestDateDue,
      Max(CASE WHEN ScoreOrder = 1 THEN TimeCommitted ELSE NULL END) BestTimeCommitted,
      Max(CASE WHEN DueOrder = 1 THEN Score ELSE NULL END) BestScore,
      Max(CASE WHEN DueOrder = 1 THEN DateDue ELSE NULL END) BestDateDue,
      Max(CASE WHEN DueOrder = 1 THEN TimeCommitted ELSE NULL END) BestTimeCommitted
   FROM Data
   GROUP BY
      ExamineeID
)
SELECT
   E.ExamineeID,
   E.LastName,
   E.FirstName,
   E.Email,
   V.Attempts,
   V.BestScore, V.BestDateDue, V.BestTimeCommitted,
   V.CurrentScore, V.CurrentDateDue, V.CurrentTimeCommitted
FROM
   exam.Examinee E
   LEFT JOIN Vals V ON E.ExamineeID = V.ExamineeID
   -- change join to INNER if you only want examinees who've tested

Наконец, вот метод SQL 2000:

SELECT
   E.ExamineeID,
   E.LastName,
   E.FirstName,
   E.Email,
   Y.Attempts,
   Y.BestScore, Y.BestDateDue, Y.BestTimeCommitted,
   Y.CurrentScore, Y.CurrentDateDue, Y.CurrentTimeCommitted
FROM
   exam.Examinee E
   LEFT JOIN ( -- change to inner if you only want examinees who've tested
      SELECT
         X.ExamineeID,
         X.Cnt Attempts,
         Max(CASE Y.Which WHEN 1 THEN T.Score ELSE NULL END) BestScore,
         Max(CASE Y.Which WHEN 1 THEN T.DateDue ELSE NULL END) BestDateDue,
         Max(CASE Y.Which WHEN 1 THEN T.TimeCommitted ELSE NULL END) BestTimeCommitted,
         Max(CASE Y.Which WHEN 2 THEN T.Score ELSE NULL END) CurrentScore,
         Max(CASE Y.Which WHEN 2 THEN T.DateDue ELSE NULL END) CurrentDateDue,
         Max(CASE Y.Which WHEN 2 THEN T.TimeCommitted ELSE NULL END) CurrentTimeCommitted
      FROM
         (
            SELECT ExamineeID, Max(Score) MaxScore, Max(DueDate) MaxDueDate, Count(*) Cnt
            FROM exam.ExamineeTest
            WHERE
               TestRevisionID = 3
               AND TestID = 2
            GROUP BY ExamineeID
         ) X
         CROSS JOIN (SELECT 1 UNION ALL SELECT 2) Y (Which)
         INNER JOIN exam.ExamineeTest T
            ON X.ExamineeID = T.ExamineeID
            AND (
               (Y.Which = 1 AND X.MaxScore = T.MaxScore)
               OR (Y.Which = 2 AND X.MaxDueDate = T.MaxDueDate)
            )
      WHERE
         T.TestRevisionID = 3
         AND T.TestID = 2
      GROUP BY
         X.ExamineeID,
         X.Cnt
   ) Y ON E.ExamineeID = Y.ExamineeID

Этот запрос вернет неожиданные дополнительные строки, если комбинация (ExamineeID, Score) или (ExamineeID, DueDate) может вернуть несколько строк. Это вероятно не маловероятно с Оценка. Если ни один из них не является уникальным, вам нужно использовать (или добавить) некоторый дополнительный столбец, который может предоставить уникальность, чтобы он мог использоваться для выбора одной строки. Если только дубликат может быть дублирован, то дополнительный предварительный запрос, который сначала получает максимальный балл, а затем согласование с максимальным сроком исполнения будет объединяться, чтобы получить самый последний балл, который был равен максимальному, в то же время, что и самый последний. данные. Дайте мне знать, если вам нужна дополнительная помощь по SQL 2000.

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

  • Индекс + вы тянете только нескольких испытуемых с большим количеством тестов каждый = CROSS APPLY выигрывает.
  • Индекс + вы проводите огромное количество экзаменов, каждый из которых состоит из нескольких тестов = ROW_NUMBER () выигрывает.
  • Нет индекса = конкатенация строк / метод упаковки значений выигрывает (здесь не показано).

Группировка по решению, которое я дал для SQL 2000, вероятно, будет работать хуже, но не гарантируется. Как я уже сказал, тестирование в порядке.

Если какой-либо из моих запросов вызовет проблемы с производительностью, дайте мне знать, и я посмотрю, что я могу сделать, чтобы помочь. Я уверен, что у меня, вероятно, есть опечатки, так как я не использовал DDL для воссоздания ваших таблиц, но я старался изо всех сил, не пытаясь это сделать.

Если производительность действительно становится критической, я бы создал таблицы ExamineeTestBest и ExamineeTestCurrent, на которые нажимал бы триггер таблицы ExamineeTest, который всегда обновлял бы их. Тем не менее, это денормализация и, возможно, нет необходимости или хорошая идея, если вы не масштабировали настолько ужасно, что получение результатов становится недопустимо долгим.

4 голосов
/ 28 мая 2011

Это не тот же подзапрос.Это три разных подзапроса.

  • count() на всех
  • TOP (1) ORDER BY Score DESC
  • TOP (1) ORDER BY DateDue DESC

Вы не можетеизбегайте выполнения его менее 3 раз.
Вопрос в том, как заставить его исполняться не более 3 раз.


Один из вариантов - написать 3 встроенных табличных функций и используйте их с внешним применением .Убедитесь, что они действительно встроены, иначе ваша производительность упадет в сто раз.Одной из этих трех функций может быть:

create function dbo.topexaminee_byscore(@ExamineeID int)
returns table
as
return (
  SELECT top (1)
    ExamineeTestID as bestExamineeTestID,
    Score as bestScore,
    DateDue as bestDateDue,
    TimeCommitted as bestTimeCommitted
  FROM exam.ExamineeTest
  WHERE (ExamineeID = @ExamineeID) AND (TestRevisionID = 3) AND (TestID = 2)
  ORDER BY Score DESC
)

Другой вариант - сделать то же самое, но с подзапросами.В любом случае, поскольку вы выбираете данные для всех учащихся, разница в производительности не должна быть слишком большой.Создайте три подзапроса, например:

select bestExamineeTestID, bestScore, bestDateDue, bestTimeCommitted
from (
  SELECT
    ExamineeTestID as bestExamineeTestID,
    Score as bestScore,
    DateDue as bestDateDue,
    TimeCommitted as bestTimeCommitted,
    row_number() over (partition by ExamineeID order by Score DESC) as takeme
  FROM exam.ExamineeTest
  WHERE (TestRevisionID = 3) AND (TestID = 2)
) as foo
where foo.takeme = 1

То же самое для ORDER BY DateDue DESC и для всех записей, с соответствующими столбцами: select ed.

Объедините эти три в exameid.

То, что будет лучше / более производительным / более читабельным, зависит от вас.Проведите некоторое тестирование.

1 голос
/ 28 мая 2011

Похоже, вы можете заменить три столбца, которые основаны на псевдониме "bestTest", с представлением.Все три из этих подзапросов имеют одно и то же предложение WHERE и одно и то же предложение ORDER BY.

То же самое для подзапроса с псевдонимом "bestNewTest".То же самое для подзапроса с псевдонимом currentTeest.

Если бы я считал правильно, это заменило бы 8 подзапросов с 3 представлениями.Вы можете присоединиться к представлениям.Я думаю, что соединения будут быстрее, но на вашем месте я бы проверил план выполнения обеих версий.

0 голосов
/ 28 мая 2011

Вы можете использовать CTE и OUTER APPLY.

;WITH testScores AS
(
    SELECT ExamineeID, ExamineeTestID, Score, DateDue, TimeCommitted
    FROM exam.ExamineeTest
    WHERE TestRevisionID = 3 AND TestID = 2
)
SELECT ExamineeID, LastName, FirstName, Email, total.Attempts,
       bestTest.*, currentTest.*
FROM exam.Examinee
LEFT OUTER JOIN
(
    SELECT ExamineeID, COUNT(ExamineeTestID) AS Attempts
    FROM testScores
    GROUP BY ExamineeID
) AS total ON exam.Examinee.ExamineeID = total.ExamineeID
OUTER APPLY
(
    SELECT TOP 1 ExamineeTestID, Score, DateDue, TimeCommitted
    FROM testScores
    WHERE exam.Examinee.ExamineeID = t.ExamineeID
    ORDER BY Score DESC
) AS bestTest (bestExamineeTestID, bestScore, bestDateDue, bestTimeCommitted)
OUTER APPLY
(
    SELECT TOP 1 ExamineeTestID, Score, DateDue, TimeCommitted
    FROM testScores
    WHERE exam.Examinee.ExamineeID = t.ExamineeID
    ORDER BY DateDue DESC
) AS currentTest (currentExamineeTestID, currentScore, currentDateDue, 
                  currentTimeCommitted)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...