Loop with inner loop and split - tsql

I have records like this in a table called "Entry":
TABLE: Entry
ID Tags
--- ------------------------------------------------------
1 Coffee, Tea, Cake, BBQ
2 Soda, Lemonade
...etc.
TABLE: Tags
ID TagName
---- -----------
1 Coffee
2 Tea
3 Soda
...
TABLE: TagEntry
ID TAGID ENTRYID
--- ----- -------
1 1 1
2 2 1
3 3 2
....
I need to loop through each record in the entire table for Entry, then for each row loop the comma delimited tags because I need to split each tag then do a Tag lookup based on tag name to grab the TagID, and then ultimately insert TagID, EntryID in a bridge table called TagEntry for each comma delimited tag
Not sure how to go about this.

Try this
;with entry as
(
select 1 id, 'Coffee, Tea, Cake, BBQ' tags
Union all
select 2, 'Soda, Lemonade'
), tags as
(
select 1 id,'Coffee' TagName union all
select 2,'Tea' union all
select 3,'Soda'
), entryxml as
(
SELECT id, ltrim(rtrim(r.value('.','VARCHAR(MAX)'))) as Item from (
select id, CONVERT(XML, N'<root><r>' + REPLACE(tags,',','</r><r>') + '</r></root>') as XmlString
from entry ) x
CROSS APPLY x.XmlString.nodes('//root/r') AS RECORDS(r)
)
select e.id EntryId, t.id TagId from entryxml e
inner join tags t on e.Item = t.TagName

This SQL will split your Entry table, for joining to the others:
with raw as (
select * from ( values
(1, 'Coffee, Tea, Cake, BBQ'),
(2, 'Soda, Lemonade')
) Entry(ID,Tags)
)
, data as (
select ID, Tag = convert(varchar(255),' '), Tags, [Length] = len(Tags) from raw
union all
select
ID = ID,
Tag = case when charindex(',',Tags) = 0 then Tags else convert(varchar(255), substring(Tags, 1, charindex(',',Tags)-1) ) end,
Tags = substring(Tags, charindex(',',Tags)+1, 255),
[Length] = [Length] - case when charindex(',',Tags) = 0 then len(Tags) else charindex(',',Tags) end
from data
where [Length] > 0
)
select ID, Tag = ltrim(Tag)
from data
where Tag <> ''
and returns this for the given input:
ID Tag
----------- ------------
2 Soda
2 Lemonade
1 Coffee
1 Tea
1 Cake
1 BBQ

Related

If there is only one zero value then group by supplier and show zero, if there is no zero, then avg all values

