Wednesday, March 16, 2011

The definitive Oracle COLUMNS TO ROWS solution

Need your Oracle query that returns columns (fields) of data to instead return rows (like key/value pairs)? Don't want to hard-code every single column name as you need to do in the commonly suggested methods(max/decode and pivot)?

You've come to the right place. I will show you how and it's crazy simple.

In my case, and probably yours, you have a query result such as this:

colname1   colname2   colname3
data            data            data

And you need it like this:

Attribute    Value
colname1   data
colname2   data
colname3   data

So did I. And I did exactly what you're doing. I scoured my books. Pivot or max/decode. I Googled till my fingers bled. Pivot or union all. More Googling. A table function example. Too heavy.

In most, if not all, cases stating that you needed to hardcode column values of your query.

I didn't want to do that as I knew the project where I needed this type of query result would change. And I didn't want to crack open a package or hard code another column in somewhere when it did.

In the middle of writing a table function, at the ends of desperation, I googled one final time.

And found. The answer.

XML.

Oracle's provided XML functions would allow me to transform my result set into XML, and in doing so, allow me to use XPATH to be able to discern both column name and value from each node (record) returned.

The solution to all your troubles my friend is below for your enjoyment, plain and simple. You may need to brush up a bit on your XPATH syntax, but having the below query @ your disposal takes care of 90% of your problems.

SELECT 

X.ATTR
,X.VAL
FROM XMLTABLE ('ROWSET/ROW/*' PASSING
DBMS_XMLGEN.GETXMLTYPE('
SELECT 
dummy AS example_col1, dummy AS example_col2, dummy AS example_col3 from dual where rownum < 2')  
COLUMNS
ATTR VARCHAR2(4000) PATH 'name()', VAL VARCHAR2(4000) PATH 'data(.)'
) X



So if you need it, allow me to explain, briefly, what's going on above.

  1. First you have YOUR query highlighted in yellow. If your query returns a result, you can use it here. To make it simple to read in my real world project, I used a view to hide most of the major SQL and was able to simply write 'select * from my_view' in the high lighted area above. In the simple example above, you'd generally want one record returned so you'd usually add some sort of predicate specifying a key, etc.
  2. Your query is returned as XML at which time you use the awesome Oracle provided function XMLTABLE to query from that XML fragment as if it were a sql result set.
  3. You'll notice the hard-coded 'ROWSET/ROW/*' specification. That tells the XMLTABLE to work on every child node of ROW returned. You can adjust this search path to return more/less specific areas of your result set. (Oracle returns your query using DBMS_XMLGEN.GETXMLTYPE with ROWSET as the main XML node and ROW as the node housing each record.)
I hope this example reaches you before you've written that pivot query or table function as it provides a tremendously flexible, yet amazingly simple (10 lines of code up there - and I broke it up a bit ;)), solution to get your columns into the rows you need to loop thru.

Please keep in mind that the example I give is extremely simple just to demonstrate the point. My final query involved a much more complex example which allowed me to not only return a key/value pair, but also return an 'id' column for the returned rows allowing me to no longer need the 'key' in the xml'd sql query as suggested in step 1 above. As such, I could turn the column to row sql into a view where I could then get all records returned and then filter by the key after.

8 comments:

  1. Can you share your final "complex" script?

    ReplyDelete
  2. THANK YOU SO MUCH!!! I was surprised by the effort it normally takes to do a simple transposee in oracle sql, this is a great solution.

    ReplyDelete
  3. Mohammed - so happy to have helped! I've taken so much from the online community that it's nice to be able to give back on occasion!

    ReplyDelete
  4. is it possible to have data in multiple columns like
    This data
    ATTR VAL
    PDATE 02/23/2013
    CARAT 4.25
    LNK_SHAPE 113
    LNK_CLARITY 94
    LNK_COLOR 35
    LNK_CUT 41
    LNK_RATIO 38
    LNK_FL 48
    LNK_TINT 4
    LNK_MILKY 57
    LNK_POLISH 141
    LNK_SYMN 145
    LNK_NC 82
    LNK_NS 85
    PRICE 133416
    PDATE 02/23/2013
    CARAT 4.25
    LNK_SHAPE 2
    LNK_CLARITY 92
    LNK_COLOR 35
    LNK_CUT 162
    PRICE 160854

    to ....
    ATTR VAL valx
    PDATE 02/23/2013 02/23/2013
    CARAT 4.25 4.25
    LNK_SHAPE 113 2
    LNK_CLARITY 94 92
    LNK_COLOR 35 35
    LNK_CUT 41 162
    LNK_RATIO 38 -
    LNK_FL 48 -
    LNK_TINT 4 -
    LNK_MILKY 57 -
    LNK_POLISH 141 -
    LNK_SYMN 145 -
    LNK_NC 82 -
    LNK_NS 85 -
    PRICE 133416 160854

    ReplyDelete
  5. I'd have to see the structure of your original table to determine where the secondary (valx) would be coming from. Anything is possible, just don't have all the data needed to answer your question at the moment!

    ReplyDelete
    Replies
    1. Hi Steve,

      Thanks for the reply
      Please have a look here http://tinypic.com/r/4vrbpf/6

      Delete
  6. THANK YOU SO MUCH!!!

    It's simple, it's the solution.

    Geimer.

    ReplyDelete
  7. Steve,
    I found your blog looking for the solution of converting columns to namevalue pairs. It works great and super easy too. I have a slight challenge and I am hoping you could help. The result set I have is attributes for locations. So after ysing your solution, my result set looks like this:

    Attr AttrVal
    STR# 1
    Open 0900
    Close 2100
    Phone 1234567890
    Timezone EST
    STR# 2
    Open 0800
    Close 2200
    Phone 9876543210
    Timezone PST

    I want it to look like this:

    STR# Attr AttrVal
    1 Open 0900
    1 Close 2100
    1 Phone 1234567890
    1 Timezone EST
    2 Open 0800
    2 Close 2200
    2 Phone 9876543210
    2 Timezone PST

    Any suggestions?

    Thx,
    Bach.

    ReplyDelete