2014-10-08 11 views
5

Per tagliare un database di produzione per il caricamento in un sistema di test, abbiamo eliminato righe in molte tabelle. Questo ci ha lasciato con cruft in un paio di tavoli, vale a dire le righe che non sono più utilizzate in nessuna relazione FK. Quello che voglio ottenere è come la garbage collection in Java.Elimina tutte le righe di tutte le tabelle che non sono più utilizzate in nessuna relazione FK

O per dirla in un altro modo: Se ho tabelle M nel database. N di essi (vale a dire la maggior parte ma non tutti) hanno relazioni con le chiavi estranee. Ho eliminato un paio di righe di alto livello (vale a dire che hanno solo relazioni FK in uscita) tramite SQL. Questo lascia le righe solo nelle tabelle correlate.

Qualcuno ha una procedura memorizzata SQL o un programma Java che trova le tabelle N e quindi segue tutte le relazioni FK per eliminare le righe che non sono più necessarie.

Se trovassi le tabelle N troppo complesse, potrei probabilmente fornire allo script un elenco di tabelle da analizzare o, preferibilmente, un elenco negativo di tabelle da ignorare.

Si noti inoltre:

  1. Abbiamo alcune tabelle che vengono utilizzati in molti (> 50) Relazioni FK, vale a dire A, B, C, ... tutte le righe di utilizzo in Z.
  2. Tutte le relazioni FK utilizzano la colonna tecnica PK, che è sempre una singola colonna.
+0

Questa domanda mostra come raccogliere le informazioni relative FK: http://stackoverflow.com/questions/ 273794/mysql-how-to-determinare-foreign-key-relations-programmaticamente –

+0

Questa domanda spiega come eliminare tutte le righe relative a una singola relazione FK: http://stackoverflow.com/questions/3164840/mysql-attempting-to -delete-all-rows-which-are-not-constrained-by-foreign-key –

+0

SQL dinamico in stored procedure: http://stackoverflow.com/questions/190776/how-to-have-dynamic-sql-in-mysql-stored-procedure –

risposta

1

Anche semplici stored procedure sono di solito un po 'brutto, e questo è stato un esercizio interessante nello spingere stored procedure ben oltre il punto in cui è facile prenderli.

Per utilizzare il codice qui sotto, avviare la shell di MySQL, use il database di destinazione, incollare il grosso blocco di stored procedure dal basso, e quindi eseguire

CALL delete_orphans_from_all_tables(); 

per eliminare tutte le righe orfani provenienti da tutte le tabelle nella vostra Banca dati.

di fornire una panoramica zoom-out:

  • delete_orphans_from_all_tables è il punto di ingresso. Tutti gli altri sproc sono preceduti da dofat per chiarire che si riferiscono a delete_orphans_from_all_tables e renderlo meno rumoroso per farli andare in giro.
  • delete_orphans_from_all_tables funziona chiamando lo dofat_delete_orphans_from_all_tables_iter fino a quando non ci sono più righe da eliminare.
  • dofat_delete_orphans_from_all_tables_iter funziona eseguendo il loop su tutte le tabelle che sono target di vincoli di chiave esterna e per ogni tabella che elimina tutte le righe a cui non si fa riferimento da nessuna parte.

Ecco il codice:

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 ; 

Per inciso, questo esercizio mi ha insegnato un paio di cose che rendono la scrittura di codice di questo livello di complessità utilizzando MySQL sprocs un business frustrante. Ne parlo solo perché potrebbero aiutarti, o un futuro lettore curioso, capire quali sono le pazzesche scelte stilistiche nel codice sopra.

  • Sintassi grossolanamente sintetica e boilerplate per cose semplici. per esempio.
    • dover dichiarare e assegnare su diverse linee
    • necessitano per impostare delimitatori attorno procedura definizioni
    • dover usare una combinazione PREPARE/EXECUTE utilizzare SQL dinamico).
  • assoluta mancanza di referential transparency:
    • PREPARE stmt FROM CONCAT(...); è un errore di sintassi, mentre @foo = CONCAT(...); PREPARE stmt FROM @foo; non è.
    • EXECUTE stmt USING @foo va bene, ma EXECUTE stmt USING foo dove foo è una variabile di procedura è un errore di sintassi.
    • Un'istruzione SELECT e una procedura la cui ultima istruzione è un'istruzione select restituiscono entrambi un set di risultati, ma praticamente tutto ciò che si vorrebbe fare con un set di risultati (come il looping su di esso o il controllo se qualcosa è IN it) può essere indirizzato solo a una dichiarazione SELECT, non a una dichiarazione CALL.
    • È possibile passare una variabile di sessione come parametro OUT a uno sproc, ma non è possibile passare una variabile sproc come parametro OUT a un sproc.
  • restrizioni del tutto arbitraria e comportamenti bizzarri che ti pigliano:
    • No SQL dinamico consentito nelle funzioni, solo nelle procedure
    • utilizzando un cursore per andare a prendere da una colonna in una variabile di procedura con lo stesso nome sempre imposta la variabile NULL ma getta alcun avviso o errore
  • la mancanza di capacità di passare in modo pulito set di risultati tra le procedure

    01.235.

    I set di risultati sono un tipo base in SQL; sono ciò che restituisce SELECT s e li si considera come oggetti quando si utilizza SQL dal livello applicazione. Ma all'interno di uno sproc MySQL, non è possibile assegnarli a variabili o passarle da uno sproc all'altro. Se hai veramente bisogno di questa funzionalità, devi avere uno sproc di scrivere un set di risultati in una tabella temporanea in modo che un altro sproc possa leggerlo.

  • costrutti e idiomi eccentrico e poco familiari:
    • tre modi equivalenti di assegnare ad una variabile - SET foo = bar, SELECT foo = bar e SELECT bar INTO foo.
    • Ci si aspetterebbe che si debbano utilizzare le variabili di procedura per tutto lo stato ed evitare le variabili di sessione per gli stessi motivi per cui si evitano i globali in un normale linguaggio di programmazione. Ma in realtà è necessario utilizzare le variabili di sessione ovunque perché molti costrutti linguistici (come i parametri OUT e EXECUTE) non accetteranno nessun altro tipo di variabile.
    • La sintassi per l'utilizzo di un cursore per eseguire il loop su un set di risultati sembra appena aliena.

