2010-09-18 5 views
8

Please help me per generare la seguente query. Dì che ho tabella clienti e tabella ordini.TSQL Ricerca dell'ordine che si è verificato in 3 mesi consecutivi

Customer Table

CustID CustName 

1  AA  
2  BB 
3  CC 
4  DD 

Ordinare Tabella

OrderID OrderDate   CustID 
100  01-JAN-2000  1 
101  05-FEB-2000  1  
102  10-MAR-2000  1 
103  01-NOV-2000  2  
104  05-APR-2001  2 
105  07-MAR-2002  2 
106  01-JUL-2003  1 
107  01-SEP-2004  4 
108  01-APR-2005  4 
109  01-MAY-2006  3 
110  05-MAY-2007  1 
111  07-JUN-2007  1 
112  06-JUL-2007  1 

voglio scoprire i clienti che avete reso ordini per tre mesi consecutivi. (È consentita la query utilizzando SQL Server 2005 e 2008).

L'output desiderato è:

CustName  Year OrderDate 

    AA  2000 01-JAN-2000  
    AA  2000 05-FEB-2000 
    AA  2000 10-MAR-2000 

    AA  2007 05-MAY-2007   
    AA  2007 07-JUN-2007   
    AA  2007 06-JUL-2007   
+0

Quale output si desidera se la riga '113, 13-AUG-2007, 1' viene aggiunta alla tabella Ordine? Un blocco di output per AA con 4 righe o due blocchi di output, ciascuno contenente 3 righe? Se preferisci, è "rigorosamente tre mesi alla volta" o "tre o più mesi alla volta". –

+0

Ci scusiamo per il ritardo, preferisco esattamente tre mesi – Gopi

+0

Vuoi dire che una stringa di 4 mesi restituirebbe 6 righe, una serie con mese 1, 2, 3 e un'altra serie con mese 2, 3, 4 o semplicemente per escludere tutte le stringhe di ordini che non sono esattamente 3 mesi? – ErikE

risposta

7

Edit: sono liberato o MAX() OVER (PARTITION BY ...) come quella sembrava di uccidere le prestazioni.

;WITH cte AS ( 
SELECT CustID , 
      OrderDate, 
      DATEPART(YEAR, OrderDate)*12 + DATEPART(MONTH, OrderDate) AS YM 
FROM  Orders 
), 
cte1 AS ( 
SELECT CustID , 
      OrderDate, 
      YM, 
      YM - DENSE_RANK() OVER (PARTITION BY CustID ORDER BY YM) AS G 
FROM  cte 
), 
cte2 As 
(
SELECT CustID , 
      MIN(OrderDate) AS Mn, 
      MAX(OrderDate) AS Mx 
FROM cte1 
GROUP BY CustID, G 
HAVING MAX(YM)-MIN(YM) >=2 
) 
SELECT  c.CustName, o.OrderDate, YEAR(o.OrderDate) AS YEAR 
FROM   Customers AS c INNER JOIN 
         Orders AS o ON c.CustID = o.CustID 
INNER JOIN cte2 c2 ON c2.CustID = o.CustID and o.OrderDate between Mn and Mx 
order by c.CustName, o.OrderDate 
+1

Deve essere utilizzato DENSE_RANK, o quattro + vendite all'interno di tre mesi saranno trascurate. –

+1

Soluzione di isole raggruppate perfette ... – ErikE

+0

Martin, ho verificato la tua richiesta e non sta dando i risultati corretti ... – ErikE

1

Qui si va:

select distinct 
CustName 
,year(OrderDate) [Year] 
,OrderDate 
from 
(
select 
o2.OrderDate [prev] 
,o1.OrderDate [curr] 
,o3.OrderDate [next] 
,c.CustName 
from [order] o1 
join [order] o2 on o1.CustId = o2.CustId and datediff(mm, o2.OrderDate, o1.OrderDate) = 1 
join [order] o3 on o1.CustId = o3.CustId and o2.OrderId <> o3.OrderId and datediff(mm, o3.OrderDate, o1.OrderDate) = -1 
join Customer c on c.CustId = o1.CustId 
) t 
unpivot 
(
    OrderDate for [DateName] in ([prev], [curr], [next]) 
) 
unpvt 
order by CustName, OrderDate 
+0

