Match and set child's ID based on condition - sql

I've got a primary incremental ID column and have to find and set all its childs (in ParentID column) based on values from two other columns (Condition1 and Condition2)
Started ParentID always has Condition2 = 1 (and the same value in Condition1 column)
Initial table
+---------------------------------------------
| ID | ParentID | Condition1 | Condition2 |
+---------------------------------------------
| 1 | null | 1000 | 1 |
| 2 | null | 1000 | null |
| 3 | null | 1000 | null |
| 4 | null | 2000 | 1 |
| 5 | null | 2000 | null |
| 6 | null | 2000 | null |
| 7 | null | 3000 | 1 |
| 8 | null | 3000 | null |
| 9 | null | 3000 | null |
+---------------------------------------------
Desired Output
+---------------------------------------------
| ID | ParentID | Condition1 | Condition2 |
+---------------------------------------------
| 1 | 1 | 1000 | 1 |
| 2 | 1 | 1000 | null |
| 3 | 1 | 1000 | null |
| 4 | 4 | 2000 | 1 |
| 5 | 4 | 2000 | null |
| 6 | 4 | 2000 | null |
| 7 | 7 | 3000 | 1 |
| 8 | 7 | 3000 | null |
| 9 | 7 | 3000 | null |
+---------------------------------------------
Current code returns only one row for each new ID
update u
set u.ParentID = u.ID
from [db].[dbo].[tbl] u
inner join [db].[dbo].[tbl] on
u.Condition2 = 1 and u.Condition1 = u.Condition1

I think one intuitive way of doing this is
UPDATE [db].[dbo].[tbl] u
SET u.ParentID = (SELECT id FROM [db].[dbo].[tbl] u2 WHERE u2.condition1 = u.condition1 and u2.condition2 = 1)
I don't mean that this is better than what you're trying to do, just that I think it's very intuitive and easy to understand if you're having issues.
I think the solution you are looking for is:
update u
set u.ParentID = u2.ID
from [db].[dbo].[tbl] u
inner join [db].[dbo].[tbl] u2 on
u2.Condition2 = 1 and u.Condition1 = u2.Condition1
The important differences here are that I have given the both tables in the join-to-self a name (u and u2). You don't want to set u.ParentID = u.ID (as in your question), you want to set u.ParentID = u2.ID (note the 2). Similarly, you don't want to join the tables on u.condition1 = u.condition1 (since that is always true in this example) or u.condition2 = 1 (since you're applying that condition to the table you're updating, not the table you joined). Even though it is a join-to-self, you need to be clear about which table you are referencing. u in your query refers to the table being updated, but not the table on the right-side of the join.

Related

Many to many outer join clause

This is my database schema:
user
| id | firstName |
|----|-----------|
| 1 | Adam |
| 2 | Bob |
permission
| id | title |
|----|-------|
| 1 | Foo |
| 2 | Bar |
| 3 | XYZ |
| 4 | ABC |
user_permissions
| user_id | permission_id |
|---------|---------------|
| 1 | 1 |
| 1 | 2 |
| 2 | 1 |
I want to check which permissions the user with id = 1 has and which he doesn't. I tried with this:
select up.permission_id, up.user_id
from permission a
right outer join user_permissions up on a.id = up.permission_id
right outer join user u on u.id = up.user_id
where u.id = 1
but I got:
| permission_id | user_id |
|---------------|---------|
| 1 | 1 |
| 2 | 1 |
and what I want to get is:
| permission_id | user_id |
|---------------|---------|
| 1 | 1 |
| 2 | 1 |
| 3 | NULL |
| 4 | NULL |
Any ideas whats wrong there?
Your where clause states that where u.id = 1, which will filter away the wanted results. you can add an or clause or coalesce function. where u.id = 1 or u.id is null or with coalesce where coalesce(u.id, 1) = 1. (Coalesce returns the value in second parameter if first parameter is null.)

SQL: Check if multiple records exist in multiple tables

