Чтобы ответить на ваш второй вопрос первым, да, лучший способ в порядке, потому что используемый вами запрос сложен для понимания, сложен в обслуживании, и даже если производительность сейчас приемлема, стыдно запрашивать Одна и та же таблица несколько раз, когда вам не нужно прибавлять в производительности, не всегда может быть приемлемой, если ваше приложение когда-либо достигнет заметного размера.
Чтобы ответить на ваш первый вопрос, у меня есть несколько способов для вас. Они предполагают 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, который всегда обновлял бы их. Тем не менее, это денормализация и, возможно, нет необходимости или хорошая идея, если вы не масштабировали настолько ужасно, что получение результатов становится недопустимо долгим.