See if multiple entries exist in another table - sql-server

Data Set 1:
Cust_Ref | ACC1 | ACC2 | ACC3
------------+-----------+-----------+---------
1000001 | ALPHA | BRAVO | CHARLIE
1000002 | ALPHA | BRAVO | CHARLIE
1000003 | ALPHA | BRAVO | CHARLIE
1000004 | DELTA | ECHO |
1000005 | DELTA | ECHO |
1000006 | FOXTROT |
1000007 | FOXTROT |
Data Set 2:
Cust_Ref | ACC
------------+--------
1000001 | ALPHA
1000001 | BRAVO
1000001 | DELTA
1000004 | DELTA
1000004 | ECHO
1000006 | FOXTROT
Data Set 1 shows the customer references and the accounts they should have. So for example 1000001 must have the accounts - ALPHA, BRAVO, CHARLIE. Customer 1000002 has DELTA and ECHO, etc.
Data Set 2 shows what accounts are actually associated with a customer reference.
Is there where I can return instances of missing accounts with T-SQL?
Example:
In the dataset I have provided customer 1000001 should have ALPHA, BRAVO, CHARLIE but Data Set 2 shows that the customer does not have CHARLIE.

Considering this DDL and sample data:
DECLARE #Table1 TABLE (
Cust_Ref VARCHAR(10) PRIMARY KEY,
ACC1 VARCHAR(10) NULL,
ACC2 VARCHAR(10) NULL,
ACC3 VARCHAR(10) NULL
)
INSERT INTO #Table1 VALUES
('1000001','ALPHA','BRAVO','CHARLIE'),
('1000002','ALPHA','BRAVO','CHARLIE'),
('1000003','ALPHA','BRAVO','CHARLIE'),
('1000004','DELTA','ECHO',NULL),
('1000005','DELTA','ECHO',NULL),
('1000006','FOXTROT','FOXTROT',NULL),
('1000007','FOXTROT','FOXTROT',NULL)
DECLARE #Table2 TABLE (
Cust_Ref VARCHAR(10) NOT NULL,
ACC VARCHAR(10) NOT NULL
)
INSERT INTO #Table2 VALUES
('1000001','ALPHA'),
('1000001','BRAVO'),
('1000001','DELTA'),
('1000004','DELTA'),
('1000004','ECHO'),
('1000006','FOXTROT')
You could use UNPIVOT and EXCEPT, this way:
SELECT Cust_Ref, ACC
FROM #Table1 UNPIVOT (ACC FOR COL IN (ACC1, ACC2, ACC3)) U
EXCEPT
SELECT Cust_Ref, ACC
FROM #Table2

(Select cust_ref, acc1 account
from dataSet1
union
Select cust_ref, acc2 account
from dataSet1
union
Select cust_ref, acc3 account
from dataSet1)z
Where Not exists (Select * from dataSet2
where cust_ref = z.CustRef
and acc = z.account)

WITH DataSet1 AS ( -- Define DataSet1
SELECT *
FROM (VALUES
(1000001, 'ALPHA', 'BRAVO', 'CHARLIE')
, (1000002, 'ALPHA', 'BRAVO', 'CHARLIE')
, (1000003, 'ALPHA', 'BRAVO', 'CHARLIE')
, (1000004, 'DELTA', 'ECHO', NULL)
, (1000005, 'DELTA', 'ECHO', NULL)
, (1000006, 'FOXTROT', NULL, NULL)
, (1000007, 'FOXTROT', NULL, NULL)
) A (Cust_Ref, ACC1, ACC2, ACC3)
), DataSet2 AS ( -- Define DataSet2
SELECT *
FROM (VALUES
(1000001, 'ALPHA')
, (1000001, 'BRAVO')
, (1000004, 'DELTA')
, (1000004, 'ECHO')
, (1000006, 'FOXTROT')
) A (Cust_Ref, ACC)
)
SELECT A.Cust_Ref
, B.ACC
FROM DataSet1 A
CROSS APPLY(VALUES -- Pivot ACC1, ACC2, and ACC3 into ACC
(A.ACC1)
, (A.ACC2)
, (A.ACC3)
) B (ACC)
WHERE B.ACC IS NOT NULL -- Remove NULL ACCs
AND NOT EXISTS ( -- Remove ACCs that exist in DataSet2
SELECT 1
FROM DataSet2
WHERE A.Cust_Ref = Cust_Ref
AND B.ACC = ACC
);