Avviso: questa query è estremamente inefficiente. :) –

+0

Denis, mi dispiace segnalare che questa query non restituisce i risultati corretti quando ci sono due ordini dallo stesso cliente nello stesso giorno. – ErikE

+0

@Emtucifor, lo so! Ma non sappiamo di cosa @CSharpy abbia bisogno! :) –

4

Ecco la mia versione. Lo stavo davvero presentando come una semplice curiosità, per mostrare un altro modo di pensare al problema. Si è rivelato più utile di così perché ha funzionato meglio della soluzione "isole raggruppate" di Martin Smith. Tuttavia, una volta che si è sbarazzato di alcune funzioni di windowing aggregate eccessivamente costose e ha fatto invece aggregati reali, la sua query ha iniziato a buttare calci.

Soluzione 1: Esecuzione di 3 mesi o più, eseguita controllando 1 mese in anticipo e indietro e utilizzando un semi-join contro quello.

WITH Months AS (
    SELECT DISTINCT 
     O.CustID, 
     Grp = DateDiff(Month, '20000101', O.OrderDate) 
    FROM 
     CustOrder O 
), Anchors AS (
    SELECT 
     M.CustID, 
     Ind = M.Grp + X.Offset 
    FROM 
     Months M 
     CROSS JOIN (
     SELECT -1 UNION ALL SELECT 0 UNION ALL SELECT 1 
    ) X (Offset) 
    GROUP BY 
     M.CustID, 
     M.Grp + X.Offset 
    HAVING 
     Count(*) = 3 
) 
SELECT 
    C.CustName, 
    [Year] = Year(OrderDate), 
    O.OrderDate 
FROM 
    Cust C 
    INNER JOIN CustOrder O ON C.CustID = O.CustID 
WHERE 
    EXISTS (
     SELECT 1 
     FROM 
     Anchors A 
     WHERE 
     O.CustID = A.CustID 
     AND O.OrderDate >= DateAdd(Month, A.Ind, '19991201') 
     AND O.OrderDate < DateAdd(Month, A.Ind, '20000301') 
    ) 
ORDER BY 
    C.CustName, 
    OrderDate; 

Soluzione 2: esatti modelli a 3 mesi. Se è una corsa di 4 o più mesi, i valori sono esclusi. Questo viene fatto controllando 2 mesi prima e due mesi indietro (essenzialmente cercando il modello N, Y, Y, Y, N).

WITH Months AS (
    SELECT DISTINCT 
     O.CustID, 
     Grp = DateDiff(Month, '20000101', O.OrderDate) 
    FROM 
     CustOrder O 
), Anchors AS (
    SELECT 
     M.CustID, 
     Ind = M.Grp + X.Offset 
    FROM 
     Months M 
     CROSS JOIN (
     SELECT -2 UNION ALL SELECT -1 UNION ALL SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 
    ) X (Offset) 
    GROUP BY 
     M.CustID, 
     M.Grp + X.Offset 
    HAVING 
     Count(*) = 3 
     AND Min(X.Offset) = -1 
     AND Max(X.Offset) = 1 
) 
SELECT 
    C.CustName, 
    [Year] = Year(OrderDate), 
    O.OrderDate 
FROM 
    Cust C 
    INNER JOIN CustOrder O ON C.CustID = O.CustID 
    INNER JOIN Anchors A 
     ON O.CustID = A.CustID 
     AND O.OrderDate >= DateAdd(Month, A.Ind, '19991201') 
     AND O.OrderDate < DateAdd(Month, A.Ind, '20000301') 
ORDER BY 
    C.CustName, 
    OrderDate; 

Ecco il mio script di tabella di carico se qualcun altro vuole giocare:

IF Object_ID('CustOrder', 'U') IS NOT NULL DROP TABLE CustOrder 
IF Object_ID('Cust', 'U') IS NOT NULL DROP TABLE Cust 
GO 
SET NOCOUNT ON 
CREATE TABLE Cust (
    CustID int identity(1,1) NOT NULL PRIMARY KEY CLUSTERED, 
    CustName varchar(100) UNIQUE 
) 

CREATE TABLE CustOrder (
    OrderID int identity(100, 1) NOT NULL PRIMARY KEY CLUSTERED, 
    CustID int NOT NULL FOREIGN KEY REFERENCES Cust (CustID), 
    OrderDate smalldatetime NOT NULL 
) 

