Iterate MySQL schemas - mysql

Problem description
I have a single-tenant MySQL database setup. That is, I have one identical schema for each client.
Now I need to run a specific query for each client. That would be easy in a multi-tenant setting (where all clients share a single schema). With my setup however, I need to iterate the schemas. More generally, I want to access a schema whose name is given by a variable. How can that be done?
What I've tried
If I try USE varSchemaName (where varSchemaName is a varchar
variable), I get the error message ERROR 1314: USE is not allowed
in stored procedures.
If I try SELECT * FROM varSchemaName.MyTable I get
Error Code: 1146. Table 'varSchemaName.MyTable' doesn't exist. Apparently MySQL considers varSchemaName to be a literal, not a
variable.

Building on the answer from fancyPants, you can call that procedure within a loop from another procedure which queries information_schema.tables to identify the databases containing MyTable and then call fancyPants' procedure with the db names as a parameter. This method is easy if the databases have a consistent naming scheme or contain identically named objects, which sounds like the case here. The structure would be something like:
DELIMITER //
DROP PROCEDURE IF EXISTS mydriver //
CREATE PROCEDURE mydriver()
BEGIN
DECLARE varSchemaName VARCHAR(64);
DECLARE done BOOLEAN;
DECLARE cur CURSOR FOR
SELECT table_schema
FROM information_schema.tables
WHERE table_name = 'MyTable';
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur;
read_loop: LOOP
FETCH cur INTO varSchemaName;
IF done THEN
LEAVE read_loop;
CLOSE cur;
END IF;
CALL fancypants_proc(varSchemaName);
END LOOP;
END //
DROP PROCEDURE IF EXISTS fancypants_proc //
CREATE PROCEDURE fancypants_proc(IN varSchemaName VARCHAR(64))
BEGIN
SET #sql = CONCAT('SELECT * FROM ', varSchemaName, '.MyTable');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END //
DELIMITER ;
CALL mydriver();

You have to build the statement first.
SET #sql = CONCAT('SELECT * FROM ', varSchemaName, '.MyTable');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
You can read more about prepared statements here.
You might be tempted to use variables for tablenames, but that doesn't work. Those parameters are for values in where clauses and so on. The above way is the way to go.

Related

How to loop comma separated values into select statement in MySQL stored procedure

