Conditionally set values in UPDATE statement - sql

I would like to have a stored procedure that will update values in a table row depending on whether or not the parameters are provided. For example, I have a situation where I want to update all the values, but also a situation where I'm only required to update two values. I was hoping to be able to do this with only one procedure, rather than writing two, which doesn't particularly appeal to me. The best I have managed to come up with myself is something like the following:
CREATE PROCEDURE dbo.UpdatePerson
#PersonId INT,
#Firstname VARCHAR(50) = NULL,
#Lastname VARCHAR(50) = NULL,
#Email VARCHAR(50) = NULL
AS
BEGIN
SET NOCOUNT ON
UPDATE Person
Set
Firstname = COALESCE(#Firstname, Firstname),
Lastname = COALESCE(#LastName, Lastname),
Email = COALESCE(#Email, Email)
WHERE PersonId = #PersonId
END
I realize that the values will be updated each time anyway, which isn't ideal. Is this an effective way of achieving this, or could it be done a better way?

I think your code is fine. The only thing I would add is a check for the case when all three params are NULL, in which case no update should be done.

SQL Server does actually have some logic to deal with non updating updates.
More details than you probably wanted to know!

Related

Trigger to avoid similar names inserted in tables isnt working and not let me insert any row

I have this trigger to avoid similar names inserted in a table. But isn't working at all and not let me insert any row (as if the condition is true everytime). I tried with IF EXISTS and happen the same. Even deleting everything in the "Actor" table (leaving with no rows) and when I insert a new one the trigger fires.
CREATE TRIGGER [dbo].[AvoidSimilarName]
ON [dbo].[Actor]
FOR INSERT
AS
BEGIN
DECLARE #Firstname varchar(25)
DECLARE #Lastname varchar(25)
SELECT #Firstname = Firstname, #Lastname = Lastname FROM INSERTED;
SELECT * FROM Actor WHERE #Firstname = Firstname AND #Lastname = Lastname
IF ##ROWCOUNT > 0
BEGIN
ROLLBACK TRANSACTION
END
END
The thing is that when I made a same example of the select that its in the trigger, works fine. Return no rows.
DECLARE #Firstname varchar(25)
DECLARE #Lastname varchar(25)
SET #Firstname = 'Johnny'
SET #Lastname = 'Depp'
SELECT * FROM Actor WHERE #Firstname = Firstname AND #Lastname = Lastname
What am I doing wrong?
Not trying to help you with the trigger because i don't think is your best option.
Why not create a unique constraint on those 2 fields?
ALTER TABLE dbo.Actor ADD CONSTRAINT UQ_ACTOR_NAME UNIQUE (firstname,lastname)
I think your trigger always fires because, since you've inserted, you're always going to get a ##rowcount of at least 1. Try setting it to > 1 and see.
While I agree with #mxix and #Vinnie that you should probably use a unique constraint for this purpose, another option is to use an INSTEAD OF trigger. As others have mentioned, your issue right now is that the data has already been inserted so your search is always finding a match, namely the row that was just inserted. You can use the INSTEAD OF trigger to search for the values about to be inserted, and decide if you want to insert them or not.
Note that if you go this route, you are responsible for inserting into the table if you deem the data should indeed be inserted. See this answer for some more discussion on emulating a before insert trigger.
Also, as #Sean and #marc_s mentioned, your trigger does not correctly handle multiple rows being inserted in one statement.

Update a variable number of parameters in a Stored Procedure

Assume the following please:
I have a table that has ~50 columns. I receive an import file that has information that needs to be updated in a variable number of these columns. Is there a method via a stored procedure where I can only update the columns that need to be updated and the rest retain their current value unchanged. Keep in mind I am not saying that the unaffected columns return to some default value but actually maintain the current value stored in them.
Thank you very much for any response on this.
Is COALESCE something that could be applied here as I have read in this thread: Updating a Table through a stored procedure with variable parameters
Or am I looking at a method more similar to what is being explained here:
SQL Server stored procedure with optional parameters updates wrong columns
For the record my sql skills are quite weak, I am just beginning to dive into this end of the pool
JD
Yes, you can use COALESCE to do this. Basically if the parameter is passed in as NULL then it will use the original value. Below is a general pattern that you can adapt.
DECLARE #LastName NVARCHAR(50) = 'John'
DECLARE #FirstName NVARCHAR(50) = NULL;
DECLARE #ID INT = 1;
UPDATE dbo.UpdateExample
SET LastName = COALESCE(#LastName, LastName), FirstName = COALESCE(#FirstName, FirstName),
WHERE ID = #ID
Also, have a read of this article, titled: The Impact of Non-Updating Updates
http://web.archive.org/web/20180406220621/http://sqlblog.com:80/blogs/paul_white/archive/2010/08/11/the_2D00_impact_2D00_of_2D00_update_2D00_statements_2D00_that_2D00_don_2D00_t_2D00_change_2D00_data.aspx
Basically,
"SQL Server contains a number of optimisations to avoid unnecessary logging or page flushing when processing an UPDATE operation that will not result in any change to the persistent database."

Masking or hiding inaccurately entered data in SQL Server 2008

OK, so my subject line isn't very descriptive, but here's the scenario:
An end-user has a legal obligation to submit transaction data to a government agency. The transactions contain the name and address of various individuals and organizations. HOWEVER, end users frequently misspell the names of the reported individuals and organizations, or they badly mangle the address, etc.
The information submitted by the end user is a legal 'document', so it cannot be altered by the agency that received it. Also, the transactions can be viewed and searched by the public. When the government agency notices an obvious misspelling or bad address, they would like to 'hide' or 'mask' that bad value with a known good value. For example, if an end user entered 'Arnie Schwarzeger', the agency could replace that name with 'Arnold Schwarzenegger'. The public that viewed the data would see (and search for) the correct spelling, but could view the original data as entered by the end user after they found the data record in question.
Hopefully that explains the business case well enough...on to the SQL part! So to address this problem, we have tables that look like this:
CREATE TABLE [dbo].[SomeUserEnteredData](
[Id] [uniqueidentifier] NOT NULL,
[LastOrOrganizationName] [nvarchar](350) NOT NULL, // data as entered by end-user
[FirstName] [nvarchar](50) NULL, // data as entered by end-user
[FullName] AS ([dbo].[FullNameValue]([FirstName],[LastName])) PERSISTED, // data as entered by end-user
[MappedName] AS ([dbo].[MappedNameValue]([FirstName],[LastName]))) // this is the 'override' data from the agency
CREATE TABLE [dbo].[CorrectionsByAgency](
[Id] [uniqueidentifier] NOT NULL,
[ReplaceName] [nvarchar](400) NOT NULL,
[KeepName] [nvarchar](400) NOT NULL)
CREATE FUNCTION [dbo].[FullNameValue]
(
#FirstName as NVARCHAR(40),
#LastOrOrganizationName as NVARCHAR(350)
)
RETURNS NVARCHAR(400)
WITH SCHEMABINDING
AS
BEGIN
DECLARE #result NVARCHAR(400)
IF #FirstName = '' OR #FirstName is NULL
SET #result = #LastOrOrganizationName
ELSE
SET #result = #LastOrOrganizationName + ', ' + #FirstName
RETURN #result
END
CREATE FUNCTION [dbo].[MappedNameValue]
(
#FirstName as NVARCHAR(50),
#LastOrOrganizationName as NVARCHAR(350)
)
RETURNS NVARCHAR(400)
AS
BEGIN
DECLARE #result NVARCHAR(400)
DECLARE #FullName NVARCHAR(400)
SET #FullName = dbo.FullNameValue(#FirstName, #LastOrOrganizationName)
SELECT top 1 #result = KeepName from CorrectionsByAgency where ReplaceName = #FullName
if #result is null
SET #result = #FullName
RETURN #result
END
Hopefully, if my sample isn't TOO convoluted, you can see that if the agency enters a name correction, it will replace all occurrences of the misspelled name. From a business logic perspective, this works exactly right: the agency staff only enters a few corrections and the corrections can override everywhere there are misspelled names.
From a server performance standpoint, this solution STINKS. The calculated SomeUserEnteredData.MappedName column can't be indexed, and no view that reads from that column can be indexed either! There's no way this can work for our needs if we can't index the MappedName values.
The only alternative I've been able to see as a possibility is to create an additional linking table between the end-user created data and the agency created data -- when the agency enters a correction record, a record is created in the linking table for every occurrence of the bad column value. The down side to this seems to be the very real likelihood of creating/destroying many (hundreds of thousands) of those linking records for every correction entered by an agency user...
Do any of you SQL geniuses out there have great ideas about how to address this problem?
I'm not sure if this is answering your question directly, but I would try to simplify the whole thing: stop using functions, persist "calculated" values and use application logic (possibly in a stored procedure) to manage the data.
Assuming that one agency correction can be applied to many user-entered names, then you could have something like this:
create table dbo.UserEnteredData (
DocumentId uniqueidentifier not null primary key,
UserEnteredName nvarchar(1000) not null,
CorrectedNameId uniqueidentifier null,
constraint FK_CorrectedNames foreign key (CorrectedNameId)
references dbo.CorrectedNames (CorrectedNameId)
)
create table dbo.CorrectedNames (
CorrectedNameId uniqueidentifier not null primary key,
CorrectedName nvarchar(1000) not null
)
Now, you need to make sure your application logic can do something like this:
External user enters dirty data
Agency user reviews the dirty data and identifies both the incorrect name and the corrected name
Application checks if the corrected name already exists
If no, create a new row in dbo.CorrectedNames
Create a new row in dbo.UserEnteredData, with the CorrectedNameId
I'm assuming that things are rather more complicated in reality and corrections are made based on addresses and other data as well as just names, but the basic relationship you describe seems simple enough. As you said, the functions add a lot of overhead and it's not clear (to me) what benefit they provide over just storing the data you need directly.
Finally, I don't understand your comment about creating/destroying linking records; it's up to your application logic to handle data changes correctly.

Updating a Table from a Stored Procedure

I am trying to learn database on my own; all of your comments are appreciated.
I have the following table.
CREATE TABLE AccountTable
(
AccountId INT IDENTITY(100,1) PRIMARY KEY,
FirstName NVARCHAR(50) NULL,
LastName NVARCHAR(50) NULL,
Street NVARCHAR(50) NULL,
StateId INT REFERENCES STATETABLE(StateId) NOT NULL
)
I would like to write a Stored procedure that updates the row. I imagine that the stored procedure would look something like this:
CREATE PROCEDURE AccountTable_Update
#Id INT,
#FirstName NVARCHAR(20),
#LastName NVARCHAR(20),
#StreetName NVARCHAR(20),
#StateId INT
AS
BEGIN
UPDATE AccountTable
Set FirstName = #FirstName
Set LastName = #LastName
Set Street = #StreetName
Set StateId = #StateId
WHERE AccountId = #Id
END
the caller provides the new information that he wants the row to have. I know that some of the fields are not entirely accurate or precise; I am doing this mostly for learning.
I am having a syntax error with the SET commands in the UPDATE portion, and I don't know how to fix it.
Is the stored procedure I am writing a procedure that you would write in real life? Is this an antipattern?
Are there any grave errors I have made that just makes you cringe when you read the above TSQL?
Are there any grave errors I have made that just makes you cringe when you read the above TSQL?
Not really "grave," but I noticed your table's string fields are set up as the datatype of NVARCHAR(50) yet your stored procedure parameters are NVARCHAR(20). This may be cause for concern. Usually your stored procedure parameters will match the corresponding field's datatype and precision.
#1: You need commas between your columns:
UPDATE AccountTable SET
FirstName = #FirstName,
LastName = #LastName,
Street = #StreetName,
StateId = #StateId
WHERE
AccountId = #Id
SET is only called once, at the very start of the UPDATE list. Every column after that is in a comma separated list. Check out the MSDN docs on it.
#2: This isn't an antipattern, per se. Especially given user input. You want parametized queries, as to avoid SQL injection. If you were to build the query as a string off of user input, you would be very, very susceptible to SQL injection. However, by using parameters, you circumvent this vulnerability. Most RDBMS's make sure to sanitize the parameters passed to its queries automagically. There are a lot of opponents of stored procedures, but you're using it as a way to beat SQL injection, so it's not an antipattern.
#3: The only grave error I saw was the SET instead of commas. Also, as ckittel pointed out, your inconsistency in the length of your nvarchar columns.

Adding a constraint to prevent duplicates in SQL Update Trigger

We have a user table, every user has an unique email and username. We try to do this within our code but we want to be sure users are never inserted (or updated) in the database with the same username of email.
I've added a BEFORE INSERT Trigger which prevents the insertion of duplicate users.
CREATE TRIGGER [dbo].[BeforeUpdateUser]
ON [dbo].[Users]
INSTEAD OF INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #Email nvarchar(MAX)
DECLARE #UserName nvarchar(MAX)
DECLARE #UserId int
DECLARE #DoInsert bit
SET #DoInsert = 1
SELECT #Email = Email, #UserName = UserName FROM INSERTED
SELECT #UserId = UserId FROM Users WHERE Email = #Email
IF (#UserId IS NOT NULL)
BEGIN
SET #DoInsert = 0
END
SELECT #UserId = UserId FROM Users WHERE UserName = #UserName
IF (#UserId IS NOT NULL)
BEGIN
SET #DoInsert = 0
END
IF (#DoInsert = 1)
BEGIN
INSERT INTO Users
SELECT
FirstName,
LastName,
Email,
Password,
UserName,
LanguageId,
Data,
IsDeleted
FROM INSERTED
END
ELSE
BEGIN
DECLARE #ErrorMessage nvarchar(MAX)
SET #ErrorMessage =
'The username and emailadress of a user must be unique!'
RAISERROR 50001 #ErrorMessage
END
END
But for the Update trigger I have no Idea how to do this.
I've found this example with google:
http://www.devarticles.com/c/a/SQL-Server/Using-Triggers-In-MS-SQL-Server/2/
But I don't know if it applies when you update multiple columns at once.
EDIT:
I've tried to add a unique constraint on these columns but it doesn't work:
Msg 1919, Level 16, State 1, Line 1
Column 'Email' in table 'Users' is of a type
that is invalid for use as a key column in an index.
You can add a unique contraint on the table, this will raise an error if you try and insert or update and create duplicates
ALTER TABLE [Users] ADD CONSTRAINT [IX_UniqueUserEmail] UNIQUE NONCLUSTERED
(
[Email] ASC
)
ALTER TABLE [Users] ADD CONSTRAINT [IX_UniqueUserName] UNIQUE NONCLUSTERED
(
[UserName] ASC
)
EDIT: Ok, i've just read your comments to another post and seen that you're using NVARCHAR(MAX) as your data type. Is there a reason why you might want more than 4000 characters for an email address or username? This is where your problem lies. If you reduce this to NVARCHAR(250) or thereabouts then you can use a unique index.
Sounds like a lot of work instead of just using one or more unique indexes. Is there a reason you haven't gone the index route?
Why not just use the UNIQUE attribute on the column in your database? Setting that will make the SQL server enforce that and throw an error if you try to insert a dupe.
You should use a SQL UNIQUE constraint on each of these columns for that.
You can create a UNIQUE INDEX on an NVARCHAR as soon as it's an NVARCHAR(450) or less.
Do you really need a UNIQUE column to be so large?
In general, I would avoid Triggers wherever possible as they can make the behaviour very hard to understand unless you know that the trigger exists. As other commentatators have said, a unique constraint is the way to go (once you have amended your column definitions to allow it).
If you ever find yourself needing to use a trigger, it may be a sign that your design is flawed. Think hard about why you need it and whether it is performing logic that belongs elsewhere.
Be aware that if you use the UNIQUE constraint/index solution with SQL Server, only one null value will be permitted in that column. So, for example, if you wanted the email address to be optional, it wouldn't work, because only one user could have a null email address. In that case, you would have to resort to another approach like a trigger or a filtered index.