In my database, I have a table with a many-to-many relationship to several other tables. I'd like to know, for several records at a time, whether an item exists in each of the other tables. Here's a simple example diagram:
---------------
| base_table |
---------------
| key | name |
---------------
| 1 | item1 |
| 2 | item2 |
| 3 | item3 |
| 4 | item4 |
---------------
-------------------------------
| table_2 |
-------------------------------
| key | base_key | other_key |
-------------------------------
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 1 | 3 |
| 4 | 2 | 1 |
| 5 | 2 | 4 |
| 6 | 4 | 4 |
-------------------------------
-------------------------------
| table_3 |
-------------------------------
| key | base_key | other_key |
-------------------------------
| 1 | 2 | 1 |
| 2 | 3 | 2 |
-------------------------------
And then I'm looking for output like this:
-----------------------------------
| name | in_table_2 | in_table_3 |
-----------------------------------
| item1 | true | false |
| item2 | true | true |
| item3 | false | true |
| item4 | true | false |
-----------------------------------
I'm using MS SQL Server.
You can use union all and aggregation to get which keys are in which tables:
select base_key, max(in_2) as in_2, max(in_3) as in_3
from ((select distinct base_key, 1 as in_2, 0 as in_3
from table2
) union all
(select distinct base_key, 0 as in_2, 1 as in_3
from table3
)
) t
group by base_key;
This returns "1" if the key is in the table and "0" if it is not. SQL Server does not have boolean types and it seems silly to create a string for this purpose.
If you actually need the name instead of the key value, just join it in.
Though I liked #GordonLindoff's post, I thought the following would have worked just as well:
SELECT DISTINCT
b.Name,
in_table_2 = CASE WHEN c.Base_key IS NULL THEN 0 ELSE 1 END,
in_table_3 = CASE WHEN d.Base_key IS NULL THEN 0 ELSE 1 END
FROM Base_Table b
LEFT JOIN Table_2 c
ON b.key = c.Base_Key
LEFT JOIN Table_3 d
ON b.key = d.Base_Key;
I'd also reiterate his comment about 1's and 0's in SQL. If you really, really need "True" or "False" in your display, do it on the front end, or change the 0 and 1 in the case statements to False and True, respectively.
Anyone have any objections to this?
I did to a SQLFiddle, listed here. http://sqlfiddle.com/#!18/d6547/28

SQL aggregation over one column giving a result from another