I have to send comma separated values into a select statement where it will update values through #sql statement.
I have common table in all Databases I need to update the table column by one update statement in the procedure.
For Example : Input Param will be ('DataBase1','Database2',....., 'Database10')
Below is the sample procedure :
DELIMITER &&
CREATE PROCEDURE update_stmt (IN DBName varchar(100))
BEGIN
Declare DBName = #DB;
**comma seperated values loop and placed into the #DB**
use #DB;
SELECT concat(update #DB.sample SET COL = 0 where ID = \'',ID,'\','; ) as stmt FROM
Test.Sample into #s;
SET #sql = #s
PREPARE stmt from #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END &&
DELIMITER ;
so that update statement will execute in each of the databases.
Here's another approach. I don't try to split the comma-separated string, I use it with FIND_IN_SET() to match schema names in INFORMATION_SCHEMA.TABLES. This filters to schemas in the list that actually exist, and tables that actually exist in that schema.
Then use a cursor to loop over the matching rows, so you don't have to split any strings, which is awkward to do in a stored procedure.
I supposed that you would want to specify the id of the row to update too, so I added that to the procedure parameters.
Also notice the use of quotes when I create #sql. You can concatenate strings, but those must be quote-delimited like any other string literal. Variables must not be inside the quoted string. There's no feature to expand variables inside string literals in MySQL.
DELIMITER &&
CREATE PROCEDURE update_stmt (IN schema_name_list VARCHAR(100), IN in_id INT)
BEGIN
DECLARE done INT DEFAULT false;
DECLARE schema_name VARCHAR(64);
DECLARE cur1 CURSOR FOR
SELECT TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'sample' AND FIND_IN_SET(TABLE_SCHEMA, schema_name_list);
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
SET #id = in_id;
OPEN cur1;
schema_loop: LOOP
FETCH cur1 INTO schema_name;
IF done THEN
LEAVE schema_loop;
END IF;
SET #sql = CONCAT('UPDATE `', schema_name, '`.sample SET col = 0 WHERE id = ?');
PREPARE stmt FROM #sql;
EXECUTE stmt USING #id;
DEALLOCATE PREPARE stmt;
END LOOP;
CLOSE cur1;
END &&
DELIMITER ;
Frankly, I hardly ever use stored procedures in MySQL. The procedure language is primitive, and the tasks I see people try to do in stored procedures could be done a lot more easily in virtually every other programming language.

MYSQL variable with CURSOR and FETCH

I wanted to use table names and run a statement with the table name as variable. I used cursor/fetch but when I run a statement with the variable it is not using the value of the variable but just seems to use the variable_name itself. I have seen example with concat where another variable was defined but what if I just wanted to reference the table name in a COMMAND?
DELIMITER $$
DROP PROCEDURE IF EXISTS test $$
CREATE PROCEDURE test()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE v_table_name TEXT;
DECLARE cur1 CURSOR FOR
SELECT TABLE_NAME FROM information_schema.tables where table_schema = 'rt';
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done= TRUE;
OPEN cur1;
myloop: loop
FETCH cur1 INTO v_table_name;
IF done THEN
LEAVE myloop;
END IF;
COMMAND table v_table_name;
END loop;
close cur1;
END $$
If by COMMAND you mean you want to use the value of a variable as an identifier in another SQL statement... you may be able to make use of a prepared statement (in the context of a MySQL stored program).
Reference: https://dev.mysql.com/doc/refman/5.6/en/sql-syntax-prepared-statements.html
As a trivial example of what that might look like:
SET #sql = CONCAT('select * from `',v_table_name,'` limit 1');
PREPARE stmt1 FROM #sql;
EXECUTE stmt1;
DEALLOCATE PREPARE stmt1;
SET #sql = NULL;
Note that this approach is not safe from SQL Injection vulnerabilities.
If that's not what you are looking for, I'm at a loss. I don't understand what you are referring to as a COMMAND.

mySQL drop tables with Wildcard using only SQL statement?

Seen a lot for dropping tables using a wildcard but not a direct SQL statement except this one:
http://azimyasin.wordpress.com/2007/08/11/mysql-dropping-multiple-tables/
It says:
SHOW TABLES LIKE ‘phpbb_%’;
then DROP TABLES, is there a neat way to combine this all into one SQL Statement?
You could use dynamic SQL to do it, inside a stored procedure. It'd look something like this (untested):
CREATE PROCEDURE drop_like (IN pattern VARCHAR(64))
BEGIN
DECLARE q tinytext;
DECLARE done INT DEFAULT FALSE;
DECLARE cur CURSOR FOR
SELECT CONCAT('DROP TABLE "', table_schema, '"."', table_name, '"')
FROM information_schema.tables WHERE table_type = 'BASE TABLE' AND table_name LIKE pattern;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur;
drop_loop: LOOP
FETCH cur INTO q;
IF done THEN
LEAVE drop_loop;
END IF;
PREPARE stmt FROM #q;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END LOOP;
CLOSE cur;
END;
Using dynamic SQL in a query, as per derobert's answer, is the only to do this with pure SQL (no app code).
I wrote a generalized procedure to do this sort of thing (run a query for every table in a database) that you can find here - to use it, you would just need to run this query:
CALL p_run_for_each_table('databasename', 'DROP TABLE `{?database}`.`{?table}`');
It works in essentially the same way as derobert's answer.
However, the writer of that blog post was probably expecting you to write app code to turn the names of tables into a single DROP statement.
To do this, you would iterate over the results of the SHOW TABLE in your code and build a single query like this:
DROP TABLE table1, table2, tablewhatever;
This can be achieved via stored procedure, for example:
CREATE DEFINER=`some_user`#`%` PROCEDURE `drop_tables`()
LANGUAGE SQL
NOT DETERMINISTIC
MODIFIES SQL DATA
SQL SECURITY DEFINER
COMMENT ''
BEGIN
#We need to declare a variable with default 0 to determine weather to continue the loop or exit the loop.
DECLARE done INT DEFAULT 0;
DECLARE archive_table_name VARCHAR(100);
#Select desired tables from `information_schema`
DECLARE cur CURSOR FOR
SELECT t.`TABLE_NAME` FROM information_schema.`TABLES` t WHERE t.`TABLE_NAME` LIKE 'some_table_name%'
AND t.CREATE_TIME BETWEEN DATE_SUB(NOW(), INTERVAL 9 MONTH) AND DATE_SUB(NOW(), INTERVAL 6 MONTH);
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN cur;
read_loop: LOOP
#Fetch one record from CURSOR and set variable (if not found, then variable `done` will be set to 1 by continue handler)
FETCH cur INTO archive_table_name;
IF done THEN
LEAVE read_loop; #If done is set to 1, then exit the loop, else continue
END IF;
#Do your work
-- Create the truncate query
SET #s = CONCAT('DROP TABLE IF EXISTS ', archive_table_name);
-- Prepare, execute and deallocate the truncate query
PREPARE drop_statement FROM #s;
EXECUTE drop_statement;
DEALLOCATE PREPARE drop_statement;
END LOOP;
CLOSE cur; #Closing the cursor
END
Pay attention to the database user, which is creating/executing the stored routine: it must have appropriate credentials for executing/dropping tables.

use a variable for table name in mysql sproc

I'm trying to pass a table name into my mysql stored procedure to use this sproc to select off of different tables but it's not working...
this is what I"m trying:
CREATE PROCEDURE `usp_SelectFromTables`(
IN TableName varchar(100)
)
BEGIN
SELECT * FROM #TableName;
END
I've also tried it w/o the # sign and that just tells me that TableName doesn't exist...which I know :)
SET #cname:='jello';
SET #vname:='dwb';
SET #sql_text = concat('select concept_id,concept_name,',#vname,' from enc2.concept a JOIN enc2.ratings b USING(concept_id) where concept_name like (''%',#cname,'%'') and 3 is not null order by 3 asc');
PREPARE stmt FROM #sql_text;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
An extra bit that caused me problems.
I wanted to set the table name and field dynamically in a query as #kyle asked, but I also wanted to store the result of that query into a variable #a within the query.
Instead of putting the variable #a into the concat literally, you need to include it as part of the string text.
delimiter //
CREATE PROCEDURE removeProcessed(table_name VARCHAR(255), keyField VARCHAR(255), maxId INT, num_rows INT)
BEGIN
SET #table_name = table_name;
SET #keyField = keyField;
SET #maxId = maxId;
SET #num_rows = num_rows;
SET #sql_text1 = concat('SELECT MIN(',#keyField,') INTO #a FROM ',#table_name);
PREPARE stmt1 FROM #sql_text1;
EXECUTE stmt1;
DEALLOCATE PREPARE stmt1;
loop_label: LOOP
SET #sql_text2 = concat('SELECT ',#keyField,' INTO #z FROM ',#table_name,' WHERE ',#keyField,' >= ',#a,' ORDER BY ',#keyField,' LIMIT ',#num_rows,',1');
PREPARE stmt2 FROM #sql_text2;
EXECUTE stmt2;
DEALLOCATE PREPARE stmt2;
...Additional looping code...
END LOOP;
END
//
delimiter ;
So in #sql_text1 assign the result of the query to #a within the string using:
') INTO #a FROM '
Then in #sql_text2 use #a as an actual variable:
,' WHERE ',#keyField,' >= ',#a,' ORDER BY '
It depends on the DBMS, but the notation usually requires Dynamic SQL, and runs into the problem that the return values from the function depend on the inputs when it is executed. This gives the system conniptions. As a general rule (and therefore probably subject to exceptions), DBMS do not allow you to use placeholders (parameters) for structural elements of a query such as table names or column names; they only allow you to specify values such as column values.
Some DBMS do have stored procedure support that will allow you to build up an SQL string and then work with that, using 'prepare' or 'execute immediate' or similar operations. Note, however, that you are suddenly vulnerable to SQL injection attacks - someone who can execute your procedure is then able to control, in part, what SQL gets executed.

