2010-09-18 8 views
8

Aidez-moi à générer la requête suivante. Dites que j'ai une table client et une table de commande.Ordre de recherche TSQL survenu pendant 3 mois consécutifs

Tableau client

CustID CustName 

1  AA  
2  BB 
3  CC 
4  DD 

Commander Tableau

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 

Je veux savoir les clients qui ont des commandes sur trois mois consécutifs. (La requête utilisant le serveur SQL 2005 et 2008 est autorisée).

La sortie désirée est:

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

Quelle sortie voulez-vous si la ligne '113, 13-AUG-2007, 1' est ajoutée à la table des commandes? Un bloc de sortie pour AA avec 4 lignes, ou deux blocs de sortie, chacun contenant 3 lignes? Si vous préférez, est-ce «strictement trois mois à la fois» ou «trois mois ou plus à la fois»? –

+0

Désolé pour le délai, je préfère exactement trois mois – Gopi

+0

Voulez-vous dire qu'une chaîne de 4 mois retournerait 6 lignes, un ensemble avec le mois 1, 2, 3 et un autre ensemble avec le mois 2, 3, 4, ou simplement pour exclure toutes les séries de commandes qui ne sont pas exactement 3 mois? – ErikE

Répondre

7

Edit: ou se sont débarrassés du MAX() OVER (PARTITION BY ...) comme cela semblait tuer la performance.

;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

Les besoins d'utilisation de DENSE_RANK, ou quatre + ventes dans un délai de trois mois, seront négligés. –

+1

Solution parfaite d'îlots groupés ... – ErikE

+0

Martin, j'ai testé votre requête et elle ne donne pas les bons résultats ... – ErikE

1

Ici, vous allez:

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

Avertissement: Cette requête est extrêmement inefficace. :) –

+0

Denis, je suis désolé de signaler que cette requête ne renvoie pas les résultats corrects lorsqu'il y a deux commandes par le même client le même jour. – ErikE

+0

@Emtucifor, je sais! Mais nous ne savons pas de quoi @CSharpy a besoin! :) –

4

Voici ma version. Je présentais vraiment cela comme une simple curiosité, pour montrer une autre façon de penser au problème. Il s'est avéré plus utile que cela parce qu'il a obtenu de meilleurs résultats que la solution «groupée» de Martin Smith. Cependant, une fois qu'il s'est débarrassé de certaines fonctions de fenêtrage agrégées trop coûteuses et a fait de vrais agrégats à la place, sa requête a commencé à donner des coups de pied.

Solution 1: Essais de 3 mois ou plus, effectués en vérifiant 1 mois en avant et en arrière et en utilisant une demi-jointure contre cela.

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; 

Solution 2: Exact modèles 3 mois. S'il s'agit d'un cycle de 4 mois ou plus, les valeurs sont exclues. Ceci est fait en vérifiant 2 mois d'avance et deux mois de retard (essentiellement en recherchant le modèle 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; 

Voici mon scénario table de chargement si quelqu'un d'autre veut jouer:

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 

Performance

Voici quelques résultats des tests de performance pour les plus ou 3 mois requêtes :

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

Ceci est seulement une course de chacun, mais les chiffres sont assez représentatifs. Il s'avère que votre requête n'était pas si mal performante, Denis, après tout. La requête de Martin bat les autres, mais au début, il utilisait des stratégies de fenêtrage trop chères. Bien sûr, comme je l'ai noté, la requête de Denis ne tire pas les bonnes lignes lorsqu'un client a deux commandes le même jour, donc sa requête est hors de conflit à moins qu'il ne soit corrigé.

De plus, différents index pourraient éventuellement faire bouger les choses. Je ne sais pas.

+0

Ne me fais pas ajouter deux autres jointures à ma solution, c'est déjà tridimensionnel. : P –

+0

Vous devez mettre à jour votre tableau de performance! –

+1

Terminé. J'ai laissé les statistiques sur votre ancienne version juste pour montrer que toutes les opérations de fenêtrage ne sont pas si bonnes. Utilisés indifféremment, ils peuvent nuire aux performances. – ErikE

0

Voici ma prise.

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 
Questions connexes