Найти строку, связанную с Min / Max, без внутреннего цикла - PullRequest
11 голосов
/ 31 июля 2009

У меня есть вопрос, связанный с T-SQL и SQL Server.

Допустим, у меня есть таблица заказов с 2 столбцами:

  • ProductId int
  • CustomerId int
  • Дата, время, время

Мне нужна дата первого заказа для каждого продукта, поэтому я выполняю этот тип запроса:

SELECT ProductId, MIN(Date) AS FirstOrder 
FROM Orders
GROUP BY ProductId

У меня есть индекс на ProductId, включая столбцы CustomerId и Date для ускорения запроса (IX_Orders). План запроса выглядит как сканирование некластеризованного индекса в IX_Orders, за которым следует агрегат потока (без сортировки благодаря индексу).

Теперь моя проблема в том, что я также хочу получить CustomerId, связанный с первым заказом для каждого продукта (продукт 26 был впервые заказан во вторник 25 клиентом 12). Сложность в том, что я не хочу никакого внутреннего цикла в плане выполнения, потому что это будет означать дополнительное чтение для ProductId в таблице, что крайне неэффективно.

Это должно быть возможно только при том же сканировании некластеризованного индекса, за которым следуют агрегаты потоков, однако я не могу найти запрос, который бы это сделал. Есть идеи?

Спасибо

Ответы [ 6 ]

3 голосов
/ 31 июля 2009

это будет обрабатывать товары с одинаковыми датами:

DECLARE @Orders table (ProductId int
                      ,CustomerId int
                      ,Date datetime
                      )

INSERT INTO @Orders VALUES (1,1,'20090701')
INSERT INTO @Orders VALUES (2,1,'20090703')
INSERT INTO @Orders VALUES (3,1,'20090702')
INSERT INTO @Orders VALUES (1,2,'20090704')
INSERT INTO @Orders VALUES (4,2,'20090701')
INSERT INTO @Orders VALUES (1,3,'20090706')
INSERT INTO @Orders VALUES (2,3,'20090704')
INSERT INTO @Orders VALUES (4,3,'20090702')
INSERT INTO @Orders VALUES (5,5,'20090703')  --duplicate dates for product #5
INSERT INTO @Orders VALUES (5,1,'20090703')  --duplicate dates for product #5
INSERT INTO @Orders VALUES (5,5,'20090703')  --duplicate dates for product #5

;WITH MinOrders AS
(SELECT
     o.ProductId, o.CustomerId, o.Date
         ,row_number() over(partition by o.ProductId order by o.ProductId,o.CustomerId) AS RankValue
     FROM @Orders o
     INNER JOIN (SELECT
                     ProductId
                         ,MIN(Date) MinDate 
                     FROM @Orders 
                     GROUP BY ProductId
                ) dt ON o.ProductId=dt.ProductId AND o.Date=dt.MinDate
 )
SELECT
    m.ProductId, m.CustomerId, m.Date
    FROM MinOrders  m
    WHERE m.RankValue=1
    ORDER BY m.ProductId, m.CustomerId

это вернет те же результаты, просто используйте тот же объявление и вставку, что и код выше:

;WITH MinOrders AS
(SELECT
     o.ProductId, o.CustomerId, o.Date
         ,row_number() over(partition by o.ProductId order by o.ProductId,o.CustomerId) AS RankValue
     FROM @Orders o
 )
SELECT
    m.ProductId, m.CustomerId, m.Date
    FROM MinOrders  m
    WHERE m.RankValue=1
    ORDER BY m.ProductId, m.CustomerId

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

2 голосов
/ 31 июля 2009
declare @Orders table (
    ProductId int,
    CustomerId int,
    Date datetime
)

insert into @Orders values (1,1,'20090701')
insert into @Orders values (2,1,'20090703')
insert into @Orders values (3,1,'20090702')
insert into @Orders values (1,2,'20090704')
insert into @Orders values (4,2,'20090701')
insert into @Orders values (1,3,'20090706')
insert into @Orders values (2,3,'20090704')
insert into @Orders values (4,3,'20090702')
insert into @Orders values (5,5,'20090703')

