7

I have seen posts which would fetch you pivot results but not unpivot, Need to know if there is any clean way to achieve ? If not the any workaround would do as well ?

Execute this to see unpivot results in Management Studio

CREATE TABLE [dbo].[Payment](
    [PaymentId] [int] NOT NULL,
    [EmployeeId] [int] NOT NULL,
    [RegularHours] [decimal](18, 0) NULL,
    [OvertimeHOurs] [decimal](18, 0) NULL
) ON [PRIMARY]

go

insert into payment values (1, 1, 40,  10)
insert into payment values (1, 2, 20,  0)

go

select * from payment 

select * from payment unpivot ([hours] for [paytype] in ([RegularHours], [OvertimeHOurs]))a

The output for first Select statement

PaymentId   EmployeeId  RegularHours                            OvertimeHOurs
----------- ----------- --------------------------------------- 
1           1           40                                      10
1           2           20                                      0

(2 row(s) affected)

The output for second Select statement & this is what i am looking for

PaymentId   EmployeeId  hours                                   paytype
----------- ----------- ----------------------------------------------------- 
1           1           40                                      RegularHours
1           1           10                                      OvertimeHOurs
1           2           20                                      RegularHours
1           2           0                                       OvertimeHOurs

(4 row(s) affected)
abatishchev
  • 98,240
  • 88
  • 296
  • 433
Pravin Pawar
  • 2,559
  • 3
  • 34
  • 40
  • link to show pivot results is [here](http://stackoverflow.com/questions/167304/is-it-possible-to-pivot-data-using-linq) – Pravin Pawar Apr 17 '12 at 09:48
  • The op is looking how to **unpivot** – TheRealTy Apr 17 '12 at 09:51
  • The commnet is added just to show how to pivot results in L2S , I am interested in getting the _unpivot_ results that too in L2S. The question description shows the way to achieve it in T-SQL – Pravin Pawar Apr 17 '12 at 09:56
  • lol, sorry didn't even realize it was your own comment. Thought someone completely mis-read your subject. – TheRealTy Apr 17 '12 at 10:03
  • Sorry, I do not understand what exactly should be the output. Should all the hours of all payments of each employee get summed up? Like: `{1 => {RegularHours => 60; OvertimeHours => 10}}`? – Michael Schnerring Apr 17 '12 at 10:07
  • Sorry, I should it be the other way round? Should all the hours of all payment types for each employee get summed up? – Michael Schnerring Apr 17 '12 at 10:15

3 Answers3

8

Ok, I cant see a way you can do it where it is translated into SQL, below is what I have come up with but this is all performed managed code.

Or... you can simply create a view in SQL.

var payments = Payments.Select (p => new {
                            OvertimeHOurs = new {
                                    p.PaymentId,
                                    p.EmployeeId,
                                    Hours = p.OvertimeHOurs,
                                    PayType = "OvertimeHOurs"
                                    },
                            RegularHours = new {
                                    p.PaymentId,
                                    p.EmployeeId,
                                    Hours = p.RegularHours,
                                    PayType = "RegularHours"
                                    }
                            }
                );
var result = payments.Select(a => a.OvertimeHOurs).Union(payments.Select (p => p.RegularHours));
result.Dump(); // LINQPad Method

SQL Generated is

-- Region Parameters
DECLARE @p0 NVarChar(1000) = 'OvertimeHOurs'
DECLARE @p1 NVarChar(1000) = 'RegularHours'
-- EndRegion
SELECT [t4].[PaymentId], [t4].[EmployeeId], [t4].[OvertimeHOurs] AS [Hours], [t4].[value] AS [PayType]
FROM (
    SELECT [t1].[PaymentId], [t1].[EmployeeId], [t1].[OvertimeHOurs], [t1].[value]
    FROM (
        SELECT [t0].[PaymentId], [t0].[EmployeeId], [t0].[OvertimeHOurs], @p0 AS [value]
        FROM [payment] AS [t0]
        ) AS [t1]
    UNION
    SELECT [t3].[PaymentId], [t3].[EmployeeId], [t3].[RegularHours], [t3].[value]
    FROM (
        SELECT [t2].[PaymentId], [t2].[EmployeeId], [t2].[RegularHours], @p1 AS [value]
        FROM [payment] AS [t2]
        ) AS [t3]
    ) AS [t4]
TheRealTy
  • 2,409
  • 3
  • 22
  • 32
  • Upvote, Wow this does fetch what i am expecting. But how can we make this generic if system expects different paytypes to be added dynamically. – Pravin Pawar Apr 17 '12 at 10:34
  • Not really, if you were to add more types you would also need to update your Model, and in turn update code. – TheRealTy Apr 17 '12 at 10:45
  • Definitely, even for UNPIVOT clause in T-SQL i need to add an entry for Paytypes (if at all i want to make it generic)..food for thought-- if we can make it generic to support 500 Paytypes. :) – Pravin Pawar Apr 17 '12 at 11:02
  • 1
    I would consider a view then too. Look here (is for pivot) but it shows how to dynamically generate the columns, may be transferable to unpivot too. http://stackoverflow.com/questions/10168566/sql-server-pivot-function – TheRealTy Apr 17 '12 at 11:05
  • View would be a good option too, Need not recompile the managed code – Pravin Pawar Apr 17 '12 at 11:09
