Crosstab, Rotate, Pivot

Shameless promotion, that's what the title is all about: Include all the keywords anyone might use when asking this question:

Question: How do I rotate a table so that different row values in a single column become different columns in a new table?

Answer: I could have sworn I talked about this years ago, but apparently not... at least not anywhere people can find it.

Is that a subtle dig at the NNTP newsgroups? Yes, I'm sorry, it is... I'll try to be less subtle: NNTP sucks, the days of the newsgroups are numbered, and the future for SQL Anywhere questions and answers lies with SQLA! <g>
SQL Anywhere doesn't have the ability to rotate (crosstab, pivot, whatever) a table, and that feature isn't coming any time soon. What it does have is EXECUTE IMMEDIATE, plus the ability to code IF expressions just about anywhere in the SELECT statement. You can combine those two features to kludge a solution.
Is "kludge" too strong a word? Maybe so, since performance is pretty good. Good enough for this technique to be used in real time by the Foxhound database monitor when analyzing data gathered from thousands of database connections. But, no question... the code's funky.
It's actually hard to explain what rotating a table is all about, it's not exactly as simple as turning rows into columns and vice versa... it's easier to use an example.

Here's a table showing sales by state and quarter:
01.-- Part 1: Initialize data.
02.BEGIN
03.   DROP TABLE t1;
04.   EXCEPTION WHEN OTHERS THEN
05.END;
06.CREATE TABLE t1 (
07.   c1 VARCHAR ( 10 ) NOT NULL,
08.   c2 VARCHAR ( 10 ) NOT NULL,
09.   c3 INTEGER        NOT NULL,
10.   PRIMARY KEY ( c1, c2 ) );
11.INSERT t1 VALUES ( 'CA', 'Q1', 1000 );
12.INSERT t1 VALUES ( 'CA', 'Q2', 2000 );
13.INSERT t1 VALUES ( 'CA', 'Q3', 9000 );
14.INSERT t1 VALUES ( 'CA', 'Q4', 7000 );
15.INSERT t1 VALUES ( 'NY', 'Q1', 4000 );
16.INSERT t1 VALUES ( 'NY', 'Q2', 5000 );
17.INSERT t1 VALUES ( 'NY', 'Q3', 1000 );
18.INSERT t1 VALUES ( 'NY', 'Q4', 6000 );
19.INSERT t1 VALUES ( 'FL', 'Q1', 9000 );
20.INSERT t1 VALUES ( 'FL', 'Q2', 7000 );
21.INSERT t1 VALUES ( 'FL', 'Q3', 2000 );
22.INSERT t1 VALUES ( 'FL', 'Q4', 1000 );
23.INSERT t1 VALUES ( 'AZ', 'Q1', 5000 );
24.INSERT t1 VALUES ( 'AZ', 'Q2', 5000 );
25.INSERT t1 VALUES ( 'AZ', 'Q3', 1000 );
26.INSERT t1 VALUES ( 'AZ', 'Q4', 3000 );
27.INSERT t1 VALUES ( 'MA', 'Q1', 2000 );
28.INSERT t1 VALUES ( 'MA', 'Q2', 6000 );
29.INSERT t1 VALUES ( 'MA', 'Q3', 5000 );
30.INSERT t1 VALUES ( 'MA', 'Q4', 3000 );
31.COMMIT;
32.SELECT * FROM t1 ORDER BY c1, c2;

So far, so good... the table is nicely normalized, everything's elegant... and useless:
01.c1  c2   c3
02.AZ  Q1  5000
03.AZ  Q2  5000
04.AZ  Q3  1000
05.AZ  Q4  3000
06.CA  Q1  1000
07.CA  Q2  2000
08.CA  Q3  9000
09.CA  Q4  7000
10.FL  Q1  9000
11.FL  Q2  7000
12.FL  Q3  2000
13.FL  Q4  1000
14.MA  Q1  2000
15.MA  Q2  6000
16.MA  Q3  5000
17.MA  Q4  3000
18.NY  Q1  4000
19.NY  Q2  5000
20.NY  Q3  1000
21.NY  Q4  6000

What folks often want to see is something like this, sales by quarter for each state:
1.c2   AZ    CA    FL    MA    NY
2.Q1  5000  1000  9000  2000  4000
3.Q2  5000  2000  7000  6000  5000
4.Q3  1000  9000  2000  5000  1000
5.Q4  3000  7000  1000  3000  6000

