Insert Data with Foreign Keys - sql

I have two tables connected with a foreign key constraint. Each table has a primary Key (ProductID ,BaseProductID ) that is an autoincrementing identity column.
Every BaseProduct has only 1 Product during the import.
BaseProduct
- BaseProductID
- BaseProductName
Product
- ProductID
- BaseProductID
- ProductName
I try to insert rows into both tables from a different Database that has the same Tables, but different seed Values. I dont want to keep the ids from the source database.
Is there a way to do this in one statement?
EDIT
The Select would be
Select ProductName FROM #SourceProduct
WHERE Not ProductName in (
select ProductName FROM #TargetBaseProduct
)
I tried the solution by Giorgi which is close, but the matching by
tgt.BaseProductID = src.BaseProductID
leads to result where source rows with BaseProductID= 1 are not inserted if the target already has an entry with BaseProductID = 1.
DECLARE #SourceBaseProduct TABLE
(
BaseProductID INT ,
BaseProductName NVARCHAR(MAX)
)
DECLARE #SourceProduct TABLE
(
ProductID INT ,
BaseProductID INT ,
ProductName NVARCHAR(MAX)
)
DECLARE #TargetBaseProduct TABLE
(
BaseProductID INT IDENTITY(1, 1) ,
BaseProductName NVARCHAR(MAX)
)
DECLARE #TargetProduct TABLE
(
ProductID INT IDENTITY(1, 1) ,
BaseProductID INT ,
ProductName NVARCHAR(MAX)
)
INSERT INTO #SourceBaseProduct
VALUES ( 1, 'BaseProduct1' ),
( 2, 'BaseProduct2' ),
( 4, 'BaseProduct3' )
INSERT INTO #SourceProduct
VALUES ( 3, 1, 'Product1' ),
( 9, 2, 'Product2' ),
( 27, 4, 'Product3' )
INSERT INTO #TargetBaseProduct
(
BaseProductName
)
VALUES ( 'Existing Product Base' )
INSERT INTO #TargetProduct(
BaseProductID ,
ProductName)
VALUES ( ##IDENTITY, 'Existing Product' )
MERGE INTO #TargetBaseProduct tgt
USING
( SELECT sbp.BaseProductID ,
sbp.BaseProductName ,
sp.ProductName
FROM #SourceBaseProduct sbp
JOIN #SourceProduct sp ON sp.BaseProductID = sbp.BaseProductID
) AS src
ON tgt.BaseProductID = src.BaseProductID
WHEN NOT MATCHED BY TARGET THEN
INSERT VALUES ( src.BaseProductName )
OUTPUT
Inserted.BaseProductID ,
src.ProductName
INTO #TargetProduct(BaseProductID, ProductName);
SELECT * FROM #TargetBaseProduct
SELECT * FROM #TargetProduct
Output
BaseProductID BaseProductName
1 Existing Product Base
2 BaseProduct2
3 BaseProduct3
ProductID BaseProductID ProductName
1 1 Existing Product
2 2 Product2
3 3 Product3
Expected result
BaseProductID BaseProductName
1 Existing Product Base
2 BaseProduct1
3 BaseProduct2
4 BaseProduct3
ProductID BaseProductID ProductName
1 1 Existing Product
2 2 Product1
3 3 Product2
4 4 Product3
EDIT
using
ON 1 = 0
like Giorgi suggested gave the right result

If I get you right, there is way with MERGE:
DECLARE #SourceBaseProduct TABLE
(
BaseProductID INT ,
BaseProductName NVARCHAR(MAX)
)
DECLARE #SourceProduct TABLE
(
ProductID INT ,
BaseProductID INT ,
ProductName NVARCHAR(MAX)
)
DECLARE #TargetBaseProduct TABLE
(
BaseProductID INT IDENTITY(1, 1) ,
BaseProductName NVARCHAR(MAX)
)
DECLARE #TargetProduct TABLE
(
ProductID INT IDENTITY(1, 1) ,
BaseProductID INT ,
ProductName NVARCHAR(MAX)
)
INSERT INTO #SourceBaseProduct
VALUES ( 1, 'BaseProduct1' ),
( 2, 'BaseProduct2' ),
( 4, 'BaseProduct3' )
INSERT INTO #SourceProduct
VALUES ( 3, 1, 'Product1' ),
( 9, 2, 'Product2' ),
( 27, 4, 'Product3' )
MERGE INTO #TargetBaseProduct tgt
USING
( SELECT sbp.BaseProductID ,
sbp.BaseProductName ,
sp.ProductName
FROM #SourceBaseProduct sbp
JOIN #SourceProduct sp ON sp.BaseProductID = sbp.BaseProductID
) AS src
ON tgt.BaseProductID = src.BaseProductID
WHEN NOT MATCHED BY TARGET THEN
INSERT VALUES ( src.BaseProductName )
OUTPUT
Inserted.BaseProductID ,
src.ProductName
INTO #TargetProduct(BaseProductID, ProductName);
SELECT * FROM #TargetBaseProduct
SELECT * FROM #TargetProduct
Output:
BaseProductID BaseProductName
1 BaseProduct1
2 BaseProduct2
3 BaseProduct3
ProductID BaseProductID ProductName
1 1 Product1
2 2 Product2
3 3 Product3
EDIT: if you want to insert existing rows too, then change
ON tgt.BaseProductID = src.BaseProductID
to
ON 1 = 0

Related

T-SQL - Query data based on different filter granularity

-- Data Setup
DECLARE #Table AS TABLE
(
[Id] INT IDENTITY(1, 1)
, [Type] TINYINT
, [TypeOne] INT
, [TypeTwo] INT
, [TypeThree] INT
) ;
INSERT INTO #Table
( [Type]
, [TypeOne]
, [TypeTwo]
, [TypeThree] )
VALUES
( 1, 1, NULL, NULL )
, ( 1, 2, NULL, NULL )
, ( 1, 3, NULL, NULL )
, ( 2, NULL, 10, NULL )
, ( 2, NULL, 20, NULL )
, ( 3, NULL, NULL, 100 )
, ( 3, NULL, NULL, 200 )
, ( 3, NULL, NULL, 300 ) ;
-- Query filters
DECLARE #IncludeTypeOne BIT = 1
, #IncludeTypeTwo BIT = 0 ;
DECLARE #TypeThree_Ids TABLE ( [TypeThree] INT ) ;
INSERT INTO #TypeThree_Ids
VALUES
( 200 )
, ( 300 ) ;
-- Goal: To query #Table based on #IncludeTypeOne, #IncludeTypeTwo, and #TypeThree_Ids values. For first two filters, there's no need to check for specific value of the type in the [TypeOne] and [TypeTwo] columns. However, for the third filter, specific values in the [TypeThree] column must match with the values in #TypeThree_Ids. Is there a way to do this without doing three separate queries and union-ing them all together (the actual table/data is quite large)?
-- Expected output
Id Type TypeOne TypeTwo TypeThree
1 1 1 NULL NULL
2 1 2 NULL NULL
3 1 3 NULL NULL
7 3 NULL NULL 200
8 3 NULL NULL 300
-- My unsuccessful try thus far
SELECT *
FROM #Table
WHERE ( ( #IncludeTypeOne = 0 AND [Type] <> 1 ) OR [Type] = 1 )
AND ( ( #IncludeTypeTwo = 0 AND [Type] <> 2 ) OR [Type] = 2 )
AND ( ( ( SELECT COUNT(1) FROM #TypeThree_Ids ) = 0 AND [Type] <> 3 ) OR [TypeThree] IN ( SELECT [TypeThree] FROM #TypeThree_Ids ) ) ;
-- Actual output
Id Type TypeOne TypeTwo TypeThree
Better to use a join than a sub query -- like this:
SELECT *
FROM #Table
LEFT JOIN #TypeThreeIds ON #Table.TypeThree= #TypeThreeIds.TypeThree
WHERE (#includetypeone = 1 AND [Type] = 1)
OR (#includetypetype = 2 AND [Type] = 2)
OR ([Type] = 3 AND #TypeThreeIds.TypeThree IS NOT NULL)

TSQL - How to avoid UNION ALL

Sample Data:
DECLARE #Parent TABLE
(
[Id] INT
, [Misc_Val] VARCHAR(5)
) ;
DECLARE #Children TABLE
(
[Id] INT
, [P_ID] INT
) ;
INSERT INTO #Parent
VALUES
( 1, 'One' )
, ( 2, 'Two' )
, ( 3, 'Three' )
, ( 5, 'Four' ) ;
INSERT INTO #Children
VALUES
( 10, 1 )
, ( 11, 1 )
, ( 21, 2 )
, ( 23, 2 )
, ( 30, 3 )
, ( 40, 4 ) ;
Goal:
To efficiently output three fields ( [Id] and [IsChild], [Misc_Val] ). Output all records from #Parent table with [IsChild] = 0 and output all MATCHING records from #Child table (#Parent.Id = #Children.P_Id) with [IsChild] = 1.
Expected Output
Id IsChild Misc_Val
1 0 One
2 0 Two
3 0 Three
5 0 Four
10 1 One
11 1 One
21 1 Two
23 1 Two
30 1 Three
My try:
SELECT [P].[Id]
, 0 AS [IsChild]
, [P].[Misc_Val]
FROM #Parent AS [P]
UNION ALL
SELECT [C].[Id]
, 1
, [P].[Misc_Val]
FROM #Parent AS [P]
JOIN #Children AS [C]
ON [C].[P_ID] = [P].[Id] ;
Is there a better way to do this than using UNION ALL? #Parent and #Children tables are quite big and so am trying to avoid querying the #Parent table twice.
UPDATE: The below answer made me realized something I missed out when creating the post with mocked data. We do need some additional data from #Parent table regardless in the final output.
You can use CROSS APPLY to add the child table to the parent table.
This may or may not be faster, it can depend on indexing and so forth. You need to check the query plan.
SELECT v.Id
, v.IsChild
, P.Misc_Val
FROM #Parent AS P
CROSS APPLY (
SELECT
P.Id,
0 AS IsChild
UNION ALL
SELECT
C.Id,
1
FROM #Children AS C
WHERE C.P_ID = P.Id
) v;
Note that the first SELECT in the apply has no FROM and therefore does not do any table access.

SQL - "NOT IN" in WHERE clause using INNER JOIN not working

I need to filter a table based in a sub table data.
I'll exemplify with a hypnotic data to be easier to explain:
Master table: Cars
Sub table: Attributes (like Color, car type, accessories)
These attributes have an id (idOption) and the selected value (idList)
So, in an example, I need to filter all the cars with the color (idOption = 10) yellow (idList = 45). I can't filter this directly because the search need to consider the other option's results (which include the types, accessories.
When I use NOT IN for just one table, it works. But when I use merging the 2 tables with INNER JOIN, it does not work.
So in summary, I need to filter the 3 idOption (when is not NULL) with a given value, and this needs to reflect in the main table, grouped by product.
Table Cars:
idProduct | Description
1 Product A
2 Product B
3 Product C
Table Attributes:
idRow idProduct idOption idList
---------------------------------------
1 1 10 45
2 2 10 46
3 3 10 47
4 1 11 10
5 2 11 98
6 1 14 56
7 3 16 28
8 2 20 55
This is the stored procedure that I created which is not working:
ALTER PROCEDURE [dbo].[SP_GET_TestSearch]
(#Param1 BIGINT = NULL,
#PValue1 BIGINT = NULL,
#Param2 BIGINT = NULL,
#PValue2 BIGINT = NULL,
#Param3 BIGINT = NULL,
#PValue3 BIGINT = NULL)
AS
SET NOCOUNT ON;
SELECT
Cars.idProduct,
Cars.[Description]
FROM
Cars
INNER JOIN
Attributes ON Cars.idProduct = Attributes.idProduct
WHERE
((#Param1 IS NULL OR (idOption NOT IN (#Param1)))
AND
(#Param2 IS NULL OR (idOption NOT IN (#Param2)))
AND
(#Param3 IS NULL OR (idOption NOT IN (#Param3))))
OR
(idOption = ISNULL(#Param1, NULL)
AND idList = ISNULL(#PValue1, NULL))
OR
(idOption = ISNULL(#Param2, NULL)
AND idList = ISNULL(#PValue2, NULL))
OR
(idOption = ISNULL(#Param3, NULL)
AND idList = ISNULL(#PValue3, NULL))
GROUP BY
Cars.idProduct, Cars.[Description]
The following code demonstrates how to implement the logic of excluding vehicles from query results if they have any "bad" property values. The rejection is handled by ... where not exists ... which is used to check each car against the "bad" property values.
Rather than using an assortment of (hopefully) paired parameters to pass the undesirable properties, the values are passed in a table. The stored procedure to implement this ought to use a table-valued parameter (TVP) to pass the table.
-- Sample data.
declare #Cars as Table ( CarId Int Identity, Description VarChar(16) );
insert into #Cars ( Description ) values
( 'Esplanade' ), ( 'Tankigator' ), ( 'Land Yacht' );
select * from #Cars;
declare #Properties as Table ( PropertyId Int Identity, Description VarChar(16) );
insert into #Properties ( Description ) values
( 'Turbochargers' ), ( 'Superchargers' ), ( 'Hyperchargers' ), ( 'Color' ), ( 'Spare Tires' );
select * from #Properties;
declare #CarProperties as Table ( CarId Int, PropertyId Int, PropertyValue Int );
insert into #CarProperties ( CarId, PropertyId, PropertyValue ) values
( 1, 1, 1 ), ( 1, 4, 24 ), ( 1, 4, 42 ), -- Two tone!
( 2, 2, 1 ), ( 2, 4, 7 ),
( 3, 1, 2 ), ( 3, 4, 0 ), ( 3, 5, 6 );
select C.CarId, C.Description as CarDescription,
P.PropertyId, P.Description as PropertyDescription,
CP.PropertyValue
from #Cars as C inner join
#CarProperties as CP on CP.CarId = C.CarId inner join
#Properties as P on P.PropertyId = CP.PropertyId
order by C.CarId, P.PropertyId;
-- Test data: Avoid vehicles that have _any_ of these property values.
-- This should be passed to the stored procedure as a table-value parameter (TVP).
declare #BadProperties as Table ( PropertyId Int, PropertyValue Int );
insert into #BadProperties ( PropertyId, PropertyValue ) values
( 2, 1 ), ( 2, 2 ), ( 2, 4 ),
( 4, 62 ), ( 4, 666 );
select BP.PropertyId, BP.PropertyValue, P.Description
from #BadProperties as BP inner join
#Properties as P on P.PropertyId = BP.PropertyId;
-- Query the data.
select C.CarId, C.Description as CarDescription
from #Cars as C
where not exists (
select 42
from #CarProperties as CP inner join
#BadProperties as BP on BP.PropertyId = CP.PropertyId and BP.PropertyValue = CP.PropertyValue
where CP.CarId = C.CarId )
order by C.CarId;
A few things here.
Firstly, this kind of catch all procedure is a bit of an anti pattern for all sorts of reasons, see here for a full explanation:- https://sqlinthewild.co.za/index.php/2018/03/13/revisiting-catch-all-queries/
Secondly, you need to be very careful of using NOT IN with nullable values in a list: http://www.sqlbadpractices.com/using-not-in-operator-with-null-values/
I've added the DDL for the tables:-
IF OBJECT_ID('Attributes') IS NOT NULL
DROP TABLE Attributes;
IF OBJECT_ID('Cars') IS NOT NULL
DROP TABLE Cars;
IF OBJECT_ID('SP_GET_TestSearch') IS NOT NULL
DROP PROCEDURE SP_GET_TestSearch
CREATE TABLE Cars
(idProduct INT PRIMARY KEY
, Description VARCHAR(20) NOT NULL);
CREATE TABLE Attributes
(idRow INT PRIMARY KEY
, idProduct INT NOT NULL FOREIGN KEY REFERENCES dbo.Cars(idProduct)
, idOption INT NOT NULL
, idList INT NOT NULL);
INSERT INTO dbo.Cars
VALUES
(1, 'Product A')
,(2 , 'Product B')
,(3, 'Product C');
INSERT INTO dbo.Attributes
(
idRow,
idProduct,
idOption,
idList
)
VALUES (1,1,10,45)
,(2,2,10,46)
,(3,3,10,47)
,(4,1,11,10)
,(5,2,11,98)
,(6,1,14,56)
,(7,3,16,28)
,(8,2,20,55);
GO
The issue with your query, is that the first part of the block is always evaluated to TRUE for any idOption that you don't specify:-
((#Param1 IS NULL OR (idOption NOT IN (#Param1)))
AND
(#Param2 IS NULL OR (idOption NOT IN (#Param2)))
AND
(#Param3 IS NULL OR (idOption NOT IN (#Param3))))
To explain; if I pass in the following:-
DECLARE #Param1 BIGINT
, #Param2 BIGINT
, #Param3 BIGINT
, #PValue1 BIGINT
, #PValue2 BIGINT
, #PValue3 BIGINT;
SET #Param1 = 11
SET #Pvalue1 = 42
SET #Param2 = 11
SET #Pvalue2 = 10
SET #Param3 = 14
SET #PValue3= 56
EXEC dbo.SP_GET_TestSearch #Param1, #PValue1, #Param2, #PValue2, #Param3, #PValue3
Then you effectively have WHERE idOption NOT IN (11,14) as the evaluation for the first part of the clause, so all other rows are returned.
I suspect you really want the WHERE clause to be:-
WHERE
(#Param1 IS NULL AND #Param2 IS NULL AND #Param3 IS NULL)
OR
(idOption = #Param1
AND idList = #PValue1)
OR
(idOption = #Param2
AND idList = #PValue2)
OR
(idOption = #Param3
AND idList = #PValue3)

Get the id of the last record in the data SQL Server

I am trying to get the last ID from at least 4 child-parent relationships between ID's and sum all related ID's quantity. I have tried below -
declare #test table (ID int not null, P_ID int null, Qty int not null)
insert into #test(ID, P_ID, Qty) values
(1 , 11 , 1),
(2 , null, 3),
(11, 21 , 2),
(21, 31 , 1),
(31, null, 3),
(12, null, 4)
select
COALESCE(T2.ID,T1.ID) as ID,
MAX(CASE WHEN T1.P_ID is not null then T1.ID END) as OldID,
SUM(Qty) as Qty
from
#test T1
left join
(select ID from #test
GROUP By ID) T2
on T2.ID = T1.P_ID
group by
COALESCE(T2.ID, T1.ID)
I am getting output -
ID OldID Qty
2 NULL 3
11 1 1
12 NULL 4
21 11 1
31 21 2
But I want my output will be like this where all ID's with no Parent ID in the first row then all previous ID's will show and SUM all relevant ID's quantity -
ID OldID3 OldID2 OldID1 Qty
2 3
12 4
31 21 11 1 7
Could someone please help me to achieve this.
Thanks in advance
Hopefully, this helps you. I have not tested it thoroughly, so apologies for any bugs.
I'm using a Common Table Expression to get the hierarchy information, then using a dynamic SQL I extract the desired number of previous IDs.
DECLARE #test TABLE (ID INT NOT NULL, P_ID INT NULL, Qty INT NOT NULL);
INSERT INTO #test(ID, P_ID, Qty) VALUES
(1 , 11 , 1),
(2 , null, 3),
(11, 21 , 2),
(21, 31 , 1),
(31, null, 3),
(12, null, 4);
IF (OBJECT_ID('tempdb..#hierarchy') IS NOT NULL)
DROP TABLE #hierarchy;
CREATE TABLE #hierarchy (
RootID INT NOT NULL, ID INT NOT NULL, [Qty] INT NOT NULL, SeqIndex INT NOT NULL
);
;WITH hierarchy AS (
SELECT ID, P_ID, Qty, ID [RootID], 0 [SeqIndex]
FROM #test
WHERE P_ID IS NULL
UNION ALL
SELECT child.ID, child.P_ID, child.Qty, parent.RootID, parent.SeqIndex + 1 [SeqIndex]
FROM #test child
JOIN hierarchy parent ON parent.ID=child.P_ID
)
INSERT #hierarchy
SELECT RootID, ID, Qty, SeqIndex
FROM hierarchy;
DECLARE
#DEPTH INT = 3,
#maxSeqIndex INT = (SELECT MAX(SeqIndex) FROM #hierarchy);
IF (#DEPTH = 0)
SELECT RootID, SUM(Qty) [Qty]
FROM #hierarchy
GROUP BY RootID;
ELSE IF (#DEPTH > #maxSeqIndex)
SELECT NULL
ELSE BEGIN
DECLARE #SQL NVARCHAR(MAX) = N'
SELECT
RootID,
';
DECLARE #idx INT = 1;
WHILE #idx <= #DEPTH BEGIN
SET #SQL += N'
(SELECT ID FROM #hierarchy i WHERE i.RootID=o.RootID AND SeqIndex='+CAST(#idx as nvarchar(10))+N') [OldID'+CAST(#maxSeqIndex-#idx+1 as nvarchar(10))+N'],';
SET #idx += 1;
END
SET #SQL += N'
SUM(Qty) [Qty]
FROM #hierarchy o
GROUP BY RootID;';
EXEC sp_executesql #SQL
END
Of course, the dynamic script could be replaced with a hard-coded SQL if that is OK for you.
Note: performance has not been considered

Recursive Function for MS SQL

I need to write a recursive function to get the references from table B and combine in a field in table A.
Table A:
ID   Name      Reference
1    Item A
2    Item B
3    Item C
Table B:
ID   Parent_ID   Reference
1    1           ABC
2    1           DEF
3    2           GHI
Expected result:
ID   Name      Reference
1    Item A     ABCDEF
2    Item B     GHI
3    Item C
I don't see any recursion here:
DECLARE #a TABLE
(
ID INT ,
Name VARCHAR(10) ,
Reference VARCHAR(100)
)
DECLARE #b TABLE
(
ID INT ,
ParentID INT ,
Reference VARCHAR(3)
)
INSERT INTO #a
VALUES ( 1, 'Item A', NULL ),
( 2, 'Item B', NULL ),
( 3, 'Item C', NULL )
INSERT INTO #b
VALUES ( 1, 1, 'ABC' ),
( 2, 1, 'DEF' ),
( 3, 2, 'GHI' )
UPDATE a
SET Reference = ca.data
FROM #a a
CROSS APPLY ( SELECT
( SELECT b.Reference
FROM #b b
WHERE a.ID = b.ParentID
ORDER BY ID
FOR XML PATH('') ,
TYPE
).value('.', 'varchar(max)') AS DATA
) ca
SELECT *
FROM #a
Output:
ID Name Reference
1 Item A ABCDEF
2 Item B GHI
3 Item C NULL