SQL Query Get the Next ID - sql

I am working on a problem where I need to get the next available Category for a gamer. If the gamer has reached the final Category (Shooter in below image) then I will need to go back to the start of the table and go to the next one available.
Below is a list of Category:
Below is a list of gamers and what their last Category_ID was:
Finally, below is the data I will be using to work with
Now I need to find the next Category_ID for each gamer using the data table. If a Gamer has reached the last Category_ID (ID 4 in this case and Gamer C) then it will reset back to top like 1 however Gamer C will pick Category 2 as there is no one for that Gamer.
How can I pick the next Category available using the data table for each gamer while keeping in mind of resetting back to top if reached end?
Below is a code example of the problem:
DECLARE #Category AS TABLE
(
Category_ID INT IDENTITY(1,1) ,
Description VARCHAR(255)
)
INSERT INTO #Category
VALUES('Sports'), ('Adventure'), ('Action'), ('Shooter')
DECLARE #Gamer AS TABLE
(
Username VARCHAR(255) ,
Last_Category_ID INT
)
INSERT INTO #Gamer
VALUES('Gamer A', 1), ('Gamer B', 2), ('Gamer C', 4), ('Gamer D', 3)
DECLARE #Data AS TABLE
(
Username VARCHAR(255) ,
Play_At DATETIME ,
Category_ID INT
)
INSERT INTO #Data
VALUES('Gamer A', GETDATE() -1, 1), ('Gamer B', GETDATE() -1, 2), ('Gamer A', GETDATE() -1, 3), ('Gamer D', GETDATE() -1, 3), ('Gamer C', GETDATE() -1, 2)
SELECT * FROM #Category
SELECT * FROM #Gamer
SELECT * FROM #Data

If I understand correctly, you want the next unused category id for each gamer. And then to start over when all are done.
Here is an approach:
For each gamer, get the maximum current category.
For each category, get the next category.
Get the first category as well.
When there is no next category use the first.
This is a good opportunity to use lateral joins:
select g.*, coalesce(cn.category_id, cf.category_id)
from #gamers g outer apply
(select max(category_id) as max_category_id
from #data d
where d.username = g.username
) c outer apply
(select top (1) cn.category_id
from #categories cn
where cn.categoryid > c.max_categoryid
) cn cross join
(select min(cf.category_id) as categoryi_d
from #categories cf
) cf;
Here is a db<>fiddle.

