How do you insert selected rows from table_source to table_target using SQL in MySQL where:
Both tables have the same schema
All columns should transfer except for the auto-increment id
Without explicitly writing all the column names, as that would be tedious
The trivial INSERT INTO table_target SELECT * FROM table_source fails on duplicate entries for primary key.
Either you list all of the fields you want in the insert...select, or you use something else externally to build the list for you.
SQL does not have something like SELECT * except somefield FROM, so you'll have to bite the bullet and write out the field names.
Column names have to be specified -
INSERT INTO table_target SELECT NULL, column_name1, column_name2, column_name3, ...
FROM table_source;
Just pass NULL as a value for the auto-increment id field.
Of course, primary key must be unique. It depends on what you want to achieve, but you could exclude rows with a primary key that already exists.
INSERT INTO table_target SELECT * FROM table_source
WHERE table_source.id NOT IN (SELECT id FROM table_target)
UPDATE: since you also need the extra rows, you should resolve the conflict first, does table_source have relationships? If not you could change those keys:
UPDATE table_source SET id = id + 1000
WHERE id IN (SELECT id FROM table_target)
Where 1000, is a constant, big enough so they go after the end of your table.
Tedious but safe and correct.
Writing INSERT statements without providing a list of columns leads to code that's hard to debug and, more importantly, very fragile code that will break if the definition of the table is changed.
If you absolutely can't write the column names out yourself then it's relatively easy to build a tool into your code that will create the comma-separated list for you.
This is my final solution to mass update with 'replace insert' command.
SET ##session.group_concat_max_len = ##global.max_allowed_packet;
SET #schema_db = 'db';
SET #tabl = 'table';
SET #cols = (SELECT CONCAT('`',GROUP_CONCAT(COLUMN_NAME SEPARATOR '`, `'), '`') FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = #schema_db AND TABLE_NAME = #tabl GROUP BY TABLE_NAME);
SET #Querystr = CONCAT('REPLACE INTO ',#schema_db,'.',#tabl,' SELECT ', #cols,' FROM import.tbl_', #tabl);
PREPARE stmt FROM #Querystr;
EXECUTE stmt;
I think you could use syntax like:
INSERT INTO table (a,b,c) VALUES (1,2,3)
ON DUPLICATE KEY UPDATE c=c+1;
REF: http://dev.mysql.com/doc/refman/5.0/en/insert-on-duplicate.html
Hope it helps
INSERT IGNORE just "bypass" the duplicate rows.
http://dev.mysql.com/doc/refman/5.5/en/insert.html
You can probably do it with prepared statements.
PREPARE table_target_insert FROM 'INSERT INTO table_target SELECT ? FROM table_source';
SET #cols:='';
SELECT #cols:=GROUP_CONCAT(IF(column_name = 'id','NULL',column_name) separator ",") FROM information_schema.columns WHERE table_name='table_source';
EXECUTE table_target_insert USING #cols;
It seems as if columns can not be given as a place holder in a MySQL Prepared Statement. I have compiled the following solution for testing:
SET #schema_db = 'DB';
SET #table = 'table';
SET #cols = (SELECT CONCAT(GROUP_CONCAT(COLUMN_NAME SEPARATOR ', '), "\n") FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = #schema_db AND TABLE_NAME = #table GROUP BY TABLE_NAME);
SET #Querystr = CONCAT('SELECT',' ', #cols,' ','FROM',' ',#schema_db,'.',#table,' ', 'Limit 5');
PREPARE stmt FROM #Querystr;
EXECUTE stmt;
You can use dynamic query:
DECLARE #Columns VARCHAR(MAX)=''
DECLARE #Query VARCHAR(MAX)=''
SELECT
#Columns = ISNULL(#Columns +',', '') + T.COLUMN_NAME
FROM
(
select name as COLUMN_NAME from sys.all_columns
where object_id = (select object_id from sys.tables where name = 'Source_Table')
and is_identity = 0
)T
set #Query = 'insert into Target_Table (' + SUBSTRING(#Columns,2 , 9999) + ') select ' + SUBSTRING(#Columns,2 , 9999) + ' from Source_Table';
PRINT #Query
EXEC(#Query)
The easiest way to do it is to use phpmyadmin to write the list of columns, then to change it as needed, in the example below I want to duplicate row with id=1078 and in this table I have id unique auto increment and alias unique.therefore I created my query as follow, with id & alias replaced by a desired value. and it worked like a charm.
INSERT INTO sy3_menuselect 1079, menutype, title, "alias", note, path, link, type, published, parent_id, level, component_id, checked_out, checked_out_time, browserNav, access, img, template_style_id, params, lft, rgt, home, language, client_id from sy3_menuwhere id=1078
Alternatively, to auto increment id, use the following Join statement:
INSERT INTO sy3_menuselect *
from (SELECT MAX(id+1 )from sy3_menu)a
join (select menutype, title, "alias", note, path, link, type, published, parent_id, level, component_id, checked_out, checked_out_time, browserNav, access, img, template_style_id, params, lft, rgt, home, language, client_idfrom sy3_menuwhere id=1079)b
Related
I know that there are several ways to find which row's column contains a string, like using [column name] regexp ' ' or [column name] like ' '
while currently what I need some help is I have a table with several columns, all of there are varchar or text and I am not sure which column contains a certain string. Just say that I want to search a "xxx from a table. Several different columns could contain this string or not. Is there a way that I could find which column contains this string?
I have a thinking and the solution could be
select * from [table name] where [column1] regexp 'xxx' or
[column2] regexp 'xxx' or ...... [column39] regexp 'xxx' or .....
[colum60] regexp 'xxx' or ... or [column 80] regexp 'xxx';
I do not want the query like this. Is there another effective way?
To give a better example, say that we are searching for a table that belongs to a blog.
We have title, URL, content, key words, tag, comment and so on. Now we just say, if any blog article is related to "database-normalization", this word may appear in the title, URL or content or anywhere, and I do not want to write it one by one like
where title regexp 'database-normalization' or content regexp 'database-normalization' or url regexp 'database-normalization'......
as when there are hundreds columns, I need to write a hundred, or in this case is there an effective way instead of write hundred or statement? Like using if-else or collections or some others to build the query.
If you want a pure dynamic way, you can try this. I've tried it long back on sql-server and hope it may help you.
#TMP_TABLE -- a temporary table
- PK, IDENTITY
- TABLE_NAME
- COLUMN_NAME
- IS_EXIST
INSERT INTO #TMP_TABLE (TABLE_NAME,COLUMN_NAME)
SELECT C.TABLE_NAME, COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS C
WHERE C.TABLE_NAME = <your-table> AND C.DATA_TYPE = 'varchar'; -- you can modify it to handle multiple table at once.
-- boundaries
SET #MINID = (SELECT ISNULL(MIN(<PK>),0) FROM #TMP_TABLE );
SET #MAXID = (SELECT ISNULL(MAX(<PK>),0) FROM #TMP_TABLE );
WHILE ((#MINID<=#MAXID) AND (#MINID<>0))
BEGIN
SELECT #TABLE_NAME = TABLE_NAME,#COLUMN_NAME = COLUMN_NAME
FROM #TMP_TABLE
WHERE <PK> = #MINID;
SET #sqlString = ' UPDATE #TMP_TABLE
SET IS_EXIST = 1
WHERE EXIST (SELECT 1 FROM '+ #TABLE_NAME+' WHERE '+ #COLUMN_NAME +' = ''demo.webstater.com'') AND <PK> = '+ #MINID;
EXEC(#sql) ;
SET #MINID = (SELECT MIN(<PK>) FROM #TMP_TABLE WHERE <PK> > #MINID );
END
SELECT * FROM #TMP_TABLE WHERE IS_EXIST = 1 ; -- will give you matched results.
If you know the columns in advance, what you proposed is probably the most effective way (if a little verbose).
Otherwise, you could get the column names from INFORMATION_SCHEMA.COLUMNS and construct dynamic SQL based on that.
His question is not to query specific columns with like clause. He has been asking to apply same pattern across columns dynamically.
Example: Table having 3 columns - FirstName, LastName, Address and pattern matching is "starts with A" then resulting query should be:
Select * From Customer where FirstName like 'A%" or LastName like 'A%" or Address like 'A%'
If you want to build query in business layer, this could easily be done with reflection along with EF.
If you are motivated to do in database then you can achieve by building query dynamically and then execute through sp_executesql.
Try this (Just pass tablename and the string to be find)-:
create proc usp_findString
#tablename varchar(500),
#string varchar(max)
as
Begin
Declare #sql2 varchar(max),#sql nvarchar(max)
SELECT #sql2=
STUFF((SELECT ', case when '+QUOTENAME(NAME)+'='''+#string+''' then 1 else 0 end as '+NAME
FROM (select a.name from sys.columns a join sys.tables b on a.[object_id]=b.[object_id] where b.name=#tablename) T1
--WHERE T1.ID=T2.ID
FOR XML PATH('')),1,1,'')
--print #string
set #sql='select '+#sql2+' from '+#tablename
print #sql
EXEC sp_executesql #sql
End
SQL Server 2014
One way is to use CASE to check the substring existence with LOCATE in mysql and return the column but all you have to check in every column of the table as below:
CREATE TABLE test(col1 VARCHAR(1000), col2 VARCHAR(1000), col3 VARCHAR(1000))
INSERT INTO test VALUES
('while currently what I need some help is I have a table with 10 columns',
'contains a certain string. Just say that I want to search a table',
'contains a certain string demo.webstater.com')
SELECT (CASE WHEN LOCATE('demo.webstater.com', col1, 1) > 0 THEN 'col1'
WHEN LOCATE('demo.webstater.com', col2, 1) > 0 THEN 'col2'
WHEN LOCATE('demo.webstater.com', col3, 1) > 0 THEN 'col3'
END) whichColumn
FROM test
OUTPUT:
whichColumn
col3
There are many ways in which you can do your analysis. You can use "LIKE A%%" if it starts from A in SQL, "REGEX" LibrarY for multiple checks.
I'm trying to update a column (in this case, a date) that is present on most of the tables on my database. Sadly, my database has more than 100 tables already created and full of information. Is there any way to loop through them and just use:
UPDATE SET date = '2016-04-20' WHERE name = 'Example'
on the loop?
One painless option would be to create a query which generates the UPDATE statements you want to run on all the tables:
SELECT CONCAT('UPDATE ', a.table_name, ' SET date = "2016-04-20" WHERE name = "Example";')
FROM information_schema.tables a
WHERE a.table_schema = 'YourDBNameHere'
You can copy the output from this query, paste it in the query editor, and run it.
Update:
As #PaulSpiegel pointed out, the above solution might be inconvenient if one be using an editor such as HeidiSQL, because it would require manually copying each record in the result set. Employing a trick using GROUP_CONCAT() would give a single string containing every desired UPDATE query in it:
SELECT GROUP_CONCAT(t.query SEPARATOR '; ')
FROM
(
SELECT CONCAT('UPDATE ', a.table_name,
' SET date = "2016-04-20" WHERE name = "Example";') AS query,
'1' AS id
FROM information_schema.tables a
WHERE a.table_schema = 'YourDBNameHere'
) t
GROUP BY t.id
You can use SHOW TABLES command to list all tables in database. Next you can check if column presented in table with SHOW COLUMNS command. It can be used this way:
SHOW COLUMNS FROM `table_name` LIKE `column_name`
If this query returns result, then column exists and you can perform UPDATE query on it.
Update
You can check this procedure on sqlfiddle.
CREATE PROCEDURE UpdateTables (IN WhereColumn VARCHAR(10),
IN WhereValue VARCHAR(10),
IN UpdateColumn VARCHAR(10),
IN UpdateValue VARCHAR(10))
BEGIN
DECLARE Finished BOOL DEFAULT FALSE;
DECLARE TableName VARCHAR(10);
DECLARE TablesCursor CURSOR FOR
SELECT c1.TABLE_NAME
FROM INFORMATION_SCHEMA.COLUMNS c1
JOIN INFORMATION_SCHEMA.COLUMNS c2 ON (c1.TABLE_SCHEMA = c2.TABLE_SCHEMA AND c1.TABLE_NAME = c2.TABLE_NAME)
WHERE c1.TABLE_SCHEMA = DATABASE()
AND c1.COLUMN_NAME = WhereColumn
AND c2.COLUMN_NAME = UpdateColumn;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET Finished = TRUE;
OPEN TablesCursor;
MainLoop: LOOP
FETCH TablesCursor INTO TableName;
IF Finished THEN
LEAVE MainLoop;
END IF;
SET #queryText = CONCAT('UPDATE ', TableName, ' SET ', UpdateColumn, '=', QUOTE(UpdateValue), ' WHERE ', WhereColumn, '=', QUOTE(WhereValue));
PREPARE updateQuery FROM #queryText;
EXECUTE updateQuery;
DEALLOCATE PREPARE updateQuery;
END LOOP;
CLOSE TablesCursor;
END
This is just an example how to iterate through all tables in database and perform some action with them. Procedure can be changed according to your needs.
Assuming you are using MySQL, You can use Stored Procedure.
This post is a very helpful.
Mysql-loop-through-tables
I'm trying to write a prodecure that updates a value in a given column-name where the users id equals given user ID.
_strong_1 is a variable that contains the column name, i.e: 'category_1', for example.
SELECT COLUMN_NAME FROM information_schema.`COLUMNS` C
WHERE table_name = 'subscribers_preferences' AND COLUMN_NAME LIKE _strong_1 INTO #columns;
SET #table = 'subscribers_preferences';
SET #s = CONCAT('UPDATE ',#table,' SET ', #columns = 1);
PREPARE stmt FROM #s;
EXECUTE stmt;
There's an error within the 'SET #s =' statement. I can get it to work with a simple SELECT statement, but UPDATE is being tricky.
Thanks in advance.
You need to put = 1 in quotes.
SET #s = CONCAT('UPDATE ',#table,' SET ', #columns, ' = 1');
Otherwise, you're comparing #columns with 1, and concatenating either 1 or 0 (probably always 0, since I doubt you have a column named 1) to the SQL, which is creating invalid SQL.
Note that the above code will only update one column. If #columns is supposed to hold 3 columns, you need to use GROUP_CONCAT in your query that sets it.
SELECT GROUP_CONCAT(CONCAT(column_name, ' = 1')) AS #columns
FROM information_schema.columns
WHERE table_name = 'subscribers_preferences' and column_name LIKE _strong_1;
SET #table = 'subscribers_preferences';
SET #s = CONCAT('UPDATE ',#table,' SET ', #columns);
I suspect you also need to add a WHERE clause to this SQL so it just updates the row for the given ID. As currently written, it will update all rows.
The fact that you need to write the query like this suggests improper normallization of your data. Instead of having each preference option in a different column, they should be different rows of the table, with the key being something like (user_id, setting_name).
In order to trim a production database for loading in a test system, we've deleted rows in many tables. This now left us with cruft in a couple of tables, namely rows which aren't used in any FK relation anymore. What I want to achieve is like the garbage collection in Java.
Or to put it another way: If I have M tables in the database. N of them (i.e. most but not all) have foreign key relations. I've deleted a couple of high level rows (i.e. which only have outgoing FK relations) via SQL. This leaves the rows in the related tables alone.
Does someone have a SQL stored procedure or a Java program which finds the N tables and then follows all the FK relations to delete rows which are no longer needed.
If finding the N tables to too complex, I could probably provide the script a list of tables to scan or, preferably, a negative list of tables to ignore.
Also note:
We have some tables which are used in many (>50) FK relations, i.e. A, B, C, ... all use rows in Z.
All FK relations use the technical PK column which is always a single column.
This issue is addressed in the MySQL Performance blog, http://www.percona.com/blog/2011/11/18/eventual-consistency-in-mysql/
He provides the following meta query, to generate queries that will identify orphaned nodes;
SELECT CONCAT(
'SELECT ', GROUP_CONCAT(DISTINCT CONCAT(K.CONSTRAINT_NAME, '.', P.COLUMN_NAME,
' AS `', P.TABLE_SCHEMA, '.', P.TABLE_NAME, '.', P.COLUMN_NAME, '`') ORDER BY P.ORDINAL_POSITION), ' ',
'FROM ', K.TABLE_SCHEMA, '.', K.TABLE_NAME, ' AS ', K.CONSTRAINT_NAME, ' ',
'LEFT OUTER JOIN ', K.REFERENCED_TABLE_SCHEMA, '.', K.REFERENCED_TABLE_NAME, ' AS ', K.REFERENCED_TABLE_NAME, ' ',
' ON (', GROUP_CONCAT(CONCAT(K.CONSTRAINT_NAME, '.', K.COLUMN_NAME) ORDER BY K.ORDINAL_POSITION),
') = (', GROUP_CONCAT(CONCAT(K.REFERENCED_TABLE_NAME, '.', K.REFERENCED_COLUMN_NAME) ORDER BY K.ORDINAL_POSITION), ') ',
'WHERE ', K.REFERENCED_TABLE_NAME, '.', K.REFERENCED_COLUMN_NAME, ' IS NULL;'
) AS _SQL
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE K
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE P
ON (K.TABLE_SCHEMA, K.TABLE_NAME) = (P.TABLE_SCHEMA, P.TABLE_NAME)
AND P.CONSTRAINT_NAME = 'PRIMARY'
WHERE K.REFERENCED_TABLE_NAME IS NOT NULL
GROUP BY K.CONSTRAINT_NAME;
I converted this to find childless parents, producing;
SELECT CONCAT(
'SELECT ', GROUP_CONCAT(CONCAT(K.REFERENCED_TABLE_NAME, '.', K.REFERENCED_COLUMN_NAME) ORDER BY K.ORDINAL_POSITION), ' ',
'FROM ', K.REFERENCED_TABLE_SCHEMA, '.', K.REFERENCED_TABLE_NAME, ' AS ', K.REFERENCED_TABLE_NAME, ' ',
'LEFT OUTER JOIN ', K.TABLE_SCHEMA, '.', K.TABLE_NAME, ' AS ', K.CONSTRAINT_NAME, ' ',
' ON (', GROUP_CONCAT(CONCAT(K.CONSTRAINT_NAME, '.', K.COLUMN_NAME) ORDER BY K.ORDINAL_POSITION),
') = (', GROUP_CONCAT(CONCAT(K.REFERENCED_TABLE_NAME, '.', K.REFERENCED_COLUMN_NAME) ORDER BY K.ORDINAL_POSITION), ') ',
'WHERE ', K.CONSTRAINT_NAME, '.', K.COLUMN_NAME, ' IS NULL;'
) AS _SQL
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE K
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE P
ON (K.TABLE_SCHEMA, K.TABLE_NAME) = (P.TABLE_SCHEMA, P.TABLE_NAME)
AND P.CONSTRAINT_NAME = 'PRIMARY'
WHERE K.REFERENCED_TABLE_NAME IS NOT NULL
GROUP BY K.CONSTRAINT_NAME;
Even simple stored procedures are usually a little ugly, and this was an interesting exercise in pushing stored procedures well beyond the point where it's easy to take them.
To use the code below, launch your MySQL shell, use your target database, paste the big block of stored procedures from below, and then execute
CALL delete_orphans_from_all_tables();
to delete all orphaned rows from all tables in your database.
To provide a zoomed-out overview:
delete_orphans_from_all_tables is the entry point. All other sprocs are prefixed with dofat to make clear that they relate to delete_orphans_from_all_tables and make it less noisy to have them kicking around.
delete_orphans_from_all_tables works by calling dofat_delete_orphans_from_all_tables_iter repeatedly until there are no more rows to delete.
dofat_delete_orphans_from_all_tables_iter works by looping over all the tables that are targets of foreign key constraints, and for each table deleting all rows that currently aren't referenced from anywhere.
Here's the code:
delimiter //
CREATE PROCEDURE dofat_store_tables_targeted_by_foreign_keys ()
BEGIN
-- This procedure creates a temporary table called TargetTableNames
-- containing the names of all tables that are the target of any foreign
-- key relation.
SET #db_name = DATABASE();
DROP TEMPORARY TABLE IF EXISTS TargetTableNames;
CREATE TEMPORARY TABLE TargetTableNames (
table_name VARCHAR(255) NOT NULL
);
PREPARE stmt FROM
'INSERT INTO TargetTableNames(table_name)
SELECT DISTINCT referenced_table_name
FROM INFORMATION_SCHEMA.key_column_usage
WHERE referenced_table_schema = ?';
EXECUTE stmt USING #db_name;
END//
CREATE PROCEDURE dofat_deletion_clause_for_table(
IN table_name VARCHAR(255), OUT result text
)
DETERMINISTIC
BEGIN
-- Given a table Foo, where Foo.col1 is referenced by Bar.col1, and
-- Foo.col2 is referenced by Qwe.col3, this will return a string like:
--
-- NOT (Foo.col1 IN (SELECT col1 FROM BAR) <=> 1) AND
-- NOT (Foo.col2 IN (SELECT col3 FROM Qwe) <=> 1)
--
-- This is used by dofat_delete_orphans_from_table to target only orphaned
-- rows.
--
-- The odd-looking `NOT (x IN y <=> 1)` construct is used in favour of the
-- more obvious (x NOT IN y) construct to handle nulls properly; note that
-- (x NOT IN y) will evaluate to NULL if either x is NULL or if x is not in
-- y and *any* value in y is NULL.
SET #db_name = DATABASE();
SET #table_name = table_name;
PREPARE stmt FROM
'SELECT GROUP_CONCAT(
CONCAT(
\'NOT (\', #table_name, \'.\', referenced_column_name, \' IN (\',
\'SELECT \', column_name, \' FROM \', table_name, \')\',
\' <=> 1)\'
)
SEPARATOR \' AND \'
) INTO #result
FROM INFORMATION_SCHEMA.key_column_usage
WHERE
referenced_table_schema = ?
AND referenced_table_name = ?';
EXECUTE stmt USING #db_name, #table_name;
SET result = #result;
END//
CREATE PROCEDURE dofat_delete_orphans_from_table (table_name varchar(255))
BEGIN
-- Takes as an argument the name of a table that is the target of at least
-- one foreign key.
-- Deletes from that table all rows that are not currently referenced by
-- any foreign key.
CALL dofat_deletion_clause_for_table(table_name, #deletion_clause);
SET #stmt = CONCAT(
'DELETE FROM ', #table_name,
' WHERE ', #deletion_clause
);
PREPARE stmt FROM #stmt;
EXECUTE stmt;
END//
CREATE PROCEDURE dofat_delete_orphans_from_all_tables_iter(
OUT rows_deleted INT
)
BEGIN
-- dofat_store_tables_targeted_by_foreign_keys must be called before this
-- will work.
--
-- Loops ONCE over all tables that are currently referenced by a foreign
-- key. For each table, deletes all rows that are not currently referenced.
-- Note that this is not guaranteed to leave all tables without orphans,
-- since the deletion of rows from a table late in the sequence may leave
-- rows from a table early in the sequence orphaned.
DECLARE loop_done BOOL;
-- Variable name needs to differ from the column name we use to populate it
-- because of bug http://bugs.mysql.com/bug.php?id=28227
DECLARE table_name_ VARCHAR(255);
DECLARE curs CURSOR FOR SELECT table_name FROM TargetTableNames;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET loop_done = TRUE;
SET rows_deleted = 0;
SET loop_done = FALSE;
OPEN curs;
REPEAT
FETCH curs INTO table_name_;
CALL dofat_delete_orphans_from_table(table_name_);
SET rows_deleted = rows_deleted + ROW_COUNT();
UNTIL loop_done END REPEAT;
CLOSE curs;
END//
CREATE PROCEDURE delete_orphans_from_all_tables ()
BEGIN
CALL dofat_store_tables_targeted_by_foreign_keys();
REPEAT
CALL dofat_delete_orphans_from_all_tables_iter(#rows_deleted);
UNTIL #rows_deleted = 0 END REPEAT;
END//
delimiter ;
As an aside, this exercise has taught me about a few things that make writing code of this level of complexity using MySQL sprocs a frustrating business. I mention all these only because they may help you, or a curious future reader, understand what look like crazy stylistic choices in the code above.
Grossly verbose syntax and boilerplate for simple things. e.g.
needing to declare and assign on different lines
needing to set delimiters around procedure definitions
needing to use a PREPARE/EXECUTE combo to use dynamic SQL).
Utter lack of referential transparency:
PREPARE stmt FROM CONCAT( ... ); is a syntax error, while #foo = CONCAT( ... ); PREPARE stmt FROM #foo; is not.
EXECUTE stmt USING #foo is fine, but EXECUTE stmt USING foo where foo is a procedure variable is a syntax error.
A SELECT statement and a procedure whose last statement is a select statement both return a result set, but pretty much everything you'd ever like to do with a result set (like looping over it or checking if something is IN it) can only be targeted at a SELECT statement, not a CALL statement.
You can pass a session variable as an OUT parameter to a sproc, but you can't pass a sproc variable as an OUT parameter to a sproc.
Totally arbitrary restrictions and bizarre behaviours that blindside you:
No dynamic SQL allowed in functions, only in procedures
Using a cursor to fetch from a column into a procedure variable of the same name always sets the variable to NULL but throws no warning or error
Lack of ability to cleanly pass result sets between procedures
Result sets are a basic type in SQL; they're what SELECTs return and you think about them as objects when using SQL from the application layer. But within a MySQL sproc, you can't assign them to variables or pass them from one sproc to another. If you truly need this functionality, you have to have one sproc write a result set into a temporary table so that another sproc can read it.
Eccentric and unfamiliar constructs and idioms:
Three equivalent ways of assigning to a variable - SET foo = bar, SELECT foo = bar and SELECT bar INTO foo.
You'd expect that you should use procedure variables for all your state and avoid session variables for the same reasons that you avoid globals in a normal programming language. But in fact you need to use session variables everywhere because so many language constructs (like OUT params and EXECUTE) won't accept any other kind of variable.
The syntax for using a cursor to loop over a result set just looks alien.
Despite these obstacles, you can still piece together small programs like this with sprocs if you are determined.
Since I had some weird SQL syntax errors, here is a solution which uses SQL from the accepted answer and Groovy. Use orphanedNodeStatistics() to get the number of nodes per table which would be deleted, dumpOrphanedNodes(String tableName) to dump the PKs of nodes which would be deleted and deleteOrphanedNodes(String tableName) to delete them.
To delete all of them, iterate over the set returned by tablesTargetedByForeignKeys()
import groovy.sql.Sql
class OrphanNodesTool {
Sql sql;
String schema;
Set<String> tablesTargetedByForeignKeys() {
def query = '''\
SELECT referenced_table_name
FROM INFORMATION_SCHEMA.key_column_usage
WHERE referenced_table_schema = ?
'''
def result = new TreeSet()
sql.eachRow( query, [ schema ] ) { row ->
result << row[0]
}
return result
}
String conditionsToFindOrphans( String tableName ) {
List<String> conditions = []
def query = '''\
SELECT referenced_column_name, column_name, table_name
FROM INFORMATION_SCHEMA.key_column_usage
WHERE referenced_table_schema = ?
AND referenced_table_name = ?
'''
sql.eachRow( query, [ schema, tableName ] ) { row ->
conditions << "NOT (${tableName}.${row.referenced_column_name} IN (SELECT ${row.column_name} FROM ${row.table_name}) <=> 1)"
}
return conditions.join( '\nAND ' )
}
List<Long> listOrphanedNodes( String tableName ) {
def query = """\
SELECT ${tableName}.${tableName}_ID
FROM ${tableName}
WHERE ${conditionsToFindOrphans(tableName)}
""".toString()
def result = []
sql.eachRow( query ) { row ->
result << row[0]
}
return result
}
void dumpOrphanedNodes( String tableName ) {
def pks = listOrphanedNodes( tableName )
println( String.format( "%8d %s", pks.size(), tableName ) )
if( pks.size() < 10 ) {
pks.each {
println( String.format( "%16d", it as long ) )
}
} else {
pks.collate( 20 ) { chunk ->
chunk.each {
print( String.format( "%16d ", it as long ) )
}
println()
}
}
}
int countOrphanedNodes( String tableName ) {
def query = """\
SELECT COUNT(*)
FROM ${tableName}
WHERE ${conditionsToFindOrphans(tableName)}
""".toString()
int result;
sql.eachRow( query ) { row ->
result = row[0]
}
return result
}
int deleteOrphanedNodes( String tableName ) {
def query = """\
DELETE
FROM ${tableName}
WHERE ${conditionsToFindOrphans(tableName)}
""".toString()
int result = sql.execute( query )
return result
}
void orphanedNodeStatistics() {
def tableNames = tablesTargetedByForeignKeys()
for( String tableName : tableNames ) {
int n = countOrphanedNodes( tableName )
println( String.format( "%8d %s", n, tableName ) )
}
}
}
(gist)
Is it possible in a sequence of SQL statements to get the value of a field and use that to name a table in another statement? I'm not sure if that's clear, so here's an psudo-example of what I'm trying to do:
// dataType is equal to "ratings"
#var = select dataType from theTable where anID = 5;
// needs to run as "from ratings-table"
select field1,field2 from #var-table where anID = 5;
I've been reading http://dev.mysql.com/doc/refman/5.0/en/user-variables.html but either I don't properly understand this, or its not the solution I'm looking for.
Yes, you can do this using prepared statements:
SET #TableName := 'ratings';
SET #CreateQuery := CONCAT('SELECT `field1`, `field2` FROM `', #TableName, '-table` WHERE `anID` = 5');
PREPARE statementCreate FROM #CreateQuery;
EXECUTE statementCreate;