Nonostante questi ostacoli, è ancora possibile mettere insieme piccoli programmi come questo con sprocs se siete determinati.

+0

Grazie, questo codice fa quello che voglio. Una domanda riguardante la sintassi, però: cosa significa 'colonna IN (...) <=> 1'? Qual è la differenza con 'colonna IN (...)'? –

+1

@AaronDigulla la differenza è che ''foo' NON IN ('bar', NULL)' è 'NULL', mentre' NOT ('bar' IN ('foo', NULL) <=> 1) 'è' TRUE'. Questo perché 'IN' restituisce NULL invece di FALSE quando l'operando di sinistra non è nell'operando di destra ma l'operando di destra contiene un NULL, e poiché' NOT NULL' è 'NULL'. Questo comportamento di IN è molto intuitivo (sebbene faccia parte dello standard ANSI SQL, non una stranezza di MySQL, ed è coerente con il modo in cui il solito modo di gestire SQL di NULL come sconosciuto quando si valutano le condizioni booleane), e io Ho visto un collega perdere mezza giornata prima. –

5

Questo problema viene risolto nel Performance blog MySQL, http://www.percona.com/blog/2011/11/18/eventual-consistency-in-mysql/

Egli fornisce il seguente meta interrogazione, per generare query che identificheranno i nodi orfani;

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; 

Ho convertito questo per trovare genitori senza figli, producendo;

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; 
+1

La prima query è quasi ciò di cui ho bisogno ma ha due bug. Uno che potrei risolvere: limita la query a uno schema aggiungendo 'AND K.TABLE_SCHEMA = '...'' prima di 'GROUP BY'. L'altro problema è che non unisce abbastanza tabelle quando A e B hanno una relazione FK con C. In questo caso, ottengo due query. La query per A riporterà tutti i nodi in B come orfani e viceversa. Puoi aggiustarlo (unisci tutte le righe con lo stesso 'REFERENCED_TABLE_NAME' in una singola query)? –

+0

Solo bisogno di qualche chiarimento, la prima domanda trova bambini senza genitori - Sei sicuro che questo è quello che vuoi? Dalla tua richiesta originale cercavi i genitori che non avevano figli - seconda domanda. In secondo luogo, puoi andare su più livelli, ma devi fare l'alias della tabella dello schema per ogni livello: quanto vuoi andare in profondità? – harvey

+0

Voglio le righe orfane; Non sono sicuro del perché pensi che volessi genitori senza figli. Forse un fraintendimento? Da quello che so, non posso cancellare le righe che sono usate in una relazione FK (cioè non posso cancellare un figlio/dipendenza a patto che una riga genitore contenga ancora il PK del child/dep). Ciò significa che non posso creare genitori senza figli senza disabilitare prima le relazioni FK. –

0

Poiché ho avuto alcuni strani errori di sintassi SQL, ecco una soluzione che utilizza SQL dalla risposta accettata e Groovy. Utilizzare orphanedNodeStatistics() per ottenere il numero di nodi per tabella che verrebbe eliminato, dumpOrphanedNodes(String tableName) per scaricare i PK dei nodi che verrebbero eliminati e deleteOrphanedNodes(String tableName) per eliminarli.

Per eliminare tutti loro, iterare l'insieme restituito da 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)

Problemi correlati