7

My tables go as follows:

Patients table

PatientId   Name
1           James
...

Visits table

Date    PatientID_FK    Weight
1/1     1               220
2/1     1               210 
...

How can I build a query that returns

PatientId    Name    Visit1Date    Visit1Weight    Visit2Date    Visit2Weight    ...
1            James   1/1           220             2/1           210
2            ...

How can we add more columns in this way? How to write that SELECT? Please help.


Some posts on StackExchange say it is impossible for a SQL statement to handle it. Is it really so?

Taryn
  • 242,637
  • 56
  • 362
  • 405
Blaise
  • 21,314
  • 28
  • 108
  • 169
  • 1
    [What have you tried](http://www.whathaveyoutried.com)? – Kermit Jan 29 '13 at 19:41
  • What RDBMS and version are you using? – Lamak Jan 29 '13 at 19:42
  • just no clue how to do it. We are using SQL server 2008R2. – Blaise Jan 29 '13 at 19:43
  • @njk, I just don't understand why you downvote my question. It is a reasonable question. I think many other people will benefit from the discussion. I searched for quite a while with no success. That is why I typed this question here. If you know there is a useful link, please just add here. Many of us will appreciate. Thanks. – Blaise Jan 29 '13 at 19:47
  • If someone asks "How to write" without trying anything, then I will down vote. I can tell you from my brief time on SO that there are many topics on this question, and perhaps you just weren't searching for those keywords. The [down voting](http://stackoverflow.com/privileges/vote-down) privilege actually explicitly states to use it for *no-effort-expended post*. – Kermit Jan 29 '13 at 19:49
  • just do a quick search with the tags [sql-server] and [pivot] and you'll get a lot of answers about this. Here is an example: http://stackoverflow.com/questions/14581937/showing-rows-of-table-as-columns-based-on-some-id – Lamak Jan 29 '13 at 19:51
  • The "thankyou" goes to njk. Thank you for the necessary hint. A keyword `PIVOT` helps me locate the article in here: http://msdn.microsoft.com/en-us/library/ms177410%28v=sql.105%29.aspx – Blaise Jan 29 '13 at 19:51
  • 1
    I would say anyone can find the answer if he/she knows how to ask the question. You expert knows instantly that this is about `PIVOT`. Please show some mercy to an ignorant like me who does not know how to describe for help. – Blaise Jan 29 '13 at 19:53
  • @Blaise It's ok, no need to talk that way about your knowledge. This can be a place to learn, and you currently got an upvote and one downvote (so the net result is +3 rep). – Lamak Jan 29 '13 at 19:58

1 Answers1

8

This type of data transformation will need to be done with both a pivot and the unpivot functions. Since your visits will be unknown, then you will want to use dynamic sql. But first, I will show you how to build the query with the values hard-coded so it makes it easier to understand how the process works.

First, you need to UNPIVOT the date and weight columns so the values are in the same column. This can be done using a UNION ALL query or the unpivot function:

UNPIVOT:

select patientid, name, rn, col, value
from
(
  select p.patientid, p.name, convert(char(5), v.date, 110) date, 
    cast(v.weight as char(5)) weight,
    row_number() over(partition by PatientID_FK order by date) rn
  from patients p
  left join visits v
    on p.patientid = v.PatientID_FK
) src
unpivot
(
  value
  for col in (date, weight)
) unpiv

See SQL Fiddle with Demo. The result of this query places the values of both the date and weight column into a single column with multiple rows. Notice that I applied a row_number() to the records so you will be able to tell what values go with each visit:

| PATIENTID |  NAME | RN |    COL | VALUE |
-------------------------------------------
|         1 | James |  1 |   date | 01-01 |
|         1 | James |  1 | weight | 220   |
|         1 | James |  2 |   date | 02-01 |
|         1 | James |  2 | weight | 210   |

PIVOT:

The next step is to apply the PIVOT function to the items in the col column, but first we need to alter the name so it gives you the names that you want.

To do that I alter the SELECT statement slightly to add the row number to the col name:

select patientid, name, 'Visit'+col + cast(rn as varchar(10)) new_col, 
  value
from ...

This will give you the new names which are the names that you want as columns:

Visitdate1 
Visitweight1
Visitdate2
Visitweight2

To PIVOT the data your query will look like the following if you hard-code the values:

select *
from
(
  select patientid, name, 'Visit'+col + cast(rn as varchar(10)) new_col, 
    value
  from
  (
    select p.patientid, p.name, convert(char(5), v.date, 110) date, 
      cast(v.weight as char(5)) weight,
      row_number() over(partition by PatientID_FK order by date) rn
    from patients p
    left join visits v
      on p.patientid = v.PatientID_FK
  ) src
  unpivot
  (
    value
    for col in (date, weight)
  ) unpiv
) s1
pivot
(
  max(value)
  for new_col in (Visitdate1,Visitweight1,
                  Visitdate2,Visitweight2)
) piv

See SQL Fiddle with Demo.

Dynamic PIVOT:

Now that I have explained the logic behind how this is set up, you will want to implement this same process using dynamic sql. You dynamic sql version will be:

DECLARE @colsUnpivot AS NVARCHAR(MAX),
    @query  AS NVARCHAR(MAX),
    @colsPivot as  NVARCHAR(MAX)

select @colsUnpivot = stuff((select ', '+quotename(C.name)
         from sys.columns as C
         where C.object_id = object_id('visits') and
               C.name not in ('PatientID_FK')
         for xml path('')), 1, 1, '')

select @colsPivot = STUFF((SELECT  ',' + quotename('Visit'+c.name 
                                          + cast(v.rn as varchar(10)))
                    from
                    (
                       select row_number() over(partition by PatientID_FK order by date) rn
                       from visits
                    ) v
                    cross apply sys.columns as C
                   where C.object_id = object_id('visits') and
                     C.name not in ('PatientID_FK')
                   group by c.name, v.rn
                   order by v.rn
            FOR XML PATH(''), TYPE
            ).value('.', 'NVARCHAR(MAX)') 
        ,1,1,'')

set @query 
  = 'select *
      from
      (
        select patientid, name, ''Visit''+col + cast(rn as varchar(10)) new_col,
          value
        from 
        (
          select p.patientid, p.name, convert(char(5), v.date, 110) date, 
            cast(v.weight as char(5)) weight,
            row_number() over(partition by PatientID_FK order by date) rn
          from patients p
          left join visits v
            on p.patientid = v.PatientID_FK
        ) x
        unpivot
        (
          value
          for col in ('+ @colsunpivot +')
        ) u
      ) x1
      pivot
      (
        max(value)
        for new_col in ('+ @colspivot +')
      ) p'

exec(@query)

See SQL Fiddle with Demo

The result from both versions is:

| PATIENTID |  NAME | VISITDATE1 | VISITWEIGHT1 | VISITDATE2 | VISITWEIGHT2 |
-----------------------------------------------------------------------------
|         1 | James |      01-01 |        220   |      02-01 |        210   |
Taryn
  • 242,637
  • 56
  • 362
  • 405