TSQL Reference Table with redundant keys - sql-server

I'm currently working on a stored procedure on SQL Server 2016. In my Database I have a table structure and need to add another table, which references to the same table as an existing one.
Thus, I have 2 times a 1:1 relation to the same table.
The occuring problem is, I reference the same keys from 2 different origin tables twice in the same target table.
Target table:
FK_Tables | Text
----------------
1 | Table One Text Id: 1
1 | Table Two Text Id: 1 // The error: Same FK_Tables 2 times
Table One:
ID | OtherField
---------
1 | 42
Table Two:
ID | CoolField
---------
1 | 22
Table One and Table Two are currently referencing to the table Reference Table.
Do you know how I can solve this problem, of the same ID twice?
Thanks!!

You need to add a column for each table you're referencing, otherwise you wouldn't know where the ID is coming from if they were all inserted into the same field. Something like this:
/*
CREATE TEST TABLES
*/
DROP TABLE IF EXISTS tbOne;
CREATE TABLE tbOne ( ID INT IDENTITY(1,1) NOT NULL PRIMARY KEY
, TXT VARCHAR(10)
);
DROP TABLE IF EXISTS tbTwo;
CREATE TABLE tbTwo ( ID INT IDENTITY(1,1) NOT NULL PRIMARY KEY
, TXT VARCHAR(10)
);
DROP TABLE IF EXISTS Target;
CREATE TABLE Target ( ID INT IDENTITY(1,1) NOT NULL PRIMARY KEY
, FKTB1 INT
, FKTB2 INT
, TXT VARCHAR(100)
);
-- 1st FK tbOne
ALTER TABLE Target ADD CONSTRAINT FK_One FOREIGN KEY (FKTB1) REFERENCES tbOne (ID);
--2nd FK tbTwo
ALTER TABLE Target ADD CONSTRAINT FK_Two FOREIGN KEY (FKTB2) REFERENCES tbTwo (ID);
-- Populate test tables
INSERT INTO tbOne (TXT)
SELECT TOP 100 LEFT(text, 10)
FROM SYS.messages
INSERT INTO tbTwo (TXT)
SELECT TOP 100 LEFT(text, 10)
FROM SYS.messages
INSERT INTO [Target] (FKTB1, FKTB2, TXT)
SELECT 1, 1, 'Test - constraint'
-- Check result set
SELECT *
FROM tbTwo
SELECT *
FROM tbOne
SELECT *
FROM [Target] T
INNER JOIN tbOne TB1
ON T.FKTB1 = TB1.ID
INNER JOIN tbTwo TB2
ON T.FKTB2 = TB2.ID

Related

Creating INSERT trigger that sets values to 0