DECLARE @i int 
SET @i = 1000 
WHILE @i > 0 BEGIN 
    WITH N AS (
     SELECT 
     Nm = 
      Char(Abs(Checksum(NewID())) % 26 + 65) 
      + Char(Abs(Checksum(NewID())) % 26 + 97) 
      + Char(Abs(Checksum(NewID())) % 26 + 97) 
      + Char(Abs(Checksum(NewID())) % 26 + 97) 
      + Char(Abs(Checksum(NewID())) % 26 + 97) 
      + Char(Abs(Checksum(NewID())) % 26 + 97) 
    ) 
    INSERT Cust 
    SELECT N.Nm 
    FROM N 
    WHERE NOT EXISTS (
     SELECT 1 
     FROM Cust C 
     WHERE 
     N.Nm = C.CustName 
    ) 

    SET @i = @i - @@RowCount 
END 
WHILE @i < 50000 BEGIN 
    INSERT CustOrder 
    SELECT TOP (50000 - @i) 
     Abs(Checksum(NewID())) % 1000 + 1, 
     DateAdd(Day, Abs(Checksum(NewID())) % 10000, '19900101') 
    FROM master.dbo.spt_values 
    SET @i = @i + @@RowCount 
END 

prestazioni

Qui ci sono alcuni risultati di test delle prestazioni per i 3 mesi-or-più query :

Query  CPU Reads Duration 
Martin 1 2297 299412 2348 
Martin 2 625 285 809 
Denis  3641 401 3855 
Erik  1855 94727 2077 

Questa è solo una corsa di ciascuno, ma i numeri sono abbastanza rappresentativi. Si scopre che la tua domanda non è stata così cattiva, dopotutto. La query di Martin supera le altre mani, ma all'inizio ha usato alcune strategie di funzioni di windowing eccessivamente costose che ha risolto.

Naturalmente, come ho notato, la query di Denis non sta tirando le righe a destra quando un cliente ha due ordini nello stesso giorno, quindi la sua query è fuori dal conflitto a meno che non lo sia.

Inoltre, diversi indici potrebbero probabilmente scuotere le cose. Non lo so.

+0

Non farmi aggiungere altri due join alla mia soluzione, è già tridimensionale. : P –

+0

È necessario aggiornare il grafico delle prestazioni! –

+1

Fatto. Ho lasciato le statistiche sulla versione precedente solo per mostrare che non tutte le operazioni con le funzioni delle finestre sono così grandi. Utilizzati indiscriminatamente possono danneggiare le prestazioni. – ErikE

0

Ecco la mia versione.

select 100 as OrderID,convert(datetime,'01-JAN-2000') OrderDate, 1 as CustID into #tmp union 
    select 101,convert(datetime,'05-FEB-2000'),  1 union 
    select 102,convert(datetime,'10-MAR-2000'),  1 union 
    select 103,convert(datetime,'01-NOV-2000'),  2 union 
    select 104,convert(datetime,'05-APR-2001'),  2 union 
    select 105,convert(datetime,'07-MAR-2002'),  2 union 
    select 106,convert(datetime,'01-JUL-2003'),  1 union 
    select 107,convert(datetime,'01-SEP-2004'),  4 union 
    select 108,convert(datetime,'01-APR-2005'),  4 union 
    select 109,convert(datetime,'01-MAY-2006'),  3 union 
    select 110,convert(datetime,'05-MAY-2007'),  1 union 
    select 111,convert(datetime,'07-JUN-2007'),  1 union 
    select 112,convert(datetime,'06-JUL-2007'),  1 


    ;with cte as 
    (
     select 
      * 
      ,convert(int,convert(char(6),orderdate,112)) - dense_rank() over(partition by custid order by orderdate) as g 
     from #tmp 
    ), 
    cte2 as 
    (
    select 
     CustID 
     ,g 
    from cte a 
    group by CustID, g 
    having count(g)>=3 
    ) 
    select 
     a.CustID 
     ,Yr=Year(OrderDate) 
     ,OrderDate 
    from cte2 a join cte b 
     on a.CustID=b.CustID and a.g=b.g 
Problemi correlati