2010-06-04 3 views
2

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

Например, создадим таблицу, в которой хранятся записи о температуре в нескольких местах.

CREATE TABLE #t (
    placeId int, 
    ts datetime, 
    temp int, 
    PRIMARY KEY (ts, placeId) 
) 

-- insert some sample data 

SET NOCOUNT ON 

DECLARE @n int, @ts datetime 
SELECT @n = 1000, @ts = '2000-01-01' 

WHILE (@n>0) BEGIN 
    INSERT INTO #t VALUES (@n % 10, @ts, @n % 37) 
    IF (@n % 10 = 0) SET @ts = DATEADD(hour, 1, @ts) 
    SET @n = @n - 1 
END 

Теперь мне нужно, чтобы получить последнюю запись для каждого из мест 1, 2, 3.

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

SELECT * FROM (
    SELECT TOP 1 placeId, temp 
    FROM #t 
    WHERE placeId = 1 
    ORDER BY ts DESC 
) t1 
UNION ALL 
SELECT * FROM (
    SELECT TOP 1 placeId, temp 
    FROM #t 
    WHERE placeId = 2 
    ORDER BY ts DESC 
) t2 
UNION ALL 
SELECT * FROM (
    SELECT TOP 1 placeId, temp 
    FROM #t 
    WHERE placeId = 3 
    ORDER BY ts DESC 
) t3 

Следующие функции выглядят лучше, но работают намного менее эффективно (30% против 70% в соответствии с оптимизатором).

SELECT placeId, ts, temp FROM (
    SELECT placeId, ts, temp, ROW_NUMBER() OVER (PARTITION BY placeId ORDER BY ts DESC) rownum 
    FROM #t 
    WHERE placeId IN (1, 2, 3) 
) t 
WHERE rownum = 1 

Проблема заключается в том, во втором плане выполнения запроса, кластерный индекс сканирование выполняется на #t и 300 строк извлекаются, отсортированные, пронумерованные, а затем фильтровали, в результате чего только 3 строки. Для первого запроса три раза выбирается одна строка.

Есть ли способ эффективно выполнить запрос без большого количества профсоюзов?

+0

+1 для включения образца кода –

ответ

1

Я нагруженные 100000 строк (которые до сих пор не было достаточно, чтобы замедлить работу), попробовал старомодный способ:

select t.* 
from #t t 
    inner join (select placeId, max(ts) ts 
       from #t 
       where placeId in (1,2,3) 
       group by placeId) xx 
    on xx.placeId = t.placeId 
    and xx.ts = t.ts 

и получил те же результаты.

Затем я изменил порядок следования столбцов в индексе, чтобы

CREATE TABLE #t ( 
    placeId int, 
    ts datetime, 
    temp int, 
    PRIMARY KEY (placeId, ts) 
) 

и во всех запросах, получил меньше страница читает и индекс стремится вместо сканирования.

Если оптимизация является вашей целью, и вы можете изменять индексы, я пересмотрел первичный ключ или, возможно, добавлю индекс покрытия.

+0

Спасибо, я как-то пропустил «старомодный путь». Он работает лучше и в моих реальных структурах данных. – VladV

2

не просто посмотреть на этот план также посмотреть на statistics io и statistics time

set statistics io on 
go 
SELECT * FROM (
    SELECT TOP 1 placeId, temp 
    FROM #t 
    WHERE placeId = 1 
    ORDER BY ts DESC 
) t1 
UNION ALL 
SELECT * FROM (
    SELECT TOP 1 placeId, temp 
    FROM #t 
    WHERE placeId = 2 
    ORDER BY ts DESC 
) t2 
UNION ALL 
SELECT * FROM (
    SELECT TOP 1 placeId, temp 
    FROM #t 
    WHERE placeId = 3 
    ORDER BY ts DESC 
) t3 

SELECT placeId, temp FROM (
    SELECT placeId, ts, temp, ROW_NUMBER() OVER (PARTITION BY placeId ORDER BY ts DESC) rownum 
    FROM #t 
    WHERE placeId IN (1, 2, 3) 
) t 
WHERE rownum = 1 

set statistics io off 
go 

Таблица '# t000000000B99. Число сканирования 3, логическое чтение 6, физическое чтение 0, чтение вперед 0, логическое считывание 0, логическое чтение 0, чтение с плавающей запятой 0, чтение 0 с плавающей запятой. Таблица '# t000000000B99'. Количество сканирования 1, логическое чтение 6, физические чтения 0, упреждающее чтение читает 0, нескладные логические читает 0, нескладные физические чтения 0, подбросить упреждающее чтение читает 0.

set statistics time on 
go 
SELECT * FROM (
    SELECT TOP 1 placeId, temp 
    FROM #t 
    WHERE placeId = 1 
    ORDER BY ts DESC 
) t1 
UNION ALL 
SELECT * FROM (
    SELECT TOP 1 placeId, temp 
    FROM #t 
    WHERE placeId = 2 
    ORDER BY ts DESC 
) t2 
UNION ALL 
SELECT * FROM (
    SELECT TOP 1 placeId, temp 
    FROM #t 
    WHERE placeId = 3 
    ORDER BY ts DESC 
) t3 

SELECT placeId, temp FROM (
    SELECT placeId, ts, temp, ROW_NUMBER() OVER (PARTITION BY placeId ORDER BY ts DESC) rownum 
    FROM #t 
    WHERE placeId IN (1, 2, 3) 
) t 
WHERE rownum = 1 

set statistics time on 
go 

Для меня нет никакой разницы между эти 2 метода, загружать больше данных и сравнение снова

Кроме того, когда вы добавляете заказ по обоим запросам она падает до 40% против 60%

SELECT * FROM (
    SELECT TOP 1 placeId, temp 
    FROM #t 
    WHERE placeId = 1 
    ORDER BY ts DESC 
) t1 
UNION ALL 
SELECT * FROM (
    SELECT TOP 1 placeId, temp 
    FROM #t 
    WHERE placeId = 2 
    ORDER BY ts DESC 
) t2 
UNION ALL 
SELECT * FROM (
    SELECT TOP 1 placeId, temp 
    FROM #t 
    WHERE placeId = 3 
    ORDER BY ts DESC 
) t3 
ORDER BY placeId 

SELECT placeId, temp FROM (
    SELECT placeId, temp, ROW_NUMBER() OVER (PARTITION BY placeId ORDER BY ts DESC) rownum 
    FROM #t 
    WHERE placeId IN (1, 2, 3) 
) t 
WHERE rownum = 1 
ORDER BY placeId 
0

Только для записи другой вариант, использующий CROSS APPLY.
В моей конфигурации он работает лучше, чем все предыдущие.

SELECT * 
FROM (VALUES (1),(2),(3)) t (placeId) 
CROSS APPLY (
    SELECT TOP 1 ts, temp 
    FROM #t 
    WHERE placeId = t.placeId 
    ORDER BY ts DESC 
) tt 

Я бы предположил, что значения VALUES могут быть отправлены на временную таблицу или переменную таблицы без особых изменений.