48

Here is my problem - I have 2 tables:

  1. WORKER, with columns |ID|OTHER_STAF| , where ID is primary key
  2. FIRM, with columns |FPK|ID|SOMETHING_ELSE| , where combination FPK and ID make primary key, and also ID is a foreign key referenced to WORKER.ID (not null, and must have same value as in WORKER).

I want to make stored procedure UPDATE_ID_WORKER, where I would like to change the value of specific ID in WORKER, and also in all instances of specific value of ID in FIRM.

Dale K
  • 25,246
  • 15
  • 42
  • 71
Slavisa
  • 966
  • 3
  • 11
  • 26
  • 2
    I would argue that, even though it's possible to do what you're asking, it's highly unlikely that it's the right thing to do. If you get into a situation where you need to change the value of the PK, than you're essentially attempting to change the identity of your data. Usually there's no need to do that and it indicates your data model is probably wrong. If you really must do it, then clone your original data and, if needed, remove the original. That operation will undoubtedly indicate what your intention is. – Mladen B. Jan 14 '21 at 14:58

6 Answers6

53

You shouldn't really do this but insert in a new record instead and update it that way.
But, if you really need to, you can do the following:

  • Disable enforcing FK constraints temporarily (e.g. ALTER TABLE foo WITH NOCHECK CONSTRAINT ALL)
  • Then update your PK
  • Then update your FKs to match the PK change
  • Finally enable back enforcing FK constraints
kevchadders
  • 8,335
  • 4
  • 42
  • 61
  • I dont like the idea to bring new coloumn in, so this is what I was looking for. Thaks everybody – Slavisa Mar 23 '10 at 14:16
  • 45
    this will "work", but is still **a very bad idea**. Not only will you will disable your FKs while other users change data, but this will support a bad design. Some day, the next developer to work on this database will be asking (here on SO) how to fix this PK update mess. – KM. Mar 23 '10 at 17:45
  • 3
    This is a great answer for people who need to get this done. But it would have been worth showing examples on how to disable AND re-enable contraints – aspirant_sensei Apr 06 '20 at 12:26
  • It would help if you would put a bold disclaimer on top of your answer that it's a bad decision to do so and a consequence of a bad database design. Also, it would help if you would enumerate all the issues, associated with that approach, mentioned in these comments, to discourage people from doing it without further considerations. – Mladen B. Jan 14 '21 at 15:03
  • Just to add more info to the turn on/off of the constraints: ** Disable the constraints on a table called tableName: ALTER TABLE tableName NOCHECK CONSTRAINT ALL; ** Re-enable the constraints on a table called tableName: ALTER TABLE tableName WITH CHECK CHECK CONSTRAINT ALL – Pepe Alvarez Nov 07 '22 at 17:45
36

First, we choose stable (not static) data columns to form a Primary Key, precisely because updating Keys in a Relational database (in which the references are by Key) is something we wish to avoid.

  1. For this issue, it doesn't matter if the Key is a Relational Key ("made up from the data"), and thus has Relational Integrity, Power, and Speed, or if the "key" is a Record ID, with none of that Relational Integrity, Power, and Speed. The effect is the same.

  2. I state this because there are many posts by the clueless ones, who suggest that this is the exact reason that Record IDs are somehow better than Relational Keys.

  3. The point is, the Key or Record ID is migrated to wherever a reference is required.

Second, if you have to change the value of the Key or Record ID, well, you have to change it. Here is the OLTP Standard-compliant method. Note that the high-end vendors do not allow "cascade update".

  • Write a proc. Foo_UpdateCascade_tr @ID, where Foo is the table name

  • Begin a Transaction

  • First INSERT-SELECT a new row in the parent table, from the old row, with the new Key or RID value

  • Second, for all child tables, working top to bottom, INSERT-SELECT the new rows, from the old rows, with the new Key or RID value

  • Third, DELETE the rows in the child tables that have the old Key or RID value, working bottom to top

  • Last, DELETE the row in the parent table that has the old Key or RID value

  • Commit the Transaction

