2010-09-18 14 views
8

Ayúdenme a generar la siguiente consulta. Supongamos que tengo una tabla de clientes y una tabla de pedidos.TSQL Orden de búsqueda que ocurrió en 3 meses consecutivos

Tabla cliente

CustID CustName 

1  AA  
2  BB 
3  CC 
4  DD 

orden de la tabla

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 

Quiero averiguar los clientes que han hecho pedidos en tres meses sucesivos. (Se permite la consulta mediante SQL Server 2005 y 2008).

La salida deseada es:

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

¿Qué resultado desea si la fila '113, 13-AUG-2007, 1' se agrega a la tabla de pedidos? ¿Un bloque de salida para AA con 4 filas, o dos bloques de salida, cada uno con 3 filas? Si lo prefiere, ¿es 'estrictamente tres meses a la vez' o 'tres o más meses a la vez'? –

+0

Disculpe la demora, prefiero exactamente tres meses – Gopi

+0

¿Quiere decir que una cadena de 4 meses devolvería 6 filas, una con el mes 1, 2, 3 y otra con el mes 2, 3, 4 o simplemente excluirla? todas las cadenas de pedidos que no son exactamente 3 meses? – ErikE

Respuesta

7

Editar: deshicieron o la MAX() OVER (PARTITION BY ...) ya que parecía matar el rendimiento.

;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

Necesita usar DENSE_RANK, o se pasarán por alto cuatro o más ventas dentro de tres meses. –

+1

Solución de islas agrupadas perfectas ... – ErikE

+0

Martin, probé su consulta y no está dando los resultados correctos ... – ErikE

1

Aquí van:

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

Advertencia: esta consulta es extremadamente ineficiente. :) –

+0

Denis, lamento informar que esta consulta no arroja los resultados correctos cuando hay dos pedidos del mismo cliente en el mismo día. – ErikE

+0

@Emtucifor, lo sé! ¡Pero no sabemos qué necesita @CSharpy! :) –

4

Aquí está mi versión. Realmente estaba presentando esto como una mera curiosidad, para mostrar otra forma de pensar sobre el problema. Resultó ser más útil que eso porque funcionó mejor que incluso la solución genial de "islas agrupadas" de Martin Smith. Sin embargo, una vez que se deshizo de algunas funciones de ventana agregadas demasiado costosas e hizo agregados reales en su lugar, su consulta comenzó a patear a tope.

Solución 1: Ejecuciones de 3 meses o más, hecho verificando 1 mes adelante y atrás y usando una semi-unión contra eso.

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; 

Solución 2: Exactas patrones de 3 meses. Si es una ejecución de 4 meses o más, los valores están excluidos. Esto se hace verificando 2 meses adelante y dos meses atrás (esencialmente buscando el patrón 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; 

Aquí está mi script de tablas de carga si alguien más quiere jugar:

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 

Rendimiento

Aquí están algunos resultados de pruebas de rendimiento para los 3 meses o más-consultas :

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

Esto es solo una carrera de cada uno, pero los números son bastante representativos. Al fin y al cabo, tu consulta no tuvo tan buen rendimiento, Denis. La consulta de Martin supera a los demás, pero al principio estaba usando algunas estrategias de funciones de ventana demasiado costosas que él arregló.

Por supuesto, como noté, la consulta de Denis no está tirando de las filas correctas cuando un cliente tiene dos pedidos en el mismo día, por lo que su consulta está fuera de discusión a menos que lo arregle.

Además, los diferentes índices podrían agitar las cosas. No lo sé.

+0

No me hagas agregar dos uniones más a mi solución, ya es tridimensional. : P –

+0

¡Necesita actualizar su tabla de rendimiento! –

+1

Hecho. Deje las estadísticas en su versión anterior solo para mostrar que no todas las funciones de ventana son tan buenas. Usados ​​indiscriminadamente pueden dañar el rendimiento. – ErikE

0

Aquí está mi opinión.

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 
Cuestiones relacionadas