Эффективно выбрать верхний ряд для каждой категории в наборе - PullRequest
2 голосов
/ 04 июня 2010

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

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

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 строки. Для первого запроса три раза выбирается одна строка.

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

Ответы [ 3 ]

2 голосов
/ 04 июня 2010

не только посмотрите на план выполнения, но и посмотрите 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. Таблица "# 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

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

ТАКЖЕ когда вы добавляете заказ к обоим запросам, он падает до 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
1 голос
/ 04 июня 2010

Я загрузил 100 000 строк (чего было недостаточно для замедления), попробовал старомодный способ:

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 голосов
/ 07 июня 2010

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

...