I have two tables : Invoice and Invoice_item, relationship 1 to many.
The Invoice_item table has columns Number_sold and Item_price, and the Invoice table has Number_sold_total and Item_price_total columns that will store total values of columns Number_sold and Item_price from the Invoice_item table with the same Invoice_ID key.
CREATE TABLE [Invoice] (
[Invoice_ID] [int] NOT NULL,
[Number_sold_total] [int] NOT NULL,
[Item_price_total] [decimal] NOT NULL,
PRIMARY KEY ([Invoice_ID]));
CREATE TABLE [Invoice_item] (
[Invoice_item_ID] [int] NOT NULL,
[Invoice_ID] [int] NOT NULL,
[Number_sold] [int] NOT NULL,
[Item_price] [decimal] NOT NULL,
PRIMARY KEY ([Invoice_item_ID],[Invoice_ID],
FOREIGN KEY ([Invoice_ID]) REFERENCES [Invoice]([Invoice_ID]);
So, if there are three rows in Invoice_item with the same Invoice_ID, the row with that Invoice_ID in Invoice table will have SUM values of corresponding columns in Invoice_item table.
Let's say i have three rows in Invoice_item table and columns Item_price with values 100,200 and 300, and they have the Invoice_ID = 3. The column Item_price_total in Invoice will have value of 600, where the Invoice_ID = 3.
QUESTION -
My task is to create an insert trigger on table Invoice that will set the values of Number_sold_total and Item_price_total to 0(ZERO) if there is no Invoice_item with corresponding Invoice_ID -> IF NOT EXISTS (Invoice.Invoice_ID = Invoice_item.Invoice_ID)...
I am using SQL Server 2017.
Ideally you would not implement this using triggers.
Instead you should use a view. If you are worried about querying performance, you can index it, at the cost of insert and delete performance.
CREATE VIEW dbo.Invoice_Totals
WITH SCHEMABINDING
AS
SELECT
i.Invoice_ID,
Number_sold = SUM(i.Number_sold),
Item_price = SUM(i.Item_price),
ItemCount = COUNT_BIG(*) -- must include count for indexed view
FROM dbo.Invoice_item;
And then index it
CREAT UNIQUE CLUSTERED INDEX CX_Invoice_Totals ON Invoice_Totals
(Invoice_ID);
If you really, really want to do this using triggers, you can use the following
CREATE OR ALTER TRIGGER TR_Invoice_Total
ON dbo.Invoice_item
AFTER INSERT, UPDATE, DELETE
AS
SET NOCOUNT ON; -- prevent spurious resultsets
IF (NOT EXISTS (SELECT 1 FROM inserted) AND NOT EXISTS (SELECT 1 FROM deleted))
RETURN; -- early bail-out if no rows
UPDATE i
SET Number_sold_total += totals.Number_sold_total,
Item_price_total += totals.Item_price_total
FROM Invoice i
JOIN (
SELECT
Invoice_ID = ISNULL(i.Invoice_ID, d.Invoice_ID),
Number_sold_total = SUM(ISNULL(i.Number_sold, 0) - ISNULL(d.Number_sold, 0)),
Item_price_total = SUM(ISNULL(i.Item_price, 0) - ISNULL(d.Item_price, 0))
FROM inserted i
FULL JOIN deleted d ON d.Invoice_ID = i.Invoice_ID
GROUP BY
ISNULL(i.Invoice_ID, d.Invoice_ID)
) totals
ON totals.Invoice_Id = i.Invoice_ID;
db<>fiddle
The steps of the trigger are as follows:
Bail out early if the modification affected 0 rows.
Join the inserted and deleted tables together on the primary key. This needs to be a full-join, because in an INSERT there are no deleted and in a DELETE there are no inserted rows.
Group up the changed rows by Invoice_ID, taking the sum of the differences.
Join back to the Invoice table
Update the Invoice table adding the total difference to each column.
This effectively recreates what the indexed view would do for you automatically.
You cannot just select the first row from inserted and deleted into variables, as there may be multiple rows affected. You must join and group them

How to get desired result in SQL Server

In my application there is a table to store text and another table to store it's respective images..
My table structure goes as follows (tbl_article):
article_id | Page_ID | article_Content
-----------+---------+-----------------
1 | 1 | hello world
2 | 1 | hello world 2
where article_id is the pk and auto incremented.
Now in my other table (tbl_img):
image_id| image_location|article_id | page_id
--------+---------------+-----------+---------
1 | imgae locat | 1 | 1
2 | image loc2 | 2 | 1
where image_id is the pk and auto incremented.
In both table I am inserting data through table valued parameter, and in second table article_id is referencing article_id of the first table.
To get auto incremented column value I am using output clause:
DECLARE #TableOfIdentities TABLE
(
IdentValue INT,
PageId INT
)
INSERT INTO tbl_article(page_id, article_content)
OUTPUT Inserted.article_id, #pageId INTO #TableOfIdentities (IdentValue, PageId)
SELECT page_id, slogan_body_header
FROM #dtPageSlogan
INSERT INTO tbl_img(page_id, image_location)
SELECT page_id, image_location
FROM #dtPageImageContent
But now I have to insert values from #TableOfIdentities into article_id of tbl_img - how to do that?
You need an additional column , a temporary article id generated from your code to link images and related articles properly. So you can use MERGE with OUTPUT, because with merge you can refer to columns from both the target and the source and build your TableOfIdentities tvp properly, then join it with dtPageImageContent to insert on tbl_img.
CREATE TABLE tbl_article (
article_id INT IDENTITY(1, 1) PRIMARY KEY
, Page_ID INT
, article_Content NVARCHAR(MAX)
);
CREATE TABLE tbl_img (
image_id INT IDENTITY(1, 1) PRIMARY KEY
, image_location VARCHAR(256)
, article_id INT
, Page_ID INT
);
DECLARE #TableOfIdentities TABLE
(
IdentValue INT,
PageId INT,
tmp_article_id INT
);
DECLARE #dtPageSlogan TABLE(
tmp_article_id INT -- generated in your code
, page_id INT
, slogan_body_header NVARCHAR(MAX)
);
DECLARE #dtPageImageContent TABLE (
page_id INT
, image_location VARCHAR(256)
, tmp_article_id INT -- needed to link each image to its article
)
-- create sample data
INSERT INTO #dtPageSlogan(tmp_article_id, page_id, slogan_body_header)
VALUES (10, 1, 'hello world');
INSERT INTO #dtPageSlogan(tmp_article_id, page_id, slogan_body_header)
VALUES (20, 1, 'hello world 2');
INSERT INTO #dtPageImageContent(page_id, image_location, tmp_article_id)
VALUES (1, 'image loc1', 10);
INSERT INTO #dtPageImageContent(page_id, image_location, tmp_article_id)
VALUES (1, 'image loc2', 20);
-- use merge to insert tbl_article and populate #TableOfIdentities
MERGE INTO tbl_article
USING (
SELECT ps.page_id, ps.slogan_body_header, ps.tmp_article_id
FROM #dtPageSlogan as ps
) AS D
ON 1 = 2
WHEN NOT MATCHED THEN
INSERT(page_id, article_content) VALUES (page_id, slogan_body_header)
OUTPUT Inserted.article_id, Inserted.page_id, D.tmp_article_id
INTO #TableOfIdentities (IdentValue, PageId, tmp_article_id)
;
-- join using page_id and tmp_article_id fields
INSERT INTO tbl_img(page_id, image_location, article_id)
-- select the "IdentValue" from your table of identities
SELECT pic.page_id, pic.image_location, toi.IdentValue
FROM #dtPageImageContent pic
-- join the "table of identities" on the common "page_id" column
INNER JOIN #TableOfIdentities toi
ON pic.page_Id = toi.PageId AND pic.tmp_article_id = toi.tmp_article_id
;
You can try it on fiddle
You need to join the #dtPageImageContent table variable with the #TableOfIdentities table variable on their common page_id to get those values:
-- add the third column "article_id" to your list of insert columns
INSERT INTO tbl_img(page_id, image_location, article_id)
-- select the "IdentValue" from your table of identities
SELECT pic.page_id, pic.image_location, toi.IdentValue
FROM #dtPageImageContent pic
-- join the "table of identities" on the common "page_id" column
INNER JOIN #TableOfIdentities toi ON pic.page_Id = toi.page_id

