SQL 2000 Left Join Top 1 of 0 to many relationship - sql

This question has been asked multiple times on SO but all the answers refer to SQL 2005 or later (e.g. OUTER APPLY) and we are still using SQL 2000 (for corporate reasons too complex to go into here!)
I have a table of Things and a table of Widgets with a 0 to Many relationship:
CREATE TABLE Things ( ThingId INT, ThingName VARCHAR(50) )
CREATE TABLE Widgets ( WidgetId INT, ThingId INT, WidgetName VARCHAR(50) )
INSERT INTO Things VALUES ( 1, 'Thing 1' )
INSERT INTO Things VALUES ( 2, 'Thing 2' )
INSERT INTO Things VALUES ( 3, 'Thing 3' )
INSERT INTO Widgets VALUES ( 1, 2, 'Thing 2 Widget 1' )
INSERT INTO Widgets VALUES ( 2, 2, 'Thing 2 Widget 2' )
INSERT INTO Widgets VALUES ( 3, 3, 'Thing 3 Widget 1' )
A standard LEFT OUTER JOIN returns the expected 4 rows
SELECT * FROM Things t LEFT OUTER JOIN Widgets w ON t.ThingId = w.ThingId
ThingId | ThingName | WidgetId | ThingId | WidgetName
---------+-----------+----------+---------+------------------
1 | Thing 1 | NULL | NULL | NULL
2 | Thing 2 | 1 | 2 | Thing 2 Widget 1
2 | Thing 2 | 2 | 2 | Thing 2 Widget 2
3 | Thing 3 | 3 | 3 | Thing 3 Widget 1
However, I only want the newest Widget for each Thing, i.e.:
ThingId | ThingName | WidgetId | ThingId | WidgetName
---------+-----------+----------+---------+------------------
1 | Thing 1 | NULL | NULL | NULL
2 | Thing 2 | 2 | 2 | Thing 2 Widget 2
3 | Thing 3 | 3 | 3 | Thing 3 Widget 1
My starting point was:
SELECT * FROM Things t LEFT OUTER JOIN (SELECT TOP 1 * FROM Widgets subw WHERE subw.ThingId = t.ThingId ORDER BY subw.WidgetId DESC) w ON t.ThingId = w.ThingId
But this is not valid because the parent t.ThingId does not exist in the sub query.
Can this be achieved using SQL 2000?

If (ThingId, WidgetId) combination is unique in table Widgets, then this will work correctly:
SELECT t.*, w.*
FROM
dbo.Things AS t
LEFT OUTER JOIN
( SELECT ThingId, MAX(WidgetId) AS WidgetId
FROM dbo.Widgets
GROUP BY ThingId
) AS
subw
ON subw.ThingId = t.ThingId
LEFT OUTER JOIN
dbo.Widgets AS w
ON w.ThingId = subw.ThingId
AND w.WidgetId = subw.WidgetId ;

Related

Using join to include null values in same table

