sqlsql-server

SQL update with join is only updating one column


I'm implementing change tracking for user data. Many changes are possible, so I've built a temp table (#ChangesToMake) to hold the basics and am joining that to retrieve details. When updating the Users table from the Change table, it's only updating one column for each UserID. I'm not sure how to make it update the other columns at the same time.

(Interestingly, if I replace the update u set [column]... lines with select *, it properly shows me all the changes I want to make, one on each line of the resulting dataset.)

Here's enough SQL to recreate my trouble. It's simplified, but reproducible.

create table #Users 
(
    UserID int identity, 
    Title varchar(6), 
    FirstName varchar(50), 
    MiddleName varchar(50), 
    LastName varchar(100), 
    Phone varchar(10), 
    Fax varchar(10), 
    Email varchar(100)
)

create table #ChangeSet 
(
    ChangeSetID int identity, 
    UserID int
)

create table #Change 
(
    ChangeID int identity, 
    ChangeSetID int, 
    ColName varchar(50), 
    OldValue varchar(100), 
    NewValue varchar(100)
)

create table #ChangesToMake 
(
    UserID int, 
    ChangeSetID int
)

--sample: user data:
insert into #Users (FirstName, LastName, Phone, Email)
values  ('Alice', 'Smith', 1234567890, 'alice@tuta.com')
    ,   ('Bob', 'Jones', 1234567890, 'bob@tuta.com')
    ,   ('Charlie', 'Brown', 1234567890, 'charlie@tuta.com')

--sample: sets of changes:
insert into #ChangeSet
values  (1), (2), (3)

--sample: details of changes:
insert into #Change
values  (1, 'LastName', 'Smith', 'Smith-Wesson')
    ,   (1, 'Fax', null, '1234567890')
    ,   (2, 'FirstName', 'Bob','Barry')
    ,   (2, 'MiddleName', null, 'X')
    ,   (3, 'Title', null, 'Dr.')
    ,   (3, 'Email', 'charlie@tuta.com', 'chuck@tuta.com')

--find sets of changes, per user:
insert into #ChangesToMake
    select distinct UserID, ChangeSetID
    from #ChangeSet x

--see sets of changes:
select * 
from #ChangesToMake 
order by UserID

--preview before:
select * 
from #Users 
where UserID in (select UserID from #ChangesToMake) 
order by UserID

--update users (original):
update u
set Title = isnull(case when c.ColName='Title' then c.NewValue end, u.Title),
    FirstName = isnull(case when c.ColName='FirstName' then c.NewValue end, u.FirstName),
    MiddleName = isnull(case when c.ColName='MiddleName' then c.NewValue end, u.MiddleName),
    LastName = isnull(case when c.ColName='LastName' then c.NewValue end, u.LastName),
    Phone = isnull(case when c.ColName='Phone' then c.NewValue end, u.Phone),
    Fax = isnull(case when c.ColName='Fax' then c.NewValue end, u.Fax),
    Email = isnull(case when c.ColName='Email' then c.NewValue end, u.Email)
from #ChangesToMake x
inner join #Users u on u.UserID = x.UserID
inner join #Change c on c.ChangeSetID = x.ChangeSetID

--review after:
select * 
from #Users 
where UserID in (select UserID from #ChangesToMake) 
order by UserID

I tried adapting Charlieface's pre-aggregate suggestion in the following way, but it also only made 1 change per UserID. Perhaps I misunderstood?

update u
set Title = isnull(case when c.CN='Title' then c.NV end, u.Title),
    FirstName = isnull(case when c.CN='FirstName' then c.NV end, u.FirstName),
    MiddleName = isnull(case when c.CN='MiddleName' then c.NV end, u.MiddleName),
    LastName = isnull(case when c.CN='LastName' then c.NV end, u.LastName),
    Phone = isnull(case when c.CN='Phone' then c.NV end, u.Phone),
    Fax = isnull(case when c.CN='Fax' then c.NV end, u.Fax),
    Email = isnull(case when c.CN='Email' then c.NV end, u.Email)
from #ChangesToMake x
cross apply (
    select c1.ChangeSetID, cs1.UserID, max(ColName) as CN, max(NewValue) as NV
    from #Change c1
    inner join #ChangeSet cs1 on cs1.ChangeSetID = c1.ChangeSetID
    group by c1.ChangeSetID, cs1.UserID, ColName
    having ColName in ('Title', 'FirstName', 'MiddleName', 'LastName', 'Phone', 'Fax', 'Email')
        and c1.ChangeSetID=x.ChangeSetID
    ) c
inner join #Users u on u.UserID = c.UserID

SQL Server makes only one (random?) of out all the changes I want:

UserID Title FirstName MiddleName LastName Phone      Fax        Email
------ ----- --------- ---------- -------- ---------- ---------- --------------
     1 NULL  Alice     NULL       Smith    1234567890 1234567890 alice@tuta.com
     2 NULL  Barry     NULL       Jones    1234567890 NULL       bob@tuta.com
     3 NULL  Charlie   NULL       Brown    1234567890 NULL       chuck@tuta.com

If I split the updates by column and update the Users table 7 separate times, it works fine. But that seems so dreadfully inefficient:

update u
set FirstName = isnull(c.NewValue, u.FirstName)
from #ChangesToMake x
inner join #Users u on u.UserID = x.UserID
inner join #Change c on c.ChangeSetID = x.ChangeSetID
where c.ColName='FirstName'

Does anyone know what detail I'm missing here?


Solution

  • The issue here, as noted in my linked answer, is that you need to update each User row exactly once. You cannot run the UPDATE multiple times per row.

    But you aren't aggregating your apply properly. You need to aggregate it only by the PK of the User table, ie UserID.

    Then just use max(case when to pivot it up into separate columns.

    Also note that the having is better as a where, and that #ChangesToMake just duplicates #ChangeSet so can be removed.

    update u
    set Title = isnull(c.Title, u.Title),
        FirstName = isnull(c.FirstName, u.FirstName),
        MiddleName = isnull(c.MiddleName, u.MiddleName),
        LastName = isnull(c.LastName, u.LastName),
        Phone = isnull(c.Phone, u.Phone),
        Fax = isnull(c.Fax, u.Fax),
        Email = isnull(c.Email, u.Email)
    from (
        select
          cs.UserID,
          max(case when c.ColName = 'Title' then c.NewValue end) as Title,
          max(case when c.ColName = 'FirstName' then c.NewValue end) as FirstName,
          max(case when c.ColName = 'MiddleName' then c.NewValue end) as MiddleName,
          max(case when c.ColName = 'LastName' then c.NewValue end) as LastName,
          max(case when c.ColName = 'Phone' then c.NewValue end) as Phone,
          max(case when c.ColName = 'Fax' then c.NewValue end) as Fax,
          max(case when c.ColName = 'Email' then c.NewValue end) as Email
        from #Change c
        join #ChangeSet cs on cs.ChangeSetID = c.ChangeSetID
        where c.ColName in ('Title', 'FirstName', 'MiddleName', 'LastName', 'Phone', 'Fax', 'Email')
        group by cs.UserID
    ) c
    inner join #Users u on u.UserID = c.UserID;
    

    db<>fiddle