views:

292

answers:

4

I need to keep track of different dates (dynamic). So for a specific Task you could have X number of dates to track (for example DDR1 meeting date, DDR2 meeting date, Due Date, etc).

My strategy was to create one table (DateTypeID, DateDescription) which would store the description of each date. Then I could create the main table (ID, TaskDescription, DateTypeID). So all the dates would be in one column and you could tell what that date represents by looking at the TypeID. The problem is displaying it in a grid. I know I should use a cross tab query, but i cannot get it to work. For example, I use a Case statement in SQL Server 2000 to pivot the table over so that each column name is the name of the date type. IF we have the following tables:

DateType Table

DateTypeID | DateDescription

 1           | DDR1
 2           | DDR2
 3           | DueDate


Tasks Table

ID | TaskDescription

1 | Create Design
2 | Submit Paperwork


Tasks_DateType Table

TasksID | DateTypeID | Date

1       |     1         | 09/09/2009
1       |     2         | 10/10/2009
2       |     1         | 11/11/2009
2       |     3         | 12/12/2009


THE RESULT SHOULD BE:

TaskDescription | DDr1 | DDR2 | DueDate

Create Design     |09/09/2009 | 10/10/2009 | null
Submit Paperwork  |11/11/2009 | null       | 12/12/2009

IF anyone has any idea how I can go about researching this, I appreciate it. The reason I do this instead of making a column for each date, has to do with the ability to let the user in the future add as many dates as they want without having to manually add columns to the table and editing html code. This also allows simple code for comparing dates or show upcoming tasks by their type (ex. 'Create design's DDR1 date is coming up' ) If anyone can point me in the right direction, I appreciate it.

A: 

I don't have personal experience with the pivot operator, it may provide a better solution.

But I've used a case statement in the past

SELECT 
    TaskDescription, 
    CASE(DateTypeID = 1, Tasks_DateType.Date) AS DDr1, 
    CASE(DateTypeID = 2, Tasks_DateType.Date) AS DDr2,
    ...
FROM Tasks 
    INNER JOIN Tasks_DateType  ON Tasks.ID = Tasks_DateType.TasksID
    INNER JOIN DateType ON Tasks_DateType.DateTypeID = DateType.DateTypeID
GROUP BY TaskDescription

This will work, but will require you to change the SQL whenever there are more Task descriptions added, so it's not ideal.

EDIT:

It appears as though the PIVOT keyword was added in SqlServer 2005, this example shows how to do a pivot query in both 2000 & 2005, but it is similar to my answer.

Nathan Koop
Nathan, I had tried your query a while ago, but the problem with that query is that I get the Task Descriptions twice. So instead of having 2 rows, i get 4 rows. Two for the first task and two for the second task. That is my problem. Thanks for the try though.
I know I've done this in the past with no issues, maybe I need a group by in there, I'll verify my code
Nathan Koop
yeah, it needed a group by, which I've added to the code sample
Nathan Koop
Thanks for the update. I tried with the group by, but SQL Server isn't liking it. The error is 'Column 'Date' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.'I think you can only run your query when you are summing or averaging values. I have another pivot table with a sum in it and the query is very similar to yours and it works fine. For some reason just trying to combine rows is not being allowed. Let me know what you think.
ugh, you're right, I've got an aggregate in mine, glad that ScottE got it working for you
Nathan Koop
A: 

If the pivoted columns are unknown (dynamic), then you'll have to build up your query manually in either ms-sql 2000 or 2005, ie with out without PIVOT.

This involves either executing dynamic sql in a stored procedure (generally a no-no) or querying a view with dynamic sql. The latter is the approach I generally go with.

For pivoting, I prefer the Rozenshtein method over case statements, as explained here:

http://www.stephenforte.net/PermaLink.aspx?guid=2b0532fc-4318-4ac0-a405-15d6d813eeb8

EDIT

You can also do this in linq-to-sql, but it emits some pretty inefficient code (at least when I view it through linqpad), so I don't recommend it. If you're still curious I can post an example of how to do it.

ScottE
Scott, The Rozenshtein method is fine when you are trying to sum or avg things together. In this example, there isn't any value I am trying to add together or in some way combine except for the TaskDescription. That is why I am having such a hard time with this. I would create the sql statement in C# for now so I don't really need a stored procedure, just need to see a query that will generate the table in my example. Thanks for the info and link though!
Sorry about that, I saw pivot and assumed you were doing aggregating. I'll dig around my brain and see if I can think of anything.
ScottE
A: 

Version-1: +simple, -must be changed every time DateType is added. So is not great for a dynamic solution:

SELECT      tt.ID,
            tt.TaskDescription,
            td1.Date AS DDR1,
            td2.Date AS DDR2,
            td3.Date AS DueDate
FROM        Tasks tt
LEFT JOIN   Tasks_DateType td1
        ON  td1.TasksID = tt.ID AND td1.DateTypeID = 1
LEFT JOIN   Tasks_DateType td2
        ON  td2.TasksID = tt.ID AND td2.DateTypeID = 2
LEFT JOIN   Tasks_DateType td3
        ON  td3.TasksID = tt.ID AND td3.DateTypeID = 3

Version-2: completely dynamic (with some limitations, but they can be handled - just google for it):

Dynamic pivot query creation. See Dynamic Cross-Tabs/Pivot Tables: you need to create one SP of UDF and then can use it for multiple purposes. This is the original post, to which you may find many links and improvements.

Version-3: just leave it for your client code to handle. I would not design my SQL to return a dynamic set of data, but rather handle it on the client (presentation layer). I just would not like to handle some dynamic columns that come as a result of my query, where I need to guess what is that exactly. The only reason I use Version-2 is when the result is presented directly as a table for a report. In all other cases for truly dynamic data I use client code. For example: having structure you have, how will you attach logic that field DueDate is mandatory - you cannot use DB constraints; how will you ensure that DDR1 is not higher then DDR2? If these are not separate (static) columns in the database (where you can use CONSTRAINTS), then the client code is the one that validates your data consistency.

Good luck!

van
Van,Fortunately for me, those date values don't really need to be controlled with constraints. THey are really there just as placeholders for the user. They can put any dates that they want in them. The dates don't usually follow an order. Thus in the future someone might want to add a 'DateStarted' or 'DateClosed' to the tables and keep track of those values as well. I am creating the SQL query client side (c#). As for your query, there isn't a 'DATE' in TD1, or TD2 or TD3 since they are all the DateType table and that table only has (ID, DateDescription) fields. Thanks for all the info!
@saba. td1, td2 and td2 are from Tasks_DateType table, so they do have *Date* field (from your table description). But if you create a query on C# client, then I would not bother and load the data to the client in similar structure how it is stored in the database, and then present it to the user *pivot-ed* based on the content of DateType table content.
van
Thanks Van for the Info. Scott's implementation was what i had been working for a few days but I just couldn't get past the last little hump (The min trick). I appreciate your help though!
+2  A: 

Here is a proper answer, tested with your data. I only used the first two date types, but you'd build this up on the fly anyway.

Select 
    Tasks.TaskDescription,     
    Min(Case DateType.DateDescription When 'DDR1' Then Tasks_DateType.Date End) As DDR1,     
    Min(Case DateType.DateDescription When 'DDR2' Then Tasks_DateType.Date End) As DDR2
From
    Tasks_DateType
    INNER JOIN Tasks ON Tasks_DateType.TaskID = Tasks.TaskID
    INNER JOIN DateType ON Tasks_DateType.DateTypeID = DateType.DateTypeID
Group By
    Tasks.TaskDescription

EDIT

van mentioned that tasks with no dates won't show up. This is correct. Using left joins (again, mentioned by van) and restructuring the query a bit will return all tasks, even though this is not your need at the moment.

Select 
    Tasks.TaskDescription,     
    Min(Case DateType.DateDescription When 'DDR1' Then Tasks_DateType.Date End) As DDR1,     
    Min(Case DateType.DateDescription When 'DDR2' Then Tasks_DateType.Date End) As DDR2
From
    Tasks 
    LEFT OUTER JOIN Tasks_DateType ON Tasks_DateType.TaskID = Tasks.TaskID
    LEFT OUTER  JOIN DateType ON Tasks_DateType.DateTypeID = DateType.DateTypeID
Group By
    Tasks.TaskDescription
ScottE
yeah, I forgot to include the Group By in my original answer
Nathan Koop
Scott,Thanks man. Finally got it to work. Like i mentioned to Nathan, the Group by clause was giving me issues. Without a 'sum' or 'avg', i couldn't get it to work. Using the 'Min' did the trick and it doesn't complain about the aggregate function. Thanks for that little trick!
FYI: the side-effect of this implementation is that it will not return you any rows for those Tasks that have 0 dates stored.
van
That is exactly how we need it to work. Thanks!
@van - good point. You could always union another query that contains tasks with no dates, and build in the empty columns.
ScottE
... or build a query FROM Tasks and then LEFT JOIN on Tasks_DataType and DateType. - then you do not need a UNION.
van