select O.* from @Orders O inner join 
(
    select ProductId,
    MIN(Date) MinDate 
    from @Orders 
    group by ProductId
) FO
on FO.ProductId = O.ProductId and FO.MinDate = O.Date

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

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

В SQL Server 2005+:

SELECT  oo.*
FROM    (
        SELECT  DISTINCT ProductId
        FROM    Orders
        ) od
CROSS APPLY
        (
        SELECT  TOP 1 ProductID, Date, CustomerID
        FROM    Orders oi
        WHERE   oi.ProductID = od.ProductID
        ORDER BY
                Date DESC
        ) oo

Номинально план запроса содержит Nested Loops.

Однако внешний цикл будет использовать Index Scan с Stream Aggregate, а внутренний цикл будет содержать Index Seek для ProductID с Top.

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

Вот результат теста для 1,000,000 строк (с 100 DISTINCT ProductID s):

SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 1 ms.

(строк обработано: 100)
Table 'Orders'. Scan count 103, logical reads 6020, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, 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 = 234 ms,  elapsed time = 125 ms.

, хотя это результат простого SELECT DISTINCT запроса:

SELECT  od.*
FROM    (
        SELECT  DISTINCT ProductId
        FROM    Orders
        ) od

И статистика:

SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 1 ms.

(строк обработано: 100)
Table 'Orders'. Scan count 3, logical reads 5648, 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 = 250 ms,  elapsed time = 125 ms.

Как мы видим, производительность такая же, и CROSS APPLY занимает всего 400 дополнительно logical reads (что, скорее всего, никогда не будет physical).

Не понимаю, как можно улучшить этот запрос.

Также преимущество этого запроса в том, что он хорошо распараллеливается. Вы можете заметить, что CPU время вдвое больше elapsed time: это связано с распараллеливанием на моем старом Core Duo.

A 4-core CPU выполнит этот запрос за половину этого времени.

Решения, использующие оконные функции, не распараллеливаются:

SELECT  od.*
FROM    (
        SELECT  ProductId, Date, CustomerID, ROW_NUMBER() OVER (PARTITION BY ProductID ORDER BY Date DESC) AS rn
        FROM    Orders
        ) od
WHERE   rn = 1

, а вот статистика:

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

(строк обработано: 100)
Table 'Orders'. Scan count 1, logical reads 5123, 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 = 406 ms,  elapsed time = 415 ms.
0 голосов
/ 31 июля 2009

Я не вижу способа сделать это красиво, не выполнив подзапрос или оконную функцию (такую ​​как row_number, rank), так как max выглядит только в одном столбце.

Однако вы можете сделать это не очень хорошо.

SELECT
    productid, 
    min(date), 
cast(
    substring( 
        min(convert(varchar(23),date,21) + cast(customerid as varchar(20)))
              , 24, 44)
    as int) customerid
from 
    orders
group by
    productid 

Это работает, только если ваш идентификатор клиента содержит менее 20 цифр.

EDIT: добавлена ​​группа по предложению

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

IX_Orders сортируются по ProductId, затем CutomerId, затем Date или это ProductId, затем Date, затем CustomerId? Если это первое, измените его на второе.

Другими словами, не используйте это:

create index IX_Orders on Orders (ProductId, CustomerId, Date) 

Используйте это вместо:

create index IX_Orders on Orders (ProductId, Date, CustomerId)

Тогда, если вы сделаете:

SELECT o1.* 
FROM [Order] o1
JOIN
    (
        SELECT ProductID, Min(Date) as Date
        FROM [Order]
        GROUP BY ProductID
    ) o2
    ON o1.ProductID = o2.ProductID AND o1.Date = o2.Date
ORDER BY ProductID

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

WITH cte AS
(
    SELECT ProductID, CustomerID, Date, 
        ROW_NUMBER() OVER(PARTITION BY ProductID ORDER BY Date ASC) AS row
    FROM [Order]
)
SELECT ProductID, CustomerId, Date
FROM cte
WHERE row = 1
ORDER BY ProductID
0 голосов
/ 31 июля 2009
SELECT
    o1.productid, 
    o1.date, 
    o1.customerid
FROM
    Orders o1
JOIN
    (select productid, min(date) as orderDate
     from Orders
     group by productid
    ) firstOrder
ON o1.productid = firstOrder.productid

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

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