How do I join an unknown number of rows to another row? - sql

I have this scenario:
Table A:
---------------
ID| SOME_VALUE|
---------------
1 | 123223 |
2 | 1232ff |
---------------
Table B:
------------------
ID | KEY | VALUE |
------------------
23 | 1 | 435 |
24 | 1 | 436 |
------------------
KEY is a reference to to Table A's ID. Can I somehow join these tables so that I get the following result:
Table C
-------------------------
ID| SOME_VALUE| | |
-------------------------
1 | 123223 |435 |436 |
2 | 1232ff | | |
-------------------------
Table C should be able to have any given number of columns depending on how many matching values that are found in Table B.
I hope this enough to explain what I'm after here.
Thanks.

You need to use a Dynamic PIVOT clause in order to do this.
EDIT:
Ok so I've done some playing around and based on the following sample data:
Create Table TableA
(
IDCol int,
SomeValue varchar(50)
)
Create Table TableB
(
IDCol int,
KEYCol int,
Value varchar(50)
)
Insert into TableA
Values (1, '123223')
Insert Into TableA
Values (2,'1232ff')
Insert into TableA
Values (3, '222222')
Insert Into TableB
Values( 23, 1, 435)
Insert Into TableB
Values( 24, 1, 436)
Insert Into TableB
Values( 25, 3, 45)
Insert Into TableB
Values( 26, 3, 46)
Insert Into TableB
Values( 27, 3, 435)
Insert Into TableB
Values( 28, 3, 437)
You can execute the following Dynamic SQL.
declare #sql varchar(max)
declare #pivot_list varchar(max)
declare #pivot_select varchar(max)
Select
#pivot_list = Coalesce(#Pivot_List + ', ','') + '[' + Value +']',
#Pivot_select = Coalesce(#pivot_Select, ', ','') +'IsNull([' + Value +'],'''') as [' + Value + '],'
From
(
Select distinct Value From dbo.TableB
)PivotCodes
Set #Sql = '
;With p as (
Select a.IdCol,
a.SomeValue,
b.Value
From dbo.TableA a
Left Join dbo.TableB b on a.IdCol = b.KeyCol
)
Select IdCol, SomeValue ' + Left(#pivot_select, Len(#Pivot_Select)-1) + '
From p
Pivot ( Max(Value) for Value in (' + #pivot_list + '
)
)as pvt
'
exec (#sql)
This gives you the following output:
Although this works at the moment it would be a nightmare to maintain. I'd recommend trying to achieve these results somewhere else. i.e not in SQL!
Good luck!

As Barry has amply illustrated, it's possible to get multiple columns using a dynamic pivot.
I've got a solution that might get you what you need, except that it puts all of the values into a single VARCHAR column. If you can split those results, then you can get what you need.
This method is a trick in SQL Server 2005 that you can use to form a string out of a column of values.
CREATE TABLE #TableA (
ID INT,
SomeValue VARCHAR(50)
);
CREATE TABLE #TableB (
ID INT,
TableAKEY INT,
BValue VARCHAR(50)
);
INSERT INTO #TableA VALUES (1, '123223');
INSERT INTO #TableA VALUES (2, '1232ff');
INSERT INTO #TableA VALUES (3, '222222');
INSERT INTO #TableB VALUES (23, 1, 435);
INSERT INTO #TableB VALUES (24, 1, 436);
INSERT INTO #TableB VALUES (25, 3, 45);
INSERT INTO #TableB VALUES (26, 3, 46);
INSERT INTO #TableB VALUES (27, 3, 435);
INSERT INTO #TableB VALUES (28, 3, 437);
SELECT
a.ID
,a.SomeValue
,RTRIM(bvals.BValues) AS ValueList
FROM #TableA AS a
OUTER APPLY (
-- This has the effect of concatenating all of
-- the BValues for the given value of a.ID.
SELECT b.BValue + ' ' AS [text()]
FROM #TableB AS b
WHERE a.ID = b.TableAKEY
ORDER BY b.ID
FOR XML PATH('')
) AS bvals (BValues)
ORDER BY a.ID
;
You'll get this as a result:
ID SomeValue ValueList
--- ---------- --------------
1 123223 435 436
2 1232ff NULL
3 222222 45 46 435 437

This looks like something a database shouldn't do. Firstly; a table cannot have arbitrary number of columns depending on whatever you'll store. So you will have to put up a maximum number of values anyway. You can get around this by using comma seperated values as value for that cell (or a similar pivot-like solution).
However; if you do have table A and B; i recommend keeping to those two tables; as they seem to be pretty normalised. Should you need a list of b.value given an input a.some_value, the following sql query gives that list.
select b.value from a,b where b.key=a.id a.some_value='INPUT_VALUE';