Need to tune the select query without adding index

I have a query related to Indexes and Execution plan. Below are few tables that I am using
Table 1 : TestReviewResult
Column_name |Type |Length
TestReviewResultId |int |4
TestNumber |int |4
ReviewerUserId |int |4
Current Index
index_name index_description index_keys
TestReviewResult_PK clustered, unique, primary key located on PRIMARY TestReviewResultId
Table 2: TestReviewFinding
Column_name Type Length
TestReviewFindingId int 4
TestReviewResultId int 4
ScreenCode varchar 100
ReviewComments varchar 8000
CurrentIndex
index_name index_description index_keys
TestReviewFinding_PK clustered, unique, primary key located on PRIMARY TestReviewFindingId
Table3: TestReviewResultComment
Column_name Type Length
TestReviewResultCommentId Int 4
TestReviewResultId Int 4
TestReviewComment varchar 8000
CurrentIndex
index_name index_description index_keys
TestReviewResultComment_CI clustered located on PRIMARY TestReviewResultId
TestReviewResultComment_PK nonclustered, unique, primary key located on PRIMARY TestReviewResultCommentId
Table 4: TestReviewFindingElement
Column_name Type Length
TestReviewFindingElementId Int 4
TestReviewFindingId Int 4
ElementCode varchar 25
CurrentIndex
index_name index_description index_keys
TestReviewFindingElementType_PK clustered, unique, primary key located on PRIMARY TestReviewFindingElementId
Below is the query that is being used
Select columnnames…. from
From TestReviewResult(NOLOCK) cr
left outer join TestReviewResultComment(NOLOCK) crc
on cr.TestReviewResultId = crc.TestReviewResultId
left outer join TestReviewFinding(NOLOCK) cf
on cf.TestReviewResultId = cr.TestReviewResultId
left outer join TestReviewFindingElement(NOLOCK) crf
on cf.TestReviewFindingId = crf.TestReviewFindingId
where cr.TestReviewResultId = #TestReviewNumber -- Test Review number is
being passed in the stored procedure
When I ran the Execution plan it suggested to add 2 indexes mentioned below
CREATE NONCLUSTERED INDEX IX_TestReviewResultId
ON [TestReviewFinding] (TestReviewResultId)
CREATE NONCLUSTERED INDEX IX_TestReviewFindingId
ON [TestReviewFindingElement] (TestReviewFindingId)
Is there any alternative way I can tune the above query without adding indexes as there are lot of write operations that do get performed
Below is the ddl that you can execute in SQL Server and check the Query execution Plan
Create table TestReviewResult(
TestReviewResultId int NOT NULL PRIMARY KEY ,
TestNumber int ,
ReviewerUserId int )
insert into TestReviewResult values(1,1,1)
insert into TestReviewResult values(2,2,2)
insert into TestReviewResult values(3,3,3)
Create table TestReviewFinding(
TestReviewFindingId int not null primary key,
TestReviewResultId int ,
ScreenCode varchar(100))
insert into TestReviewFinding values(1,1,'A')
insert into TestReviewFinding values(2,2,'B')
insert into TestReviewFinding values(3,3,'C')
Create table TestReviewResultComment(
TestReviewResultCommentId int not null primary key nonclustered,
TestReviewResultId int ,
TestReviewComment varchar( 8000)
)
CREATE CLUSTERED INDEX [CI_TestReviewResultId]
ON TestReviewResultComment (TestReviewResultId);
insert into TestReviewResultComment values(1,1,'A')
insert into TestReviewResultComment values(2,2,'B')
insert into TestReviewResultComment values(3,3,'C')
Create table TestReviewFindingElement(
TestReviewFindingElementId int not null primary key,
TestReviewFindingId int ,
ElementCode varchar(25))
insert into TestReviewFindingElement values(1,1,'A')
insert into TestReviewFindingElement values(2,3,'B')
insert into TestReviewFindingElement values(3,3,'C')
When I m running the below query I am getting Index scans on 2 tables
TestReviewFindingElement and TestReviewFinding
Select *
From TestReviewResult(NOLOCK) cr
left outer join TestReviewResultComment(NOLOCK) crc
on cr.TestReviewResultId = crc.TestReviewResultId
left outer join TestReviewFinding(NOLOCK) cf
on cf.TestReviewResultId = cr.TestReviewResultId
left outer join TestReviewFindingElement(NOLOCK) crf
on cf.TestReviewFindingId = crf.TestReviewFindingId
where cr.TestReviewResultId = #TestReviewNumber -- Test Review number is
being passed in the stored procedure.You can use --> 1
Is there any way to modify the select query to avoid scan without adding index

