0

I want to write a SQL script that can point out which of my records in my whole database aren't being used as a reference by other tables.

For example, if I had a 'Comments' table, I'd like to find out which records in my table doesn't have a relationship entry in Users, nor in Posts but without specifying these tables by name. Something like:

'COLLECT ALL TABLES'
   'LOOP OVER EACH ROW IN THIS TABLE'
      'CHECK OTHER TABLES TO SEE IF THIS PARTICULAR PK_ID IS PRESENT AS A FOREIGN KEY'
         'IF FOUND NOT FOUND'
            'DELETE THIS ROW FROM THIS TABLE'

I know how to do the 'IF NOT FOUND' and 'DELETE' part, but I'm struggling to find the first three steps. Any thoughts and/or useful tips or links are welcome. I'm happy to share my script as a comment on this post once its complete, as I think it can be useful by a lot of SQL developers.

THEoneANDonly
  • 382
  • 1
  • 2
  • 16
  • id suggest a set based approach NOT a row by row approach. you can find and delete rows with joins (https://stackoverflow.com/questions/3826929/sql-query-to-find-missing-rows-between-two-related-tables) and https://stackoverflow.com/questions/16481379/how-to-delete-using-inner-join-with-sql-server. if you are looking for generic logic, it can be developed, but honestly it is best to do this for the tables you are interested in. – Jeremy Aug 26 '20 at 17:22
  • Thank you for the answer, Jeremy! I will check out the resources you've provided me :-) – THEoneANDonly Aug 26 '20 at 17:29
  • Dear Jeremy, I have a Table 'Description' that holds the description for my ~80 tables. I'd like to find out which Description is no longer used. However I do see what you mean by saying 'best to do this for the tables you are interested in', but im interested in all 80 tables. I guess, that if I follow your comment, i'd better start with the biggest tables, and work my way around the database from there. But this will take a long time, I think it'd better write a script that can loop through all of the tables,I don't mind letting it run the whole night, as it will take some time to execute. – THEoneANDonly Aug 26 '20 at 17:34
  • better yet, sql server has internal views to see table and column definitions https://www.mssqltips.com/sqlservertutorial/196/informationschematables/ and https://www.mssqltips.com/sqlservertutorial/183/informationschemacolumns/. you can write some SQL to write SQL select queries with joins, then execute those in a script window – Jeremy Aug 26 '20 at 18:56
  • doing some searching you can potentially get defined FKs as well. https://dataedo.com/kb/query/sql-server/list-foreign-keys – Jeremy Aug 26 '20 at 18:57

1 Answers1

0

I figured out to write the script myself, here it is (using Dynamic SQL)

I had 185 tables and ~190.000 Description entries, I ended up with ~15.000 Description rows after the execution of this script. (the issue was that a new description was created upon changing an object's description, instead of updating it. After fixing this bug in the code I took on the database). The script took ~3 hours to complete.

USE [YOUR_DATABASE]

PRINT 'BEGIN SCRIPT'


-- put all the tables in your database in a table (where we will perform a cursor on)
SELECT TABLE_NAME INTO ALL_TABLES
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_CATALOG='YOUR_DATABASE' 

-- delete non relevant entities in ALL_TABLES table
delete from ALL_TABLES where TABLE_NAME = 'ALL_TABLES'  -- delete the 'all_tables' table
delete from ALL_TABLES where TABLE_NAME = 'DESCRIPTION' -- your to-check table

-- hide rowcounts
set nocount on


-- Variables to use in the cursors
DECLARE 
    @tableName NVARCHAR(MAX), 
    @descriptionId   INT,
    @descriptionInUse BIT;



-- cursor to get all the entities from ALL_TABLES
DECLARE cursor_allTables CURSOR
FOR SELECT 
        TABLE_NAME
    FROM 
        dbo.ALL_TABLES;


-- cursor to get all the entities out of your to-check table ( mine was Description )
DECLARE cursor_descriptionCleanUp CURSOR
FOR SELECT 
        Id
    FROM 
        dbo.Description;



-- start iterating over descriptions
OPEN cursor_descriptionCleanUp;
FETCH NEXT FROM cursor_descriptionCleanUp INTO 
    @descriptionId;

    
WHILE @@FETCH_STATUS = 0
    BEGIN
        -- set BIT to check if description is in use
        set @descriptionInUse = 0;
        -- start iterating over ALL_TABLE rows
        OPEN cursor_allTables;
            FETCH NEXT FROM cursor_allTables INTO 
                @tableName;
            WHILE @@FETCH_STATUS = 0 AND @descriptionInUse= 0
                BEGIN
                    -- check if this table has a DescriptionId
                    IF COL_LENGTH('dbo.'+@tableName, 'DescriptionId') IS NOT NULL
                        BEGIN
                            -- check the table
                            DECLARE @sql as nvarchar(MAX);
                            DECLARE @rowCount as int; 
                            SET @sql = 'SELECT DescriptionId FROM '+ @tableName+' where DescriptionId = '+ CAST(@descriptionId as nvarchar(MAX))
                            EXEC (@sql)
                            SELECT @rowCount = @@ROWCOUNT;
                            
                            -- If EXEC (@sql) returned more than 0 rows, this Description row is in use in a table -> mark it as used 
                            IF @rowCount > 0
                                BEGIN
                                    set @descriptionInUse= 1;
                                END
                        END
                        
                    FETCH NEXT FROM cursor_allTables INTO 
                        @tableName;
                END;
            
        CLOSE cursor_allTables;
        
    --if description row is not in use -> delete this row
        IF @descriptionInUse = 0
            BEGIN
                DELETE FROM Description where Id = @descriptionId;
                PRINT 'DELETE DESCRIPTION WITH ID: ' + CAST(@descriptionId as nvarchar(MAX))
                PRINT'----------------------------------------------'
            END

            -- fetch next Description Id  from Descriptions
        FETCH NEXT FROM cursor_descriptionCleanUp INTO 
            @descriptionId;
    END;

CLOSE cursor_descriptionCleanUp;

-- clean up 
DEALLOCATE cursor_descriptionCleanUp;
DEALLOCATE cursor_allTables;
drop table ALL_TABLES
-- set nocount back to on 
set nocount off

I hope it can help others :-)

THEoneANDonly
  • 382
  • 1
  • 2
  • 16