Following is my table structure.
AttributeMaster - This table is a master attribute table that will be available for each and every request.
AttrMasterId
AttrName
1
Expense Items
2
Business Reason
AttributeValue - When the user fills the data from grid, if a column is empty we don't store its value in the database.
For each request, there are multiple line items (TaskId). Every task must have attributes from attribute master. Now, if the user doesn't an attribute, then we don't store it in the database.
AttrValId
RequestId
TaskId
AttrMasterId
AttrValue
RecordStatus
1
200
1
1
Furniture
A
2
200
2
1
Infra
A
3
200
2
2
Relocation
A
In the above scenario, for request 200, for task Id - 1, I only have value for one attribute.
For task Id - 2, I have both attributes filled.
The query result should give me 4 rows, 2 for each task ID, with null placeholders in AttrValue column.
select * from AttributeMaster cam
left join AttributeValue cav on cam.AttrMasterId = cav.AttrMasterId
and cav.requestId = 36498 and cav.recordStatus = 'A'
right outer join (select distinct AttrMasterId from attrValue cav1 where cav1.requestId = 36498 ) ctI on cti.AttrMasterId = cav.AttrMasterId;
So far, I've tried different joins, tried to self join attribute value table as above, still no results to fill the empty rows.
Any help or pointers would be appreciated. Thanks.
Edit 1:
Expected Output is as follows:
RequestId
TaskId
AttrMasterId
AttrValue
RecordStatus
200
1
1
Furniture
A
200
1
2
NULL
NULL
200
2
1
Infra
A
200
2
2
Relocation
A
Working Fiddle for SQL Server
Since there really should be a Task table, I added that as a CTE term in the first solution. The second form just uses your existing tables directly, with the same result.
WITH Task (TaskId) AS (
SELECT DISTINCT TaskId FROM AttributeValue
)
, pairs (TaskId, AttrMasterId) AS (
SELECT Task.TaskId, AttributeMaster.AttrMasterId
FROM AttributeMaster CROSS JOIN Task
)
SELECT pairs.*
, AttributeMaster.*
, cav.*
FROM pairs
JOIN AttributeMaster
ON pairs.AttrMasterId = AttributeMaster.AttrMasterId
LEFT JOIN AttributeValue AS cav
ON pairs.AttrMasterId = cav.AttrMasterId AND pairs.TaskId = cav.TaskId
AND cav.requestId = 200 AND cav.recordStatus = 'A'
ORDER BY pairs.TaskId, pairs.AttrMasterId
;
+--------+--------------+--------------+-----------------+-----------+-----------+--------+--------------+------------+--------------+
| TaskId | AttrMasterId | AttrMasterId | AttrName | AttrValId | RequestId | TaskId | AttrMasterId | AttrValue | RecordStatus |
+--------+--------------+--------------+-----------------+-----------+-----------+--------+--------------+------------+--------------+
| 1 | 1 | 1 | Expense Items | 1 | 200 | 1 | 1 | Furniture | A |
| 1 | 2 | 2 | Business Reason | NULL | NULL | NULL | NULL | NULL | NULL |
| 2 | 1 | 1 | Expense Items | 2 | 200 | 2 | 1 | Infra | A |
| 2 | 2 | 2 | Business Reason | 3 | 200 | 2 | 2 | Relocation | A |
+--------+--------------+--------------+-----------------+-----------+-----------+--------+--------------+------------+--------------+
The second form is without the added Task CTE term...
WITH pairs AS (
SELECT DISTINCT AttributeValue.TaskId, AttributeMaster.AttrMasterId
FROM AttributeMaster CROSS JOIN AttributeValue
)
SELECT pairs.*
, AttributeMaster.*
, cav.*
FROM pairs
JOIN AttributeMaster
ON pairs.AttrMasterId = AttributeMaster.AttrMasterId
LEFT JOIN AttributeValue AS cav
ON pairs.AttrMasterId = cav.AttrMasterId AND pairs.TaskId = cav.TaskId
AND cav.requestId = 200 AND cav.recordStatus = 'A'
ORDER BY pairs.TaskId, pairs.AttrMasterId
;
Here is another solution that does not require a CTE.
This also uses the TaskID like #jon-armstrong 's answer
declare #AttributeMaster table (MasterID int, Name varchar(50))
declare #AttributeValues table (ValueID int, RequestID int, TaskID int, MasterID int, Value varchar(50), Status varchar(1))
insert into #AttributeMaster (MasterID, Name)
values (1, 'Expense'), (2, 'Business')
insert into #AttributeValues (ValueID, RequestID, TaskID, MasterID, Value, Status)
values (1, 200, 1, 1, 'Furniture', 'A'),
(2, 200, 2, 1, 'Infra', 'A'),
(3, 200, 2, 2, 'Relocation', 'A')
select t.RequestID, t.TaskID, t.MasterID, v.Value, v.Status, t.Name
from ( select distinct m.MasterID, v.TaskID, v.RequestID, m.Name
from #AttributeMaster m cross join #AttributeValues v
) t
left join #AttributeValues v on v.MasterID = t.MasterID and v.TaskID = t.TaskID
and v.RequestID = 200 and v.Status = 'A'
order by t.TaskID, t.MasterID
the result is
RequestID TaskID MasterID Value Status Name
200 1 1 Furniture A Expense
200 1 2 NULL NULL Business
200 2 1 Infra A Expense
200 2 2 Relocation A Business

Select all records of one table that contain two records in another with certain id