Related

Find data by multiple Lookup table clauses

declare #Character table (id int, [name] varchar(12));
insert into #Character (id, [name])
values
(1, 'tom'),
(2, 'jerry'),
(3, 'dog');
declare #NameToCharacter table (id int, nameId int, characterId int);
insert into #NameToCharacter (id, nameId, characterId)
values
(1, 1, 1),
(2, 1, 3),
(3, 1, 2),
(4, 2, 1);
The Name Table has more than just 1,2,3 and the list to parse on is dynamic
NameTable
id | name
----------
1 foo
2 bar
3 steak
CharacterTable
id | name
---------
1 tom
2 jerry
3 dog
NameToCharacterTable
id | nameId | characterId
1 1 1
2 1 3
3 1 2
4 2 1
I am looking for a query that will return a character that has two names. For example
With the above data only "tom" will be returned.
SELECT *
FROM nameToCharacterTable
WHERE nameId in (1,2)
The in clause will return every row that has a 1 or a 3. I want to only return the rows that have both a 1 and a 3.
I am stumped I have tried everything I know and do not want to resort to dynamic SQL. Any help would be great
The 1,3 in this example will be a dynamic list of integers. for example it could be 1,3,4,5,.....
Filter out a count of how many times the Character appears in the CharacterToName table matching the list you are providing (which I have assumed you can convert into a table variable or temp table) e.g.
declare #Character table (id int, [name] varchar(12));
insert into #Character (id, [name])
values
(1, 'tom'),
(2, 'jerry'),
(3, 'dog');
declare #NameToCharacter table (id int, nameId int, characterId int);
insert into #NameToCharacter (id, nameId, characterId)
values
(1, 1, 1),
(2, 1, 3),
(3, 1, 2),
(4, 2, 1);
declare #RequiredNames table (nameId int);
insert into #RequiredNames (nameId)
values
(1),
(2);
select *
from #Character C
where (
select count(*)
from #NameToCharacter NC
where NC.characterId = c.id
and NC.nameId in (select nameId from #RequiredNames)
) = 2;
Returns:
id
name
1
tom
Note: Providing DDL+DML as shown here makes it much easier for people to assist you.
This is classic Relational Division With Remainder.
There are a number of different solutions. #DaleK has given you an excellent one: inner-join everything, then check that each set has the right amount. This is normally the fastest solution.
If you want to ensure it works with a dynamic amount of rows, just change the last line to
) = (SELECT COUNT(*) FROM #RequiredNames);
Two other common solutions exist.
Left-join and check that all rows were joined
SELECT *
FROM #Character c
WHERE EXISTS (SELECT 1
FROM #RequiredNames rn
LEFT JOIN #NameToCharacter nc ON nc.nameId = rn.nameId AND nc.characterId = c.id
HAVING COUNT(*) = COUNT(nc.nameId) -- all rows are joined
);
Double anti-join, in other words: there are no "required" that are "not in the set"
SELECT *
FROM #Character c
WHERE NOT EXISTS (SELECT 1
FROM #RequiredNames rn
WHERE NOT EXISTS (SELECT 1
FROM #NameToCharacter nc
WHERE nc.nameId = rn.nameId AND nc.characterId = c.id
)
);
A variation on the one from the other answer uses a windowed aggregate instead of a subquery. I don't think this is performant, but it may have uses in certain cases.
SELECT *
FROM #Character c
WHERE EXISTS (SELECT 1
FROM (
SELECT *, COUNT(*) OVER () AS cnt
FROM #RequiredNames
) rn
JOIN #NameToCharacter nc ON nc.nameId = rn.nameId AND nc.characterId = c.id
HAVING COUNT(*) = MIN(rn.cnt)
);
db<>fiddle

Dynamic values in where clause

I am trying to read rows one by one from a table and pass values to the query condition. I am unable to proceed further. do I have to use counter /iteration or is there any options. Any help is much appreciated
I want to pass values into the value like condition
create table #temp
(
userid int,
typeid int
)
insert into #temp values (1, 101)
insert into #temp values (1, 221)
insert into #temp values (3, 401)
insert into #temp values (4, 501)
create table #target
(
userid int,
roleid int,
value varchar(max)
)
insert into #target values (1, 000, 'something here userid:1 typeid:101 something here')
insert into #target values (1, 001, 'something here userid:1 typeid:221 something here')
insert into #target values (1, 001, 'something here userid:1 typeid:331 something here')
insert into #target values (3, 002, 'something here userid:3 typeid:401 something here')
select t.userid, d.roleid, t.typeid
from #target d
inner join #temp t on t.userid = d.userid
and value like '%userid:'t.userid' typeid:'t.typeid'%'
This is the result I get:
userid
roleid
typeid
1
000
101
1
001
221
3
002
401
Seems you just need to familiarise yourself with the string concatenation operator:
select t.userid, roleid, typeid
-- How to debug such a query
--, [value]
--,'%userid:' + convert(varchar(32),t.userid) + ' typeid:' + convert(varchar(32),t.typeid) + '%'
--, case when d.[value] like '%userid:' + convert(varchar(32),t.userid) + ' typeid:' + convert(varchar(32),t.typeid) + '%' then 1 else 0 end
from #target d
inner join #temp t on t.userid = d.userid
and d.[value] like '%userid:' + convert(varchar(32),t.userid) + ' typeid:' + convert(varchar(32),t.typeid) + '%'
Returns
userid
roleid
typeid
1
0
101
1
1
221
3
2
401

JOIN tables based on 6 or 7 digits key in 1st table with 7 digits key in second table by using CASE and MAX function

Table 1:
Id1 Data1
123123 David
123124 Jan
1231344 Juro
1234126 Marco
Table 2:
Id2 Data2
1231230 Info 1
1231231 Info 2
1231232 Info 3
1231240 Info 4
1231241 Info 5
1231242 Info 6
Each id from Table 1 can have 1 or more matches in Table 2 based on first 6 digits.
For example 123123 from Table 1 matches 1231230, 1231231 and 1231232 in Table 2.
I'm trying to create join to match maximum id2 from Table 2 based on id1 from Table 1.
I would just join using LIKE:
SELECT
tb1.id1,
tb1.data1
MAX(tb2.[id2]) AS id2
FROM [dbo].[table1] tb1
LEFT JOIN [dbo].[table2] tb2
ON tb2.[id2] LIKE CONCAT(tb1.[id1], '%')
GROUP BY
tb1.id1,
tb1.data1
ORDER BY
tb1.id1 DESC;
This approach might still leave open the possibility of using an index on the second table. In any case, it is slightly easier to read than your version.
This is working solution:
SELECT tb1.*,
MAX(tb2.[id2]) as id2
FROM [dbo].[table1] tb1
LEFT JOIN [dbo].[table2] tb2
ON CASE
WHEN LEN(tb1.[id1]) = 7 and tb1.[id1] = tb2.[id2] THEN 1
WHEN LEN(tb1.[id1]) = 6 and tb1.[id1] = SUBSTRING(tb2.[id2],1,6) THEN 1
ELSE 0
END = 1
GROUP BY tb1.id1
,tb1.data1
ORDER BY tb1.id1 desc
You can try this as well:
Declare #t table (id1 varchar(50) , data1 varchar(50))
insert into #t values (123123,'David')
insert into #t values (123124,'Jan')
insert into #t values (1231344,'Juro')
Declare #t1 table (id2 varchar(50) , data2 varchar(50))
insert into #t1 values (1231230,'Info 1')
insert into #t1 values (1231231,'Info 2')
insert into #t1 values (1231232,'Info 3')
insert into #t1 values (1231240,'Info 4')
insert into #t1 values (1231241,'Info 5')
insert into #t1 values (1231242,'Info 6')
select * from #t a JOIN #t1 B
on b.id2 like '%' + a.id1 + '%'

How can I connect into one column all records that have the same ID?

I have a table with 2 columns ID (not PK) and NAME. I want to take all the names that have the same ID and connect them into one column - how can I do this?
e.g.
ID Name
---- ----
1 A
2 B
3 C
3 D
5 E
3 F
result :
1 A
2 B
3 CDF
5 E
Try this code
SELECT Id,GROUP_CONCAT(Name SEPARATOR ' ') FROM tablename GROUP BY Id;
In Oracle the GROUP_CONCAT() is name wm_concat().
With MS Sql Server you may want to try this:
create table test
( id int, name varchar(10))
insert test (id,name) values (1,'A')
insert test (id,name) values (2,'B')
insert test (id,name) values (3,'C')
insert test (id,name) values (3,'D')
insert test (id,name) values (5,'E')
insert test (id,name) values (4,'F')
SELECT DISTINCT id AS Tabelle,
Spalten = STUFF((SELECT DISTINCT ',' + name
FROM test a
WHERE a.id = b.id
FOR XML PATH ('')), 1, 1, '')
from test b

SQL Query to return rows where a list of numbers is between start and end values

There is a table in Oracle with the columns:
id | start_number | end_number
---+--------------+------------
1 | 100 | 200
2 | 151 | 200
3 | 25 | 49
4 | 98 | 99
5 | 49 | 100
There is a list of numbers (50, 99, 150).
I want an sql statement that returns all the ids where any of the numbers in the list of numbers is found equal to or between the start_number and the end_number.
Using the above example; 1, 4 and 5 should be returned.
1 - 150 is between or equal to 100 and 200
2 - none of the numbers are between or equal to 151 and 200
3 - none of the numbers are between or equal to 25 and 49
4 - 99 is between or equal to 98 and 99
5 - 50 and 99 are between or equal to 49 and 100
drop table TEMP_TABLE;
create table TEMP_TABLE(
THE_ID number,
THE_START number,
THE_END number
);
insert into TEMP_TABLE(THE_ID, THE_START, THE_END) values (1, 100, 200);
insert into TEMP_TABLE(THE_ID, THE_START, THE_END) values (2, 151, 200);
insert into TEMP_TABLE(THE_ID, THE_START, THE_END) values (3, 25, 49);
insert into TEMP_TABLE(THE_ID, THE_START, THE_END) values (4, 98, 99);
insert into TEMP_TABLE(the_id, the_start, the_end) values (5, 49, 100);
The following is the solution I came up with based on the comments and answers below plus some additional research:
SELECT
*
from
TEMP_TABLE
where
EXISTS (select * from(
select column_value as id
from table(SYS.DBMS_DEBUG_VC2COLL(50,99,150))
)
where id
BETWEEN TEMP_TABLE.the_start AND TEMP_TABLE.the_end
)
This works too:
SELECT
*
from
TEMP_TABLE
where
EXISTS (select * from(
select column_value as id
from table(sys.ku$_vcnt(50,99,150))
)
where id
BETWEEN TEMP_TABLE.the_start AND TEMP_TABLE.the_end
)
Here is a full example:
create table #list (
number int
)
create table #table (
id int,
start_number int,
end_number int
)
insert into #list values(50)
insert into #list values(99)
insert into #list values(150)
insert into #table values(1,100,200)
insert into #table values(2,151,200)
insert into #table values(3,25,49)
insert into #table values(4,98,99)
insert into #table values(5,49,100)
select distinct a.* from #table a
inner join #list l --your list of numbers
on l.number between a.start_number and a.end_number
drop table #list
drop table #table
You'll simply need to remove the code about #table (create, insert and drop) and put your table in the select.
It partly depends on how your are storing your list of numbers. I'll assume that they're in another table for now, as even then you have many options.
SELECT
*
FROM
yourTable
WHERE
EXISTS (SELECT * FROM yourList WHERE number BETWEEN yourTable.start_number AND yourTable.end_number)
Or...
SELECT
*
FROM
yourTable
INNER JOIN
yourList
ON yourList.number BETWEEN yourTable.start_number AND yourTable.end_number
Both of those are the simplest expressions, and work well for small data sets. If your list of numbers is relatively small, and your original data is relatively large, however, this may not scale well. This is because both of the above scan the whole of yourTable and then check each record against yourList.
What may be preferable is to scan the list, and then attempt to use indexes to check against the original data. This would require you to be able to reverse the BETWEEN statement to yourTable.start_number BETWEEN x and y
This can only be done if you know the maximum gap between start_number and end_number.
SELECT
*
FROM
yourList
INNER JOIN
yourTable
ON yourTable.end_number >= yourList.number
AND yourTable.start_number <= yourList.number
AND yourTable.start_number >= yourList.number - max_gap
To achieve this I would store the value of max_gap in another table, and update it as the values in yourTable change.
You will want to create a temporary table to hold your numbers, if the numbers aren't already in one. Then it becomes relatively simple:
SELECT DISTINCT mt.ID FROM MyTable mt
INNER JOIN TempTable tt --your list of numbers
ON tt.number Between mt.start_number and mt.end_number
To create the table based on an array of passed values, you can use table definitions in your procedure. I'm light on Oracle syntax and don't have TOAD handy, but you should be able to get something like this to work:
CREATE OR REPLACE PROCEDURE FindIdsFromList
AS
DECLARE
TYPE NumberRecord IS RECORD (Number int NOT NULL)
TYPE NumberList IS TABLE OF NumberRecord;
NumberList myNumberList;
BEGIN
myNumberList := (50,99,150);
SELECT DISTINCT mt.ID FROM MyTable mt
INNER JOIN myNumberList nt --your list of numbers
ON nt.Number Between mt.start_number and mt.end_number
END