I will give you example of table that I have:
Supplier | Value
sup1 | 4
sup2 | 1
sup1 | 0
sup1 | 3
sup2 | 5
I need a result that will do average by supplier, but if there is value 0 for a supplier, do not average, but return 0 instead
It should look like this:
Supplier | Value
sup1 | 0
sup2 | 3
This is a little trick but it should work :
SELECT Supplier,
CASE WHEN MIN(ABS(Value)) = 0 THEN 0 ELSE AVG(Value) END
FROM TableTest
GROUP BY Supplier
EDIT : Using the ABS() function let you avoid having problems with negative values
DECLARE #TAB TABLE (SUPPLIER VARCHAR(50),VALUE INTEGER)
INSERT INTO #TAB
SELECT 'sup1',4
UNION ALL
SELECT 'sup2',1
UNION ALL
SELECT 'sup1',0
UNION ALL
SELECT 'sup1',3
UNION ALL
SELECT 'sup2',5
SELECT * FROM #TAB
SELECT T1.SUPPLIER,CASE WHEN EXISTS(SELECT 1 FROM #TAB T WHERE T.SUPPLIER = T1.SUPPLIER AND T.VALUE = 0) THEN 0 ELSE AVG(T1.VALUE) END AS VALUE
FROM #TAB T1
GROUP BY T1.SUPPLIER
Result
SUPPLIER VALUE
sup1 0
sup2 3
Using the following query is one of the way to do.
First I push the supplier which has the Value = 0, then based on the result, I will do the remaining calculation and finally using UNION to get the expected result:
DECLARE #ZeroValue TABLE (Supplier VARCHAR (20));
INSERT INTO #ZeroValue (Supplier)
SELECT Supplier FROM TestTable WHERE Value = 0
SELECT Supplier, 0 AS Value FROM #ZeroValue
UNION
SELECT T.Supplier, AVG(T.Value) AS Value
FROM TestTable T
JOIN #ZeroValue Z ON Z.Supplier != T.Supplier
GROUP BY T.Supplier
Schema used for the sample:
CREATE TABLE TestTable (Supplier VARCHAR (20), Value INT);
INSERT INTO TestTable (Supplier, Value) VALUES
('sup1', 4), ('sup2', 1), ('sup1', 0), ('sup1', 3), ('sup2', 5);
Please find the working demo on db<>fiddle

Select rows which has multiple values in junction table

I have three tables:
posts (id, content)
posts_tags (posts_id, tags_id)
tags (id, tag)
How do I select all posts that have (at least) 2 specific tags (lets say tags with id 1 and 2)?
For example, posts table:
id content
---- -------
1 post1
2 post2
3 post3
tags table:
id tag
---- ------
1 tag1
2 tag2
3 tag3
posts_tags table:
posts_id tags_id
---------- ---------
1 1
1 2
2 1
3 1
3 2
3 3
I then expect the following result:
id content
---- ---------
1 post1
3 post3
Post with ID 3 (since it has tags 1, 2, and 3) and post with id 1 (since it has tags with id 1, and 2) but not post 2 since it doesn't have tag with id 2.
Assume I can not change the table structure.
SELECT *
FROM posts p
JOIN posts_tags pt ON pt.posts_id = p.id
WHERE pt.tags_id IN (1,2);
SELECT *
FROM posts p
JOIN posts_tags pt ON pt.posts_id = p.id
WHERE pt.tags_id = 1 OR pt.tags_id = 2;
SELECT *
FROM posts p
JOIN posts_tags pt ON pt.posts_id = p.id
WHERE pt.tags_id = 1 AND pt.tags_id = 2;
EDIT: Quick and dirty
WITH j AS (
SELECT pt.posts_id AS post,
p.content AS content,
STRING_AGG(pt.tags_id::TEXT,',') AS agg
FROM posts p
JOIN posts_tags pt ON pt.posts_id = p.id
GROUP BY pt.posts_id, p.content
)
SELECT post,content
FROM j
WHERE STRING_TO_ARRAY(agg,',') #> ('{2,1}'::TEXT[])
Found an answer to my own question:
SELECT posts.*
FROM posts
INNER JOIN posts_tags ON posts_tags.posts_id = posts.id
INNER JOIN tags ON tags.id = posts_tags.tags_id
WHERE tags.id IN (1, 2)
GROUP BY posts.id
HAVING COUNT(*) > 1

Find all records NOT in any blocked range where blocked ranges are in a table

I have a table TaggedData with the following fields and data
ID GroupID Tag MyData
** ******* *** ******
1 Texas AA01 Peanut Butter
2 Texas AA15 Cereal
3 Ohio AA05 Potato Chips
4 Texas AA08 Bread
I have a second table of BlockedTags as follows:
ID StartTag EndTag
** ******** ******
1 AA00 AA04
2 AA15 AA15
How do I select from this to return all data matching a given GroupId but NOT in any blocked range (inclusive)? For the data given if the GroupId is Texas, I don't want to return Cereal because it matches the second range. It should only return Bread.
I did try left joins based queries but I'm not even that close.
Thanks
create table TaggedData (
ID int,
GroupID varchar(16),
Tag char(4),
MyData varchar(50))
create table BlockedTags (
ID int,
StartTag char(4),
EndTag char(4)
)
insert into TaggedData(ID, GroupID, Tag, MyData)
values (1, 'Texas', 'AA01', 'Peanut Butter')
insert into TaggedData(ID, GroupID, Tag, MyData)
values (2, 'Texas' , 'AA15', 'Cereal')
insert into TaggedData(ID, GroupID, Tag, MyData)
values (3, 'Ohio ', 'AA05', 'Potato Chips')
insert into TaggedData(ID, GroupID, Tag, MyData)
values (4, 'Texas', 'AA08', 'Bread')
insert into BlockedTags(ID, StartTag, EndTag)
values (1, 'AA00', 'AA04')
insert into BlockedTags(ID, StartTag, EndTag)
values (2, 'AA15', 'AA15')
select t.* from TaggedData t
left join BlockedTags b on t.Tag between b.StartTag and b.EndTag
where b.ID is null
Returns:
ID GroupID Tag MyData
----------- ---------------- ---- --------------------------------------------------
3 Ohio AA05 Potato Chips
4 Texas AA08 Bread
(2 row(s) affected)
So, to match on given GroupID you change the query like that:
select t.* from TaggedData t
left join BlockedTags b on t.Tag between b.StartTag and b.EndTag
where b.ID is null and t.GroupID=#GivenGroupID
I Prefer the NOT EXISTS simply because it gives you more readability, usability and better performance usually in large data (several cases get better execution plans):
would be like this:
SELECT * from TaggedData
WHERE GroupID=#GivenGroupID
AND NOT EXISTS(SELECT 1 FROM BlockedTags WHERE Tag BETWEEN StartTag ANDEndTag)

t-sql outer join across three tables

I have three tables:
CREATE TABLE person
(id int,
name char(50))
CREATE TABLE eventtype
(id int,
description char(50))
CREATE TABLE event
(person_id int,
eventtype_id int,
duration int)
What I want is a single query which gives me a list of the total duration of each eventtype for each person, including all zero entries. E.g. if there are 10 people and 15 different eventtypes, there should be 150 rows returned, irrespective of the contents of the event table.
I can get an outer join to work between two tables (e.g. durations for all eventtypes), but not with a second outer join.
Thanks!
You'll have to add a CROSS APPLY to the mix to get the non-existing relations.
SELECT q.name, q.description, SUM(q.Duration)
FROM (
SELECT p.Name, et.description, Duration = 0
FROM person p
CROSS APPLY eventtype et
UNION ALL
SELECT p.Name, et.description, e.duration
FROM person p
INNER JOIN event e ON e.person_id = p.id
INNER JOIN eventtype et ON et.id = e.eventtypeid
) q
GROUP BY
q.Name, q.description
You can cross join person and eventtype, and then just join the result to the event table:
SELECT
p.Name,
et.Description,
COALESCE(e.duration,0)
FROM
person p
cross join
eventtype et
left join
event e
on
p.id = e.person_id and
et.id = e.eventtype_id
A cross join is one where, for each row in the left table, it's joined to every row in the right table.
If you want a row for every combination of person and eventtype, that suggets a CROSS JOIN. To get the duration we need to join to event, but this needs to be an OUTER join since there might not always be a row. Your use of "total" suggests there there could be more than one event for a given combination of person and event, so we'll need a SUM in there as well.
Sample data:
insert person values ( 1, 'Joe' )
insert person values ( 2, 'Bob' )
insert person values ( 3, 'Tim' )
insert eventtype values ( 1, 'Cake' )
insert eventtype values ( 2, 'Pie' )
insert eventtype values ( 3, 'Beer' )
insert event values ( 1, 1, 10 )
insert event values ( 1, 2, 10 )
insert event values ( 1, 2, 5 )
insert event values ( 2, 1, 10 )
insert event values ( 2, 2, 7 )
insert event values ( 3, 2, 8 )
insert event values ( 3, 3, 16 )
insert event values ( 1, 1, 10 )
The query:
SELECT
PET.person_id
, PET.person_name
, PET.eventtype_id
, PET.eventtype_description
, ISNULL(SUM(E.duration), 0) total_duration
FROM
(
SELECT
P.id person_id
, P.name person_name
, ET.id eventtype_id
, ET.description eventtype_description
FROM
person P
CROSS JOIN eventtype ET
) PET
LEFT JOIN event E ON PET.person_id = E.person_id
AND PET.eventtype_id = E.eventtype_id
GROUP BY
PET.person_id
, PET.person_name
, PET.eventtype_id
, PET.eventtype_description
Output:
person_id person_name eventtype_id eventtype_description total_duration
----------- ----------- ------------ --------------------- --------------
1 Joe 1 Cake 20
1 Joe 2 Pie 15
1 Joe 3 Beer 0
2 Bob 1 Cake 10
2 Bob 2 Pie 7
2 Bob 3 Beer 0
3 Tim 1 Cake 0
3 Tim 2 Pie 8
3 Tim 3 Beer 16
Warning: Null value is eliminated by an aggregate or other SET operation.
(9 row(s) affected)

TSQL recursive CTE threaded sorting

I have the following table:
ID parentID name
1 0 car1
2 1 tire
3 2 rubber
4 0 car2
5 2 nut
6 3 black
To help with testing...
CREATE TABLE #TT (ID int
,ParentID int
,Name varchar(25)
)
INSERT #TT
SELECT 1,0,'car1' UNION ALL
SELECT 2,1,'tire' UNION ALL
SELECT 3,2,'rubber' UNION ALL
SELECT 4,0,'car2' UNION ALL
SELECT 5,2,'nut' UNION ALL
SELECT 6,3,'black'
I'm trying to create a "threaded" hierarchy, but I want to list the child nodes under their parents like so:
ID parentID name
1 0 car1
2 1 tire
3 2 rubber
6 3 black
5 2 nut
4 0 car2
If I use a recursive CTE like this one...
;WITH Features
AS
(
SELECT *
FROM #TT
WHERE ParentID = 0
UNION ALL
SELECT F.*
FROM #TT AS F
INNER JOIN Features
ON F.ParentID = Features.ID
)
SELECT *
FROM Features
I end up with this...
ID parentID name
1 0 car1
4 0 car2
2 1 tire
3 2 rubber
5 2 nut
6 3 black
any ideas? Thank you in advance.
You can build a tree path as you go along, and order it by that
Something like
DECLARE #TT TABLE(ID int, ParentID int, Name varchar(25))
INSERT #TT
SELECT 1,0,'car1' UNION ALL
SELECT 2,1,'tire' UNION ALL
SELECT 3,2,'rubber' UNION ALL
SELECT 4,0,'car2' UNION ALL
SELECT 5,2,'nut' UNION ALL
SELECT 6,3,'black'
SELECT *
FROM #TT
;WITH Features AS (
SELECT *,
CAST(ID AS VARCHAR(MAX)) + '/' AS TreePath
FROM #TT
WHERE ParentID = 0
UNION ALL
SELECT tt.*,
f.TreePath + CAST(tt.ID AS VARCHAR(10)) + '/'
FROM #TT tt INNER JOIN
Features f ON tt.ParentID = f.ID
)
SELECT *
FROM Features
ORDER BY TreePath
Try adding an ORDER BY clause such as:
SELECT * FROM Features
ORDER BY parentID