I have a questionnaire application, where a user will submit answers. Some of the questions are text based and some have fixed options.
The values are saved to the tAnswers table as either the entered text value, or the Id of the selected option. There is a QuestionTypeId column which defines if the answer is a reference to tOptions.Id.
I want to select the answers, returning the entered value or the value related to the selected Id.
For example;
SET NOCOUNT ON
DECLARE #tSubmissions TABLE (Id INT)
DECLARE #tSubmissionQuestions TABLE (SubmissionId INT, QuestionId INT)
DECLARE #tQuestions TABLE (Id INT, QuestionText NVARCHAR(MAX), ColName NVARCHAR(MAX), QuestionTypeId INT)
DECLARE #tOptions TABLE (Id INT, OptionValue NVARCHAR(MAX), OptionGroupId INT)
DECLARE #tAnswers TABLE (Id INT IDENTITY(1,1), SubmissionId INT, QuestionId INT, AnswerValue NVARCHAR(MAX))
INSERT INTO #tQuestions VALUES (1, 'What is your name?', 'Name', 1)
INSERT INTO #tQuestions VALUES (2, 'What is your age?', 'Age', 1)
INSERT INTO #tQuestions VALUES (3, 'What is your gender?', 'Gender', 2)
INSERT INTO #tQuestions VALUES (4, 'What is your favourite colour?', 'Colour', 2)
-- Answers for question 3 - gender
INSERT INTO #tOptions VALUES (1, 'Male', 1)
INSERT INTO #tOptions VALUES (2, 'Female', 1)
-- answers for question 4 - colour
INSERT INTO #tOptions VALUES (3, 'Blue', 2)
INSERT INTO #tOptions VALUES (4, 'Green', 2)
INSERT INTO #tOptions VALUES (5, 'Red', 2)
INSERT INTO #tOptions VALUES (6, 'Yellow', 2)
INSERT INTO #tSubmissions VALUES (1)
INSERT INTO #tSubmissions VALUES (2)
INSERT INTO #tSubmissions VALUES (3)
INSERT INTO #tSubmissionQuestions VALUES (1, 1)
INSERT INTO #tSubmissionQuestions VALUES (1, 2)
INSERT INTO #tSubmissionQuestions VALUES (1, 3)
INSERT INTO #tSubmissionQuestions VALUES (1, 4)
INSERT INTO #tSubmissionQuestions VALUES (2, 1)
INSERT INTO #tSubmissionQuestions VALUES (2, 2)
INSERT INTO #tSubmissionQuestions VALUES (2, 3)
INSERT INTO #tSubmissionQuestions VALUES (2, 4)
INSERT INTO #tSubmissionQuestions VALUES (3, 1)
INSERT INTO #tSubmissionQuestions VALUES (3, 2)
INSERT INTO #tSubmissionQuestions VALUES (3, 3)
INSERT INTO #tSubmissionQuestions VALUES (3, 4)
-- form submissions
INSERT INTO #tAnswers (SubmissionId, QuestionId, AnswerValue) VALUES (1, 1, 'Tony Stark')
INSERT INTO #tAnswers (SubmissionId, QuestionId, AnswerValue) VALUES (1, 2, '39')
INSERT INTO #tAnswers (SubmissionId, QuestionId, AnswerValue) VALUES (1, 3, '1') -- reference to #tOptions
INSERT INTO #tAnswers (SubmissionId, QuestionId, AnswerValue) VALUES (1, 4, '5') -- reference to #tOptions
INSERT INTO #tAnswers (SubmissionId, QuestionId, AnswerValue) VALUES (2, 1, 'Pepper Potts')
INSERT INTO #tAnswers (SubmissionId, QuestionId, AnswerValue) VALUES (2, 2, '38')
INSERT INTO #tAnswers (SubmissionId, QuestionId, AnswerValue) VALUES (2, 3, '2') -- reference to #tOptions
INSERT INTO #tAnswers (SubmissionId, QuestionId, AnswerValue) VALUES (2, 4, '6') -- reference to #tOptions
INSERT INTO #tAnswers (SubmissionId, QuestionId, AnswerValue) VALUES (3, 1, 'James Rhodes')
INSERT INTO #tAnswers (SubmissionId, QuestionId, AnswerValue) VALUES (3, 2, '41') -- has choosen to not answer question 3
INSERT INTO #tAnswers (SubmissionId, QuestionId, AnswerValue) VALUES (3, 4, '3') -- reference to #tOptions
SELECT
s.Id as SubmissionId, q.Id as QuestionId, a.AnswerValue
FROM
#tSubmissions s
INNER JOIN #tSubmissionQuestions sq
ON sq.SubmissionId = s.Id
INNER JOIN #tQuestions q
ON q.Id = sq.QuestionId
LEFT JOIN #tAnswers a
ON a.QuestionId = q.Id
AND a.SubmissionId = s.Id
DBFiddle
This returns;
SubmissionId | QuestionId | AnswerValue
=============|============|===============
1 | 1 | Tony Stark
1 | 2 | 39
1 | 3 | 1 <-- this is the Id of the selected option
1 | 4 | 5 <-- this is the Id of the selected option
2 | 1 | Pepper Potts
2 | 2 | 38
2 | 3 | 2 <-- this is the Id of the selected option
2 | 4 | 6 <-- this is the Id of the selected option
3 | 1 | James Rhodes
3 | 2 | 41
3 | 3 | NULL <-- the option was not selected
3 | 4 | 3 <-- this is the Id of the option
Instead I would like;
SubmissionId | QuestionId | AnswerValue
=============|============|===============
1 | 1 | Tony Stark
1 | 2 | 39
1 | 3 | Male <-- this is the value of the selected option
1 | 4 | Red <-- this is the value of the selected option
2 | 1 | Pepper Potts
2 | 2 | 38
2 | 3 | Female <-- this is the value of the selected option
2 | 4 | Yellow <-- this is the value of the selected option
3 | 1 | James Rhodes
3 | 2 | 41
3 | 3 | NULL <-- the option was not selected
3 | 4 | Blue <-- this is the value of the selected option
How do I conditionally pull values from the tOptions table?
I guess this is what you're looking for:
Another LEFT JOIN on tOptions to select the values, in case of QuestionTypeId = 2
I just added the ISNUMERIC to avoid conversion errors.
SELECT
s.Id as SubmissionId,
q.Id as QuestionId,
COALESCE(t.OptionValue,a.AnswerValue) AS AnswerValue
FROM
#tSubmissions s
INNER JOIN #tSubmissionQuestions sq
ON sq.SubmissionId = s.Id
INNER JOIN #tQuestions q
ON q.Id = sq.QuestionId
LEFT JOIN #tAnswers a
ON a.QuestionId = q.Id
AND a.SubmissionId = s.Id
LEFT JOIN #tOptions t
ON q.QuestionTypeId = 2
AND ISNUMERIC(a.AnswerValue) = 1
AND a.AnswerValue = t.Id
I would make two columns in the Answers table. One that you have AnswerValue NVARCHAR(MAX) NULL and another one AnswerOptionID int NULL. It would make joining way more efficient and it would eliminate problems when engine tries to convert text "Tony Stark" into integer.
But, given the schema as is, here is one variant.
I added LEFT JOIN to the #tOptions table. Note, that I'm converting integer IDs into text, not other way around.
SELECT
s.Id as SubmissionId, q.Id as QuestionId
-- , a.AnswerValue, Options.OptionValue
,CASE WHEN q.QuestionTypeId = 2
THEN Options.OptionValue
ELSE a.AnswerValue
END AS AnswerText
FROM
#tSubmissions s
INNER JOIN #tSubmissionQuestions sq ON sq.SubmissionId = s.Id
INNER JOIN #tQuestions q ON q.Id = sq.QuestionId
LEFT JOIN #tAnswers a
ON a.QuestionId = q.Id
AND a.SubmissionId = s.Id
LEFT JOIN #tOptions AS Options
ON q.QuestionTypeId = 2
AND a.AnswerValue = CAST(Options.Id AS NVARCHAR(MAX))
;
Please try this.
SELECT
s.Id as SubmissionId, q.Id as QuestionId,
CASE WHEN q.QuestionTypeId = 1 THEN
a.AnswerValue
ELSE
ISNULL((SELECT CONVERT(VARCHAR(100),OptionValue) FROM #tOptions o WHERE o.Id = a.AnswerValue),a.AnswerValue)
END AS AnswerValue
FROM
#tSubmissions s
INNER JOIN #tSubmissionQuestions sq
ON sq.SubmissionId = s.Id
INNER JOIN #tQuestions q
ON q.Id = sq.QuestionId
LEFT JOIN #tAnswers a
ON a.QuestionId = q.Id
AND a.SubmissionId = s.Id
ORDER BY s.Id ASC
Related
Let's say I have a table with id and category like the table below
D_id | D_category
-----------------
1 | A
2 | A
3 | A
1 | B
2 | B
4 | B
5 | B
1 | C
2 | C
4 | C
5 | C
6 | C
Hence the rules are like this
values in category A should not be appear in category B and category C
values in category B should not be appear in category C
The end result should be like this
D_id | D_category
-----------------
1 | A
2 | A
3 | A
4 | B
5 | B
6 | C
I will provide a solution that works but its not an ideal solution can anyone help me to provide a better solution in case there are more categories meaning that if there are more category then it should follow the rules the values in previous categories should not appear in any other categories
DECLARE #A TABLE(
D_id INT NOT NULL,
D_category VARCHAR(MAX));
INSERT INTO #A(D_id,D_category)
VALUES (1, 'A'),
(2, 'A'),
(3, 'A'),
(1, 'B'),
(2, 'B'),
(4, 'B'),
(5, 'B'),
(1, 'C'),
(2, 'C'),
(4, 'C'),
(5, 'C'),
(6, 'C')
DELETE t
FROM #A t
WHERE t.D_category = 'B' AND EXISTS (SELECT 1 FROM #A t2 WHERE t2.D_category = 'A' and t.D_id = t2.D_id)
DELETE t
FROM #A t
WHERE t.D_category = 'C' AND EXISTS (SELECT 1 FROM #A t2 WHERE t2.D_category = 'B' and t.D_id = t2.D_id)
DELETE t
FROM #A t
WHERE t.D_category = 'C' AND EXISTS (SELECT 1 FROM #A t2 WHERE t2.D_category = 'A' and t.D_id = t2.D_id)
select * from #A
Just check that the specified record doesn't exist earlier in the sequence.
select *
from #A A1
where not exists (
select 1
from #A A2
where A2.D_id = A1.D_id
and A2.D_category < A1.D_category
)
or just make use of row_number()
select *
from
(
select *, r = row_number() over (partition by D_id order by D_category)
from #A
) a
where a.r = 1
Delete using the join syntax:
delete a
from my_table a
join my_table b on a.D_id = b.D_id
and a.D_category > b.D_category
See live demo.
I want to group the record from the multiple tables.
Sample data:
create table UserTable (
Id integer not null,
Name varchar(12) not null
);
insert into UserTable values (1, 'A B');
insert into UserTable values (2, 'A C');
insert into UserTable values (3, 'A C A C');
insert into UserTable values (4, 'A C C');
insert into UserTable values (5, 'A C B');
insert into UserTable values (6, 'A C C');
insert into UserTable values (7, 'A C D');
insert into UserTable values (8, 'A C E');
insert into UserTable values (9, 'A C F');
create table LogTable (
LogId integer not null,
Username varchar(12) not null,
Event varchar(12) not null
);
insert into LogTable values (1, 'A C A C', 'Read');
insert into LogTable values (2, 'A C F', 'Write');
insert into LogTable values (3, 'A C F', 'Read');
insert into LogTable values (4, 'A C C', 'Update');
insert into LogTable values (5,'A C C', 'Read');
insert into LogTable values (6,'A C F', 'Read');
insert into LogTable values (7,'A C F', 'Update');
insert into LogTable values (7,'A C F', 'Write');
insert into LogTable values (7,'A C E','Update');
insert into LogTable values (7,'A C F', 'Delete');
insert into LogTable values (10,'A C B', 'Delete');
insert into LogTable values (11, 'A C F','Copy');
insert into LogTable values (12, 'A C B','Read');
insert into LogTable values (13, 'A C F','Update');
insert into LogTable values (14, 'A C F','Copy');
insert into LogTable values (15, 'A C F','Read');
insert into LogTable values (16, 'A C F','Update');
insert into LogTable values (17, 'A C F','Copy');
insert into LogTable values (18, 'A C C','Read');
insert into LogTable values (19, 'A C D','Update');
create table Activity (
Id integer not null,
ActivityType varchar(12) not null,
UserId varchar(12) not null
);
insert into Activity values (1, 'Videos', 8);
insert into Activity values (2, 'Text', 7);
insert into Activity values (3, 'Page', 7);
insert into Activity values (4, 'Text', 7);
insert into Activity values (5, 'Text', 9);
insert into Activity values (6, 'Chat', 8);
insert into Activity values (7, 'Chat', 5);
insert into Activity values (7, 'File', 8);
insert into Activity values (7, 'Videos', 1);
insert into Activity values (7, 'Text', 4);
insert into Activity values (10, 'Image', 4);
insert into Activity values (11, 'Image', 6);
insert into Activity values (12, 'Chat', 3);
insert into Activity values (13, 'Chat', 2);
insert into Activity values (14, 'Page', 1);
insert into Activity values (15, 'Vidoes',1);
insert into Activity values (16, 'Vidoes',6);
insert into Activity values (17, 'Vidoes',5);
insert into Activity values (18, 'Vidoes',5);
insert into Activity values (19, 'Chat', 5);
What I have tried:
SELECT UT.Id,UT.Name,
SUM(CASE
WHEN LT.Event = 'Read' THEN 1
ELSE 0 END
) AS [USER READ],
SUM(CASE
WHEN LT.Event = 'Delete' THEN 1
ELSE 0 END
) AS [USER DELETE],
SUM(CASE
WHEN AC.ActivityType = 'Videos' THEN 1
WHEN AC.ActivityType = 'Text' THEN 1
WHEN AC.ActivityType = 'Page' THEN 1
WHEN AC.ActivityType = 'Image' THEN 1
ELSE 0 END
) AS [LEARNING ACTIVITY],
SUM(CASE WHEN AC.ActivityType = 'Chat' THEN 1 ELSE 0 END) AS [Chat]
FROM UserTable UT
LEFT JOIN Activity AC ON UT.Id = AC.UserId
LEFT JOIN LogTable LT ON LT.Username = UT.Name
GROUP BY UT.Id, UT.Name
Desired Output:
Id | Name | LEARNING ACTIVITY | Chat | USER READ | USER DELETE|
------------------------------------------------------------------------
1 | A B | 2 | 0 | 0 | 0 |
2 | A C | 0 | 1 | 0 | 0 |
3 | A C A C | 0 | 1 | 1 | 0 |
4 | A C C | 2 | 0 | 2 | 0 |
5 | A C B | 0 | 2 | 1 | 1 |
6 | A C R | 1 | 0 | 0 | 0 |
7 | A C D | 3 | 0 | 0 | 0 |
8 | A C E | 1 | 1 | 0 | 0 |
9 | A C F | 1 | 0 | 3 | 1 |
How can I aggregate two tables which are not related and group by with Id and Name?
Join and Aggregate Activity with Users
Learning Activity is the sum of (Videos, Text, Page and Image) as ActivityType
Chat is all the rows having the Chat as ActivityType
Join and Aggregate LogTable with Users
You should aggregate before joining, this avoids getting a many-to-many-join which results in overcounting:
SELECT UT.Id,UT.Name,
coalesce([LEARNING ACTIVITY],0),
coalesce([Chat],0),
coalesce([USER READ],0),
coalesce([USER DELETE],0)
FROM UserTable UT
LEFT JOIN
(
select UserId,
SUM(CASE
WHEN ActivityType = 'Videos' THEN 1
WHEN ActivityType = 'Text' THEN 1
WHEN ActivityType = 'Page' THEN 1
WHEN ActivityType = 'Image' THEN 1
ELSE 0
END) AS [LEARNING ACTIVITY],
SUM(CASE WHEN ActivityType = 'Chat' THEN 1 ELSE 0 END) AS [Chat]
from Activity
group by UserId
) AC
ON UT.Id = AC.UserId
LEFT JOIN
(
select Username,
SUM(CASE
WHEN Event = 'Read' THEN 1
ELSE 0 END
) AS [USER READ],
SUM(CASE
WHEN Event = 'Delete' THEN 1
ELSE 0 END
) AS [USER DELETE]
from LogTable
group by UserName
) LT
ON LT.Username = UT.Name
See fiddle
Simplifying the CASEs (COALESCE takes care of NULLs):
SELECT UT.Id,UT.Name,
coalesce([LEARNING ACTIVITY],0),
coalesce([Chat],0),
coalesce([USER READ],0),
coalesce([USER DELETE],0)
FROM UserTable UT
LEFT JOIN
(
select UserId,
SUM(CASE WHEN ActivityType IN ('Videos','Text','Page','Image') THEN 1 END) AS [LEARNING ACTIVITY],
SUM(CASE WHEN ActivityType = 'Chat' THEN 1 END) AS [Chat]
from Activity
group by UserId
) AC
ON UT.Id = AC.UserId
LEFT JOIN
(
select Username,
SUM(CASE WHEN Event = 'Read' THEN 1 END) AS [USER READ],
SUM(CASE WHEN Event = 'Delete' THEN 1 END) AS [USER DELETE]
from LogTable
group by UserName
) LT
ON LT.Username = UT.Name
I recommend you make use of windowing functions and outer apply to do the aggregations you need. Here is the query I came up with that matches your expected result:
https://learn.microsoft.com/en-us/sql/t-sql/queries/select-over-clause-transact-sql?view=sql-server-ver15
https://learn.microsoft.com/en-us/u-sql/statements-and-expressions/select/from/select-selecting-from-cross-apply-and-outer-apply
select distinct
UserId,
UT.Name,
sum(case when A.ActivityType in ('Videos','Text','Page','Image') then 1 else 0 end) over (partition by UserId) [Learning Activity],
sum(case when A.ActivityType = 'Chat' then 1 else 0 end) over (partition by UserId) Chat,
coalesce(LT2.[User Read],0) as [User Read],
coalesce(LT2.[User Delete],0) as [User Delete]
from Activity A
join UserTable UT
on A.UserId = UT.Id
outer apply
(
select distinct
sum(case when LT.[Event] = 'Read' then 1 else 0 end) over (partition by UserId) [User Read],
sum(case when LT.[Event] = 'Delete' then 1 else 0 end) over (partition by UserId) [User Delete]
from LogTable LT
where LT.Username = UT.Name
) LT2
I have a table where I'm trying to get the following result: CategoryType should be set to 0 for the member record with the lowest categoryID and 1 if that ID is NULL. CategoryType is not a field from the table so it should be added via a Case statement. I wrote a script using the Row_Number() window function to organize the records but I'm not sure how to put a 0 for the lowest record. I tried using a Min(Case) statement but that didn't work.
declare #t table(memberid int, lastname varchar(50), firstname varchar(50), categoryid int)
insert into #t
values(1, 'Jones', 'Tom', 2), (1, 'Jones', 'Tom', 4),
(2, 'Hanson', 'Ron', 3), (2, 'Hanson', 'Ron', 4),
(2, 'Hanson', 'Ron', 5),
(3, 'Smith', 'Jack', NULL);
This is the result I'm trying to get:
MemberID LastName FirstName CategoryID CategoryType
1 Jones Tom 2 0
1 Jones Tom 4 NULL
2 Hanson Ron 3 0
2 Hanson Ron 4 NULL
2 Hanson Ron 5 NULL
3 Smith Jack NULL 1
Or this:
select *
,case
when categoryid is null then 1
when FIRST_VALUE(categoryid) over (partition by memberid order by categoryid) = categoryid then 0 else null
end as x
from #t
I think you want this
SELECT *,CASE WHEN categoryid IS NULL THEN 1
WHEN ROW_NUMBER() OVER (PARTITION BY categoryid ORDER BY memberid) = 1
THEN 0
END CategoryType FROM #T
Easier to read/extend (if needed):
SELECT a.MemberID,a.LastName,a.FirstName,a.CategoryID
,CASE
WHEN a.rn = 1 AND a.CategoryID IS NULL THEN 1
WHEN a.rn = 1 THEN 0
ELSE NULL
END AS [CategoryType]
FROM (
SELECT t.MemberID,t.LastName,t.FirstName,t.CategoryID
,ROW_NUMBER()OVER(PARTITION BY t.MemberID ORDER BY t.CategoryID) AS [rn]
FROM #t t
) a
;
I think you want:
select t.*,
(case when categoryid is null
then 1
when row_number() over (partition by memberid order by categoryid) = 1
then 0
end) as categorytype
from #t t;
Here is a db<>fiddle.
Hope Can Help You
declare #t table(memberid int,
lastname varchar(50),
firstname varchar(50),
categoryid int)
insert into #t
values(1, 'Jones', 'Tom', 2),
(1, 'Jones', 'Tom', 4),
(2, 'Hanson', 'Ron', 3),
(2, 'Hanson', 'Ron', 4),
(2, 'Hanson', 'Ron', 5),
(3, 'Smith', 'Jack', NULL)
Select
A.*,
Case When a.categoryid is null Then '1'
When b.categoryid is not null Then '0' Else null End CategoryType
From #t A
Left Join (
Select memberid, min(categoryid) categoryid From #t
Group By memberid
) B On B.memberid = A.memberid and B.categoryid = A.categoryid
I am using SQLite 3. I have a table MyTable, as follows:
Create table mytable (ID as INTEGER, OrderID as INTEGER, a as INTGER, b as INTEGER);
Insert into mytable (ID, OrderID,a,b) values (1, 1,1,1);
Insert into mytable (ID, OrderID,a,b) values (1, 2,1,2);
Insert into mytable (ID, OrderID,a,b) values (2, 1,1,3);
Insert into mytable (ID, OrderID,a,b) values (2, 3,2,1);
Insert into mytable (ID, OrderID,a,b) values (3, 1,2,3);
Now if I using the following statement:
Select * from mytable ORDER BY a desc, b desc;
I will get all rows in a different order, as follows:
(3, 1, 2, 3);
(2, 3, 2, 1);
(2, 1, 1, 3);
(1, 2, 1, 2);
(1, 1, 1, 1);
Now I want to update the order ID to the sequence number of the rows appear in the above results, as follows:
(3, 1, 2, 3);
(2, 2, 2, 1);
(2, 3, 1, 3);
(1, 4, 1, 2);
(1, 5, 1, 1);
How to do so?
Try This :-
Select ROW_NUMBER() OVER (ORDER BY (SELECT 1 ) ) AS ordNo, * INTO #TempTable from
mytable ORDER BY a desc, b desc;
UPDATE mytable SET OrderID = ordNo FROM #TempTable WHERE mytable.ID
=#TempTable.ID AND mytable.a=#TempTable.a AND mytable.b=#TempTable.b
Assuming that the a and b values are unique:
update mytable t
set orderid = (select count(*)
from mytable t2
where t2.a > t.a or
(t2.a = t.a and t2.b >= t.b)
);
There is a column [rowid][1] responsible for every rowid
Most tables in a typical SQLite database schema are rowid tables.
Rowid tables are distinguished by the fact that they all have a unique, non-NULL, signed 64-bit integer rowid that is used as the access key for the data in the underlying B-tree storage engine.
Due to old SQLite version didn't support ROW_NUMBER window function, you can use a subquery in select to make it.
You can try to use correlated subquery and UPDATE by rowid.
Schema (SQLite v3.18)
Create table mytable (ID INT, OrderID INT, a INT, b INT);
Insert into mytable (ID, OrderID,a,b) values (1, 1,1,1);
Insert into mytable (ID, OrderID,a,b) values (1, 2,1,2);
Insert into mytable (ID, OrderID,a,b) values (2, 1,1,3);
Insert into mytable (ID, OrderID,a,b) values (2, 3,2,1);
Insert into mytable (ID, OrderID,a,b) values (3, 1,2,3);
update mytable
set orderid=
(
SELECT (select count(*)
from mytable tt
where tt.a > t1.a or
(tt.a = t1.a and tt.b >= t1.b)) rn
FROM mytable t1
where mytable.rowid=t1.rowid
);
Query #1
SELECT * FROM mytable order by OrderID;
| ID | OrderID | a | b |
| --- | ------- | --- | --- |
| 3 | 1 | 2 | 3 |
| 2 | 2 | 2 | 1 |
| 2 | 3 | 1 | 3 |
| 1 | 4 | 1 | 2 |
| 1 | 5 | 1 | 1 |
View on DB Fiddle
I have 5 tables in my database representing an inherited EAV model:
CREATE TABLE AttributeNames
("ID" int, "Name" varchar(8))
;
INSERT INTO AttributeNames
("ID", "Name")
VALUES
(1, 'Color'),
(2, 'FuelType'),
(3, 'Doors'),
(4, 'Price')
;
CREATE TABLE MasterCars
("ID" int, "Name" varchar(10))
;
INSERT INTO MasterCars
("ID", "Name")
VALUES
(5, 'BMW'),
(6, 'Audi'),
(7, 'Ford')
;
CREATE TABLE MasterCarAttributes
("ID" int, "AttributeNameId" int, "Value" varchar(10), "MasterCarId" int)
;
INSERT INTO MasterCarAttributes
("ID", "AttributeNameId", "Value", "MasterCarId")
VALUES
(100, 1, 'Red', 5),
(101, 2, 'Gas', 5),
(102, 3, '4', 5),
(102, 4, '$100K', 5),
(103, 1, 'Blue', 6),
(104, 2, 'Diesel', 6),
(105, 3, '3', 6),
(106, 4, '$80k', 6),
(107, 1, 'Green', 7),
(108, 2, 'Diesel', 7),
(109, 3, '5', 7),
(110, 4, '$60k', 7)
;
CREATE TABLE LocalCars
("ID" int, "MasterCarId" int)
;
INSERT INTO LocalCars
("ID", "MasterCarId")
VALUES
(8, '5'),
(9, '6'),
(10, NULL)
;
CREATE TABLE LocalCarAttributes
("ID" int, "AttributeNameId" int, "Value" varchar(6), "LocalCarId" int)
;
INSERT INTO LocalCarAttributes
("ID", "AttributeNameId", "Value", "LocalCarId")
VALUES
(43, 1, 'Yellow', 8),
(44, 3, '6', 9),
(45, 1, 'Red', 10),
(46, 2, 'Gas', 10),
(47, 3, '2', 10),
(48, 4, '$60k', 10)
;
I can retrieve all of master car attributes as follows:
SELECT MC.ID, MCA.AttributeNameId, MCA.Value
FROM MasterCars MC
left join MasterCarAttributes MCA on MC.ID = MCA.MasterCarId
order by MC.ID;
Likewise, I can retrieve all of the local car attributes as follows:
SELECT LC.ID, LCA.AttributeNameId, LCA.Value
FROM LocalCars LC
left join LocalCarAttributes LCA on LC.ID = LCA.LocalCarId
order by LC.ID;
If LocalCars.MasterCarId is not NULL, then that local car can inherit the attributes of that master car. A local car attribute with the same AttributeNameId overrides any master attribute with the same AttributeNameId.
So given the data above, I have 3 local cars each with 4 attributes (color, fuelType, doors, price). Inherited attribute values in bold:
Local Car Id = 1 (Yellow, Gas, 4, $100K)
Local Car Id = 2 (Blue, Diesel, 6, $80k)
Local Car Id = 3 (Red, Gas, 2, $60k)
I'm trying to find the necessary joins required to join the two queries above together to give a complete set of local cars attributes, some inherited:
LocalCarId AttributeNameId Value
------------------------------------------
1 1 Yellow
1 2 Gas
1 3 4
1 4 $100K
2 1 Blue
2 2 Diesel
2 3 6
2 4 $80K
3 1 Red
3 2 Gas
3 3 2
3 4 $60K
or possibly even:
LocalCarId AttributeNameId LocalValue MasterValue
-------------------------------------------------------------
1 1 Yellow Red
1 2 NULL Gas
1 3 NULL 4
1 4 NULL $100K
2 1 NULL Blue
2 2 NULL Diesel
2 3 6 3
2 4 NULL $80K
3 1 Red NULL
3 2 Gas NULL
3 3 2 NULL
3 4 $60K NULL
The problem can be solved by performing a union on all of your local car attributes and master car attributes. Each record is marked with an [IsMasterAttribute] flag. The next step is then use the ROW_NUMBER() window function to rank each of the duplicate attributes. The final step is to only select attributes which has a rank of 1.
;WITH CTE_CombinedAttributes
AS
(
SELECT 1 AS IsMasterAttribute
,LC.ID
,MC.ID AS MasterCarId
,MCA.AttributeNameId
,MCA.Value
FROM MasterCars MC
LEFT OUTER JOIN MasterCarAttributes MCA on MC.ID = MCA.MasterCarId
INNER JOIN LocalCars LC ON LC.MasterCarId = MC.ID
UNION ALL
SELECT 0 AS IsMasterAttribute
,LC.ID
,LC.MasterCarId
,LCA.AttributeNameId
,LCA.Value
FROM LocalCars LC
LEFT OUTER JOIN LocalCarAttributes LCA on LC.ID = LCA.LocalCarId
)
,
CTE_RankedAttributes
AS
(
SELECT [IsMasterAttribute]
,[ID]
,[AttributeNameId]
,[Value]
,ROW_NUMBER() OVER (PARTITION BY [ID], [AttributeNameId] ORDER BY [IsMasterAttribute]) AS [AttributeRank]
FROM CTE_CombinedAttributes
)
SELECT [IsMasterAttribute]
,[ID]
,[AttributeNameId]
,[Value]
FROM CTE_RankedAttributes
WHERE [AttributeRank] = 1
ORDER BY [ID]
The second output is also possible by performing a simple pivot on the final result:
;WITH CTE_CombinedAttributes
AS
(
SELECT 1 AS IsMasterAttribute
,LC.ID
,MC.ID AS MasterCarId
,MCA.AttributeNameId
,MCA.Value
FROM MasterCars MC
LEFT OUTER JOIN MasterCarAttributes MCA on MC.ID = MCA.MasterCarId
INNER JOIN LocalCars LC ON LC.MasterCarId = MC.ID
UNION ALL
SELECT 0 AS IsMasterAttribute
,LC.ID
,LC.MasterCarId
,LCA.AttributeNameId
,LCA.Value
FROM LocalCars LC
LEFT OUTER JOIN LocalCarAttributes LCA on LC.ID = LCA.LocalCarId
)
,
CTE_RankedAttributes
AS
(
SELECT [IsMasterAttribute]
,[ID]
,[AttributeNameId]
,[Value]
,ROW_NUMBER() OVER (PARTITION BY [ID], [AttributeNameId] ORDER BY [IsMasterAttribute]) AS [AttributeRank]
FROM CTE_CombinedAttributes
)
SELECT [ID]
,[AttributeNameId]
,MAX(
CASE [IsMasterAttribute]
WHEN 0 THEN [Value]
END
) AS LocalValue
,MAX(
CASE [IsMasterAttribute]
WHEN 1 THEN [Value]
END
) AS MasterValue
FROM CTE_RankedAttributes
GROUP BY [ID], [AttributeNameId]
ORDER BY [ID]
SQL Fiddle Demo
SELECT LC."ID" as LocalCarID,
COALESCE(LCA."AttributeNameId", MCA."AttributeNameId") as "AttributeNameId",
COALESCE(LCA."Value", MCA."Value") as "Value"
FROM LocalCars LC
LEFT JOIN MasterCars MC
ON LC."MasterCarId" = MC."ID"
LEFT JOIN MasterCarAttributes MCA
ON MC."ID" = MCA."MasterCarId"
LEFT JOIN LocalCarAttributes LCA
ON ( MCA."AttributeNameId" = LCA."AttributeNameId"
OR MCA."AttributeNameId" IS NULL)
-- This is the important part
-- Try to join with a MasterAtribute otherwise use the Car Atribute.
AND LC."ID" = LCA."ID"
OUTPUT
| LocalCarID | AttributeNameId | Value |
|------------|-----------------|--------|
| 1 | 1 | Blue |
| 1 | 2 | Gas |
| 2 | 1 | Green |
| 2 | 2 | Diesel |