The wizard Revisited: Dynamic crosstabs using MySQL stored procedures
Since I read the excellent articleMySQL wizardry
By Giuseppe Maxia some years ago I wanted to implement crosstabs in pure mysql-and had to wait a long time until 5.0 made it possible. and then of course I didn't find the time. but after Roland bouman blogged on this same topic
Yesterday, it was definitely the right moment to try out the real thing: fully dynamic crosstabs in a pure MySQL stored procedure.
If you're new to crosstabs make sure you read the article mentioned above first. it's likewise instructive and witty. what I do here in pure SQL is actually just a translated and slightly simplified version of Giuseppe Maxia's Perl code.
He was so lucky to know one of those real geeks (just called "the wizard") who led him through all the tedious steps involved in generating crosstabs. despite the title, I couldn't revisit the wizard. knowing that he possibly only exists in our dreams, I had to work through this all alone... but let's just have a look at the code first, being only a quick suggestion, ready for your improvements.
Create procedure xtab ('col _ name' varchar (32), 'col _ alias 'varchar (32 ),
'Col _ from 'varchar (256), 'col _ value' varchar (32 ),
'Row _ name' varchar (32), 'row _ from 'varchar (256 ))
Deterministic
Reads SQL data
SQL Security invoker
Comment 'generate dynamic crosstabs'
Begin
Declare 'xtab _ col_name 'varchar (32) default '';
Declare 'xtab _ col_alias 'varchar (32) default '';
Declare 'xtab _ query'varchar (4096) default '';
Declare 'done' bit (1) default 0;
Declare 'column _ cursor 'cursor
Select 'temp _ col_name ', 'temp _ col_alias' from 'xtab _ columns ';
Declare continue handler for not found set 'done' = 1;
-- We have to use a temporary table here as MySQL doesn' t
-- Allow us to declare a cursor in prepared statements
Drop table if exists 'xtab _ columns ';
Set @ column_query: = Concat ('create temporary table 'xtab _ columns '',
'Select distinct ',
''', 'Col _ name', ''as 'temp _ col_name ',',
''', 'Col _ alias ', ''as 'temp _ col_alias '',
'Col _ from ');
Prepare 'column _ query' from @ column_query;
Execute 'column _ query ';
Deallocate prepare 'column _ query ';
Open 'column _ cursor ';
Column_loop: loop
Fetch 'column _ cursor 'into 'xtab _ col_name ', 'xtab _ col_alias ';
If 'done' then leave column_loop; end if;
Set 'xtab _ query' = Concat ('xtab _ query ',
'/Tsum (if ('', 'col _ name','' = /'',
'Xtab _ col_name ','/',',
'Col _ value', ', 0) '',
'Xtab _ col_alias ', '',/N ');
End loop column_loop;
Close 'column _ cursor ';
Drop table if exists 'xtab _ columns ';
Set 'xtab _ query' = Concat ('select', 'row _ name', '',/N ',
'Xtab _ query', '/t ',
If ('col _ value' = '1 ',
'Count (*)',
Concat ('sum ('', 'col _ value ','')')
),
As 'Total'/N ',
'Row _ from ');
-- Uncomment the following line if you want to see
-- Generated crosstab query for debugging purposes
-- Select 'xtab _ query ';
-- Execute crosstab
Set @ xtab_query = 'xtab _ query ';
Prepare 'xtab' from @ xtab_query;
Execute 'xtab ';
Deallocate prepare 'xtab ';
End
The procedure uses dynamic SQL via prepare/execute twice: first to build the expressions defining the columns in the crosstab and finally to execute the dynamic crosstab query. it takes six parameters: the first three control the building of the crosstab columns: col_name is the original column on which the crosstab shoshould be done, col_alias cocould be a different column to be used for the names displayed in the column headers of the crosstab (if you use a join in the next parameter ), col_from is the query part starting with from to be used to retrieve the crosstab values. the next three parameters control the actual crosstab query: col_value can be set to 1 if you simply want to count in your crosstab, but it can be set to a column name as well to sum up an arbitrary value. row_name is the main crosstab query starting again with from, it can as well contain joins, where conditions, order by clses and usually contains a group by statement for the column mentioned in row_name.
There were two issues with prepared statements to work around (as of MySQL 5.0.15) that didn't contribute to a cleaner code structure:
* MySQL doesn't yet accept local variables in the prepared statement syntax (see my feature request bug #13572 ). so we have to pollute the global variable namespace whenever we use dynamic SQL inside a stored routine.
* I didn't find a way to define a cursor inside a prepared statement: mySQL yields an error on a create procedure statement if it contains an open statement for a cursor that doesn't have an explicit corresponding declare before it.
To work around the second point it was necessary to use a temporary table. the user invoking the procedure therefore needs the create temporary table privilege. you cocould Of course change the SQL security to definer, but I didn't want to do that, as it cocould give the user the possibility to circumvent access restrictions on tables by just accessing them via a crosstab.
But now let's finally create a tiny test database to try out the xtab stored procedure.
Create Table employees (
Id int auto_increment primary key,
Shop_id int,
Gender Enum ('M', 'F '),
Name varchar (32 ),
Salary int
);
Create Table shops (
Shop_id int auto_increment primary key,
Shop varchar (32)
);
Insert into shops (shop)
Values ('zurich '), ('New York'), ('London ');
Insert into employees (shop_id, gender, name, salary)
Values
(1, 'M', 'Jon Simpson, 4500 ),
(1, 'F', 'Barbara breitenmoser ', 4700 ),
(2, 'F', 'kirsten ruegg', 5600 ),
(3, 'M', 'ralph Teller, 5100 ),
(3, 'M', 'Peter Jonson, 5200 );
The two examples show that the procedure works for counting as well as for summing up values.
Mysql> call xtab ('gender', 'gender', 'From ployees', 1, 'shop ',
'From employees inner join shops using (shop_id) group by shop ');
+ ---------- + ------ + ------- +
| Shop | M | f | Total |
+ ---------- + ------ + ------- +
| London | 2 | 0 | 2 |
| New York | 0 | 1 | 1 |
| Zurich | 1 | 1 | 2 |
+ ---------- + ------ + ------- +
3 rows in SET (0.03 Sec)
Mysql> call xtab ('gender', 'gender', 'From ployees', 'salary ', 'shop ',
'From employees inner join shops using (shop_id) group by shop ');
+ ---------- + ------- + ------ + ------- +
| Shop | M | f | Total |
+ ---------- + ------- + ------ + ------- +
| London | 10300 | 0 | 10300 |
| New York | 0 | 0 | 5600 | 5600 |
| Zurich | 4500 | 4700 | 9200 |
+ ---------- + ------- + ------ + ------- +
3 rows in SET (0.01 Sec)
Address: http://www.futhark.ch/mysql/106.html