MySQL Stored Procedures : Use a variable as the database name in a cursor declaration

I need to use a variable to indicate what database to query in the declaration of a cursor. Here is a short snippet of the code :
CREATE PROCEDURE `update_cdrs_lnp_data`(IN dbName VARCHAR(25), OUT returnCode SMALLINT)
cdr_records:BEGIN
DECLARE cdr_record_cursor CURSOR FOR
SELECT cdrs_id, called, calling FROM dbName.cdrs WHERE lrn_checked = 'N';
# Setup logging
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
#call log_debug('Got exception in update_cdrs_lnp_data');
SET returnCode = -1;
END;
As you can see, I'm TRYING to use the variable dbName to indicate in which database the query should occur within. However, MySQL will NOT allow that. I also tried things such as :
CREATE PROCEDURE `update_cdrs_lnp_data`(IN dbName VARCHAR(25), OUT returnCode SMALLINT)
cdr_records:BEGIN
DECLARE cdr_record_cursor CURSOR FOR
SET #query = CONCAT("SELECT cdrs_id, called, calling FROM " ,dbName, ".cdrs WHERE lrn_checked = 'N' ");
PREPARE STMT FROM #query;
EXECUTE STMT;
# Setup logging
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
#call log_debug('Got exception in update_cdrs_lnp_data');
SET returnCode = -1;
END;
Of course this doesn't work either as MySQL only allows a standard SQL statement in the cursor declaration.
Can anyone think of a way to use the same stored procedure in multiple databases by passing in the name of the db that should be affected?
The answer of Vijay Jadhav is the right way to solve this limitation by MySQL. Actually, you need 3 proc to accomplish it:
proc1 using Vijay Jadhav's way, works like a data collector. You need to pass the variables to proc1 and let it create the tmp table for proc2. There is one limiation of Vijay's way, he should create a TEMPORARY table by using "CREATE TEMPORARY TABLE tmp_table_name SELECT ...". Because temporary table is thread safe.
proc2 declare the cursor on the tmp table which is created by proc1. Since the tmp table is already known and hard coded into the declaration, no more "table not found" error.
proc3 works like a "main" function, with all the parameters need to be sent to proc1 and proc2. proc3 simply calls proc1 first and then proc2 with the parameters need by each proc.
p.s Need to set system variable "sql_notes" to 0, otherwise proc1 will stop on DROP TABLE command.
Here is my example:
CREATE PROCEDURE `proc1`(SourceDBName CHAR(50), SourceTableName CHAR(50))
BEGIN
DECLARE SQLStmt TEXT;
SET #SQLStmt = CONCAT('DROP TEMPORARY TABLE IF EXISTS tmp_table_name');
PREPARE Stmt FROM #SQLStmt;
EXECUTE Stmt;
DEALLOCATE PREPARE Stmt;
SET #SQLStmt = CONCAT('CREATE TEMPORARY TABLE tmp_table_name SELECT ... FROM ',SourceDBName,'.',SourceTableName,' WHERE ... ');
PREPARE Stmt FROM #SQLStmt;
EXECUTE Stmt;
DEALLOCATE PREPARE Stmt;
END$$
CREATE PROCEDURE `proc2`(TargetDBName CHAR(50), TargetTemplateTableName CHAR(50))
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE FieldValue CHAR(50);
DECLARE CursorSegment CURSOR FOR SELECT ... FROM tmp_table_name;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN CursorSegment;
REPEAT
FETCH CursorSegment INTO FieldValue;
IF NOT done THEN
...
END IF;
UNTIL done END REPEAT;
CLOSE CursorSegment;
END$$
CREATE PROCEDURE `proc3`(SourceDBName CHAR(50), SourceTableName CHAR(50), TargetDBName CHAR(50), TargetTemplateTableName CHAR(50))
BEGIN
CALL proc1(SourceDBName, SourceTableName);
CALL proc2(TargetDBName, TargetTemplateTableName);
END$$
No, you can't do that in cursors.
Maybe just prepared statements may do the job? :
delimiter ;;
create procedure test(in dbName varchar(40))
begin
set #query := CONCAT("SELECT * FROM " , dbName, ".db;");
PREPARE s from #query;
EXECUTE s;
DEALLOCATE PREPARE s;
end;;
delimiter ;
call test("mysql");
Try to create (temporary) table using prepared statement in a different procedure.
SET #query = CONCAT("CREATE TABLE temp_table AS SELECT cdrs_id, called, calling FROM " ,dbName, ".cdrs WHERE lrn_checked = 'N' ");
...
And then select data from that table in your 'test' procedure.
The answer to this is that it cannot be done. You cannot use variables in the cursor declaration. I appreciate noonex's response. However, his solution does not allow me to walk through the results. It simply executes the query.
create procedure test(in dbName varchar(40))
READS SQL DATA <- this line returns will allow you to walk through the results
begin
...
$result = call test("mysql");