Re the Other Answers

The other answers are incorrect.

  • Disabling constraints and then enabling them, after UPDATing the required rows (parent plus all children) is not something that a person would do in an online production environment, if they wish to remain employed. That advice is good for single-user databases.

  • The need to change the value of a Key or RID is not indicative of a design flaw. It is an ordinary need. That is mitigated by choosing stable (not static) Keys. It can be mitigated, but it cannot be eliminated.

  • A surrogate substituting a natural Key, will not make any difference. In the example you have given, the "key" is a surrogate. And it needs to be updated.

    • Please, just surrogate, there is no such thing as a "surrogate key", because each word contradicts the other. Either it is a Key (made up from the data) xor it isn't. A surrogate is not made up from the data, it is explicitly non-data. It has none of the properties of a Key.
  • There is nothing "tricky" about cascading all the required changes. Refer to the steps given above.

  • There is nothing that can be prevented re the universe changing. It changes. Deal with it. And since the database is a collection of facts about the universe, when the universe changes, the database will have to change. That is life in the big city, it is not for new players.

  • People getting married and hedgehogs getting buried are not a problem (despite such examples being used to suggest that it is a problem). Because we do not use Names as Keys. We use small, stable Identifiers, such as are used to Identify the data in the universe.

    • Names, descriptions, etc, exist once, in one row. Keys exist wherever they have been migrated. And if the "key" is a RID, then the RID too, exists wherever it has been migrated.
  • Don't update the PK! is the second-most hilarious thing I have read in a while. Add a new column is the most.

PerformanceDBA
  • 32,198
  • 10
  • 64
  • 90
  • 1
    How do you propose to do this - "Second, for all child tables, working top to bottom, INSERT-SELECT the new rows, from the old rows, with the new Key or RID value"? You will get PK constraints all the way down. Wouldn't you rather update the child tables? – Radio- Jan 12 '16 at 23:52
  • 2
    You say _high-end vendors do not allow "cascade update"_ but isn't this the perfect scenario when it is appropriate? I would think that the complex multi step processes described in these answers would be more dangerous and error prone than relying on "cascade update". If really paranoid, could "cascade update" constraints be temporarily added, then the key changed, and then the constraints be removed? That would ensure referential integrity at all times, and simplify the process. – John Rees Mar 09 '16 at 04:46
  • "with none of that Relational Integrity, Power, and Speed. The effect is the same." If you get the same integrity and speed, the only difference is more typing (increased maintenance cost) and data updates that take longer to work through (more maintenance cost). I have no idea what you mean by "power" – StingyJack Apr 17 '17 at 14:25
  • Could you expand a little on "Stable" vs "static". possibly with an example? thx. – RayLoveless Oct 27 '17 at 02:38
  • I would also like an example of stable data columns to form a primary key. – Victorio Berra Jul 30 '19 at 15:17
  • I appreciate your points, but you also seem to be contradicting yourself a bit. In the beginning, you say updating keys in relational databases is something we want to avoid, but then you continue to downplay its trickiness and why it makes you want to avoid it. In the end, it seems like you only want to mitigate it, because you believe it can't be completely avoided, leading me to think that if you _had_ a way to completely avoid it, you would. – Saeb Amini Jul 16 '20 at 08:10
  • This answer is a better answer then the accepted one, so I upvoted it. However, this part is not true: "The need to change the value of a Key or RID is not indicative of a design flaw. It is an ordinary need...". It shouldn't be an ordinary need to change someone's identity (which PK really stands for). If you need to do that with your data, then use data cloning. Long story short, you shouldn't really need to change the identity (PK) of your data ever. I'm happy to see any real world example that proves me wrong, of course. – Mladen B. Jan 14 '21 at 15:08
  • 1
    @SaebAmini  That is not logic, it is trained from the desired outcome back to a speculated cause. Logic works forward, not backward. Don't worry about my beliefs, look at the facts in the example. 1) It is to be avoided by design, for reasons given. The Identifier must be made up of stable (not immutable) columns, which is easy because the things that Identify something in the real world are stable. End of that story.  2) It cannot be avoided altogether. In those cases, I give the correct solution. End of different story. – PerformanceDBA Mar 18 '21 at 11:09
  • 3) If you are seeking a justification for NonKeys masquerading as Keys, providing none of the properties of a Key, you won't find it in the definition of Keys, that prohibits it. Not even in the definition of handling exceptions. You will have to make something up. – PerformanceDBA Mar 18 '21 at 11:12
  • 1
    @MladenB.  1) Thanks.  2) It is true. Objective truth does not care if you agree. You are applying it incorrectly. I said that the need to change a Key, any Key, is ordinary. That means you had better have the proper Transaction coded for it, for every table. 3) Now, then, it is rare. So what, it must be catered for. Thus it has nothing to do with whether the key is likely to change, or not.  4) Taking the position of choosing NonKeys such that the NonKeys never change, means you do not understand what a Key is; what properties a genuine Key has; what it lost in a NonKey. – PerformanceDBA Mar 18 '21 at 11:22
  • 1
    5) The designation PK is an implementation issue, it is whatever you declare it is. Sure, at best, it is the genuine Relational Key. Often it is not. At worst, it is an `ID`. – PerformanceDBA Mar 18 '21 at 11:24