2
var result = new List<UnpivotedDbRecord>();
Payments.ForEach(r =>
                    {
                        result.Add(new UnpivotedDbRecord
                                        {
                                            EmployeeId = r.EmployeeId,
                                            PaymentId = r.PaymentId,
                                            PaymentType = "Regular",
                                            Hours = r.RegularHours
                                        });
                        result.Add(new UnpivotedDbRecord
                                        {
                                            EmployeeId = r.EmployeeId,
                                            PaymentId = r.PaymentId,
                                            PaymentType = "Overtime",
                                            Hours = r.OvertimeHours
                                        });
                    });
Michael Schnerring
  • 3,584
  • 4
  • 23
  • 53
  • Well, how would a received record look like? The table layout in your question isn't dynamic, yet, because you have a regular and overtime column. Or better: what exactly do you mean with "generic"? – Michael Schnerring Apr 17 '12 at 10:41
  • 1
    `System.Data.Linq.Table` does not contain the Method `ForEach()` you would need to call `ToList()` first which would generate a poor performing SQL statement such as `SELECT [t0].[PaymentId], [t0].[EmployeeId], [t0].[RegularHours], [t0].[OvertimeHOurs] FROM [payment] AS [t0] ` – TheRealTy Apr 17 '12 at 10:49
  • Thanks for this one. Great to know. :) – Michael Schnerring Apr 17 '12 at 10:56
  • With Generic, I mean to support multiple (500) paytypes – Pravin Pawar Apr 17 '12 at 11:07
2

With 500 paytypes you will need to use reflection to extract the fields you want to unpivot, which can be slow, but effective if you aren't handling large amounts of data. Because of this, you won't be able have the unpivot operation translate to SQL, you will have to do it in LINQ to Objects after bringing over the pivoted data. (Theoretically you could write a code generator to create the query but I am not sure how well SQL translation would handle the 500 columns, or how well the database engine would handle a 500 element union.)

So here is the type for the answer - the ShallowCopy method is used to avoid setting the pivot (flat) data for each paytype:

public class UnpivotData {
    public int PaymentId { get; set; }
    public int EmployeeId { get; set; }
    public string PayType { get; set; }
    public decimal Hours { get; set; }

    public UnpivotData ShallowCopy() => (UnpivotData)this.MemberwiseClone();
}

Now the extension method to unpivot your data is straightforward - extract the pivot data and then return one new object per paytype. This method takes an array of strings for the names of the pivot data and a lambda predicate for selecting the field names that are for paytypes.

public static class IEnumerableExt {
    public static IEnumerable<UnpivotData> Unpivot<T>(this IEnumerable<T> src, string[] pivotFieldNames, string unPivotName, string unPivotValue, Func<string, bool> unpivotFieldNameFn) {
        var srcPIs = typeof(T).GetProperties();
        var srcPivotPIs = srcPIs.Where(pi => pivotFieldNames.Contains(pi.Name));
        var srcUnpivotPIs = srcPIs.Where(pi => unpivotFieldNameFn(pi.Name)).ToList();
        var ansPIs = typeof(UnpivotData).GetProperties();
        var ansPivotPIs = ansPIs.Where(pi => pivotFieldNames.Contains(pi.Name));
        var srcAnsPivotPIs = srcPivotPIs.Zip(ansPivotPIs, (spi, api) => new { spi, api }).ToList();
        var unPivotNamePI = ansPIs.First(pi => pi.Name == unPivotName);
        var unPivotValuePI = ansPIs.First(pi => pi.Name == unPivotValue);

        foreach (var d in src) {
            var ansbase = new UnpivotData();
            foreach (var sapi in srcAnsPivotPIs)
                sapi.api.SetValue(ansbase, sapi.spi.GetValue(d));

            foreach (var spi in srcUnpivotPIs) {
                var ans = ansbase.ShallowCopy();
                unPivotNamePI.SetValue(ans, spi.Name);
                unPivotValuePI.SetValue(ans, spi.GetValue(d));

                yield return ans;
            }
        }
    }
}

Now you can use the Unpivot method to unpivot any number of paytypes:

var result = payments.AsEnumerable().Unpivot(new[] { "PaymentId", "EmployeeId" }, "PayType", "Hours", fn => fn.EndsWith("Hours"));
NetMage
  • 26,163
  • 3
  • 34
  • 55