I have two tables of 1:m relation. Need to select which People records have both records in Actions table whit id 1 and 2
People
+----+------+--------------+
| id | name | phone_number |
+----+------+--------------+
| 1 | John | 111111111111 |
+----+------+--------------+
| 3 | Jane | 222222222222 |
+----+------+--------------+
| 4 | Jack | 333333333333 |
+----+------+--------------+
Action
+----+------+------------+
| id | PplId| ActionId |
+----+------+------------+
| 1 | 1 | 1 |
+----+------+------------+
| 2 | 1 | 2 |
+----+------+------------+
| 3 | 2 | 1 |
+----+------+------------+
| 4 | 4 | 2 |
+----+------+------------+
Output
+----+------+--------------+----------
|PplId| name | Phone |ActionId |
+-----+------+-------------+----+-----
| 1 | John | 111111111111| 1 |
+-----+------+-------------+----+-----
| 1 | John | 111111111111| 2 |
+-----+------+-------------+----+-----
Return records of People that have both Have Actionid 1 and Action id 2(Have records in Actions).
Window functions are one method. Assuming actions are not duplicated for a person:
select pa.*
from (select p.*, a.action, count(*) over (partition by p.id) as num_actions
from people p join
action a
on p.id = a.pplid
where a.action in (1, 2)
) pa
where num_actions = 2;
In my opinion, getting two rows with the action detail seems superfluous -- you already know the actions. If you only want the people, then exists comes to mind:
select p.*
from people p
where exists (select 1 from actions where a.pplid = p.id and a.action = 1) and
exists (select 1 from actions where a.pplid = p.id and a.action = 2);
With the right index (actions(pplid, action)), I would expect two exists to be faster than group by.
Try this below query using subquery and join
select a.Pplid, name, phone, actionid from (
select a.pplid as Pplid, name, phone_number as phone
from People P
join Action A on a.pplid= p.id
group by a.pplid, name, phone_number
having count(*)>1 )P
join Action A on a.Pplid= p.Pplid
Try something like this
IF OBJECT_ID('tempdb..#People') IS NOT NULL DROP TABLE #People
CREATE TABLE #People (id INT, name VARCHAR(255), phone_number VARCHAR(50))
INSERT #People
SELECT 1, 'John', '111111111111' UNION ALL
SELECT 3, 'Jane', '222222222222' UNION ALL
SELECT 4, 'Jack', '333333333333'
IF OBJECT_ID('tempdb..#Action') IS NOT NULL DROP TABLE #Action
CREATE TABLE #Action (id INT, PplId INT, ActionId INT)
INSERT #Action
SELECT 1, 1, 1 UNION ALL
SELECT 2, 1, 2 UNION ALL
SELECT 3, 2, 1 UNION ALL
SELECT 4, 4, 2
GO
SELECT p.ID AS PplId
, p.name
, p.phone_number AS Phone
, a.ActionId
FROM #People p
JOIN #Action a
ON p.ID = a.PplId
WHERE p.ID IN ( SELECT PplId
FROM #Action
WHERE ActionId IN (1, 2)
GROUP BY PplId
HAVING COUNT(*) = 2 )
AND a.ActionId IN (1, 2)
GO

Creating natural hierarchical order using recursive SQL

I have a table holding categories with an inner parent child relationship.
The table looks like this:
ID | ParentID | OrderID
---+----------+---------
1 | Null | 1
2 | Null | 2
3 | 2 | 1
4 | 1 | 1
OrderID is the order inside the current level.
I want to create a recursive SQL query to create the natural order of the table.
Meaning the output will be something like:
ID | Order
-----+-------
1 | 100
4 | 101
2 | 200
3 | 201
Appreciate any help.
Thanks
I am not really sure what you mean by "natural order", but the following query generates the results you want for this data:
with t as (
select v.*
from (values (1, NULL, 1), (2, NULL, 2), (3, 2, 1), (4, 1, 1)) v(ID, ParentID, OrderID)
)
select t.*,
(100 * coalesce(tp.orderid, t.orderid) + (case when t.parentid is null then 0 else 1 end)) as natural_order
from t left join
t tp
on t.parentid = tp.id
order by natural_order;

SQL left join two tables independently

