Performing operation on Select statement result - sql

I am creating a stored procedure which takes following parameters:
#LessonId char(15), #TeacherName char(25), #TeacherSessionId char(10),
#CurrentTeacher char(25) OUTPUT
This is what I want to do:
Inserts a new row if the LessonId doesn't exist in the table
If it already exists it needs to do the following:
a) check if the TeacherName & TeacherSessionId are the same as mine ( input parameters), if yes just update the LoginTime
b) if the TeacherName OR TeacherSessionId in database are not the same as mine, this lesson belongs to someone else so return the teacher's name
What is the best way to avoid running too many SELECT operations on the table.
I know that I can do something like this:
IF NOT EXISTS (SELECT * FROM Lessontb WHERE LessonId = #LessonId )
BEGIN
INSERT...
END
ELSE
BEGIN
IF EXISTS (SELECT TeacherName FROM Lessontb
WHERE TeacherName <> #TeacherName
OR TeacherSessionId <> #TeacherSessionId )
BEGIN
SET #CurrentTeacher = TeacherName
END
IF EXISTS (SELECT TeacherName FROM Lessontb
WHERE TeacherName = #TeacherName
AND TeacherSessionId = #TeacherSessionId )
BEGIN
UPDATE ... LoginTime
END
END
I am quite sure there must be a more clean way to do this, for instance running the 2nd and 3rd steps on the result set of the first SELECT but I am not sure how.

Potential problem. It appears that you are using LessionID as the key column. But later down the code, you ignore the key column.
Potential solution: Do one select and load them into variables, also save the clustered key column into the variable as well. Work out your logic and then issue one update. If you update with the clustered key, the overhead would be very less.
Naz

Related

SQL - Unique key across 2 columns of same table?

I use SQL Server 2016. I have a database table called "Member".
In that table, I have these 3 columns (for the purpose of my question):
idMember [INT - Identity - Primary Key]
memEmail
memEmailPartner
I want to prevent a row to use an email that already exists in the table.
Both email columns are not mandatory, so they can be left blank (NULL).
If I create a new Member:
If not blank, the values entered for "memEmail" and "memEmailPartner" (independently) should not be found in any other rows in columns memEmail nor memEmailPartner.
So if I want to create a row with email (dominic#email.com) I must not find any occurrences of that value in memEmail or memEmailPartner.
If I update an existing Member:
I must not find any occurrences of that value in memEmail or memEmailPartner, with the exception that I am updating the row (idMembre) which already have the value in memEmail or memEmailPartner.
--
From what I read on Google, it should be possible to do something with a Function-Based Check Constraint but I can't make that work.
Anyone have a solution to my problem ?
Thank you.
I may have misunderstood exactly what you were asking but it looks like you want a simple upsert query with IF EXISTS conditions.
DECLARE #emailAddress VARCHAR(255)= 'dominic#email.com', --dummy value
#id INT= 2; --dummy value
IF NOT EXISTS
(
SELECT 1
FROM #Member
WHERE memEmail = #emailAddress
OR memEmailPartner = #emailAddress
)
BEGIN
SELECT 'insert';
END;
ELSE IF EXISTS
(
SELECT 1
FROM #Member
WHERE idMember = #id
)
BEGIN
SELECT 'update';
END;
A trigger is the traditional way of doing doing what you're asking for. Here's a simple demo;
--if object_id('member') is not null drop table member
go
create table member (
idMember INT Identity Primary Key,
memEmail varchar(100),
memEmailPartner varchar(100)
)
go
create trigger trg_member on member after insert, update as
begin
set nocount on
if exists (select 1 from member m join inserted i on i.memEmail = m.memEmail and i.idMember <> m.idMember) or
exists (select 1 from member m join inserted i on i.memEmail = m.memEmailPartner and i.idMember <> m.idMember) or
exists (select 1 from member m join inserted i on i.memEmailPartner = m.memEmail and i.idMember <> m.idMember) or
exists (select 1 from member m join inserted i on i.memEmailPartner = m.memEmailPartner and i.idMember <> m.idMember)
begin
raiserror('Email addresses must be unique.', 16, 1)
rollback
end
end
go
insert member(memEmail, memEmailPartner) values('a#a.com', null), ('b#b.com', null), (null, 'c#c.com'), (null, 'd#d.com')
go
select * from member
insert member(memEmail, memEmailPartner) values('a#a.com', null) -- should fail
go
insert member(memEmail, memEmailPartner) values(null, 'a#a.com') -- should fail
go
insert member(memEmail, memEmailPartner) values('c#c.com', null) -- should fail
go
insert member(memEmail, memEmailPartner) values(null, 'c#c.com') -- should fail
go
insert member(memEmail, memEmailPartner) values('e#e.com', null) -- should work
go
insert member(memEmail, memEmailPartner) values(null, 'f#f.com') -- should work
go
select * from member
-- Make sure updates still work!
update member set memEmail = memEmail, memEmailPartner = memEmailPartner
I've not tested this extensively but it should be enough to get you started if you want to try this approach.
StuartLC notes the potential for the UDF check constraint to fail in set based updates and/or various other conditions, triggers don't have this problem.
Stuart also suggests reconsidering whether this should really be a database constraint or managed through business logic elsewhere. I'm inclined to agree - my gut feel here is that sooner or later you will come across a situation that requires email addresses to be reused, or in some other way not strictly unique.
TL;DR
The wisdom of applying this kind of business rule logic in the database needs to be reconsidered - this check is likely a better candidate for your application, or a stored procedure which acts as an insert gate keeper instead of direct new row inserts into the table.
Ignoring the Warnings
That said, I do believe that what you want is however possible in a constraint UDF, albeit with potentially atrocious performance consequences*1, and likely prone to race conditions in set based updates
Here's a user defined function which applies the unique email logic across both columns. Note that by the time the constraint is checked, that the row is IN the table already, hence the new row itself needs to be excluded from the duplicate checks.
My code also is depedent on ANSI NULL behaviour, i.e. that the predicates NULL = NULL and X IN (NULL) both return NULL, and hence are excluded from the failure check (in order to meet your requirement that NULLS do not fail the rule).
We also need to check for the insert of BOTH new columns being non-null, but duplicated.
So here's the a UDF doing the checking:
CREATE FUNCTION dbo.CheckUniqueEmails(#id int, #memEmail varchar(50),
#memEmailPartner varchar(50))
RETURNS bit
AS
BEGIN
DECLARE #retval bit;
IF #memEmail = #memEmailPartner
OR EXISTS (SELECT 1 FROM MyTable WHERE memEmail IS NOT NULL
AND memEmail IN(#memEmail, #memEmailPartner) AND idMember <> #id)
OR EXISTS (SELECT 1 FROM MyTable WHERE memEmailPartner IS NOT NULL
AND memEmailPartner IN(#memEmail, #memEmailPartner) AND idMember <> #id)
SET #retval = 0
ELSE
SET #retval = 1;
RETURN #retval;
END;
GO
Which is then enforced in a CHECK constraint:
ALTER TABLE MyTable ADD CHECK (dbo.CheckUniqueEmails(
idMember, memEmail, memEmailPartner) = 1);
I've put a SQLFiddle up here
Uncomment the 'failed' test cases to ensure that the above check constraint is working.
I haven't tested this with updates, and as per Martin's advice on the link, this will likely break on an insert with multiple rows.
*1 - we'll need indexes on BOTH email address columns.

SQL alter script - copying from one table to another

I'm trying to figure out how to implement the alter script described below. I'm familiar with the basics if insert/select already, but this is a lot more complex.
I have a legacy table and need to move its data to a new table with more columns. The new table has already been made public to some select users, who may have already manually moved the common data over.
So for each row in LegacyTable:
see if it already exists in NewImprovedTable (by checking for a match on a string field that exists in both tables)
if not, copy its over to NewImprovedTable
regardless of whether it had been copied to NewImprovedTable automatically just now, or previously by the user...
auto-populate a new Name field in NewImprovedTable (must be unique - e.g. "Legacy1", "Legacy2", etc.)
set an IsLegacy flag in NewImprovedTable
I need to implement this in both MS SQL and Oracle, but once I work out the logic on one I can figure out the syntax on the other.
The solution I settled on (in SQL Server - still need to port to Oracle):
IF NOT EXISTS(SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'NewImprovedTable'
AND COLUMN_NAME = 'legacyFlg')
BEGIN
ALTER TABLE [NewImprovedTable]
ADD legacyFlg TINYINT NULL
ALTER TABLE [LegacyTable]
ADD improvedId INT NULL
END
GO
IF NOT EXISTS(SELECT 1 FROM ImprovedTable WHERE legacyFlg = 1)
BEGIN
MERGE ImprovedTable AS TARGET
USING LegacyTable AS SOURCE
ON (TARGET.stringField = SOURCE.stringField)
WHEN NOT MATCHED THEN
INSERT (name, <other columns>, legacyFlg)
VALUES('Legacy' + SOURCE.stringField, <other column values>, 1)
WHEN MATCHED THEN
UPDATE SET TARGET.legacyFlg = 1;
END
GO
IF NOT EXISTS(SELECT 1 FROM LegacyTable WHERE improvedId <> 0)
BEGIN
MERGE LegacyTable AS TARGET
USING NewImprovedTable AS SOURCE
ON (SOURCE.stringField = TARGET.stringField)
WHEN MATCHED THEN
UPDATE SET TARGET.improvedId = SOURCE.pId;
END
GO
You could try using this, where 'input' is the string you are trying to confirm if already exists:
SELECT * FROM`NewImprovedTable` WHERE `Variable`='input'
This will return the whole row if found any match, if not it will return null, you can play with that
As for the unique ID field you need to create a primary key on your table with the auto increment option enable, for example
CREATE TABLE Persons
(
P_Id INT NOT NULL AUTO_INCREMENT,
LastName varchar(255) NOT NULL,
FirstName varchar(255),
Address varchar(255),
City varchar(255),
PRIMARY KEY (P_Id)
)
In this last example P_Id is set as an autoincrement variable, each time you crate a new row it will auto fill this column with a unique number.
You should check this page
http://www.w3schools.com/sql/sql_primarykey.asp

3 tables, 2 DBs, 1 Stored Procedure

I'm a novice when it comes to Stored Procedures in SQL Server Management Studio. I have an application that I was told to make the following changes to using a stored procedure:
Step 1. User types in an item number.
Step 2. Customer name, address, etc. displays in the other fields on the same form.
There are 3 tables: Bulk orders, Small orders, and Customer information.
Bulk orders and small orders are in Database_1 and Customer information is in Database_2.
The primary key for small orders is the order number. A column in small orders contains the customer number for each order. That customer number is the primary key in the customer table.
The bulk orders table is similar.
I want to include a conditional statement that says: if order number is found in small orders table, show data from customer table that coorelates with that order number. I've attempted this multiple ways, but keep getting a "The multi-part identifier.... could not be bound" error.
I.E:
SELECT DB1.db.Customer_Table.Customer_Column AS CustomerNumber;
IF(CustomerNumber NOT LIKE '%[a-z]%')
BEGIN
SELECT * FROM db.small_orders_table;
END
ELSE
BEGIN
SELECT * FROM db.buld_orders_table;
END
Please help.
Sounds like it's 2 databases on the same server...in that case, you'll need to specify the fully qualified table name (database.schema.table) when referencing a table on the other database from where your stored procedure is found.
Database_1.db.small_orders_tables
first of all, you cannot use aliases as variables. If you want to assign a value to a variable in order to test it, you have to do a SELECT statement like SELECT #var = DB1.db.Customer_Table.Customer_Column FROM <YourTableFullName> WHERE <condition>. Then you can use the #var (which must be declared before) for your test.
About the error you're experiencing, youre using fully qualified names in a wrong way. If you're on the same server (different databases), you need to specify just the database name on the top and then the schema of your objects. Suppose to have the following database objects on the Database1:
USE Database1;
GO
CREATE TABLE dbo.Table1
(
id int IDENTITY(1, 1) NOT NULL PRIMARY KEY CLUSTERED
, val varchar(30)
);
GO
INSERT INTO dbo.Table1 (val) VALUES ('test1');
GO
INSERT INTO dbo.Table1 (val) VALUES ('test2');
GO
INSERT INTO dbo.Table1 (val) VALUES ('test3');
GO
And the following ones on Database2:
USE Database2;
GO
CREATE TABLE dbo.Table2
(
id int IDENTITY(1, 1) NOT NULL PRIMARY KEY CLUSTERED
, val varchar(30)
);
GO
Now, suppose that you want to read from the first table the value with id = 2, and then to apply your IF. Let's declare a variable and test it:
USE Database1;
GO
DECLARE #var varchar(30);
-- since you're on Database1, you don't need to specify full name
SELECT #var = val FROM dbo.Table1 WHERE id = 2;
IF #var = 'test2'
BEGIN
SELECT id, val FROM dbo.Table1;
END
ELSE
BEGIN
-- in this case the database name is needed
SELECT id, val FROM Database2.dbo.Table2;
END
GO
Does it help?

sql server cannot access inserted table in a trigger

I am trying to create a simple to insert trigger that gets the count from a table and adds it to another like this
CREATE TABLE [poll-count](
id VARCHAR(100),
altid BIGINT,
option_order BIGINT,
uip VARCHAR(50),
[uid] VARCHAR(100),
[order] BIGINT
PRIMARY KEY NONCLUSTERED([order]),
FOREIGN KEY ([order]) references ord ([order]
)
GO
CREATE TRIGGER [get-poll-count]
ON [poll-count]
FOR INSERT
AS
BEGIN
DECLARE #count INT
SET #count = (SELECT COUNT (*) FROM [poll-count] WHERE option_order = i.option_order)
UPDATE [poll-options] SET [total] = #count WHERE [order] = i.option_order
END
GO
when i ever i try to run this i get this error:
The multi-part identifier "i.option_order" could not be bound
what is the problem?
thanks
Your trigger currently assumes that there will always be one-row inserts. Have you tried your trigger with anything like this?
INSERT dbo.[poll-options](option_order --, ...)
VALUES(1 --, ...),
(2 --, ...);
Also, you say that SQL Server "cannot access inserted table" - yet your statement says this. Where do you reference inserted (even if this were a valid subquery structure)?
SET #count = (SELECT COUNT (*) FROM [poll-count]
WHERE option_order = i.option_order)
-----------------------^ "i" <> "inserted"
Here is a trigger that properly references inserted and also properly handles multi-row inserts:
CREATE TRIGGER dbo.pollupdate
ON dbo.[poll-options]
FOR INSERT
AS
BEGIN
SET NOCOUNT ON;
;WITH x AS
(
SELECT option_order, c = COUNT(*)
FROM dbo.[poll-options] AS p
WHERE EXISTS
(
SELECT 1 FROM inserted
WHERE option_order = p.option_order
)
GROUP BY option_order
)
UPDATE p SET total = x.c
FROM dbo.[poll-options] AS p
INNER JOIN x
ON p.option_order = x.option_order;
END
GO
However, why do you want to store this data on every row? You can always derive the count at runtime, know that it is perfectly up to date, and avoid the need for a trigger altogether. If it's about the performance aspect of deriving the count at runtime, a much easier way to implement this write-ahead optimization for about the same maintenance cost during DML is to create an indexed view:
CREATE VIEW dbo.[poll-options-count]
WITH SCHEMABINDING
AS
SELECT option_order, c = COUNT_BIG(*)
FROM dbo.[poll-options]
GROUP BY option_order;
GO
CREATE UNIQUE CLUSTERED INDEX oo ON dbo.[poll-options-count](option_order);
GO
Now the index is maintained for you and you can derive very quick counts for any given (or all) option_order values. You'll have test, of course, whether the improvement in query time is worth the increased maintenance (though you are already paying that price with the trigger, except that it can affect many more rows in any given insert, so...).
As a final suggestion, don't use special characters like - in object names. It just forces you to always wrap it in [square brackets] and that's no fun for anyone.

Fastest way to return a primary key value SQL Server 2005

I have a two column table with a primary key (int) and a unique value (nvarchar(255))
When I insert a value to this table, I can use Scope_identity() to return the primary key for the value I just inserted. However, if the value already exists, I have to perform an additional select to return the primary key for a follow up operation (inserting that primary key into a second table)
I'm thinking there must be a better way to do this - I considered using covered indexes but the table only has two columns, most of what I've read on covered indexes suggests they only help where the table is significantly larger than the index.
Is there any faster way to do this? Would a covered index be faster even if its the same size as the table?
Building an index won't gain you anything since you have already created your value column as unique (which builds a index in the background). Effectively a full table scan is no different from an index scan in your scenario.
I assume you want to have a sort of insert-if-not-already-existsts behaviour. There is no way getting around a second select
if not exists (select ID from where name = #...)
insert into ...
select SCOPE_IDENTITY()
else
(select ID from where name = #...)
If the value happens to exist, the query will usually have been cached, so there should be no performance hit for the second ID select.
[Update statment here]
IF (##ROWCOUNT = 0)
BEGIN
[Insert statment here]
SELECT Scope_Identity()
END
ELSE
BEGIN
[SELECT id statment here]
END
I don't know about performance but it has no big overhead
As has already been mentioned this really shouldn't be a slow operation, especially if you index both columns. However if you are determined to reduce the expense of this operation then I see no reason why you couldn't remove the table entirely and just use the unique value directly rather than looking it up in this table. A 1-1 mapping like this is (theoretically) redundant. I say theoretically because there may be performance implications to using an nvarchar instead of an int.
I'll post this answer since everyone else seems to say you have to query the table twice in the event that the record exists... that's not true.
Step 1) Create a unique-index on the other column:
I recommend this as the index:
-- We're including the "ID" column so that SQL will not have to look far once the "WHERE" clause is finished.
CREATE INDEX MyLilIndex ON dbo.MyTable (Column2) INCLUDE (ID)
Step 2)
DECLARE #TheID INT
SELECT #TheID = ID from MyTable WHERE Column2 = 'blah blah'
IF (#TheID IS NOT NULL)
BEGIN
-- See, you don't have to query the table twice!
SELECT #TheID AS TheIDYouWanted
END
ELSE
INSERT...
SELECT SCOPE_IDENTITY() AS TheIDYouWanted
Create a unique index for the second entry, then:
if not exists (select null from ...)
insert into ...
else
select x from ...
You can't get away from the index, and it isn't really much overhead -- SQL server supports index columns upto 900-bytes, and does not discriminate.
The needs of your model are more important than any perceived performance issues, symbolising a string (which is what you are doing) is a common method to reduce database size, and this indirectly (and generally) means better performance.
-- edit --
To appease timothy :
declare #x int = select x from ...
if (#x is not null)
return x
else
...
You could use OUTPUT clause to return the value in the same statement. Here is an example.
DDL:
CREATE TABLE ##t (
id int PRIMARY KEY IDENTITY(1,1),
val varchar(255) NOT NULL
)
GO
-- no need for INCLUDE as PK column is always included in the index
CREATE UNIQUE INDEX AK_t_val ON ##t (val)
DML:
DECLARE #id int, #val varchar(255)
SET #val = 'test' -- or whatever you need here
SELECT #id = id FROM ##t WHERE val = #val
IF (#id IS NULL)
BEGIN
DECLARE #new TABLE (id int)
INSERT INTO ##t (val)
OUTPUT inserted.id INTO #new -- put new ID into table variable immediately
VALUES (#val)
SELECT #id = id FROM #new
END
PRINT #id