Related

How to multi-select filter an EAV table in SQL Server

I have an EAV table with attributes and would like to do a hybrid selection of the items based on variables that are passed into a stored procedure.
Sample table:
| group_id | item_id | key | value |
+----------+---------+--------+-------+
| 1 | AA | length | 10 |
| 1 | AA | width | 10 |
| 1 | AA | color | white |
| 1 | AA | brand | beta |
| 1 | BB | length | 25 |
| 1 | BB | brand | alpha |
| 2 | CC | brand | alpha |
Sample query:
declare #attributes nvarchar(max) = 'brand name, length'
declare #attributeValues nvarchar(max) = 'alpha, beta, 25'
declare #id int = 1
select *
into #allProductsFromGroup
from items
where group_id = #id
select item_id
from #allProductsFromGroup #all
where [key] in (select value from string_split(#attributes, ','))
and [value] in (select value from string_split(#attributeValues, ','))
Expected output:
| item_id |
+---------+
| BB |
I could hard-code in and and or statements for each key, but there are many, and I am looking for a more scalable solution.
Passing in and parsing JSON would be good, like:
[
{ "brand": "aplha" },
{ "brand": "beta" },
{ "length": 25 }
]
How can I write the second select to dynamically return a subset of allProductsFromGroup that dynamically include multiple results from the same group (multi-select brand or multi-select length), but exclude from other groups (color, length, etc.)?
The target query might look something like this:
with q as
(
select item_id,
max( case when [key] = 'brand' then [value] end ) brand,
max( case when [key] = 'length' then cast( [value] as int ) end ) length,
from #allProductsFromGroup
group by Item_id
)
select item_id
from q
where brand in ('alpha','beta') and length=25
You just have to build it from the incoming data (yuck). A simpler query form to generate might be something like
select item_id
from #allProductsFromGroup
where [key] = 'brand' and [value] in ('alpha','beta')
intersect
select item_id
from #allProductsFromGroup
where [key] = 'length' and [value] = 25
mapping and criteria to intersect, and or criteria to union. It's likely to be cheaper too, as each query can seek an index on (key,value).
It's probably a late answer, but if you can pass conditions as JSON, the next approach is also a possible solution. The JSON must be in the same format as in the answer and you may use more than two conditions:
Table:
CREATE TABLE Data (
group_id int,
item_id varchar(2),
[key] varchar(100),
[value] varchar(100)
)
INSERT INTO Data (group_id, item_id, [key], [value])
VALUES
(1, 'AA', 'length', '10'),
(1, 'AA', 'width', '10'),
(1, 'AA', 'color', 'white'),
(1, 'AA', 'brand', 'beta'),
(1, 'BB', 'length', '25'),
(1, 'BB', 'brand', 'alpha'),
(2, 'CC', 'brand', 'alpha')
Conditions as JSON:
DECLARE #conditions varchar(max) = N'
[
{"key": "brand", "values": ["alpha", "beta"]},
{"key": "length", "values": ["25"]}
]
'
Statement:
SELECT d.item_id
FROM Data d
JOIN (
SELECT j1.[key], j2.[value]
FROM OPENJSON(#conditions) WITH (
[key] varchar(100) '$.key',
[values] nvarchar(max) '$.values' AS JSON
) j1
CROSS APPLY OPENJSON(j1.[values]) j2
) o ON d.[key] = o.[key] AND d.[value] = o.[value]
GROUP BY d.item_id
HAVING COUNT(*) = (SELECT COUNT(*) FROM OPENJSON(#conditions))
Result:
item_id
BB

How to use SQL to concatenate two rows with grouping and CASE

I have some genomic data that is in SQL Server 2016 Express and it is currently shaped in a long format with a reference genome and test genomes split by a SubjectID, genes, and codons (e.g. a 3-tuple).
What I really need is to reshape my data into one which the tuples are concatenated together but ONLY when there is a mutation (as compared to the reference genome) in the tuple. This will be a more usable format for everyone.
My data looks like this
DECLARE #myTable TABLE
(
SubjectID VARCHAR(MAX),
country VARCHAR(MAX),
gene VARCHAR(MAX),
position INT,
ReferenceNucleotide VARCHAR(1),
TestNucleotide VARCHAR(1),
codon INT,
nucleotide_order INT
)
INSERT INTO #myTable
VALUES
('1-0002','India','gyrA', 65,'A','x', 92,1),
('1-0002','India','gyrA', 66,'T','x', 92,2),
('1-0002','India','gyrA', 67,'C','C', 92,3),
('1-0002','India','gyrA', 68,'T','T', 93,1),
('1-0002','India','gyrA', 69,'A','A', 93,2),
('1-0002','India','gyrA', 70,'C','C', 93,3),
('1-0002','India','gyrA', 71,'G','G', 94,1),
('1-0002','India','gyrA', 72,'A','A', 94,2),
('1-0002','India','gyrA', 73,'C','C', 94,3),
('1-0002','India','gyrA', 74,'A','A', 95,1),
('1-0002','India','gyrA', 75,'G','C', 95,2),
('1-0002','India','gyrA', 76,'C','C', 95,3),
('1-0002','India','gyrA', 77,'C','C', 96,1),
('1-0002','India','gyrA', 78,'T','T', 96,2),
('1-0002','India','gyrA', 79,'G','N', 96,3)
However, there are a couple of conditions
If all three nucleotides are the same for the reference and test genome I want a 'WT'
If there is any difference in nucleotides, I want the 3-tuple from the test genome (in the nucleotide order)
I need to group by SubjectID and gene because I have lots of Subjects & genes
And my result would look like
1-0002 India gyrA 92 xxC
1-0002 India gyrA 93 WT
1-0002 India gyrA 94 WT
1-0002 India gyrA 95 ACC
1-0002 India gyrA 96 CTN
I can identify where the codons are that need to have the 3-tuple but am struggling with how to concatentate them
DECLARE #myCodons TABLE (SubjectID varchar(max), country varchar(max), gene varchar(max), codon int, WT int)
INSERT INTO #myCodons
SELECT
SubjectID, country, gene, codon,
SUM(CASE WHEN RefNucleotide=TestNucleotide THEN 0 ELSE 1 END) AS WT
FROM
#myTable
GROUP BY
SubjectID, country, gene, codon
SELECT *
FROM #myCodons
ORDER BY codon
Start with something like:
select T1.SubjectID, T1.country, T1.gene, T1.codon,
T1.RefGenome + T2.RefGenome + T3.RefGenome RefGenome,
T1.TestGenome + T2.TestGenome + T3.TestGenome TestGenome
from #myTable T1
inner join #myTable T2 on T1.SubjectID = T2.SubjectID and T1.country = T2.country
and T1.gene = T2.gene and T1.codon = T2.codon and T2.nucleotide_order = 2
inner join #myTable T3 on T1.SubjectID = T3.SubjectID and T1.country = T3.country
and T1.gene = T3.gene and T1.codon = T3.codon and T3.nucleotide_order = 3
where T1.nucleotide_order = 1
You can then build on this with case statements to work out whether to display the test genome or 'WT'
An alternative approach which might be of use:
select SubjectID, country, gene, codon, case when RefGenomeStr = TestGenomeStr then 'WT' else TestGenomeStr end wanted_string
from #myTable t1
cross apply(
SELECT
STUFF((
SELECT
', ' +RefGenome
FROM #myTable t2
WHERE t2.SubjectID= t1.SubjectID and t2.country = t1.country and t2.gene = t1.gene and t2.codon = t1.codon
FOR XML PATH ('')
)
, 1, 1, '')
, STUFF((
SELECT
', ' +TestGenome
FROM #myTable t2
WHERE t2.SubjectID= t1.SubjectID and t2.country = t1.country and t2.gene = t1.gene and t2.codon = t1.codon
FOR XML PATH ('')
)
, 1, 1, '')
) ca (RefGenomeStr,TestGenomeStr)
where nucleotide_order = 1
Result:
+----+-----------+---------+------+-------+---------------+
| | SubjectID | country | gene | codon | wanted_string |
+----+-----------+---------+------+-------+---------------+
| 1 | 1-0002 | India | gyrA | 92 | x, x, C |
| 2 | 1-0002 | India | gyrA | 93 | WT |
| 3 | 1-0002 | India | gyrA | 94 | WT |
| 4 | 1-0002 | India | gyrA | 95 | A, C, C |
| 5 | 1-0002 | India | gyrA | 96 | C, T, N |
+----+-----------+---------+------+-------+---------------+
Demo

Insert multuple rows at once with a calculated column from prior inserts into SQL Server

I'm trying to figure out how to do a multi-row insert as one statement in SQL Server, but where one of the columns is a column computer based on the data as it stands after every insert row.
Let's say I run this simple query and get back 3 records:
SELECT *
FROM event_courses
WHERE event_id = 100
Results:
id | event_id | course_id | course_priority
---+----------+-----------+----------------
10 | 100 | 501 | 1
11 | 100 | 502 | 2
12 | 100 | 503 | 3
Now I want to insert 3 more records into this table, except I need to be able to calculate the priority for each record. The priority should be the count of all courses in this event. But if I run a sub-query, I get the same priority for all new courses:
INSERT INTO event_courses (event_id, course_id, course_priority)
VALUES (100, 500,
(SELECT COUNT (id) + 1 AS cnt_event_courses
FROM event_courses
WHERE event_id = 100)),
(100, 501,
(SELECT COUNT (id) + 1 AS cnt_event_courses
FROM event_courses
WHERE event_id = 1))
Results:
id | event_id | course_id | course_priority
---+----------+-----------+-----------------
10 | 100 | 501 | 1
11 | 100 | 502 | 2
12 | 100 | 503 | 3
13 | 100 | 504 | 4
14 | 100 | 505 | 4
15 | 100 | 506 | 4
Now I know I could easily do this in a loop outside of SQL and just run a bunch of insert statement, but that's not very efficient. There's got to be a way to calculate the priority on the fly during a multi-row insert.
Big thanks to #Sean Lange for the answer. I was able to simplify it even further for my application. Great lead! Learned 2 new syntax tricks today ;)
DECLARE #eventid int = 100
INSERT event_courses
SELECT #eventid AS event_id,
course_id,
course_priority = existingEventCourses.prioritySeed + ROW_NUMBER() OVER(ORDER BY tempid)
FROM (VALUES
(1, 501),
(2, 502),
(3, 503)
) courseInserts (tempid, course_id) -- This basically creates a temp table in memory at run-time
CROSS APPLY (
SELECT COUNT(id) AS prioritySeed
FROM event_courses
WHERE event_id = #eventid
) existingEventCourses
SELECT *
FROM event_courses
WHERE event_id = #eventid
Here is an example of how you might be able to do this. I have no idea where your new rows values are coming from so I just tossed them in a derived table. I doubt your final solution would look like this but it demonstrates how you can leverage ROW_NUMBER for accomplish this type of thing.
declare #EventCourse table
(
id int identity
, event_id int
, course_id int
, course_priority int
)
insert #EventCourse values
(100, 501, 1)
,(100, 502, 2)
,(100, 503, 3)
select *
from #EventCourse
insert #EventCourse
(
event_id
, course_id
, course_priority
)
select x.eventID
, x.coursePriority
, NewPriority = y.MaxPriority + ROW_NUMBER() over(partition by x.eventID order by x.coursePriority)
from
(
values(100, 504)
,(100, 505)
,(100, 506)
)x(eventID, coursePriority)
cross apply
(
select max(course_priority) as MaxPriority
from #EventCourse ec
where ec.event_id = x.eventID
) y
select *
from #EventCourse

How to insert master detail records in T-SQL?

I need to copy some master-detail records, along the lines of:
INSERT INTO Order
(
SupplierId
,DateOrdered
)
SELECT
SupplierID
,DateOrdered
FROM Order
WHERE SupplierId = 10
DECLARE #OrderId int;
Select #OrderId = Scope_Identity;
INSERT INTO OrderItem
(
Quantity
,ProductCode
,Price
,FkOrderId
)
SELECT
Quantity
,ProductCode
,Price
,FkOrderId
FROM OrderItem
WHERE FkOrderId = #OrderId
This will not work. The reason is that there are multiple Orders for Supplier = 10. So what is the best way to iterate through each Order where Supplier = 10, Add the order, and then add the relevant child OrderItem BEFORE going onto the next Order Record where supplier=10. I think I am talking about batching, possibly cursors, but I am a newbie to T-SQL / Store Procedures.
I would appreciate advice on the above.
Thanks.
EDIT
Some more information which I hope will clarify by virtue of some sample data.
Original Order Table
Id SupplierId DateOrdered
1 10 01/01/2000
2 10 01/01/2000
Original OrderItem Table
Id Quantity ProductCode Price FkOrderId
1 20 X1 100 1
2 10 Y1 50 1
3 30 X1 100 2
4 20 Y1 50 2
Final Order Table
Id SupplierId DateOrdered
1 10 01/01/2000
2 10 01/01/2000
3 10 01/01/2000 (Clone of 1)
4 10 01/01/2000 (Clone of 2)
Final OrderItem Table
Id Quantity ProductCode Price FkOrderId
1 20 X1 100 1
2 10 Y1 50 1
3 30 X1 100 2
4 20 Y1 50 2
5 20 X1 100 3 (Clone of 1, linked to clone Order=3)
6 10 Y1 50 3 (Clone of 2, linked to clone Order=3)
7 30 X1 100 4 (Clone of 3, linked to clone Order=4)
8 20 Y1 50 5 (Clone of 4, linked to clone Order=4)
So I need some help with the code can do this cloning of Order and OrderItem to achieve the "final" table records.
It seems I need to do something like:
For each matching record in "Order"
Clone Order Record
Clone OrderItem Record where FkOrderId = OldOrderId
Next
This answers your question (no cursors either)
SQL Fiddle
MS SQL Server 2008 Schema Setup:
CREATE TABLE [Order]
(
Id Int Primary Key Identity,
SupplierId Int,
DateOrdered Date
)
SET IDENTITY_INSERT [Order] ON
INSERT INTO [Order] (Id, SupplierId, DateOrdered)
VALUES
(1, 10, '01/01/2000'),
(2, 10, '01/01/2000')
SET IDENTITY_INSERT [Order] OFF
CREATE TABLE [OrderItem]
(
ID INT Primary Key Identity,
Quantity Int,
ProductCode CHAR(2),
Price Int,
FKOrderId Int
)
SET IDENTITY_INSERT [OrderItem] ON
INSERT INTO [OrderItem] (Id, Quantity, ProductCode, Price, FKOrderId)
VALUES
(1, 20, 'X1', 100, 1),
(2, 10, 'Y1', 50, 1),
(3, 30, 'X1', 100, 2),
(4, 20, 'Y1', 50, 2)
SET IDENTITY_INSERT [OrderItem] OFF
Query 1:
DECLARE #NewEntries TABLE (ID Int, OldId Int);
MERGE INTO [Order]
USING [Order] AS cf
ON 1 = 0 -- Ensure never match - therefore an Insert
WHEN NOT MATCHED AND cf.SupplierId = 10 THEN
INSERT(SupplierId, DateOrdered) Values(cf.SupplierId, cf.DateOrdered)
Output inserted.Id, cf.Id INTO
#NewEntries(Id, OldId);
INSERT INTO [OrderItem]
(
Quantity
,ProductCode
,Price
,FkOrderId
)
SELECT
Quantity
,ProductCode
,Price
,NE.ID
FROM [OrderItem] OI
INNER JOIN #NewEntries NE
ON OI.FKOrderId = NE.OldId ;
SELECT *
FROM [OrderItem];
Results:
| ID | QUANTITY | PRODUCTCODE | PRICE | FKORDERID |
|----|----------|-------------|-------|-----------|
| 1 | 20 | X1 | 100 | 1 |
| 2 | 10 | Y1 | 50 | 1 |
| 3 | 30 | X1 | 100 | 2 |
| 4 | 20 | Y1 | 50 | 2 |
| 5 | 20 | X1 | 100 | 3 |
| 6 | 10 | Y1 | 50 | 3 |
| 7 | 30 | X1 | 100 | 4 |
| 8 | 20 | Y1 | 50 | 4 |
Add an additional column to the Order table called OriginalOrderId. Make it nullable, FK'd back to OrderId, and put an index on it. Use "INSERT INTO [Order]... SELECT ... OUTPUT INSERTED.* INTO #ClonedOrders From ...". Add an index on #ClonedOrders.OriginalOrderId. Then you can do "INSERT INTO OrderItem ... SELECT co.OrderId, ... FROM #ClonedOrders co INNER JOIN OrderItem oi ON oi.OrderId = co.OriginalOrderId". This will get you the functionality that you're looking for, along with the performance benefits of set based statements. It will also leave you evidence of the original source of the orders and a field that you can use to differentiate cloned orders from non-cloned orders.
in this case you have to use output clause.. let me give you one sample script that will help you to relate with your requirement
Declare #Order AS Table(id int identity(1,1),SupplierID INT)
DECLARE #outputOrder AS TABLE
(Orderid INT)
INSERT INTO #Order (SupplierID)
Output inserted.id into #outputOrder
Values (102),(202),(303)
select * from #outputOrder
next step for your case would be use newly generated orderid from outputorder table & join to get orderitems from input table
This will handle your first table.
PS: Supply your questions in this state and they will be answered faster.
IF OBJECT_ID('Orders') IS NOT NULL DROP TABLE Orders
IF OBJECT_ID('OrderItem') IS NOT NULL DROP TABLE OrderItem
IF OBJECT_ID('tempdb..#FinalOrders') IS NOT NULL DROP TABLE #FinalOrders
CREATE TABLE Orders (OrdersID INT, SupplierID INT, DateOrdered DATETIME)
CREATE TABLE OrderItem (OrderItemID INT, Quantity INT, FkOrderId INT)
INSERT INTO Orders VALUES (1,20,'01/01/2000'),(2,20,'01/01/2000')
INSERT INTO OrderItem VALUES
(1,20,1),
(2,10,1),
(3,30,2),
(4,20,2)
SELECT
a.OrderItemID,
b.SupplierID,
b.DateOrdered
INTO #FinalOrders
FROM OrderItem as a
INNER JOIN Orders as b
ON a.FkOrderId = b.OrdersID
SELECT * FROM #FinalOrders
This can be achieved with a cursor. But please note that cursors will pose significant performance drawbacks.
DECLARE #SupplierID AS INT
DECLARE #OrderId AS INT
DECLARE #DateOrdered AS DATE
DECLARE #OrderIdNew AS INT
Declare #Order AS Table(OrderId INT,SupplierID INT,DateOrdered Date)
INSERT INTO #Order
SELECT
ID
,SupplierID
,DateOrdered
FROM [Order]
WHERE SupplierId = 10
DECLARE CUR CURSOR FAST_FORWARD FOR
SELECT
OrderId
,SupplierID
,DateOrdered
FROM #Order
OPEN CUR
FETCH NEXT FROM CUR INTO #OrderId, #SupplierID, #DateOrdered
WHILE ##FETCH_STATUS = 0
BEGIN
INSERT INTO [Order]
(
SupplierId
,DateOrdered
)
VALUES
(#SupplierID,#DateOrdered)
Select #OrderIdNew=##IDENTITY
INSERT INTO [OrderItem]
([Quantity]
,[ProductCode]
,[Price]
,[FkOrderId])
SELECT [Quantity]
,[ProductCode]
,[Price]
,#OrderIdNew
FROM [OrderItem]
WHERE [FkOrderId]=#OrderId
FETCH NEXT FROM CUR INTO #OrderId, #SupplierID, #DateOrdered
END
CLOSE CUR;
DEALLOCATE CUR;
You could try doing and inner join between Order and OrderItems where the clause of the inner join is SupplierId = 10,
or just modify your where to achieve the same result.
Try doing something along the lines of:
INSERT INTO OrderItem
(
Quantity
,ProductCode
,Price
,FkOrderId
)
SELECT
Quantity
,ProductCode
,Price
,FkOrderId
FROM OrderItem
where FkOrderId in (Select Id FROM Order WHERE SupplierId = 10)

How to replace a functional (many) OUTER APPLY (SELECT * FROM)

Applies to Microsoft SQL Server 2008 R2.
The problem is
If we have a few dozen Outer Apply (30) then they begin to work pretty slowly. In the middle of the Outer Apply I have something more complicated than a simple select, a view.
Details
I'm writing a sort of attributes assigned to tables (in the database). Generally, a few tables, holds a reference to a table of attributes (key, value).
Pseudo structure looks like this:
DECLARE #Lot TABLE (
LotId INT PRIMARY KEY IDENTITY,
SomeText VARCHAR(8))
INSERT INTO #Lot
OUTPUT INSERTED.*
VALUES ('Hello'), ('World')
DECLARE #Attribute TABLE(
AttributeId INT PRIMARY KEY IDENTITY,
LotId INT,
Val VARCHAR(8),
Kind VARCHAR(8))
INSERT INTO #Attribute
OUTPUT INSERTED.* VALUES
(1, 'Foo1', 'Kind1'), (1, 'Foo2', 'Kind2'),
(2, 'Bar1', 'Kind1'), (2, 'Bar2', 'Kind2'), (2, 'Bar3', 'Kind3')
LotId SomeText
----------- --------
1 Hello
2 World
AttributeId LotId Val Kind
----------- ----------- -------- --------
1 1 Foo1 Kind1
2 1 Foo2 Kind2
3 2 Bar1 Kind1
4 2 Bar2 Kind2
5 2 Bar3 Kind3
I can now run a query such as:
SELECT
[l].[LotId]
, [SomeText]
, [Oa1].[AttributeId]
, [Oa1].[LotId]
, 'Kind1Val' = [Oa1].[Val]
, [Oa1].[Kind]
, [Oa2].[AttributeId]
, [Oa2].[LotId]
, 'Kind2Val' = [Oa2].[Val]
, [Oa2].[Kind]
, [Oa3].[AttributeId]
, [Oa3].[LotId]
, 'Kind3Val' = [Oa3].[Val]
, [Oa3].[Kind]
FROM #Lot AS l
OUTER APPLY(SELECT * FROM #Attribute AS la WHERE la.[LotId] = l.[LotId] AND la.[Kind] = 'Kind1') AS Oa1
OUTER APPLY(SELECT * FROM #Attribute AS la WHERE la.[LotId] = l.[LotId] AND la.[Kind] = 'Kind2') AS Oa2
OUTER APPLY(SELECT * FROM #Attribute AS la WHERE la.[LotId] = l.[LotId] AND la.[Kind] = 'Kind3') AS Oa3
LotId SomeText AttributeId LotId Kind1Val Kind AttributeId LotId Kind2Val Kind AttributeId LotId Kind3Val Kind
----------- -------- ----------- ----------- -------- -------- ----------- ----------- -------- -------- ----------- ----------- -------- --------
1 Hello 1 1 Foo1 Kind1 2 1 Foo2 Kind2 NULL NULL NULL NULL
2 World 3 2 Bar1 Kind1 4 2 Bar2 Kind2 5 2 Bar3 Kind3
The simple way to get the pivot table of attribute values ​​and results for Lot rows that do not have attribute such a Kind3.
I know Microsoft PIVOT and it is not simple and do not fits here.
Finally, what will be faster and will give the same results?
In order to get the result you can unpivot and then pivot the data.
There are two ways that you can perform this. First, you can use the UNPIVOT and the PIVOT function:
select *
from
(
select LotId,
SomeText,
col+'_'+CAST(rn as varchar(10)) col,
value
from
(
select l.LotId,
l.SomeText,
cast(a.AttributeId as varchar(8)) attributeid,
cast(a.LotId as varchar(8)) a_LotId,
a.Val,
a.Kind,
ROW_NUMBER() over(partition by l.lotid order by a.attributeid) rn
from #Lot l
left join #Attribute a
on l.LotId = a.LotId
) src
unpivot
(
value
for col in (attributeid, a_Lotid, val, kind)
) unpiv
) d
pivot
(
max(value)
for col in (attributeid_1, a_LotId_1, Val_1, Kind_1,
attributeid_2, a_LotId_2, Val_2, Kind_2,
attributeid_3, a_LotId_3, Val_3, Kind_3)
) piv
See SQL Fiddle with Demo.
Or starting in SQL Server 2008+, you can use CROSS APPLY with a VALUES clause to unpivot the data:
select *
from
(
select LotId,
SomeText,
col+'_'+CAST(rn as varchar(10)) col,
value
from
(
select l.LotId,
l.SomeText,
cast(a.AttributeId as varchar(8)) attributeid,
cast(a.LotId as varchar(8)) a_LotId,
a.Val,
a.Kind,
ROW_NUMBER() over(partition by l.lotid order by a.attributeid) rn
from #Lot l
left join #Attribute a
on l.LotId = a.LotId
) src
cross apply
(
values ('attributeid', attributeid),('LotId', a_LotId), ('Value', Val), ('Kind', Kind)
) c (col, value)
) d
pivot
(
max(value)
for col in (attributeid_1, LotId_1, Value_1, Kind_1,
attributeid_2, LotId_2, Value_2, Kind_2,
attributeid_3, LotId_3, Value_3, Kind_3)
) piv
See SQL Fiddle with Demo.
The unpivot process takes the multiple columns for each LotID and SomeText and converts it into rows giving the result:
| LOTID | SOMETEXT | COL | VALUE |
--------------------------------------------
| 1 | Hello | attributeid_1 | 1 |
| 1 | Hello | LotId_1 | 1 |
| 1 | Hello | Value_1 | Foo1 |
| 1 | Hello | Kind_1 | Kind1 |
| 1 | Hello | attributeid_2 | 2 |
I added a row_number() to the inner subquery to be used to create the new column names to pivot. Once the names are created the pivot can be applied to the new columns giving the final result
This could also be done using dynamic SQL:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT ',' + QUOTENAME(col+'_'+rn)
from
(
select
cast(ROW_NUMBER() over(partition by l.lotid order by a.attributeid) as varchar(10)) rn
from Lot l
left join Attribute a
on l.LotId = a.LotId
) t
cross apply (values ('attributeid', 1),
('LotId', 2),
('Value', 3),
('Kind', 4)) c (col, so)
group by col, rn, so
order by rn, so
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT LotId,
SomeText,' + #cols + '
from
(
select LotId,
SomeText,
col+''_''+CAST(rn as varchar(10)) col,
value
from
(
select l.LotId,
l.SomeText,
cast(a.AttributeId as varchar(8)) attributeid,
cast(a.LotId as varchar(8)) a_LotId,
a.Val,
a.Kind,
ROW_NUMBER() over(partition by l.lotid order by a.attributeid) rn
from Lot l
left join Attribute a
on l.LotId = a.LotId
) src
cross apply
(
values (''attributeid'', attributeid),(''LotId'', a_LotId), (''Value'', Val), (''Kind'', Kind)
) c (col, value)
) x
pivot
(
max(value)
for col in (' + #cols + ')
) p '
execute(#query)
See SQL Fiddle with Demo
All three versions will give the same result:
| LOTID | SOMETEXT | ATTRIBUTEID_1 | LOTID_1 | VALUE_1 | KIND_1 | ATTRIBUTEID_2 | LOTID_2 | VALUE_2 | KIND_2 | ATTRIBUTEID_3 | LOTID_3 | VALUE_3 | KIND_3 |
-----------------------------------------------------------------------------------------------------------------------------------------------------------
| 1 | Hello | 1 | 1 | Foo1 | Kind1 | 2 | 1 | Foo2 | Kind2 | (null) | (null) | (null) | (null) |
| 2 | World | 3 | 2 | Bar1 | Kind1 | 4 | 2 | Bar2 | Kind2 | 5 | 2 | Bar3 | Kind3 |

Resources