0

Current working on a project where I have to update the data on 85 tables replacing the current empty string to a NULL value. it is a simple SQL query for this but since it is a sensitive environment, if anything goes wrong, we need to revert this.

The main idea was to create a table to save the data to roll back. but I am trying to avoid creating 85 tables.

I will give a smaller example:

there is 4 tables


   ------------------------------------
   |            airplane              |
   ------------------------------------
   | air_ID | color | tail_number     |
   ------------------------------------
   |  1     | red   |                 |
   |  2     | green |                 |
   |  3     | black |  21AF           |
   ------------------------------------
   ------------------------------------
   |            bus                   |
   ------------------------------------
   | bus_ID | color | tag_number      |
   ------------------------------------
   |  1     | red   |  AAY-464        |
   |  2     | green |                 |
   |  3     | black |                 |
   ------------------------------------
   ------------------------------------
   |            train                 |
   ------------------------------------
   | tr_ID  | color | designated_name |
   ------------------------------------
   |  1     | red   |  99212          |
   |  2     | green |                 |
   |  3     | black |                 |
   ------------------------------------
   ------------------------------------
   |         Cruise_Ship              |
   ------------------------------------
   | sea_ID | color | hull_number     |
   ------------------------------------
   |  1     | red   |                 |
   |  2     | green |  MAGDA          |
   |  3     | black |                 |
   ------------------------------------

So I created a temp table with the data


    -------------------------------------------------
    |         update_table                          |
    -------------------------------------------------
    | table_name | ID_colname | ID  |  col_name     |
    -------------------------------------------------
    |   airplane |   air_ID   |  1  | tail_number   |
    |   airplane |   air_ID   |  2  | tail_number   |
    |    bus     |   bus_ID   | 2   | tag_number    |
    |    bus     |   bus_ID   | 3   | tag_number    |
    |    train   |   tr_ID    |  2  |designated_name|
    |    train   |   tr_ID    |  3  |designated_name|
    |Cruise_Ship |   sea_ID   |  1  |  hull_number  |
    |Cruise_Ship |   sea_ID   |  3  |  hull_number  |
    -------------------------------------------------

With this table I was trying to generate a dynamic SQL to update all the tables with one call


    SET @SQLString = N'UPDATE @table 
        SET  @value = '+ @empty +'
        where @key = @id';

    SET @ParmDefinition = N'@table nvarchar(max),
        @value nvarchar(max) ,
        @key nvarchar(max) ,
        @id int';

    DECLARE @table nvarchar(255)
    DECLARE @value nvarchar(255)
    DECLARE @key nvarchar(255)
    DECLARE @id int

    select @table = table_name, @id = ID, @key = ID_colname , @value = col_name from update_table

    EXECUTE sp_executesql
        @SQLString
        ,@ParmDefinition
        ,@table
        ,@value
        ,@key
        ,@id
        ;

But this is not working, anyone has a idea on how to improve this query?

This is a high profile environment and the developers are not the ones executing the code, so it need to be customer proof. The code is ran overnight to not disturb daytime operations.

Raxl
  • 99
  • 1
  • 8
  • 2
    How big is the entire environment? Take a backup, restore the backup database, run your update, and if it hits the fan, roll it back. – dfundako Jun 25 '19 at 18:13
  • You can think about cursor. – mkRabbani Jun 25 '19 at 18:13
  • 3
    @mk No one should ever think about cursors. – dfundako Jun 25 '19 at 18:14
  • 3
    @mkRabbani whenever I think about cursors the thought is "how can I avoid using them today?". – Brian Jun 25 '19 at 18:15
  • 1
    This depends on what is the requirement is. – mkRabbani Jun 25 '19 at 18:16
  • 1
    If this has to be done on all columns on 85 tables, I would certainly use a cursor. However, what's going to stop people from adding a blank string later on? A constraint? To @mkRabbani defense, I wouldn't write 85 separate update statements. I'd handle that dynamically by building out the query using sys.tables and sys.columns At least to build the queries to run. Then, I would run them one at a time, mainly to prevent the server from locking up for extended times during the update and to just double check it. Naturally I'd have a backup – S3S Jun 25 '19 at 18:17
  • I don't see the need to do this in one script/call. Updating a column on a table from blank string to NULL is 2 lines of code. Thats 170 lines total and you can then run them in a controlled manner, one at a time, confirm it looks good, and move on to the next one. If you F something up with a poorly done dynamic script, you're going to look way worse than if you write out 85 update statements and run them by hand. A sensitive environment requires defensive programming and safety first. – dfundako Jun 25 '19 at 18:20
  • Create a script which generates a script of 85 `UPDATE`s from INFORMATION SCHEMA. – Serg Jun 25 '19 at 18:20
  • i also doubt that `EXECUTE` can execute multiple SQL statements separated by semicon (`;`) pretty sure `EXECUTE` is mean for a single statement... Whats your SQL Server version? – Raymond Nijland Jun 25 '19 at 18:22
  • SQL server 2008.trying to avoid cursors as well. I realized that I am selecting all the values into the variables but the execute will only ran once. just ran out of ideas on how to update it. The location is secured and that is the customer call. – Raxl Jun 25 '19 at 18:26
  • 1
    @Raxl Your edit is bad news. Are you letting non-technical staff make a production change to 85 tables in a high profile environment? – dfundako Jun 25 '19 at 18:28
  • Contractual agreement, I might just create the 85 rollback tables, that might be the safer bet – Raxl Jun 25 '19 at 18:34
  • *" that might be the safer bet "* i agree with @dfundako the only safe way to play russian roulette is without bullets i more or less meant not sure about the customer skill (non-technical staff?) within SQL Server here? – Raymond Nijland Jun 25 '19 at 18:49
  • 1
    @RaymondNijland EXECUTE or sp_executesql absolutely can run multiple statements in a single go. Here, give it a shot. execute('select * from sys.databases;select * from sys.tables') – Sean Lange Jun 25 '19 at 18:52
  • 1
    What's wrong with backup - update - test - [restore] method @dfundako suggested? – Serg Jun 25 '19 at 18:57
  • Yea thats the problem if you know MySQL/SQL Server (MSSQL) syntax and both use `EXECUTE` more or less well MySQL uses `PREPARE/EXECUTE` and only allow one SQL statement you sometimes start mixing things @SeanLange – Raymond Nijland Jun 25 '19 at 19:04