Outer SELECT from INSERT RETURNING and inner SELECT statement

I am trying to write a query that copies a row in a table to that same table, gives it a new sequential primary key, and associates it with a new foreign key. I need to associate the new primary key with another foreign key that's not inserted and exists in a different relational table (a lookup table).
I'd like to be able to do this as a single transaction, but I can't seem to find a way to associate the original row with the copied row as the unique id is new for the copied row. This is going to be a bit of a mouthful, but here's my specific question:
Can an outer SELECT clause enclose an inner INSERT with inner SELECT clause and RETURNING such that values from both the inner SELECT and the INSERT's RETURNING clause are selected and properly joined? Here's what I've attempted:
WITH batch_select AS (
SELECT id, owner_id, 1992 AS project_id
FROM batch
WHERE project_id = 1921
),
batch_insert AS (
INSERT INTO batch (owner_id, project_id)
SELECT bs.owner_id, bs.experiment_id
FROM batch_select bs
RETURNING id
)
SELECT bs.id AS origin_id, bi.id AS destination_id
FROM batch_select bs, batch_insert bi;
I need the origin_id to correspond to the destination_id. Obviously right now it's just a CROSS JOIN where everything is paired with everything and isn't very useful. I'd also be using the results of the last SELECT statement to run the INSERT into the lookup table, something like this (batch_join_select query could be implemented in the last insert, but has been left for clarity):
WITH batch_select AS (
SELECT id, owner_id, 1992 AS project_id
FROM batch
WHERE project_id = 1921
),
batch_insert AS (
INSERT INTO batch (owner_id, project_id)
SELECT bs.owner_id, bs.experiment_id
FROM batch_select bs
RETURNING id
),
batch_join_select AS (
SELECT bs.id AS origin_id, bi.id AS destination_id
FROM batch_select bs, batch_insert bi
)
INSERT INTO lookup_batch_container (batch_id, container_id)
SELECT bjs.destination_id, lbc.container_id
FROM batch_join_select bjs
INNER JOIN lookup_batch_container lbc ON lbc.batch_id = bjs.origin_id;
I found a similar question on the dba exchange, but the accepted answer doesn't correctly associate the two when there's more than one row.
Do I just have to do this with several transactions?
[EDIT] Adding some minimal schema:
Table lookup_batch_container
Column | Type | Modifiers
--------------+---------+-----------
batch_id | integer | not null
container_id | integer | not null
Indexes:
"lookup_batch_container_batch_id_container_id_key" UNIQUE CONSTRAINT, btree (batch_id, container_id)
Foreign-key constraints:
"lookup_batch_container_batch_id_fkey" FOREIGN KEY (batch_id) REFERENCES batch(id) ON DELETE CASCADE
"lookup_batch_container_container_id_fkey" FOREIGN KEY (container_id) REFERENCES container(id) ON DELETE CASCADE
Table batch
Column | Type | Modifiers
------------------+-----------------------------+------------------------------------------------------------------------------------
id | integer | not null default nextval('batch_id_seq'::regclass)
owner_id | integer | not null
project_id | integer | not null
Indexes:
"batch_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"batch_project_id_fkey" FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE
"batch_owner_id_fkey" FOREIGN KEY (owner_id) REFERENCES owner(id) ON DELETE CASCADE
Referenced by:
TABLE "lookup_batch_container" CONSTRAINT "lookup_batch_container_batch_id_fkey" FOREIGN KEY (batch_id) REFERENCES batch(id) ON DELETE CASCADE
Table container
Column | Type | Modifiers
-----------------------+-----------------------------+------------------------------------------------------------------------------
id | integer | not null default nextval('stirplate_source_file_container_id_seq'::regclass)
owner_id | integer | not null
status | container_status_enum | not null default 'new'::container_status_enum
name | text | not null
Indexes:
"container_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"container_owner_id_fkey" FOREIGN KEY (owner_id) REFERENCES owner(id) ON DELETE CASCADE
Referenced by:
TABLE "lookup_batch_container" CONSTRAINT "lookup_batch_container_container_id_fkey" FOREIGN KEY (container_id) REFERENCES container(id) ON DELETE CASCADE
with batch_select as (
select id, owner_id, 1992 as project_id
from batch
where project_id = 1921
), batch_insert as (
insert into batch (owner_id, project_id)
select owner_id, project_id
from batch_select
order by id
returning *
)
select unnest(oid) as oid, unnest(did) as did
from (
select
array_agg(distinct bs.id order by bs.id) as oid,
array_agg(distinct bi.id order by bi.id) as did
from
batch_select bs
inner join
batch_insert bi using (owner_id, project_id)
) s
;
oid | did
-----+-----
1 | 4
2 | 5
3 | 6
Given this batch table:
create table batch (
id serial primary key,
owner_id integer,
project_id integer
);
insert into batch (owner_id, project_id) values
(1,1921),(1,1921),(2,1921);
It could be simpler if the primary key were (id, owner_id, project_id). Isn't it?

