SQL. Filter null fields in resonse - mysql

I have table users, with fields: id, name, age, gender, country, city, comment. Fields maybe null. For example:
cursor.execute('select * from users where id = 12')
cursor.fetchone()
(12, 'alex', 33, 'male', None, None, None)
How I can get back from query only not null fields?
This query must return me just
(12, 'alex', 33, 'male')
I can easily do it with a programming language, but I losing my resources by getting redundant info from tables and it also forced me to add redundant code.

So, you need to get customized table-oriented result with columns dynamically generated with SQL ANSI.
You will need a kit with two procedures:
Check if a column has only NULL values or not.
Gather all columns with NO NULL values.
With these two procedures you can check each column of a table strcuture and get an output withtout columns with NULL values.
The first procedure is generic:
set delimiter //
create procedure check_field_null(col varchar(64),
schemaname varchar(255),
tablename varchar(255),
out QN int)
BEGIN
SET #sSQL = concat('SELECT #N := COUNT(*) FROM ', schemaname, '.', tablename , ' WHERE (', col, ' <=> NULL);');
prepare stm from #sSQL;
execute stm;
set QN =#N;
deallocate prepare stm;
END
//
The second procedure means that you have the above mentioned method to identify NOT NULLS columns and put all to search.
set delimiter //
create procedure cur_cs_customer(inout allcols varchar(255), in my_schema_name varchar(64), in my_table_name varchar(64))
BEGIN
DECLARE Count_Null int default 0;
DECLARE initial INT DEFAULT 0;
DECLARE MYCOL char(64);
DECLARE ch_done INT DEFAULT 0;
DECLARE cs_cur1 CURSOR
FOR SELECT C.COLUMN_NAME
FROM information_schema.COLUMNS C
WHERE C.TABLE_SCHEMA = my_schema_name
AND C.TABLE_NAME = my_table_name;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET ch_done = true;
open cs_cur1;
read_cs_cur1:
LOOP
FETCH cs_cur1 INTO MYCOL;
IF (ch_done ) THEN
LEAVE read_cs_cur1;
END IF;
IF NOT isnull(MYCOL) THEN
call check_field_null(MYCOL,
my_schema_name,
my_table_name,
Count_Null);
if Count_Null = 0 then
/* Only it inlcudes fields with not null values */
set initial = initial + 1;
if initial = 1 then
SET allcols = MYCOL;
else
SET allcols = concat( cast(allcols as char(255)), ',', MYCOL);
end if;
end if;
END IF;
END LOOP read_cs_cur1;
close cs_cur1;
select allcols;
END
//
After, you may run as follows:
set delimiter ;
call cur_cs_customer(#my_args, 'schema_name', 'users');
select #my_args;
set #stm = concat('SELECT ', #my_args, ' FROM users;');
PREPARE stmt1 FROM #stm;
execute stmt1;
deallocate prepare stmt1;
Further explanation may be checked in an article here! .
I hope this tip may help you.

Related

MySQL use a wild card in table name

So I have a software that store data on those tables. I know how those tables start but there will be always a suffix to them that's a number which I have no idea to know
example of those table name is "itemid5_4423"
I know there is a table with the name itemid5 but i have no way to know the suffix number
is there a wild card something similar to this logic select * from itemid5_*;
Let's say you have 2 tables like this:
create table itemid5_1111 (id int, itemname varchar(100));
create table itemid5_2222 (id int, itemname varchar(100));
You insert data into them:
insert into itemid5_1111 values (1, 'first table');
insert into itemid5_2222 values (2, 'second table');
Your goal is to get output like this from all itemid5* tables.
+------+--------------+
| id | itemname |
+------+--------------+
| 1 | first table |
| 2 | second table |
+------+--------------+
You can do that by typing:
select * from itemid5_1111
union all select * from itemid5_2222;
But, that's a lot of manual typing. You can make a stored procedure to dynamically query table names starting with itemid5 and then create a SQL dynamically and execute it.
Stored procedure
delimiter $$
drop procedure if exists get_items$$
create procedure get_items()
begin
declare eof boolean default false;
declare mytable varchar(255);
declare first_run boolean default true;
declare tablenames_cursor cursor for
select table_name from information_schema.tables
where table_name like 'itemid%';
declare continue handler for not found
set eof = true;
set #my_query = '';
open tablenames_cursor;
read_loop: loop
fetch tablenames_cursor into mytable;
if eof then
leave read_loop;
end if;
if first_run then
set #my_query = concat('select * from ', mytable);
set first_run = false;
else
set #my_query = concat(#my_query, ' union all ', 'select * from ', mytable);
end if;
end loop;
close tablenames_cursor;
prepare stmt from #my_query;
execute stmt;
deallocate prepare stmt;
end$$
delimiter ;
You call this procedure like so to get your results:
call get_items();
If you created a 3rd table like so:
create table itemid5_3333 (id int, itemname varchar(100));
insert into itemid5_3333 values (3, 'third table');
And then, you called the proc, you'd get
call get_items();
+------+--------------+
| id | itemname |
+------+--------------+
| 1 | first table |
| 2 | second table |
| 3 | third table |
+------+--------------+
i think using the data dictionary to retrieve the result would help, run this.
select * from information_schema.tables where table_name like 'itemid5_% ';
you can choose the columns you want that this query outputs, table_name is one of them you need, like we used it in where clause.
SELECT
REPLACE
(
GROUP_CONCAT(
CONCAT("SELECT * FROM ", `TABLE_NAME`)
),
",",
" UNION ALL "
)
INTO #sq
FROM
information_schema.tables
WHERE
`TABLE_SCHEMA` = "test";
USE
test;
PREPARE
stmt1
FROM
#sq;
EXECUTE
stmt1;
DELIMITER //
CREATE PROCEDURE merge_tables(IN in_sname VARCHAR(64),IN in_tname VARCHAR(64))
READS SQL DATA
BEGIN
DECLARE sname VARCHAR(64);
DECLARE tname VARCHAR(64);
DECLARE cname VARCHAR(64);
DECLARE done INT DEFAULT FALSE;
DECLARE table_cur CURSOR FOR SELECT table_schema, table_name FROM
information_schema.TABLES WHERE table_schema = in_sname AND table_name LIKE
'table%';
DECLARE column_cur CURSOR FOR SELECT `COLUMN_NAME` FROM
`INFORMATION_SCHEMA`.`COLUMNS` where table_schema = in_sname and table_name
= in_tname;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
-- build column list (Using the column list for table listed in second
parameter in PROC Call)
SET #column = '';
OPEN column_cur;
column_cur_loop: LOOP
FETCH column_cur INTO cname;
IF done THEN
-- SET #column := CONCAT(#column, ') ');
LEAVE column_cur_loop;
END IF;
IF #column = '' THEN
SET #column := CONCAT(#column,cname);
ELSE
SET #column := CONCAT(#column,',',cname);
END IF;
END LOOP;
CLOSE column_cur;
-- Build UNION Query for all table starting with table%)
SET done = FALSE;
SET #sql = '';
OPEN table_cur;
table_list_loop: LOOP
FETCH table_cur INTO sname, tname;
IF done THEN
LEAVE table_list_loop;
END IF;
IF #sql = '' THEN
SET #sql := CONCAT('INSERT INTO MERGED_TABLE (', #column , ') SELECT
', #column , ' FROM `', sname, '`.`', tname, '`');
ELSE
SET #sql := CONCAT(#sql, ' UNION ALL SELECT ' , #column , ' FROM `',
sname, '`.`', tname, '`');
END IF;
END LOOP;
CLOSE table_cur;
PREPARE stmt FROM #sql; -- prepare and execute the dynamically
EXECUTE stmt; -- created query.
DEALLOCATE PREPARE stmt;
END //
DELIMITER ;`
call merge_tables(testdb,table1)
testdb is Schema Name where tables reside
table1 is one of the tables which needs to be merged to get column names
table% in the procedure is the prefix of the all the tables that needs to be merged.

MySQL Stored Prcedure Debugging Output

So I've written a fairly simple MySQL stored procedure to retrieve values from a database for a personal app that I'm building. From everything I can see, the procedure should work just fine, but it's returning the wrong results.
Here's the procedure code:
USE randyrip_kdb;
DROP PROCEDURE IF EXISTS spGetAllTracksSong;
DELIMITER //
CREATE PROCEDURE spGetAllTracksSong(IN startRecord INT, IN rowsReturned INT, IN searchArtist VARCHAR(255), IN searchTitle VARCHAR(244), IN orderBy VARCHAR(20), IN duets TINYINT(1))
BEGIN
DECLARE spStart INT;
DECLARE spRows INT;
DECLARE whereClause VARCHAR(255) DEFAULT '';
DECLARE whereArtist VARCHAR(255) DEFAULT '';
DECLARE whereSong VARCHAR(255) DEFAULT '';
DECLARE outputSQL VARCHAR(1000) DEFAULT '';
SET spStart=startRecord;
SET spRows=rowsReturned;
IF searchArtist!='' THEN SET whereArtist= CONCAT('artist LIKE \'%',searchArtist,'%\' '); END IF;
IF searchTitle!='' THEN SET whereSong= CONCAT('song_title LIKE \'%',searchTitle,'%\' '); END IF;
IF whereArtist != '' && whereSong !='' THEN SET whereClause=CONCAT('WHERE ', whereArtist,'AND ',whereSong);
ELSEIF whereArtist !='' THEN SET whereClause= CONCAT('WHERE',whereArtist);
ELSE SET whereClause = CONCAT('WHERE',whereSong);
END IF;
IF duets=1 && whereClause !='' THEN SET whereClause=CONCAT(whereClause,' AND is_duet=1');
END IF;
SET orderBy = IFNULL(orderBy, 'song_title');
IF orderBy='date' THEN SET orderBy='date_added DESC'; END IF;
/*select whereClause;
select orderBy;
select startRecord;
select rowsReturned;*/
SET outputSQL=CONCAT('SELECT song_title, artist, comments, disc_number FROM track ', whereClause,'ORDER BY ' ,orderBy,' LIMIT ' ,spStart,',',spRows);
SELECT outputSQL;
SELECT song_title, artist, comments, disc_number FROM track whereClause ORDER BY orderBy LIMIT spStart,spRows;
END//
DELIMITER ;
I'm calling the Stored Procedure with these parameters:
call spGetAllTracksSong(0,20,'elvis costello','peace, love','date',0);
The variable outputSQL is correctly generating the query I want, and when I run it it's returning two rows as expected. However, the procedure itself is returning 20 rows, none of which match the criteria.
If anyone has any ideas as to what I'm doing incorrectly, that would be great. From all that I can see, everything should be fine however.
Randy,
if you use variables in the SQL query (like "FROM track whereClause"), you need to execute with EXECUTE, otherwise it will not be evaluated. Replace your last select with this:
set #sql = outputSQL;
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
Alternatively, you could try not to use dynamic SQL.

Procedure fails if second call creates other columns

Fellow residents of the Stack Exchange,
I am currently trying to create a procedure that will reformat data in a very major way. I get a list of data on tickets (summary for random months). The input views always have the same columns, but since I have to display it with the months as columns (which are usually entered as a value in the Monat column on the view).
Sooo, after some major research, trial and error and a lot of headaches I got to make it work. The procedure accepts a single ticket-number and 'returns' all stats on that single ticket.
I do this by:
Iterate over each distinct month, building a CREATE TEMPORARY TABLE statement.
Execute that statement, then deallocate the statement
Iterate over each ticket-value, sorting them into the temporary table with a INSERT INTO ON DUPLICATE KEY UPDATE statement (which I build, execute and deallocate each iteration anew)
Again Iterate over each distinct month, building a last update to fill the summary columns (I know, I could combine this into Step 1, but I tried to keep the steps as separate as I could, just to make it easier to read, I can optimize once I'm done making it work as I want).
Select the temporary table, so it's being returned
Clean up some loose ends, because I'm a habitually neat person.
The good thing: It Works!
The bad thing: It works only for one input value. Whenever I change it to another ticket number, that would require different months for columns it fails with an "Error Code 1054, Unknown Column in field list", referring to an old column (month) the current query ought not have.
I can run the procedure as many times as I want, as long as the columns of the temporary table are identical.
This behavior resets, whenever I drop and recreate the procedure or create a new connection.
Obviously, I'm forgetting to do a cleaning step somewhere along the way, and me being fairly new to SQL in general and MySQL in particular probably didn't even know to look for it :(.
Help would be most appreciated, thanks,
Fred
DELIMITER //
CREATE PROCEDURE proc_get_relevant_tickets(bID VARCHAR(10))
DETERMINISTIC
READS SQL DATA
BEGIN
# Declare storage vairables for withdrawing data from view
DECLARE ID, FiID, ZVB, ZNVB, ZGes, WVB, WNVB, WGes INTEGER;
DECLARE Mon VARCHAR(50);
DECLARE RowVerb, RowNVerb, RowGes, Counter INTEGER;
DECLARE verbucht, nichtverbucht, gesamt VARCHAR(50);
DECLARE currentticket INTEGER;
DECLARE statezges, statewges LONGTEXT;
# Declare runtime stuff
DECLARE done INT DEFAULT FALSE;
DECLARE declaration LONGTEXT;
DECLARE temptext VARCHAR(50);
DECLARE Months CURSOR FOR SELECT DISTINCT Monat FROM view_list_of_tickets t WHERE bID = t.ID;
DECLARE `Values` CURSOR FOR SELECT * FROM view_list_of_tickets t WHERE bID = t.ID;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
# Clean up in case something interrupted last execution
DROP TEMPORARY TABLE IF EXISTS temp_storage_5437485;
# If there are any entries to work upon
IF (SELECT COUNT(t.ID) FROM view_list_of_tickets t WHERE bID = t.ID > 0)
THEN
# Create temporary table to put the values into
SET declaration = 'CREATE TEMPORARY TABLE `temp_storage_5437485` (ID INTEGER PRIMARY KEY, TicketNr INTEGER, Category VARCHAR(50), ';
SET done = FALSE;
OPEN Months;
read_loop: LOOP
FETCH Months INTO temptext;
IF done THEN
LEAVE read_loop;
END IF;
SET declaration = CONCAT(declaration, '`', temptext, ' Zeit` INTEGER DEFAULT 0, ');
SET declaration = CONCAT(declaration, '`', temptext, ' Wert` INTEGER DEFAULT 0, ');
END LOOP;
CLOSE Months;
SET declaration = CONCAT(declaration, '`Gesamt Zeit` INTEGER, `Gesamt Wert` INTEGER);');
SELECT declaration INTO #declaration;
PREPARE stmt FROM #declaration;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
# End of creating the storage container
# Cycle through values and input into temporary table
SET done = FALSE;
SET verbucht = 'Verbucht';
SET nichtverbucht = 'Nicht Verbucht';
SET gesamt = 'Gesamt';
SET currentticket = 0;
SET Counter = 0;
SET RowVerb = 1;
SET RowNVerb = 2;
SET RowGes = 3;
OPEN `Values`;
read_values: LOOP
FETCH `Values` INTO ID, FiID, ZVB, ZNVB, ZGes, WVB, WNVB, WGes, Mon;
IF done THEN
LEAVE read_values;
END IF;
# If a new ticket has been arrived at, increment the counters
IF currentticket > 0 AND ID <> currentticket THEN
SET currentticket = ID;
SET Counter = Counter + 1;
SET RowVerb = RowVerb + 3;
SET RowNVerb = RowNVerb + 3;
SET RowGes = RowGes + 3;
END IF;
IF currentticket = 0 AND ID <> currentticket THEN
SET currentticket = ID;
END IF;
# Insert first (Verbucht) row
SET declaration = CONCAT('INSERT INTO `temp_storage_5437485` (`ID`, `TicketNr`, `', Mon, ' Zeit`, `', Mon, ' Wert`) VALUES (');
SET declaration = CONCAT(declaration, RowVerb, ', ', ID, ', ', ZVB, ', ', WVB, ') ON DUPLICATE KEY UPDATE ');
SET declaration = CONCAT(declaration, '`', Mon, ' Zeit`=', ZVB, ', `', Mon, ' Wert`=', WVB, ';');
SELECT declaration INTO #declaration;
PREPARE stmt FROM #declaration;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
# Insert second (Nicht Verbucht) row
SET declaration = CONCAT('INSERT INTO `temp_storage_5437485` (`ID`, `TicketNr`, `', Mon, ' Zeit`, `', Mon, ' Wert`) VALUES (');
SET declaration = CONCAT(declaration, RowNVerb, ', ', ID, ', ', ZNVB, ', ', WNVB, ') ON DUPLICATE KEY UPDATE ');
SET declaration = CONCAT(declaration, '`', Mon, ' Zeit`=', ZNVB, ', `', Mon, ' Wert`=', WNVB, ';');
SELECT declaration INTO #declaration;
PREPARE stmt FROM #declaration;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
# Insert third (Gesamt) row
SET declaration = CONCAT('INSERT INTO `temp_storage_5437485` (`ID`, `TicketNr`, `', Mon, ' Zeit`, `', Mon, ' Wert`) VALUES (');
SET declaration = CONCAT(declaration, RowGes, ', ', ID, ', ', ZGes, ', ', WGes, ') ON DUPLICATE KEY UPDATE ');
SET declaration = CONCAT(declaration, '`', Mon, ' Zeit`=', ZGes, ', `', Mon, ' Wert`=', WGes, ';');
SELECT declaration INTO #declaration;
PREPARE stmt FROM #declaration;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
UPDATE temp_storage_5437485 SET Category = verbucht WHERE temp_storage_5437485.ID = RowVerb LIMIT 5;
UPDATE temp_storage_5437485 SET Category = nichtverbucht WHERE temp_storage_5437485.ID = RowNVerb LIMIT 5;
UPDATE temp_storage_5437485 SET Category = gesamt WHERE temp_storage_5437485.ID = RowGes LIMIT 5;
END LOOP;
CLOSE `Values`;
# End of cycling for values input
# Being calculating the total values
SET declaration = 'UPDATE temp_storage_5437485 SET `Gesamt Zeit` = (';
SET statezges = '';
SET statewges = '';
SET done = FALSE;
OPEN Months;
read_loop: LOOP
FETCH Months INTO temptext;
IF done THEN
LEAVE read_loop;
END IF;
# If not the first entry, add more
IF statezges <> '' THEN
SET statezges = CONCAT(statezges, ' + ');
END IF;
IF statewges <> '' THEN
SET statewges = CONCAT(statewges, ' + ');
END IF;
# Add column name
SET statezges = CONCAT(statezges, 'temp_storage_5437485.`', temptext, ' Zeit`');
SET statewges = CONCAT(statewges, 'temp_storage_5437485.`', temptext, ' Wert`');
END LOOP;
CLOSE Months;
SET declaration = CONCAT(declaration, statezges, '), `Gesamt Wert` = (', statewges, ') LIMIT 100000');
SELECT declaration INTO #declaration;
PREPARE stmt FROM #declaration;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
# End calculating the total values
SELECT * FROM temp_storage_5437485;
DROP TEMPORARY TABLE IF EXISTS temp_storage_5437485;
ELSE
SELECT 'is null';
END IF;
DROP TEMPORARY TABLE IF EXISTS temp_storage_5437485;
END //
DELIMITER ;

MySql, split a string and insert into table

I have two inputs for my stored procedure. One is the 'RoledID' and second one is the 'MenuIDs'. 'MenusIDs' is a list of comma separated menus ids that need to be inserted with RoledID. RoleId is just an INT and we need to put this RoledID against each MenuID. My table 'RolesMenus' contains two columns one for MenuID and one for RoleID.
Now I need to split MenuIDs and insert each MenuID with RoleID.
How can I write a stored procedure for it?
You can build one INSERT query (because statement allows to insert multiple records) and run it with prepared statements, e.g. -
SET #MenuIDs = '1,2,3';
SET #RoledID = 100;
SET #values = REPLACE(#MenuIDs, ',', CONCAT(', ', #RoledID, '),('));
SET #values = CONCAT('(', #values, ', ', #RoledID, ')'); -- This produces a string like this -> (1, 100),(2, 100),(3, 100)
SET #insert = CONCAT('INSERT INTO RolesMenus VALUES', #values); -- Build INSERT statement like this -> INSERT INTO RolesMenus VALUES(1, 100),(2, 100),(3, 100)
-- Execute INSERT statement
PREPARE stmt FROM #insert;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
As you see, it can be done without stored procedure.
Give this a go. It may need some tweaking if the MenuIDs string does not conform to 'menuId,menuId,menuId'.
Also I do not know what data type the menuId column is in your target table (INT?) so you may have to put some numeric checking in too (in case '1,2,3,banana,4,5' is passed in as the MenuIds input parameter).
DELIMITER $$
DROP PROCEDURE IF EXISTS `insert_role_menuids`$$
CREATE PROCEDURE `insert_role_menuids`(IN RoleID INT,IN MenuIDs varchar(500))
BEGIN
declare idx,prev_idx int;
declare v_id varchar(10);
set idx := locate(',',MenuIDs,1);
set prev_idx := 1;
WHILE idx > 0 DO
set v_id := substr(MenuIDs,prev_idx,idx-prev_idx);
insert into RolesMenus (RoleId,MenuId) values (RoleID,v_id);
set prev_idx := idx+1;
set idx := locate(',',MenuIDs,prev_idx);
END WHILE;
set v_id := substr(MenuIDs,prev_idx);
insert into RolesMenus (RoleId,MenuId) values (RoleID,v_id);
END$$
DELIMITER ;
for this solution, you must create a table with the name split_table, it can have a id(autoincrement) if you need it and must have a column where to store the value (I call it valor)
DELIMITER $$
USE `dbaname`$$
DROP PROCEDURE IF EXISTS `Split`$$
CREATE DEFINER=`root`#`localhost` PROCEDURE `Split`(
IN cadena VARCHAR(8000),
IN delimitador VARCHAR(10)
)
BEGIN
TRUNCATE split_table;
SET #posicion = 1;
SET #ldel = LENGTH(delimitador);
SET #valor = SUBSTRING_INDEX(cadena, delimitador, 1);
WHILE #valor <> '' AND #posicion > 0 DO
SET #valor = SUBSTRING_INDEX(cadena, delimitador, 1);
INSERT INTO split_table(valor) VALUES (#valor);
SET #posicion = POSITION(delimitador IN cadena);
SET #largo = LENGTH(cadena);
IF #largo >= #posicion THEN
SET cadena = SUBSTR(cadena, #posicion + #ldel, #largo - #posicion);
SET #valor = SUBSTRING_INDEX(cadena, delimitador, 1);
ELSE
SET #posicion = 0;
END IF;
END WHILE;
END$$
DELIMITER ;
First create procedure
CREATE DEFINER=`root`#`localhost` PROCEDURE `split_str_save_to_tmp_table`(
IN _str TEXT,
IN _table_name VARCHAR(80)
)
BEGIN
#DROP FIRST OLD TABLE
SET #q = CONCAT('DROP TEMPORARY TABLE IF EXISTS ', _table_name);
PREPARE st FROM #q;
EXECUTE st;
#CREATE TABLE
SET #q = CONCAT('CREATE TEMPORARY TABLE ', _table_name, '(id INT UNSIGNED NOT NULL PRIMARY KEY (id) )' );
PREPARE st FROM #q;
EXECUTE st;
SET #ids = REPLACE(_str, ',', '),(');
SET #ids = CONCAT('(', #ids, ')');
#INSERT INTO TABLE
SET #q = CONCAT('INSERT INTO ' , _table_name ,' VALUES');
SET #q = CONCAT(#q, #ids);
PREPARE st FROM #q;
EXECUTE st;
DEALLOCATE PREPARE st;
END
Then call
call split_str_save_to_tmp_table('1,2,3,4,5', 'tmp_split_product');
SELECT * FROM tmp_split_product
AFAIK MySQL does not have a function to split strings. Here is the MySQL manual for string related functions. In the comments section should be some information about workarounds for splitting string with substring-functions but not really usable:
MySQL manual

Select N random records per group

Hallo and good sunday to everybody.
I need to select N random records from each group.
Starting from the query of Quassnoi
http://explainextended.com/2009/03/01/selecting-random-rows/
to select X random record I wrote this store procedure
delimiter //
drop procedure if exists casualiPerGruppo //
create procedure casualiPerGruppo(in tabella varchar(50),in campo varchar(50),in numPerGruppo int)
comment 'Selezione di N record casuali per gruppo'
begin
declare elenco_campi varchar(255);
declare valore int;
declare finite int default 0;
declare query1 varchar(250);
declare query2 varchar(250);
declare query3 varchar(250);
declare query4 varchar(250);
declare cur_gruppi cursor for select gruppo from tmp_view;
declare continue handler for not found set finite = 1;
drop table if exists tmp_casuali;
set #query1 = concat('create temporary table tmp_casuali like ', tabella);
prepare stmt from #query1;
execute stmt;
deallocate prepare stmt;
set #query2 = concat('create or replace view tmp_view as select ',campo,' as gruppo from ',tabella,' group by ',campo);
prepare stmt from #query2;
execute stmt;
deallocate prepare stmt;
open cur_gruppi;
mio_loop:loop
fetch cur_gruppi into valore;
if finite = 1 then
leave mio_loop;
end if;
set #query3 = concat("select group_concat(column_name) into #elenco_campi
from information_schema.columns
where table_name = '",tabella,"' and table_schema = database()");
prepare stmt from #query3;
execute stmt;
deallocate prepare stmt;
set #query4 = concat('insert into tmp_casuali select ',
#elenco_campi,' from (
select #cnt := count(*) + 1,
#lim :=', numPerGruppo,
' from ',tabella,
' where ',campo,' = ', valore,
' ) vars
straight_join
(
select r.*,
#lim := #lim - 1
from ', tabella, ' r
where (#cnt := #cnt - 1)
and rand() < #lim / #cnt and ', campo, ' = ', valore ,
') i');
prepare stmt from #query4;
execute stmt;
deallocate prepare stmt;
end loop;
close cur_gruppi;
select * from tmp_casuali;
end //
delimiter ;
that I recall in this way to give you an idea:
create table prova (
id int not null auto_increment primary key,
id_gruppo int,
altro varchar(10)
) engine = myisam;
insert into prova (id_gruppo,altro) values
(1,'aaa'),(2,'bbb'),(3,'ccc'),(1,'ddd'),(1,'eee'),(2,'fff'),
(2,'ggg'),(2,'hhh'),(3,'iii'),(3,'jjj'),(3,'kkk'),(1,'lll'),(4,'mmm');
call casualiPerGruppo('prova','id_gruppo',2);
My problem is that Quassnoi query, even though is very performant, it takes even 1 second on a large recorset. So if I apply it within my sp several times, the total time increases a lot.
Can you suggest me a better way to solve my problem?
Thanks in advance
EDIT.
create table `prova` (
`id` int(11) not null auto_increment,
`id_gruppo` int(11) default null,
`prog` int(11) default null,
primary key (`id`)
) engine=myisam charset=latin1;
delimiter //
drop procedure if exists inserisci //
create procedure inserisci(in quanti int)
begin
declare i int default 0;
while i < quanti do
insert into prova (id_gruppo,prog) values (
(floor(1 + (rand() * 100))),
(floor(1 + (rand() * 30)))
);
set i = i + 1;
end while;
end //
delimiter ;
call inserisci(1000000);
#Clodoaldo:
My stored procedure
call casualipergruppo('prova','id_gruppo',2);
gives me 200 records and takes about 23 seconds. Your stored procedure keeps on giving me Error Code : 1473 Too high level of nesting for select even though I increase varchar value to 20000. I don't know if there is any limit on unions involved in a query.
I removed the tabella and campo parameters from the procedure just to make it easier to understand. I'm sure you can bring them back.
delimiter //
drop procedure if exists casualiPerGruppo //
create procedure casualiPerGruppo(in numPerGruppo int)
begin
declare valore int;
declare finite int default 0;
declare query_part varchar(200);
declare query_union varchar(2000);
declare cur_gruppi cursor for select distinct id_gruppo from prova;
declare continue handler for not found set finite = 1;
create temporary table resultset (id int, id_gruppo int, altro varchar(10));
set #query_part = 'select id, id_gruppo, altro from (select id, id_gruppo, altro from prova where id_gruppo = #id_gruppo order by rand() limit #numPerGruppo) ss#id_gruppo';
set #query_part = replace(#query_part, '#numPerGruppo', numPerGruppo);
set #query_union = '';
open cur_gruppi;
mio_loop:loop
fetch cur_gruppi into valore;
if finite = 1 then
leave mio_loop;
end if;
set #query_union = concat(#query_union, concat(' union ', #query_part));
set #query_union = replace(#query_union, '#id_gruppo', valore);
end loop;
close cur_gruppi;
set #query_union = substr(#query_union, 8);
set #query_union = concat('insert into resultset ', #query_union);
prepare stmt from #query_union;
execute stmt;
deallocate prepare stmt;
select * from resultset order by id_gruppo, altro;
drop table resultset;
end //
delimiter ;
Wow. That's a complicated way to do something very simple. Try this:
Assuming you have sequential ids (otherwise you could get no rows).
create view random_prova as
select * from prova
where id = (select min(id) from prova) +
floor(RAND(0) * (select max(id) - min(id) from prova));
This will give you 1 random row.
To get multiple rows, either loop in a stored procedure or server program until you get enough rows, or programatically create a query that employs union.
eg, this will give you 3 random rows:
select * from random_prova
union
select * from random_prova
union
select * from random_prova;
Note that using RAND(0) instead of RAND() means getting a different random number for each invocation. RAND() will give the same value for each invocation in the one statement (so using RAND() with union won't give you multiple rows).
There are some shortcomings with using union - it is possible to get the same row twice by chance. Programatically calling this until you get enough rows is safer.
To give better performance, use something like java to randomly select the ids for a simple query like
select * from prova where id in (...)
and have java (or perl or whatever) fill in the list with random ids - you would avoid the inefficiency of having to get the id range every time.
Post if your ids are not sequential - there is an efficient way, but I its explanation is long.