The "Crosstab, Rotate, Pivot" article from earlier this year should have been named "Crosstab, Rotate, Pivot, Denormalize" because it showed how to take a perfectly nice table like this

and muck it up to look like this

without hard-coding any of the data values in the SQL.
What about going the other way?
What if you have a fat squat denormalized table with a bazillion columns

and you want to turn it into a tall skinny table with a bazillion rows?

And then (for bonus points) what if you wanted to keep rotating to get a different fat squat version like this?

First things first; here's the first fat squat table:
Here's the tall skinny table:
1 | <br> CREATE TABLE t1_normalized (<br> c1 VARCHAR ( 10 ) NOT NULL ,<br> c2 VARCHAR ( 128 ) NOT NULL ,<br> c3 INTEGER NOT NULL , <br> PRIMARY KEY ( c1, c2 ) );<br>
|
Here's the code that fills the tall skinny table (Step 2) and then rotates it again to create the second fat squat table (Step 3):
1 | <br> BEGIN <br><br> DECLARE @sql LONG VARCHAR ;<br><br>
|
Step 2 is the interesting part: it builds a SQL statement dynamically, at runtime, and then uses the magic
EXECUTE IMMEDIATE feature to execute that statement. EXECUTE IMMEDIATE isn't a new feature, it's been around forever, and although the code in this article has only been tested on SQL Anywhere 11.0.1 and 12.0.0 I'm pretty sure it works on older versions like 9 and 10, maybe even earlier.
The code on lines 19 through 23 looks in the SYSTABLE and SYSCOLUMN system tables for all the "_count" columns in table "t1". (Yes, this code depends on the being able to identify the columns of interest by how they are named: see the LIKE predicate on line 23.)
08.SELECT STRING (
09. 'INSERT t1_normalized \x0d\x0a',
10. LIST (
11. STRING (
12. 'SELECT c1, \x0d\x0a',
13. ' ''', SYSCOLUMN.column_name, ''',\x0d\x0a',
14. ' ', SYSCOLUMN.column_name, '\x0d\x0a',
15. ' FROM ', SYSTABLE.table_name ),
16. '\x0d\x0aUNION ALL \x0d\x0a'
17. ORDER BY SYSCOLUMN.column_id ) )
18. INTO @sql
19. FROM SYSCOLUMN
20. INNER JOIN SYSTABLE
21. ON SYSTABLE.table_id = SYSCOLUMN.table_id
22. WHERE SYSTABLE.table_name = 't1'
23. AND SYSCOLUMN.column_name LIKE '%?_count' ESCAPE '?';
24.
25.SELECT @sql; -- for display
26.
27.EXECUTE IMMEDIATE @sql;
The code on lines 8 through 18 builds a SQL statement in a string, and puts that string in the local LONG VARCHAR variable @sql.
The value placed in @sql consists of an 'INSERT...' (line 9) followed by a LIST (line 10) of 'SELECT...' statements (lines 12 through 15) separated by the 'UNION ALL' operator (line 16). The
STRING() and
LIST() functions are magic too, just like
EXECUTE IMMEDIATE... all well worth learning.
The LIST - ORDER BY on line 17 doesn't affect the final outcome, but it
is nice to have the generated SELECTs appear in the same order as the columns appear in the original fat squat table.
Confused yet?
Maybe it will help to see what gets put in @sql; an INSERT statement which creates one row in t1_normalized for every column in t1:
1 | <br> INSERT t1_normalized <br> SELECT c1, <br> 'Ford_count' ,<br> Ford_count<br> FROM t1<br> UNION ALL <br> SELECT c1, <br> 'Hyundai_count' ,<br> Hyundai_count<br> FROM t1<br> UNION ALL <br> SELECT c1, <br> 'Honda_count' ,<br> Honda_count<br> FROM t1<br> UNION ALL <br> SELECT c1, <br> 'Chevrolet_count' ,<br> Chevrolet_count<br> FROM t1<br>
|
Here are the tables again; first, the fat squat input "FROM t1":

...and the tall skinny output "INSERT t1_normalized":

Oh, by the way... the code in Step 3 uses pretty much the same technique shown in
Crosstab, Rotate, Pivot to produce the rotated-again fat squat table:

Does it work in the real world?
If you have the impression I sit around all day coding "SELECT FROM t1", let me set the record straight: I own and maintain some of the nastiest real-world code that's ever been created. And it's in that real world where the real problems arise, like the need to normalize and denormalize data to make it possible for the inevitable Queries From Hell to run in finite time. Necessity is the mother of Invention (although I do have to admit, it does sound pompous to use the word "invention" in a sentence :)
OK, so here's some real code, warts and all, the horribly denormalized table that supports the "Peaks since" line in the
Foxhound database monitor:

Truly fat, truly squat:
1 | <br>CREATE TABLE rroad_peaks (<br><br> -- *****************************************************************<br> -- ***** THIS TABLE IS A CANDIDATE FOR POST-SETUP DATA UPGRADE *****<br> -- *****************************************************************<br><br> sampling_id UNSIGNED INTEGER NOT NULL,<br> peaks_calculated_to_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peaks_calculated_after TIMESTAMP NOT NULL DEFAULT CURRENT TIMESTAMP,<br> peaks_calculated_after_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> earliest_preserved_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_sample_elapsed_msec BIGINT NOT NULL DEFAULT 0,<br> peak_canarian_query_elapsed_msec BIGINT NOT NULL DEFAULT 0, <br> peak_ActiveReq BIGINT NOT NULL DEFAULT 0,<br> peak_CheckpointUrgency BIGINT NOT NULL DEFAULT 0,<br> peak_interval_Chkpt BIGINT NOT NULL DEFAULT 0,<br> peak_ConnCount BIGINT NOT NULL DEFAULT 0,<br> peak_CurrIO BIGINT NOT NULL DEFAULT 0,<br> peak_DBFileFragments BIGINT NOT NULL DEFAULT 0,<br> peak_LockCount BIGINT NOT NULL DEFAULT 0,<br> peak_MultiProgrammingLevel BIGINT NOT NULL DEFAULT 0,<br> peak_RecoveryUrgency BIGINT NOT NULL DEFAULT 0,<br> peak_UnschReq BIGINT NOT NULL DEFAULT 0,<br> peak_executing_connection_count BIGINT NOT NULL DEFAULT 0,<br> peak_idle_connection_count BIGINT NOT NULL DEFAULT 0,<br> peak_waiting_connection_count BIGINT NOT NULL DEFAULT 0,<br> peak_total_blocked_connection_count BIGINT NOT NULL DEFAULT 0,<br> peak_total_temporary_file_bytes BIGINT NOT NULL DEFAULT 0,<br> peak_rate_Bytes DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,<br> peak_rate_Disk DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,<br> peak_rate_BytesReceived DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,<br> peak_rate_BytesSent DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,<br> peak_rate_CachePanics DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,<br> peak_CacheSatisfaction DECIMAL ( 30, 6 ) NOT NULL DEFAULT 1.0, -- a ratio, with the smallest value defined as the "peak"<br> peak_rate_DiskRead DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,<br> peak_rate_DiskWrite DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,<br> peak_rate_FullCompare DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,<br> peak_rate_IndAdd DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,<br> peak_rate_IndLookup DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,<br> peak_IndSatisfaction DECIMAL ( 30, 6 ) NOT NULL DEFAULT 1.0, -- a ratio, with the smallest value defined as the "peak"<br> peak_rate_LogWrite DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,<br> peak_cpu_percentage_string VARCHAR ( 10 ) NOT NULL DEFAULT '',<br> peak_cpu_percentage_number DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,<br> peak_rate_QueryLowMemoryStrategy DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,<br> peak_rate_Req DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,<br> peak_rate_Commit DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,<br> peak_rate_Rlbk DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,<br> peak_sample_elapsed_msec_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_canarian_query_elapsed_msec_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0, <br> peak_ActiveReq_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_CheckpointUrgency_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_interval_Chkpt_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_ConnCount_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_CurrIO_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_DBFileFragments_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_LockCount_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_MultiProgrammingLevel_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_RecoveryUrgency_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_UnschReq_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_executing_connection_count_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_idle_connection_count_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_waiting_connection_count_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_total_blocked_connection_count_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_total_temporary_file_bytes_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_rate_Bytes_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_rate_Disk_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_rate_BytesReceived_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_rate_BytesSent_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_rate_CachePanics_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_CacheSatisfaction_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_rate_DiskRead_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_rate_DiskWrite_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_rate_FullCompare_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_rate_IndAdd_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_rate_IndLookup_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_IndSatisfaction_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_rate_LogWrite_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_cpu_percentage_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_rate_QueryLowMemoryStrategy_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_rate_Req_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_rate_Commit_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> peak_rate_Rlbk_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> PRIMARY KEY ( sampling_id ) );<br>
|
Here's the code to normalize the "_sample_set_number" columns into a tall, skinny table:
1 | <br>BEGIN<br><br>DECLARE @sql LONG VARCHAR;<br><br>DECLARE LOCAL TEMPORARY TABLE normalized_peaks (<br> sampling_id UNSIGNED INTEGER NOT NULL,<br> column_name VARCHAR ( 128 ) NOT NULL,<br> sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,<br> PRIMARY KEY ( sampling_id, column_name ) );<br><br>SELECT STRING ( <br> 'INSERT normalized_peaks \x0d\x0a',<br> LIST ( <br> STRING ( <br> 'SELECT sampling_id, \x0d\x0a',<br> ' ''', SYSCOLUMN.column_name, ''',\x0d\x0a',<br> ' ', SYSCOLUMN.column_name, '\x0d\x0a',<br> ' FROM ', SYSTABLE.table_name ),<br> '\x0d\x0aUNION ALL \x0d\x0a'<br> ORDER BY SYSCOLUMN.column_id ) )<br> INTO @sql<br> FROM SYSCOLUMN <br> INNER JOIN SYSTABLE <br> ON SYSTABLE.table_id = SYSCOLUMN.table_id <br> WHERE SYSTABLE.table_name = 'rroad_peaks'<br> AND SYSCOLUMN.column_name LIKE '%?_sample_set_number' ESCAPE '?';<br><br>SELECT @sql; -- for display<br><br>EXECUTE IMMEDIATE @sql;<br><br>SELECT * FROM rroad_peaks ORDER BY sampling_id;<br>SELECT * FROM normalized_peaks ORDER BY sampling_id, column_name;<br><br>END;<br>
|
Here's what the normalized table looks like:

And just to drive the point home, here's the answer to the question "Why go to all that trouble with EXECUTE IMMEDIATE blah blah, why not just hand-code the solution?"
The answer is, the rroad_peaks table is subject to change (especially new columns) and the normalization logic is just one more piece of code that does
NOT have to change along with it; i.e., this is the generated INSERT, this is the code that doesn't have to be maintained:
1 | <br>INSERT normalized_peaks <br>SELECT sampling_id, <br> 'peaks_calculated_to_sample_set_number',<br> peaks_calculated_to_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peaks_calculated_after_sample_set_number',<br> peaks_calculated_after_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'earliest_preserved_sample_set_number',<br> earliest_preserved_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_sample_elapsed_msec_sample_set_number',<br> peak_sample_elapsed_msec_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_canarian_query_elapsed_msec_sample_set_number',<br> peak_canarian_query_elapsed_msec_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_ActiveReq_sample_set_number',<br> peak_ActiveReq_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_CheckpointUrgency_sample_set_number',<br> peak_CheckpointUrgency_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_interval_Chkpt_sample_set_number',<br> peak_interval_Chkpt_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_ConnCount_sample_set_number',<br> peak_ConnCount_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_CurrIO_sample_set_number',<br> peak_CurrIO_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_DBFileFragments_sample_set_number',<br> peak_DBFileFragments_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_LockCount_sample_set_number',<br> peak_LockCount_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_MultiProgrammingLevel_sample_set_number',<br> peak_MultiProgrammingLevel_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_RecoveryUrgency_sample_set_number',<br> peak_RecoveryUrgency_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_UnschReq_sample_set_number',<br> peak_UnschReq_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_executing_connection_count_sample_set_number',<br> peak_executing_connection_count_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_idle_connection_count_sample_set_number',<br> peak_idle_connection_count_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_waiting_connection_count_sample_set_number',<br> peak_waiting_connection_count_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_total_blocked_connection_count_sample_set_number',<br> peak_total_blocked_connection_count_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_total_temporary_file_bytes_sample_set_number',<br> peak_total_temporary_file_bytes_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_rate_Bytes_sample_set_number',<br> peak_rate_Bytes_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_rate_Disk_sample_set_number',<br> peak_rate_Disk_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_rate_BytesReceived_sample_set_number',<br> peak_rate_BytesReceived_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_rate_BytesSent_sample_set_number',<br> peak_rate_BytesSent_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_rate_CachePanics_sample_set_number',<br> peak_rate_CachePanics_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_CacheSatisfaction_sample_set_number',<br> peak_CacheSatisfaction_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_rate_DiskRead_sample_set_number',<br> peak_rate_DiskRead_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_rate_DiskWrite_sample_set_number',<br> peak_rate_DiskWrite_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_rate_FullCompare_sample_set_number',<br> peak_rate_FullCompare_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_rate_IndAdd_sample_set_number',<br> peak_rate_IndAdd_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_rate_IndLookup_sample_set_number',<br> peak_rate_IndLookup_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_IndSatisfaction_sample_set_number',<br> peak_IndSatisfaction_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_rate_LogWrite_sample_set_number',<br> peak_rate_LogWrite_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_cpu_percentage_sample_set_number',<br> peak_cpu_percentage_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_rate_QueryLowMemoryStrategy_sample_set_number',<br> peak_rate_QueryLowMemoryStrategy_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_rate_Req_sample_set_number',<br> peak_rate_Req_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_rate_Commit_sample_set_number',<br> peak_rate_Commit_sample_set_number<br> FROM rroad_peaks<br>UNION ALL <br>SELECT sampling_id, <br> 'peak_rate_Rlbk_sample_set_number',<br> peak_rate_Rlbk_sample_set_number<br> FROM rroad_peaks<br>
|