2 Answers2

3

According to SQL Server documentation:

If a SELECT statement returns more than one row and the variable references a non-scalar expression, the variable is set to the value returned for the expression in the last row of the result set.

That means your variables were assigned with the values from the last row only. Therefore only [Cruise_Ship].[hull_number] will be pass to sp_executesql and that is the only column being updated with your script.

To store mutliple values, a table variable should be used.

sp_executesql does accept parameters and it is used for building dynamic queries.

However I don't think you can pass table-valued variable as parameters.

added: Check this for how to pass table-valued variable to sp_executesql.

This is where your code goes wrong.

select @table = table_name, @id = ID, @key = ID_colname , @value = col_name from update_table

I know this is not elegant but the following code should do the job. And I whould suggest to wrap it in a TRANSACTION whether you did a backup or not.

DECLARE @empty NVARCHAR(10) 
SET @empty = 'NULL'

SELECT
    'UPDATE ' + s.name + '.' + t.name + ' 
    SET [' + c.name + '] =  ' + @empty + ' 
    WHERE LTRIM([' + c.name + ']) = ''''
    END;'
FROM sys.tables t
INNER JOIN sys.schemas s
    ON t.schema_id = s.schema_id
INNER JOIN sys.columns c
    ON t.object_id = c.object_id
ORDER BY
    s.name
   ,t.name
   ,c.column_id
Adam Yan
  • 502
  • 2
  • 7
0

I agree with Adam Yam, I just expanded the answer to include the needed data from the update_tables.

This way the execution of the SQL will only happen for those 8 entries on the table.

the result is the update to the 4 tables from the example correctly.

DECLARE @currentId INT 
SELECT @currentId = MIN(tabl.ID) from udpate_table tabl

DECLARE @sql NVARCHAR(MAX)

WHILE (1 = 1)
BEGIN  
   --- execute for the current pk
    BEGIN
        SELECT @sql =
        'UPDATE ' + s.name + '.' + t.name + ' 
        SET [' + c.name + '] = '''' 
           where  ' + s.name + '.' + t.name + '.' + tab.ID_colname + ' = ' + convert (varchar(20), tab.ID) + ' '
        FROM sys.tables t
        INNER JOIN sys.schemas s
            ON t.schema_id = s.schema_id
        INNER JOIN sys.columns c
            ON t.object_id = c.object_id
        JOIN update_table tab
            on t.name = tab.table_name
            and c.name = tab.col_name
            and tab.ID = @currentId
        ORDER BY
            tab.ID
           ,s.name
           ,t.name
           ,c.column_id
    END

    exec sp_executesql @sql

    -- select the next id to handle    
  SELECT TOP 1 @currentId = tabl.ID
  FROM update_table tabl
  WHERE tabl.ID > @currentId 
  ORDER BY tabl.ID

  IF @@ROWCOUNT = 0 BREAK;

END
Raxl
  • 99
  • 1
  • 8
  • If I understand it correctly, the table udpate_table is no longer needed with my script. The Loop is very nice. It would be even better to add this in the loop: PRINT 'Completed on ' + CAST(SYSDATETIME() AS VARCHAR) + ' Query: '+ @sql – Adam Yan Jun 26 '19 at 21:06