I am trying (and failing) to join some tables in a SQLite database. The data itself is complicated but I think I have boiled it down to an illustrative example.
Here are the three tables I want to join.
Table: Events
+----+---------+-------+-----------+
| id | user_id | class | timestamp |
+----+---------+-------+-----------+
| 1 | 'user1' | 6 | 100 |
| 2 | 'user1' | 12 | 400 |
| 3 | 'user1' | 4 | 900 |
| 4 | 'user2' | 6 | 400 |
| 5 | 'user2' | 3 | 800 |
| 6 | 'user2' | 8 | 900 |
+----+---------+-------+-----------+
Table: Games
+---------+---------+------------+-----------+
| user_id | game_id | game_class | timestamp |
+---------+---------+------------+-----------+
| 'user1' | 1 | 'A' | 200 |
| 'user2' | 2 | 'A' | 300 |
| 'user1' | 3 | 'B' | 500 |
| 'user1' | 4 | 'A' | 600 |
| 'user1' | 5 | 'A' | 700 |
+---------+---------+------------+-----------+
Table: AScores
+---------+-------+
| game_id | score |
+---------+-------+
| 1 | 8 |
| 2 | 2 |
| 4 | 9 |
| 5 | 6 |
+---------+-------+
I would like to join these to provide an additional column on the first table containing the users current score in game class A at the time of the event. I.e. I would like theresult of the join to look like this:
Desired Result
+----+----------+-------+-----------+-----------------+
| id | user_id | class | timestamp | current_a_score |
+----+----------+-------+-----------+-----------------+
| 1 | 'user1' | 6 | 100 | (null) |
| 2 | 'user1' | 12 | 400 | 8 |
| 3 | 'user1' | 4 | 900 | 6 |
| 4 | 'user2' | 6 | 400 | 2 |
| 5 | 'user2' | 3 | 800 | 2 |
| 6 | 'user2' | 8 | 900 | 2 |
+----+----------+-------+-----------+-----------------+
The following simple join pulls together the two tables AScores and Games.
SELECT * FROM AScores
INNER JOIN Games
ON AScores.game_id = Games.game_id
And so I was hoping to join this to the Events table as a sub-query. Something like this:
SELECT Events.*, AScoredGames.time_stamp AS game_time_stamp, AScoredGames.score
FROM Events
LEFT OUTER JOIN (
SELECT AScores.score, Games.* FROM AScores
INNER JOIN Games
ON AScores.game_id = Games.game_id
) AS AScoredGames
ON Events.user_id = AScoredGames.user_id
AND Events.time_stamp >= AScoredGames.time_stamp
ORDER BY Events.time_stamp ASC
That results in the following:
+----+---------+-------+------------+-----------------+-------+
| id | user_id | class | time_stamp | game_time_stamp | score |
+----+---------+-------+------------+-----------------+-------+
| 1 | user1 | 6 | 100 | NULL | NULL |
| 2 | user1 | 12 | 400 | 200 | 8 |
| 4 | user2 | 6 | 400 | 300 | 2 |
| 5 | user2 | 3 | 800 | 300 | 2 |
| 6 | user2 | 8 | 900 | 300 | 2 |
| 3 | user1 | 4 | 900 | 200 | 8 |
| 3 | user1 | 4 | 900 | 600 | 9 |
| 3 | user1 | 4 | 900 | 700 | 6 |
+----+---------+-------+------------+-----------------+-------+
So I need to group by Events.id to get rid of the triplicated row with Events.id 3. But what I want to do is to choose the row with the maximum game_time_stamp but then use the row's score. If I do MAX(game_time_stamp) as my aggregation I still have to independently aggregate the score. Is there a way to tie the row choice in the score column's aggregation function to the result of the game_time_stamp column's aggregation function?
(N.B. Existing answers to questions like Select first record in a One-to-Many relation using left join and SQL Server: How to Join to first row seem to suggest I cannot and say one must use a WHERE clause over a sub-query. But I am struggling with that (I'll post another question about that) and I can think of at least one solution and I am hoping there are better ones.)
The following query should do it. It uses a NOT EXISTS condition with a correlated subquery to locate the relevant game record for each event.
SELECT e.*, s.score current_a_score
FROM
events e
LEFT JOIN games g
ON g.user_id = e .user_id
AND g.timestamp < e.timestamp
AND NOT EXISTS (
SELECT 1
FROM games g1
WHERE
g1.user_id = e .user_id
AND g1.timestamp < e.timestamp
AND g1.timestamp > g.timestamp
)
LEFT JOIN ascores s
ON s.game_id = g.game_id
ORDER BY e.id
This DB Fiddle demo with your test data returns :
| id | user_id | class | timestamp | current_a_score |
| --- | ------- | ----- | --------- | --------------- |
| 1 | user1 | 6 | 100 | |
| 2 | user1 | 12 | 400 | 8 |
| 3 | user1 | 4 | 900 | 6 |
| 4 | user2 | 6 | 400 | 2 |
| 5 | user2 | 3 | 800 | 2 |
| 6 | user2 | 8 | 900 | 2 |
I have one work-around, but it feels hacky and relies on the specifics of my data. First note that the time_stamps are all multiples of 100 while the scores are all below 10. I can acombine these in a way that will not interfere with my comparison but will mean they are both encoded in one numeric column. This query gives the desired result:
SELECT Events.id, MIN(Events.user_id) AS user_id, MIN(Events.class) AS class, MIN(Events.time_stamp) AS time_stamp, MAX(AScoredGames.combination) % 10 AS current_a_score
FROM Events
LEFT OUTER JOIN (
SELECT AScores.score, AScores.score + (Games.time_stamp - 10) AS combination, Games.* FROM AScores
INNER JOIN Games
ON AScores.game_id = Games.game_id) AS AScoredGames
ON Events.user_id = AScoredGames.user_id AND Events.time_stamp >= AScoredGames.time_stamp
GROUP BY Events.id
ORDER BY id ASC
(The combining is done in AScores.score + (Games.time_stamp - 10) and so the aggregate function becomes MAX(AScoredGames.combination) % 10.)
Actual Result
+----+---------+-------+------------+-----------------+
| id | user_id | class | time_stamp | current_a_score |
+----+---------+-------+------------+-----------------+
| 1 | user1 | 6 | 100 | NULL |
| 2 | user1 | 12 | 400 | 8 |
| 3 | user1 | 4 | 900 | 6 |
| 4 | user2 | 6 | 400 | 2 |
| 5 | user2 | 3 | 800 | 2 |
| 6 | user2 | 8 | 900 | 2 |
+----+---------+-------+------------+-----------------+

Merge columns on two left joins

I have 3 tables as shown:
Video
+----+--------+-----------+
| id | name | videoSize |
+----+--------+-----------+
| 1 | video1 | 1MB |
| 2 | video2 | 2MB |
| 3 | video3 | 3MB |
+----+--------+-----------+
Survey
+----+---------+-----------+
| id | name | questions |
+----+---------+-----------+
| 1 | survey1 | 1 |
| 2 | survey2 | 2 |
| 3 | survey3 | 3 |
+----+---------+-----------+
Sequence
+----+---------+-----------+----------+
| id | videoId | surveyId | sequence |
+----+---------+-----------+----------+
| 1 | null | 1 | 1 |
| 2 | 2 | null | 2 |
| 3 | null | 3 | 3 |
+----+---------+-----------+----------+
I would like to query Sequence and join on both of video and survey tables and merge common columns without specifying the column names (in this case name) like this:
Query Result:
+----+---------+-----------+----------+---------+-----------+-----------+
| id | videoId | surveyId | sequence | name | videoSize | questions |
+----+---------+-----------+----------+---------+-----------+-----------+
| 1 | null | 1 | 1 | survey1 | null | 1 |
| 2 | 2 | null | 2 | video2 | 2MB | null |
| 3 | null | 3 | 3 | survey3 | null | 3 |
+----+---------+-----------+----------+---------+-----------+-----------+
Is this possible?
BTW the below sql doesn't work as it doesn't merge on the name field:
SELECT * FROM "Sequence"
LEFT JOIN "Survey" ON "Survey"."id" = "Sequence"."surveyId"
LEFT JOIN "Video" ON "Video"."id" = "Sequence"."videoId"
This query will show what you want:
select
s.*,
coalesce(y.name, v.name) as name, -- picks the right column
v.videoSize,
y.questions
from sequence s
left join survey y on y.id = s.surveyId
left join video v on v.id = s.videoId
However, the SQL standard requires you to name the columns you want. The only exception being * as shown above.

Create a combined list from two tables

I have a table with CostCenter_ID (int) and a second table with Process_ID (int).
I'd like to combine the results of both tables so that each cost center ID is assigned to all process IDs, like so:
|CostCenterID | ProcessID |
---------------------------
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 2 | 1 |
| 2 | 2 |
| 2 | 3 |
| 3 | 1 |
| 3 | 2 |
| 3 | 3 |
I've done it before but I'm drawing a blank. I've tried this:
SELECT CostCenter_ID,NULL FROM dbo.Cost_Centers
UNION ALL
SELECT NULL,Process_ID FROM dbo.Processes
which returns this:
|CostCenterID | ProcessID |
---------------------------
| 1 | NULL |
| NULL | 1 |
| NULL | 2 |
| NULL | 3 |
Try:
select a.CostCenterID, b.ProcessID
from table1 a
cross join table2 b
or:
select a.CostCenterID, b.ProcessID
from table1 a
,table2 b
NB: cross join is the better method as it makes it clearer to the reader what your intentions are.
More info (with pics) here: http://www.w3resource.com/sql/joins/cross-join.php