If I have these tables:
Thing
id | name
---+---------
1 | thing 1
2 | thing 2
3 | thing 3
Photos
id | thing_id | src
---+----------+---------
1 | 1 | thing-i1.jpg
2 | 1 | thing-i2.jpg
3 | 2 | thing2.jpg
Ratings
id | thing_id | rating
---+----------+---------
1 | 1 | 6
2 | 2 | 3
3 | 2 | 4
How can I join them to produce
id | name | rating | photo
---+---------+--------+--------
1 | thing 1 | 6 | NULL
1 | thing 1 | NULL | thing-i1.jpg
1 | thing 1 | NULL | thing-i2.jpg
2 | thing 2 | 3 | NULL
2 | thing 2 | 4 | NULL
2 | thing 2 | NULL | thing2.jpg
3 | thing 3 | NULL | NULL
Ie, left join on each table simultaneously, rather than left joining on one than the next?
This is the closest I can get:
SELECT Thing.*, Rating.rating, Photo.src
From Thing
Left Join Photo on Thing.id = Photo.thing_id
Left Join Rating on Thing.id = Rating.thing_id
You can get the results you want with a union, which seems the most obvious, since you return a field from either ranking or photo.
Your additional case (have none of either), is solved by making the joins left join instead of inner joins. You will get a duplicate record with NULL, NULL in ranking, photo. You can filter this out by moving the lot to a subquery and do select distinct on the main query, but the more obvious solution is to replace union all by union, which also filters out duplicates. Easier and more readable.
select
t.id,
t.name,
r.rating,
null as photo
from
Thing t
left join Rating r on r.thing_id = t.id
union
select
t.id,
t.name,
null,
p.src
from
Thing t
left join Photo p on p.thing_id = t.id
order by
id,
photo,
rating
Here's what I came up with:
SELECT
Thing.*,
rp.src,
rp.rating
FROM
Thing
LEFT JOIN (
(
SELECT
Photo.src,
Photo.thing_id AS ptid,
Rating.rating,
Rating.thing_id AS rtid
FROM
Photo
LEFT JOIN Rating
ON 1 = 0
)
UNION
(
SELECT
Photo.src,
Photo.thing_id AS ptid,
Rating.rating,
Rating.thing_id AS rtid
FROM
Rating
LEFT JOIN Photo
ON 1 = 0
)
) AS rp
ON Thing.id IN (rp.rtid, rp.ptid)
MySQL has no support for full outer joins so you have to hack around it using a UNION:
Here's the fiddle: http://sqlfiddle.com/#!2/d3d2f/13
SELECT *
FROM (
SELECT Thing.*,
Rating.rating,
NULL AS photo
FROM Thing
LEFT JOIN Rating ON Thing.id = Rating.thing_id
UNION ALL
SELECT Thing.*,
NULL,
Photo.src
FROM Thing
LEFT JOIN Photo ON Thing.id = Photo.thing_id
) s
ORDER BY id, photo, rating

Selecting the same row multiple times

I have a table that has some children of a master object. Any child can occur more than once, and there is a Occurences column that contains that number, so the data in the table is something like:
ChildID | ParentID | Occurences
-------------------------------
1 | 1 | 2
2 | 1 | 2
3 | 2 | 1
4 | 2 | 3
I need to get a list of all the children, with each child appearing the corect number of times in the result, something like
IDENT | ChildID | ParentID
--------------------------
1 | 1 | 1
2 | 1 | 1
3 | 2 | 1
4 | 2 | 1
5 | 3 | 2
6 | 4 | 2
7 | 4 | 2
8 | 4 | 2
I can do this with a cursor that loops the table and inserts as many rows as neccessary, but I don't think that that is the best solution possible.
Thanks for the help
Create script included:
DECLARE #Children TABLE (ChildID int, ParentID int, Occurences int)
INSERT #Children
SELECT 1, 1, 2 UNION ALL
SELECT 2, 1, 2 UNION ALL
SELECT 3, 2, 1 UNION ALL
SELECT 4, 2, 3
;with C as
(
select ChildID,
ParentID,
Occurences - 1 as Occurences
from #Children
union all
select ChildID,
ParentID,
Occurences - 1 as Occurences
from C
where Occurences > 0
)
select row_number() over(order by ChildID) as IDENT,
ChildID,
ParentID
from C
order by IDENT
;WITH CTEs
AS
(
SELECT 1 [Id]
UNION ALL
SELECT [Id] + 1 FROM CTEs WHERE [Id] < 100
)
SELECT ROW_NUMBER() OVER(ORDER BY c1.ChildID, c1.ParentID) [rn]
, c1.ChildID, c1.ParentID
FROM CTEs ct
JOIN #Children c1 ON c1.Occurences >= ct.[Id]
Another way to generate sequence is using predefined table, e.g. master.dbo.spt_values:
SELECT ROW_NUMBER() OVER(ORDER BY c1.ChildID, c1.ParentID) [rn]
, c1.ChildID, c1.ParentID
FROM master.dbo.spt_values ct
JOIN #Children c1 ON c1.Occurences > ct.number
AND ct.type = 'P'