I'm not 100% clear on if your #Data table represents the starting category however this might be what you need, if not by all means please clarify with some clear expected results for the sample data.
Using an apply to determine the starting category for each user (if indeed that's what your description entails) and another to get the maximum possible category, select the username and, using a case expression, the corresponding next category name or the starting category if it's already the max category.
select
Username,
( select description
from #category c
where c.category_Id=case when Last_Category_ID<m.Id then Last_Category_ID + 1 else startcat.Id end
) NextCategory
from #gamer g
outer apply (
select min(category_id) Id
from #data d
where d.username=g.Username
)startcat
outer apply (
select Max(category_Id) Id
from #category
)m

Related

Want to compare 4 different columns with the result of CTE

I have created a CTE (common table Expression) as follows:
DECLARE #N VARCHAR(100)
WITH CAT_NAM AS (
SELECT ID, NAME
FROM TABLE1
WHERE YEAR(DATE) = YEAR(GETDATE())
)
SELECT #N = STUFF((
SELECT ','''+ NAME+''''
FROM CAT_NAM
WHERE ID IN (20,23,25,30,37)
FOR XML PATH ('')
),1,1,'')
The result of above CTE is 'A','B','C','D','F'
Now I need to check 4 different columns CAT_NAM_1,CAT_NAM_2,CAT_NAM_3,CAT_NAM_4 in the result of CTE and form it as one column like follow:
Select
case when CAT_NAM_1 in (#N) then CAT_NAM_1
when CAT_NAM_2 in (#N) then CAT_NAM_2
when CAT_NAM_3 in (#N) then CAT_NAM_3
when CAT_NAM_4 in (#N) then CAT_NAM_4
end as CAT
from table2
When I'm trying to do the above getting error please help me to do.
If my approach is wrong help me with right one.
I am not exactly sure what you are trying to do, but if I understand the following script shows one possible technique. I have created some table variables to mimic the data you presented and then wrote a SELECT statement to do what I think you asked (but I am not sure).
DECLARE #TABLE1 AS TABLE (
ID INT NOT NULL,
[NAME] VARCHAR(10) NOT NULL,
[DATE] DATE NOT NULL
);
INSERT INTO #TABLE1(ID,[NAME],[DATE])
VALUES (20, 'A', '2021-01-01'), (23, 'B', '2021-02-01'),
(25, 'C', '2021-03-01'),(30, 'D', '2021-04-01'),
(37, 'E', '2021-05-01'),(40, 'F', '2021-06-01');
DECLARE #TABLE2 AS TABLE (
ID INT NOT NULL,
CAT_NAM_1 VARCHAR(10) NULL,
CAT_NAM_2 VARCHAR(10) NULL,
CAT_NAM_3 VARCHAR(10) NULL,
CAT_NAM_4 VARCHAR(10) NULL
);
INSERT INTO #TABLE2(ID,CAT_NAM_1,CAT_NAM_2,CAT_NAM_3,CAT_NAM_4)
VALUES (1,'A',NULL,NULL,NULL),(2,NULL,'B',NULL,NULL);
;WITH CAT_NAM AS (
SELECT ID, [NAME]
FROM #TABLE1
WHERE YEAR([DATE]) = YEAR(GETDATE())
AND ID IN (20,23,25,30,37,40)
)
SELECT CASE
WHEN EXISTS(SELECT 1 FROM CAT_NAM WHERE CAT_NAM.[NAME] = CAT_NAM_1) THEN CAT_NAM_1
WHEN EXISTS(SELECT 1 FROM CAT_NAM WHERE CAT_NAM.[NAME] = CAT_NAM_2) THEN CAT_NAM_2
WHEN EXISTS(SELECT 1 FROM CAT_NAM WHERE CAT_NAM.[NAME] = CAT_NAM_3) THEN CAT_NAM_3
WHEN EXISTS(SELECT 1 FROM CAT_NAM WHERE CAT_NAM.[NAME] = CAT_NAM_4) THEN CAT_NAM_4
ELSE '?' -- not sure what you want if there is no match
END AS CAT
FROM #TABLE2;
You can do a bit of set-based logic for this
SELECT
ct.NAME
FROM table2 t2
CROSS APPLY (
SELECT v.NAME
FROM (VALUES
(t2.CAT_NAM_1),
(t2.CAT_NAM_2),
(t2.CAT_NAM_3),
(t2.CAT_NAM_4)
) v(NAME)
INTERSECT
SELECT ct.NAM
FROM CAT_NAM ct
WHERE ct.ID IN (20,23,25,30,37)
) ct;

Get parents based on child id SQL

I have the following scenario in a Microsoft SQL environment:
CREATE TABLE grps
(
[id] varchar(50),
[parentid] varchar(50),
[value] varchar(50)
);
INSERT INTO grps
([id], [parentid], [value])
VALUES
('-5001', '0', null),
('-5002', '-5001', null),
('-5003', '-5002', '50'),
('-5004', '-5003', null),
('-5005', '0', null),
('-5006', '0', null),
('-5007', '0', null),
('-5008', '-5006', null);
I'm trying to get parents based on the id of a child. If the id queried is the last parent then it should only return the last item.
Examples:
If I query: id = '-5004' it should return ('-5004', '-5003', null),
('-5003', '-5002', '50'),
('-5002', '-5001', null),
('-5001', '0', null)
If I query id = '-5007' it should return ('-5007', '0', null)
It would be awesome if it could list the id queried first and the rest in an orderly fashion up the "tree".
I've tried several different approaches with CTE's but with no luck unfortunately. So I'm looking for some help or ideas here.
Thanks in advance.
You were on the right track with CTE's. It can be done by using recursive CTE! Here is how the recursive CTE looks like:
DECLARE #ID varchar(50) = '5004';
WITH CTE AS
(
--This is called once to get the minimum and maximum values
SELECT id, parentid, value
FROM grps
WHERE id= #ID
UNION ALL
--This is called multiple times until the condition is met
SELECT g.id, g.parentid, g.value
FROM CTE c, grps g
WHERE g.id= c.parentid
--If you don't like commas between tables then you can replace the 2nd select
--statement with this:
--SELECT g.id, g.parentid, g.value
--FROM CTE c
--INNER JOIN grps g ON g.id= c.parentid
--This can also be written with CROSS JOINS!
--Even though it looks more like another way of writing INNER JOINs.
--SELECT g.id, g.parentid, g.value
--FROM CTE c
--CROSS JOIN grps g
--WHERE g.id = c.parentid
)
SELECT * FROM CTE
Beware that the maximum recursion is 100 unless you add option (maxrecursion 0) to the end of the last select statement. The 0 means infinite but you can also set it to any value you want.
Enjoy!
I'm trying my best to give hierarchyid some love in the world. First, the setup:
CREATE TABLE grps
(
[id] varchar(50),
[parentid] varchar(50),
[value] varchar(50),
h HIERARCHYID NULL
);
SELECT * FROM grps
INSERT INTO grps
([id], [parentid], [value])
VALUES
('-5001', '0', null),
('-5002', '-5001', null),
('-5003', '-5002', '50'),
('-5004', '-5003', null),
('-5005', '0', null),
('-5006', '0', null),
('-5007', '0', null),
('-5008', '-5006', null);
WITH cte AS (
SELECT id ,
parentid ,
value ,
CAST('/' + id + '/' AS nvarchar(max)) AS h
FROM grps
WHERE parentid = 0
UNION ALL
SELECT child.id ,
child.parentid ,
child.value ,
CAST(parent.h + child.id + '/' AS NVARCHAR(MAX)) AS h
FROM cte AS [parent]
JOIN grps AS [child]
ON child.parentid = parent.id
)
UPDATE g
SET h = c.h
FROM grps AS g
JOIN cte AS c
ON c.id = g.id
All I'm doing here is adding a hierarchyid column to your table definition and calculating the value for it. To determine answer your original problem, now it looks something like this:
SELECT g.id ,
g.parentid ,
g.value ,
g.h.ToString()
FROM dbo.grps AS g
JOIN grps AS c
ON c.h.IsDescendantOf(g.h) = 1
WHERE c.id = '-5004'
To make this more performant, you should index both the id and h columns independently (that is, in separate indexes).
Also, a couple of notes
Having the id columns be varchar when the data looks numeric is fishy at best, but more importantly it's inefficient. If it were me, I'd use an int. But perhaps your actual data is messier (i.e you have ids like 'A1234').
I'd also use NULL instead of 0 for the parentid to represent top-level (i.e. those with no parent) members. But that's more of a personal choice rather than one that has any real performance implications.

How to compare n:m assignments?

I have two tables (entity and kind) plus a n:m table (entity_kind).
CREATE TABLE
entity
(
entity_id INT
, name NVARCHAR(100)
, PRIMARY KEY(entity_id)
)
CREATE TABLE
kind
(
kind_id INT
, name NVARCHAR(100)
, PRIMARY KEY(kind_id)
)
CREATE TABLE
entity_kind
(
entity_id INT
, kind_id INT
, PRIMARY KEY(entity_id, kind_id)
)
Test data:
INSERT INTO
entity
VALUES
(1, 'Entity A')
, (2, 'Entity B')
, (3, 'Entity C')
INSERT INTO
kind
VALUES
(1, 'Kind 1')
, (2, 'Kind 2')
, (3, 'Kind 3')
, (4, 'Kind 4')
INSERT INTO
entity_kind
VALUES
(1, 1)
, (1, 3)
, (2, 1)
, (2, 2)
, (3, 4)
My code so far:
DECLARE
#selected_entities
TABLE
(
entity_id INT
)
DECLARE
#same_kinds BIT;
INSERT INTO
#selected_entities
VALUES
(1), (2)
-- Missing code here
SELECT
#same_kinds AS "same_kinds"
The table var #selected_entities is filled with entities that should be compared.
The logical var #same_kinds should indicate whether the selected entities have exactly the same kinds assigned.
How can I achieve this?
This is a compare two sets of things type problem. The query I'm going to show gives all pairs along with a flag. You can easily incorporate comparing a subquery by changing the first two entity tables to the table of ids you want to compare.
This query has a few parts. First, it produces all pairs of entities from the entity tables. This is important, because this will pick up even entities that have no "kinds" associated with them. You want a flag, rather than just a list of those that match.
Then the heart of the logic is to do a self-join on the entity-kinds table with the match on "kind". This is then aggregated by the two entities. The result is a count of the kinds that two entities share.
The final logic is to compare this count to the count of "kinds" on each entity. If all of these counts are the same, then the entities match. If not, they do not. This approach does assume that there are no duplicates in entity_kinds.
select e1.entity_id as e1, e2.entity_id as e2,
(case when count(ek1.entity_id) = max(ek1.numkinds) and
count(ek2.entity_id) = count(ek1.entity_id) and
max(ek1.numkinds) = max(ek2.numkinds)
then 1
else 0
end) as IsSame
from entity e1 join
entity e2
on e1.entity_id < e2.entity_id left outer join
(select ek.*, count(*) over (partition by entity_id) as numkinds
from entity_kind ek
) ek1
on e1.entity_id = ek1.entity_id left outer join
(select ek.*, count(*) over (partition by entity_id) as numkinds
from entity_kind ek
) ek2
on e2.entity_id = ek2.entity_id and
ek2.kind_id = ek1.kind_id
group by e1.entity_id, e2.entity_id;
The SQL Fiddle is here.
You can do this with two checks: First, if the kind-count on each entity is not the same, then they cannot match. Second, provided the count is the same, you just need to find one kind that doesn't match the list of an arbitrary other entity (I just take the first entity in the compare list). In code:
DECLARE #firstEntity int = (SELECT TOP 1 entity_id from #selected_entities)
IF EXISTS(SELECT TOP 1 se.entity_id FROM #selected_entities se
INNER JOIN entity_kind ek ON ek.entity_id = se.entity_id
WHERE ek.kind_id NOT IN (SELECT kind_id from entity_kind where entity_id = #firstEntity)
OR ((SELECT COUNT(1) FROM entity_kind WHERE entity_id = ek.entity_id)
<> (SELECT COUNT(1) FROM entity_kind WHERE entity_id = #firstEntity)))
SET #same_kinds = 0
ELSE
SET #same_kinds = 1
DECLARE #first_entity_id INT;
SET #first_entity_id = (SELECT TOP(1) se.entity_id FROM #selected_entities se);
DECLARE #dummyvar INT;
SELECT DISTINCT #dummyvar = COUNT(ek.kind_id)
FROM dbo.entity_kind ek
LEFT JOIN (
SELECT ek.kind_id
FROM dbo.entity_kind ek
WHERE ek.entity_id = #first_entity_id
) k ON ek.kind_id = k.kind_id
WHERE ek.entity_id IN (SELECT se.entity_id FROM #selected_entities se)
GROUP BY ek.entity_id;
SET #same_kinds = CASE WHEN ##ROWCOUNT = 1 THEN 1 ELSE 0 END;
SELECT #same_kinds AS [#same_kinds];
Note: #selected_entities should be declared thus:
DECLARE
#selected_entities
TABLE
(
entity_id INT PRIMARY KEY
)

Tag Cloud based on weighted usaage

I'd like to create a weighted usage ranking / popularity query (or batch update, if the query proves to strenuous for real-time use!) but I've been drawing a blank. Hopefully you'll have a better idea as to how to do this.
I've simplified my database to help illustrate the problem (see diagram, below!) Basically, when a User selects a specific Blog via a Tag, I add an entry to the TagLog table. Assume for this example that the collection of Blogs and Tags remain static. Assuming the above, I'd like to do the following:
Find the Top 10 Blogs for any given Tag
Find the Top 10 Blogs for any given Tag and User
The real difficulty comes from the fact that I'd like to weight the results such that more recent TagLog entries have greater significance.
Any help in this regard would be greatly appreciated! Thanks...
This should get you headed somewhere useful:
-- Sample data.
declare #Blogs as Table ( BlogId Int Identity, URL VarChar(256) )
insert into #Blogs ( URL ) values
( 'www.google.com' ), ( 'www.java.com' )
declare #Tags as Table ( TagId Int Identity, BlogId Int, Tag VarChar(64) )
insert into #Tags ( BlogId, Tag ) values
( 1, 'Not Evil' ), ( 2, 'Buggy' )
declare #TagLog as Table ( TagId Int, UserGuid UniqueIdentifier, Visited DateTime )
insert into #TagLog ( TagId, UserGuid, Visited ) values
( 1, NewId(), '20130502' ), ( 1, NewId(), '20130508' ), ( 1, NewId(), '20130515' ),
( 2, NewId(), '20130501' ), ( 2, NewId(), '20130508' ), ( 2, NewId(), '20130515' )
declare #Now as DateTime = '20130516' -- Test value.
-- Display all sample data.
select *, DateDiff( day, TL.Visited, #Now ) as Age -- Use appropriate units, e.g. week, minute.
from #Blogs as B inner join
#Tags as T on T.BlogId = B.BlogId inner join
#TagLog as TL on TL.TagId = T.TagId
-- Compute a weight based on age.
-- Use the reciprocal of the age so that newer visits have higher weight.
-- Add 1.0 to avoid divide by zero errors.
select T.TagId, Count( 42 ) as Visits, Sum( 1.0 / ( DateDiff( day, TL.Visited, #Now ) + 1.0 ) ) as AgeWeight
from #Blogs as B inner join
#Tags as T on T.BlogId = B.BlogId inner join
#TagLog as TL on TL.TagId = T.TagId
group by T.TagId

How to get result of LEFT join into another table as one field

Not sure how to describe this so I will show example:
table PAGES
id int
parent int
name nvarchar
status tinyint
table PAGES_MODULES
id int
id_parent int
module_type nvarchar
module_id int
status int
One page can have more than one linked modules. Example records:
id parent name status
1 -1 Xyz 1
2 -1 Yqw 1
id id_parent module_type module_id status
1 1 ARTICLE 1 1
2 1 GALLERY 2 1
3 2 CATEGORY 3 1
What I need is to create select which will not return 2 results if I do select left join page_modules.
I would like to have select which returns linked modules as this:
id parent name status modules
1 -1 Xyz 1 ARTICLE GALLERY
2 -1 Yqw 1 CATEGORY
Is that possible?
Thanks.
UPDATE
I have tried COALESE, CROSS APPLY and SELECT within SELECT methods and came to these conclusions:
http://blog.feronovak.com/2011/10/multiple-values-in-one-column-aka.html
Hope I can publish these here, not meaning to spam or something.
You'd need to create a custom aggregate function that could concatenate the strings together, there is no built-in SQL Server function that does this.
You can create a custom aggregate function (assuming your using the latest version of SQL) using a .Net assembly. Here's the MS reference on how to do this (the example in the article is actually for a CONCATENATE function just like you require): http://msdn.microsoft.com/en-us/library/ms182741.aspx
Use group_concat() to smoosh multiple rows' worth of data into a single field like that. Note that it does have a length limit (1024 chars by default), so if you're going to have a zillion records being group_concatted, you'll only get the first few lines worth unless you raise the limit.
SELECT ..., GROUP_CONCAT(modules SEPARATOR ' ')
FROM ...
GROUP BY ...
Note that it IS an aggregate function, so you must have a group-by clause.
-- ==================
-- sample data
-- ==================
declare #pages table
(
id int,
parent int,
name nvarchar(max),
status tinyint
)
declare #pages_modules table
(
id int,
id_parent int,
module_type nvarchar(max),
module_id int,
status int
)
insert into #pages values (1, -1, 'Xyz', 1)
insert into #pages values (2, -1, 'Yqw', 1)
insert into #pages_modules values (1, 1, 'ARTICLE', 1, 1)
insert into #pages_modules values (2, 1, 'GALLERY', 2, 1)
insert into #pages_modules values (3, 2, 'CATEGORY', 3, 1)
-- ==================
-- solution
-- ==================
select
*,
modules = (
select module_type + ' ' from #pages_modules pm
where pm.id_parent = p.id
for xml path('')
)
from #pages p
You need to join both tables and then GROUP BY by pages.id, pages.parent, pages.status, pages.name and pages.status. Your modules field in your resultset is then a string aggregate function, i.e in Oracle LISTAGG(pages_modules.modules, ' ') as modules.