删除所有表中不再使用任何FK关系的所有行

Aar*_*lla 5 mysql garbage-collection stored-procedures

为了修剪生产数据库以便在测试系统中加载,我们删除了许多表中的行.现在这让我们陷入了几个表格,即不再用于任何FK关系的行.我想要实现的就像Java中的垃圾收集.

或者换句话说:如果我在数据库中有M个表.他们中的N个(即大多数但不是全部)具有外键关系.我通过SQL删除了几个高级行(即只有传出的FK关系).这样就只在相关表中留下了行.

有人有SQL存储过程或Java程序找到N个表,然后遵循所有FK关系来删除不再需要的行.

如果发现N表太复杂,我可能会为脚本提供要扫描的表列表,或者最好是要忽略的表的负列表.

另请注意:

  1. 我们有一些表,其在许多使用(> 50)FK关系,即A,B,C,...在所有使用行Z.
  2. 所有FK关系都使用技术PK列,该列始终是单列.

har*_*vey 5

MySQL性能博客中解决了此问题,http://www.percona.com/blog/2011/11/18/eventual-consistency-in-mysql/

他提供了以下元查询,以生成将识别孤立节点的查询;

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;
Run Code Online (Sandbox Code Playgroud)

我改变了这个,找到没有孩子的父母,生产;

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;
Run Code Online (Sandbox Code Playgroud)


Mar*_*ery 3

即使是简单的存储过程通常也有点难看,这是一个有趣的练习,它使存储过程远远超出了易于使用的程度。

要使用下面的代码,启动 MySQL shell、use目标数据库,粘贴下面的大块存储过程,然后执行

CALL delete_orphans_from_all_tables();
Run Code Online (Sandbox Code Playgroud)

从数据库中的所有表中删除所有孤立行。

要提供缩小的概述:

  • delete_orphans_from_all_tables是入口点。所有其他存储过程都以 为前缀,dofat以明确它们的相关性delete_orphans_from_all_tables并减少它们的干扰。
  • delete_orphans_from_all_tables其工作原理是dofat_delete_orphans_from_all_tables_iter重复调用,直到没有更多的行可以删除。
  • dofat_delete_orphans_from_all_tables_iter其工作原理是循环遍历作为外键约束目标的所有表,并为每个表删除当前未从任何地方引用的所有行。

这是代码:

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 ;
Run Code Online (Sandbox Code Playgroud)

顺便说一句,这个练习教会了我一些事情,这些事情使得使用 MySQL 存储过程编写这种复杂程度的代码成为一件令人沮丧的事情。我提到所有这些只是因为它们可能会帮助您或未来好奇的读者理解上面代码中看起来疯狂的风格选择。

  • 非常冗长的语法和简单事物的样板。例如
    • 需要在不同的行上声明和分配
    • 需要在过程定义周围设置分隔符
    • 需要使用PREPARE/EXECUTE组合来使用动态 SQL)。
  • 完全缺乏引用透明度
    • PREPARE stmt FROM CONCAT( ... );是语法错误,而@foo = CONCAT( ... ); PREPARE stmt FROM @foo;不是。
    • EXECUTE stmt USING @foo没问题,但是EXECUTE stmt USING foowhere foois a procedure variable 是语法错误。
    • 最后一个SELECT语句是 select 语句的语句和过程都返回一个结果集,但几乎您想要对结果集执行的所有操作(例如循环它或检查是否是它IN)只能针对一个SELECT声明,不是一个CALL声明。
    • 您可以将会话变量作为 OUT 参数传递给存储过程,但不能将存储过程变量作为 OUT 参数传递给存储过程。
  • 完全任意的限制和奇怪的行为让你措手不及:
    • 函数中不允许使用动态 SQL,只能在过程中使用
    • 使用游标从列中提取到同名的过程变量中始终将变量设置为,NULL但不会引发警告或错误
  • 缺乏在过程之间干净地传递结果集的能力

    结果集是SQL中的基本类型;它们是SELECT返回的内容,当从应用程序层使用 SQL 时,您将它们视为对象。但在 MySQL 存储过程中,您无法将它们分配给变量或将它们从一个存储过程传递到另一个存储过程。如果您确实需要此功能,则必须让一个存储过程将结果集写入临时表,以便另一个存储过程可以读取它。

  • 古怪和不熟悉的结构和习语:
    • 分配给变量的三种等效方法 - SET foo = barSELECT foo = barSELECT bar INTO foo
    • 您可能希望对所有状态使用过程变量,并避免使用会话变量,其原因与在普通编程语言中避免全局变量的原因相同。但事实上,您需要在任何地方使用会话变量,因为许多语言结构(如 OUT params 和EXECUTE)不会接受任何其他类型的变量。
    • 使用游标循环结果集的语法看起来很陌生。

尽管存在这些障碍,如果您有决心,您仍然可以将这样的小程序与存储过程拼凑在一起。