15

If you are sure that this change is suitable for the environment you're working in: set the FK conditions on the secondary tables to UPDATE CASCADING.

For example, if using SSMS as GUI:

  1. right click on the key
  2. select Modify
  3. Fold out 'INSERT And UPDATE Specific'
  4. For 'Update Rule', select Cascade.
  5. Close the dialog and save the key.

When you then update a value in the PK column in your primary table, the FK references in the other tables will be updated to point at the new value, preserving data integrity.

pbeentje
  • 303
  • 2
  • 7
  • 1
    This is how I expected it to work, but on MS SQL I get the error message "Cannot update identity column" when trying to update a PK value. I really wonder what "update cascading" is good for when you can't change primary keys anyway. – Cito Mar 23 '19 at 13:33
  • 3
    @Cito: an identity column is not the same as a primary key, though an identity column can be selected to be the primary key for a table. Your error isn't related to attempting to update a primary key - it's an error from attempting to update an identity column. – pbeentje Mar 24 '19 at 22:59
  • @pbeentje You're right, it seems this is how they call an autoincrement column in T-SQL. So if you have an autoincrementing PK, you can't update its values in T-SQL. I tried to set IDENTITY_INSERT OFF, but as the name says that helps only with inserts, not with updates. Admittedly, the need to change PK values happens more often with natural PKs than with these autoincrementing PKs. – Cito Mar 25 '19 at 20:16
7

When you find it necessary to update a primary key value as well as all matching foreign keys, then the entire design needs to be fixed.

It is tricky to cascade all the necessary foreign keys changes. It is a best practice to never update the primary key, and if you find it necessary, you should use a Surrogate Primary Key, which is a key not derived from application data. As a result its value is unrelated to the business logic and never needs to change (and should be invisible to the end user). You can then update and display some other column.

for example:

BadUserTable
UserID     varchar(20) primary key --user last name
other columns...

when you create many tables that have a FK to UserID, to track everything that the user has worked on, but that user then gets married and wants a ID to match their new last name, you are out of luck.

GoodUserTable
UserID    int identity(1,1) primary key
UserLogin varchar(20) 
other columns....

you now FK the Surrogate Primary Key to all the other tables, and display UserLogin when necessary, allow them to login using that value, and when they need to change it, you change it in one column of one row only.