TSQL to insert a set of rows and dependent rows

I have 2 tables:
Order (with a identity order id field)
OrderItems (with a foreign key to order id)
In a stored proc, I have a list of orders that I need to duplicate. Is there a good way to do this in a stored proc without a cursor?
Edit:
This is on SQL Server 2008.
A sample spec for the table might be:
CREATE TABLE Order (
OrderID INT IDENTITY(1,1),
CustomerName VARCHAR(100),
CONSTRAINT PK_Order PRIMARY KEY (OrderID)
)
CREATE TABLE OrderItem (
OrderID INT,
LineNumber INT,
Price money,
Notes VARCHAR(100),
CONSTRAINT PK_OrderItem PRIMARY KEY (OrderID, LineNumber),
CONSTRAINT FK_OrderItem_Order FOREIGN KEY (OrderID) REFERENCES Order(OrderID)
)
The stored proc is passed a customerName of 'fred', so its trying to clone all orders where CustomerName = 'fred'.
To give a more concrete example:
Fred happens to have 2 orders:
Order 1 has line numbers 1,2,3
Order 2 has line numbers 1,2,4,6.
If the next identity in the table was 123, then I would want to create:
Order 123 with lines 1,2,3
Order 124 with lines 1,2,4,6
On SQL Server 2008 you can use MERGE and the OUTPUT clause to get the mappings between the original and cloned id values from the insert into Orders then join onto that to clone the OrderItems.
DECLARE #IdMappings TABLE(
New_OrderId INT,
Old_OrderId INT)
;WITH SourceOrders AS
(
SELECT *
FROM Orders
WHERE CustomerName = 'fred'
)
MERGE Orders AS T
USING SourceOrders AS S
ON 0 = 1
WHEN NOT MATCHED THEN
INSERT (CustomerName )
VALUES (CustomerName )
OUTPUT inserted.OrderId,
S.OrderId INTO #IdMappings;
INSERT INTO OrderItems
SELECT New_OrderId,
LineNumber,
Price,
Notes
FROM OrderItems OI
JOIN #IdMappings IDM
ON IDM.Old_OrderId = OI.OrderID

Resources