2012-04-11 10 views
8

con los siguientes datosDatos de grupo por el cambio de la agrupación de valor de la columna con el fin

create table #ph (product int, [date] date, price int) 
insert into #ph select 1, '20120101', 1 
insert into #ph select 1, '20120102', 1 
insert into #ph select 1, '20120103', 1 
insert into #ph select 1, '20120104', 1 
insert into #ph select 1, '20120105', 2 
insert into #ph select 1, '20120106', 2 
insert into #ph select 1, '20120107', 2 
insert into #ph select 1, '20120108', 2 
insert into #ph select 1, '20120109', 1 
insert into #ph select 1, '20120110', 1 
insert into #ph select 1, '20120111', 1 
insert into #ph select 1, '20120112', 1 

me gustaría para producir el siguiente resultado:

product | date_from | date_to | price 
    1  | 20120101 | 20120105 | 1 
    1  | 20120105 | 20120109 | 2 
    1  | 20120109 | 20120112 | 1 

Si el grupo I por precio y mostrar la max y la fecha mínima, entonces recibiré lo siguiente, que no es lo que quiero (consulte el exceso de fechas).

product | date_from | date_to | price 
    1  | 20120101 | 20120112 | 1 
    1  | 20120105 | 20120108 | 2 

Así que, esencialmente lo que estoy buscando que hacer es grupo por el cambio de ritmo en los datos basados ​​en columnas grupo de productos y precios.

¿Cuál es la forma más limpia de lograr esto?

+4

Esta es una instancia de lo que se conoce como el problema 'Gaps and Islands', fyi. – AakashM

+0

@AakashM echará un vistazo a eso, traté de buscar pero no tenía una definición tan clara para el problema. Gracias – MrEdmundo

+0

np. Tener una 'tabla de números' (en este caso, una 'tabla de fechas') ayudará inmensamente. – AakashM

Respuesta

18

Hay una técnica (más o menos) conocida de resolver este tipo de problemas, que implica dos ROW_NUMBER() llamadas, así:

WITH marked AS (
    SELECT 
    *, 
    grp = ROW_NUMBER() OVER (PARTITION BY product  ORDER BY date) 
     - ROW_NUMBER() OVER (PARTITION BY product, price ORDER BY date) 
    FROM #ph 
) 
SELECT 
    product, 
    date_from = MIN(date), 
    date_to = MAX(date), 
    price 
FROM marked 
GROUP BY 
    product, 
    price, 
    grp 
ORDER BY 
    product, 
    MIN(date) 

Salida:

product date_from date_to  price 
------- ---------- ------------- ----- 
1  2012-01-01 2012-01-04  1  
1  2012-01-05 2012-01-08  2  
1  2012-01-09 2012-01-12  1  
+0

Gracias, acabo de echar un vistazo a lo que terminé implementando y es lo mismo, pero lo hice en dos CTE diferentes, no se me ocurrió utilizar la resta en uno. Gracias. – MrEdmundo

0

Una solución que he llegado con la que es relativamente "limpia" es:

;with cte_sort (product, [date], price, [row]) 
as 
    (select product, [date], price, row_number() over(partition by product order by [date] asc) as row 
    from #ph) 

select a.product, a.[date] as date_from, c.[date] as date_to, a.price 
from cte_sort a 
left outer join cte_sort b on a.product = b.product and (a.row+1) = b.row and a.price = b.price 
outer apply (select top 1 [date] from cte_sort z where z.product = a.product and z.row > a.row order by z.row) c 
where b.row is null 
order by a.[date] 

he usado un CTE con row_number porque entonces no tiene que preocuparse acerca de si cualquier fecha faltan si se utiliza funciones como dateadd. Obviamente, solo necesita la aplicación externa si desea tener la columna date_to (que yo).

Esta solución resuelve mi problema, sin embargo, tengo un pequeño problema para lograr que funcione tan rápido como me gustaría en mi mesa de 5 millones de filas.

2

Soy nuevo en este foro así que espero que mi contribución sea útil.

Si realmente no desea utilizar un CTE (aunque creo que ese es probablemente el mejor enfoque) puede obtener una solución utilizando un código basado en el conjunto. ¡Deberá probar el rendimiento de este código !.

He añadido una tabla de temperatura adicional para que pueda usar un identificador único para cada registro, pero sospecho que ya tendrá esta columna en su tabla fuente. Así que aquí está la mesa temporal.

If Exists (SELECT Name FROM tempdb.sys.tables WHERE name LIKE '#phwithId%') 
     DROP TABLE #phwithId  

    CREATE TABLE #phwithId 
    (
     SaleId INT 
     , ProductID INT 
     , Price Money 
     , SaleDate Date 
    ) 
    INSERT INTO #phwithId SELECT row_number() over(partition by product order by [date] asc) as SalesId, Product, Price, Date FROM ph 

Ahora el cuerpo principal de la instrucción Select

SELECT 
     productId 
     , date_from 
     , date_to 
     , Price 
    FROM 
     ( 
      SELECT 
       dfr.ProductId 
       , ROW_NUMBER() OVER (PARTITION BY ProductId ORDER BY ChangeDate) AS rowno1   
       , ChangeDate AS date_from 
       , dfr.Price 
      FROM 
       (  
        SELECT 
         sl1.ProductId AS ProductId 
         , sl1.SaleDate AS ChangeDate 
         , sl1.price 
        FROM 
         #phwithId sl1 
        LEFT JOIN 
         #phwithId sl2 
         ON sl1.SaleId = sl2.SaleId + 1 
        WHERE 
         sl1.Price <> sl2.Price OR sl2.Price IS NULL 
       ) dfr 
     ) da1 
    LEFT JOIN 
     ( 
      SELECT 
       ROW_NUMBER() OVER (PARTITION BY ProductId ORDER BY ChangeDate) AS rowno2 
       , ChangeDate AS date_to  
      FROM 
       ( 
        SELECT 
         sl1.ProductId 
         , sl1.SaleDate AS ChangeDate 
        FROM 
         #phwithId sl1 
        LEFT JOIN 
         #phwithId sl3 
         ON sl1.SaleId = sl3.SaleId - 1 
        WHERE 
         sl1.Price <> sl3.Price OR sl3.Price IS NULL   
       ) dto 

     ) da2 
     ON da1.rowno1 = da2.rowno2 

mediante unión de los compensado por 1 registro fuente de datos (+ o) que puede identificar cuando los precios cambian los cubos y luego es sólo una cuestión de obtener las fechas de inicio y finalización para los depósitos en un solo registro.

Todo un poco incómodo y no estoy seguro de que vaya a dar un mejor rendimiento, pero disfruté el desafío.

-1
Create function [dbo].[AF_TableColumns](@table_name nvarchar(55)) 
returns nvarchar(4000) as 
begin 
declare @str nvarchar(4000) 
    select @str = cast(rtrim(ltrim(column_name)) as nvarchar(500)) + coalesce('   ' + @str , '   ') 
    from information_schema.columns 
    where table_name = @table_name 
    group by table_name, column_name, ordinal_position 
    order by ordinal_position DESC 
return @str 
end 

--select dbo.AF_TableColumns('YourTable') Select * from YourTable 
Cuestiones relacionadas