KM.
  • 101,727
  • 34
  • 178
  • 212
  • 1
    Slavisa ***has*** a surrogate. It make no difference if the "key" is a real Key or a Record ID. Whne the "key" value changes, it has to be changed. – PerformanceDBA Aug 10 '15 at 12:24
  • When the PK has to change, that means your current data needs to be identified by a different/new value, which is actually a change of the data identity. If you properly design your database, this situation should never happen. Usually, this case is an indication of a poorly chosen PK for your data. – Mladen B. Jan 14 '21 at 15:14
4

Don't update the primary key. It could cause a lot of problems for you keeping your data intact, if you have any other tables referencing it.

Ideally, if you want a unique field that is updateable, create a new field.

Daniel A. White
  • 187,200
  • 47
  • 362
  • 445
  • 1
    I'm very carefull in other procedures, so Im pozitive that confict wont happened. Is there any way to update this primary key? – Slavisa Mar 23 '10 at 11:01
  • @Slavisa, clone your data and, if needed, remove the original data. What you're seeking is identity change and it shouldn't ever happen if you choose your PKs wisely. – Mladen B. Jan 14 '21 at 15:15
2

You could use this recursive function for generate necessary T-SQL script.

CREATE FUNCTION dbo.Update_Delete_PrimaryKey
(
    @TableName      NVARCHAR(255),
    @ColumnName     NVARCHAR(255),
    @OldValue       NVARCHAR(MAX),
    @NewValue       NVARCHAR(MAX),
    @Del            BIT
)
RETURNS NVARCHAR 
(
    MAX
)
AS
BEGIN
    DECLARE @fks TABLE 
            (
                constraint_name NVARCHAR(255),
                table_name NVARCHAR(255),
                col NVARCHAR(255)
            );
    DECLARE @Sql                  NVARCHAR(MAX),
            @EnableConstraints     NVARCHAR(MAX);

    SET @Sql = '';
    SET @EnableConstraints = '';

    INSERT INTO @fks
      (
        constraint_name,
        table_name,
        col
      )
    SELECT oConstraint.name     constraint_name,
           oParent.name         table_name,
           oParentCol.name      col
    FROM   sys.foreign_key_columns sfkc
           --INNER JOIN sys.foreign_keys sfk
           --     ON  sfk.[object_id] = sfkc.constraint_object_id

           INNER JOIN sys.sysobjects oConstraint
                ON  sfkc.constraint_object_id = oConstraint.id
           INNER JOIN sys.sysobjects oParent
                ON  sfkc.parent_object_id = oParent.id
           INNER JOIN sys.all_columns oParentCol
                ON  sfkc.parent_object_id = oParentCol.object_id
                AND sfkc.parent_column_id = oParentCol.column_id
           INNER JOIN sys.sysobjects oReference
                ON  sfkc.referenced_object_id = oReference.id
           INNER JOIN sys.all_columns oReferenceCol
                ON  sfkc.referenced_object_id = oReferenceCol.object_id
                AND sfkc.referenced_column_id = oReferenceCol.column_id
    WHERE  oReference.name = @TableName
           AND oReferenceCol.name = @ColumnName
    --AND (@Del <> 1 OR sfk.delete_referential_action = 0)
    --AND (@Del = 1 OR sfk.update_referential_action = 0)

    IF EXISTS(
           SELECT 1
           FROM   @fks
       )
    BEGIN
        DECLARE @Constraint     NVARCHAR(255),
                @Table          NVARCHAR(255),
                @Col            NVARCHAR(255)  

        DECLARE Table_Cursor CURSOR LOCAL 
        FOR
            SELECT f.constraint_name,
                   f.table_name,
                   f.col
            FROM   @fks AS f

        OPEN Table_Cursor FETCH NEXT FROM Table_Cursor INTO @Constraint, @Table,@Col  
        WHILE (@@FETCH_STATUS = 0)
        BEGIN
            IF @Del <> 1
            BEGIN
                SET @Sql = @Sql + 'ALTER TABLE ' + @Table + ' NOCHECK CONSTRAINT ' + @Constraint + CHAR(13) + CHAR(10);
                SET @EnableConstraints = @EnableConstraints + 'ALTER TABLE ' + @Table + ' CHECK CONSTRAINT ' + @Constraint 
                    + CHAR(13) + CHAR(10);
            END

            SET @Sql = @Sql + dbo.Update_Delete_PrimaryKey(@Table, @Col, @OldValue, @NewValue, @Del);
            FETCH NEXT FROM Table_Cursor INTO @Constraint, @Table,@Col
        END

        CLOSE Table_Cursor DEALLOCATE Table_Cursor
    END

    DECLARE @DataType NVARCHAR(30);
    SELECT @DataType = t.name +
           CASE 
                WHEN t.name IN ('char', 'varchar', 'nchar', 'nvarchar') THEN '(' +
                     CASE 
                          WHEN c.max_length = -1 THEN 'MAX'
                          ELSE CONVERT(
                                   VARCHAR(4),
                                   CASE 
                                        WHEN t.name IN ('nchar', 'nvarchar') THEN c.max_length / 2
                                        ELSE c.max_length
                                   END
                               )
                     END + ')'
                WHEN t.name IN ('decimal', 'numeric') THEN '(' + CONVERT(VARCHAR(4), c.precision) + ',' 
                     + CONVERT(VARCHAR(4), c.Scale) + ')'
                ELSE ''
           END
    FROM   sys.columns c
           INNER JOIN sys.types t
                ON  c.user_type_id = t.user_type_id
    WHERE  c.object_id = OBJECT_ID(@TableName)
           AND c.name = @ColumnName

    IF @Del <> 1
    BEGIN
        SET @Sql = @Sql + 'UPDATE [' + @TableName + '] SET [' + @ColumnName + '] = CONVERT(' + @DataType + ', ' + ISNULL('N''' + @NewValue + '''', 'NULL') 
            + ') WHERE [' + @ColumnName + '] = CONVERT(' + @DataType + ', ' + ISNULL('N''' + @OldValue + '''', 'NULL') +
            ');' + CHAR(13) + CHAR(10);
        SET @Sql = @Sql + @EnableConstraints;
    END
    ELSE
        SET @Sql = @Sql + 'DELETE [' + @TableName + '] WHERE [' + @ColumnName + '] = CONVERT(' + @DataType + ', N''' + @OldValue 
            + ''');' + CHAR(13) + CHAR(10);
    RETURN @Sql;
END
GO

DECLARE @Result NVARCHAR(MAX);
SET @Result = dbo.Update_Delete_PrimaryKey('@TableName', '@ColumnName', '@OldValue', '@NewValue', 0);/*Update*/
EXEC (@Result)
SET @Result = dbo.Update_Delete_PrimaryKey('@TableName', '@ColumnName', '@OldValue', NULL, 1);/*Delete*/
EXEC (@Result)
GO

DROP FUNCTION Update_Delete_PrimaryKey;
  • Was it really easier to write a bulk of the code, which I doubt even OP has had patience to read, instead of first explaining your logic with human language, prior to writing this much of the code? – Mladen B. Jan 14 '21 at 15:17
  • Consider you have two table which have relations with each other or one table that have self relational, in the 1st scenario first relation can be cascade on update & delete but second relation can't be cascade because of may cause cycles or multiple cascade paths and the same for the 2th scenario. If you want update second primary key, first must disable relation then update 2 tables and finally enable relation, this function generate these steps. I 'll update my answer by samples for these 2 scenario for better understanding. @MladenB. – Mohammad Hajinezhad Jan 14 '21 at 18:40
  • Thank you a lot for a brief explanation of your answer, I really appreciate it. Disabling relations could lead to problems if the database is on production server, where remote clients could be adding/updating data at the same time, which would ruin the consistency of your data. The safest way to go is to duplicate/clone the original data and set the new record with the desired values. – Mladen B. Jan 15 '21 at 20:42