Here's how you can do that in SQL Anywhere:
01.-- Part 2: Pivot c1 values into columns.
02.BEGIN
03.DECLARE @sql LONG VARCHAR;
04.SET @sql = 'SELECT c2';
05.FOR f_fetch
06.AS c_fetch NO SCROLL CURSOR FOR
07.SELECT DISTINCT t1.c1 AS @c1
08.  FROM t1
09. ORDER BY t1.c1
10.FOR READ onLY
11.DO
12.   SET @sql = STRING (
13.      @sql,
14.      ', SUM ( ( IF t1.c1 = ''',
15.      @c1,
16.      ''' THEN 1 ELSE 0 ENDIF ) * t1.c3 ) AS "',
17.      @c1,
18.      '"' );
19.END FOR;
20.SET @sql = STRING (
21.   @sql,
22.   ' INTO #t1 FROM t1 GROUP BY c2' );
23.MESSAGE @sql TO CONSOLE;
24.EXECUTE IMMEDIATE @sql;
25.SELECT * FROM #t1 ORDER BY c2; -- pivot table
26.END;

What if you want to see sales by state for each quarter?
1.c1   Q1    Q2    Q3    Q4
2.AZ  5000  5000  1000  3000
3.CA  1000  2000  9000  7000
4.FL  9000  7000  2000  1000
5.MA  2000  6000  5000  3000
6.NY  4000  5000  1000  6000

Here's the code for that:
01.-- Part 3: Pivot c2 values into columns.
02.BEGIN
03.DECLARE @sql LONG VARCHAR;
04.SET @sql = 'SELECT c1';
05.FOR f_fetch
06.AS c_fetch NO SCROLL CURSOR FOR
07.SELECT DISTINCT t1.c2 AS @c2
08.  FROM t1
09. ORDER BY t1.c2
10.FOR READ onLY
11.DO
12.   SET @sql = STRING (
13.      @sql,
14.      ', SUM ( ( IF t1.c2 = ''',
15.      @c2,
16.      ''' THEN 1 ELSE 0 ENDIF ) * t1.c3 ) AS "',
17.      @c2,
18.      '"' );
19.END FOR;
20.SET @sql = STRING (
21.   @sql,
22.   ' INTO #t1 FROM t1 GROUP BY c1' );
23.MESSAGE @sql TO CONSOLE;
24.EXECUTE IMMEDIATE @sql;
25.SELECT * FROM #t1 ORDER BY c1; -- pivot table
26.END;

Here's where the magic lies, in the SUMs of row values multiplied by 1 or 0; the following SELECT statements are generated by the code shown above:
01.SELECT c2,
02.       SUM ( ( IF t1.c1 = 'AZ' THEN 1 ELSE 0 ENDIF ) * t1.c3 ) AS "AZ", 
03.       SUM ( ( IF t1.c1 = 'CA' THEN 1 ELSE 0 ENDIF ) * t1.c3 ) AS "CA", 
04.       SUM ( ( IF t1.c1 = 'FL' THEN 1 ELSE 0 ENDIF ) * t1.c3 ) AS "FL", 
05.       SUM ( ( IF t1.c1 = 'MA' THEN 1 ELSE 0 ENDIF ) * t1.c3 ) AS "MA", 
06.       SUM ( ( IF t1.c1 = 'NY' THEN 1 ELSE 0 ENDIF ) * t1.c3 ) AS "NY" 
07.  INTO #t1 
08.  FROM t1 
09. GROUP BY c2
10.SELECT c1, 
11.       SUM ( ( IF t1.c2 = 'Q1' THEN 1 ELSE 0 ENDIF ) * t1.c3 ) AS "Q1", 
12.       SUM ( ( IF t1.c2 = 'Q2' THEN 1 ELSE 0 ENDIF ) * t1.c3 ) AS "Q2", 
13.       SUM ( ( IF t1.c2 = 'Q3' THEN 1 ELSE 0 ENDIF ) * t1.c3 ) AS "Q3", 
14.       SUM ( ( IF t1.c2 = 'Q4' THEN 1 ELSE 0 ENDIF ) * t1.c3 ) AS "Q4" 
15.  INTO #t1 
16.  FROM t1 
17. GROUP BY c1

The "SUM IF 1 OR 0" trick is an old one, used by generations of programmers to write funky code for icky problems, and not just in SQL. In fact, the code shown above is really simple compared with what goes on in the world, just a starting point.

If folks are interested I've got a couple of other demos that build on this technique... not the raw incomprehensible code you find in real-world applications, but hammered down into the basics for presentation... that's actually harder to do, the hammering down, harder than writing the original incomprehensible stuff :)

'메모' 카테고리의 다른 글

태풍 이름... 펌.  (0) 2012.08.27
clipcache 프로그램이란? 펌  (0) 2012.08.23
sybase sql anywhere upgrade database...펌  (0) 2012.08.19
이것이 일본의 실체입니다. 펌.  (0) 2012.08.17
sybase dbvalid, validate table...펌  (0) 2012.08.16

+ Recent posts