Get parents based on child id SQL - 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.

Related

'Merge Fields' - alike SQL Server function

I try to find a way to let the SGBD perform a population of merge fields within a long text.
Create the structure :
CREATE TABLE [dbo].[store]
(
[id] [int] NOT NULL,
[text] [nvarchar](MAX) NOT NULL
)
CREATE TABLE [dbo].[statement]
(
[id] [int] NOT NULL,
[store_id] [int] NOT NULL
)
CREATE TABLE [dbo].[statement_merges]
(
[statement_id] [int] NOT NULL,
[merge_field] [nvarchar](30) NOT NULL,
[user_data] [nvarchar](MAX) NOT NULL
)
Now, create test values
INSERT INTO [store] (id, text)
VALUES (1, 'Waw, stackoverflow is an amazing library of lost people in the IT hell, and i have the feeling that $$PERC_SAT$$ of the users found a solution, personally I asked $$ASKED$$ questions.')
INSERT INTO [statement] (id, store_id)
VALUES (1, 1)
INSERT INTO [statement_merges] (statement_id, merge_field, user_data)
VALUES (1, '$$PERC_SAT$$', '85%')
INSERT INTO [statement_merges] (statement_id, merge_field, user_data)
VALUES (1, '$$ASKED$$', '12')
At the time being my app is delivering the final statement, looping through merges, replacing in the stored text and output
Waw, stackoverflow is an amazing library of lost people in the IT
hell, and i have the feeling that 85% of the users found a solution,
personally I asked 12 questions.
I try to find a way to be code-independent and serve the output in a single query, as u understood, select a statement in which the stored text have been populated with user data. I hope I'm clear.
I looked on TRANSLATE function but it looks like a char replacement, so I have two choices :
I try a recursive function, replacing one by one until no merge_fields is found in the calculated text; but I have doubts about the performance of this approach;
There is a magic to do that but I need your knowledge...
Consider that I want this because the real texts are very long, and I don't want to store it more than once in my database. You can imagine a 3 pages contract with only 12 parameters, like start date, invoiced amount, etc... Everything else cant be changed for compliance.
Thank you for your time!
EDIT :
Thanks to Randy's help, this looks to do the trick :
WITH cte_replace_tokens AS (
SELECT replace(r.text, m.merge_field, m.user_data) as [final], m.merge_field, s.id, 1 AS i
FROM store r
INNER JOIN statement s ON s.store_id = r.id
INNER JOIN statement_merges m ON m.statement_id = s.id
WHERE m.statement_id = 1
UNION ALL
SELECT replace(r.final, m.merge_field, m.user_data) as [final], m.merge_field, r.id, r.i + 1 AS i
FROM cte_replace_tokens r
INNER JOIN statement_merges m ON m.statement_id = r.id
WHERE m.merge_field > r.merge_field
)
select TOP 1 final from cte_replace_tokens ORDER BY i DESC
I will check with a bigger database if the performance is good...
At least, I can "populate" one statement, I need to figure out to be able to extract a list as well.
Thanks again !
If a record is updated more than once by the same update, the last wins. None of the updates are affected by the others - no cumulative effect. It is possible to trick SQL using a local variable to get cumulative effects in some cases, but it's tricky and not recommended. (Order becomes important and is not reliable in an update.)
One alternate is recursion in a CTE. Generate a new record from the prior as each token is replaced until there are no tokens. Here is a working example that replaces 1 with A, 2 with B, etc. (I wonder if there is some tricky xml that can do this as well.)
if not object_id('tempdb..#Raw') is null drop table #Raw
CREATE TABLE #Raw(
[test] [varchar](100) NOT NULL PRIMARY KEY CLUSTERED,
)
if not object_id('tempdb..#Token') is null drop table #Token
CREATE TABLE #Token(
[id] [int] NOT NULL PRIMARY KEY CLUSTERED,
[token] [char](1) NOT NULL,
[value] [char](1) NOT NULL,
)
insert into #Raw values('123456'), ('1122334456')
insert into #Token values(1, '1', 'A'), (2, '2', 'B'), (3, '3', 'C'), (4, '4', 'D'), (5, '5', 'E'), (6, '6', 'F');
WITH cte_replace_tokens AS (
SELECT r.test, replace(r.test, l.token, l.value) as [final], l.id
FROM [Raw] r
CROSS JOIN #Token l
WHERE l.id = 1
UNION ALL
SELECT r.test, replace(r.final, l.token, l.value) as [final], l.id
FROM cte_replace_tokens r
CROSS JOIN #Token l
WHERE l.id = r.id + 1
)
select * from cte_replace_tokens where id = 6
It's not recommended to do such tasks inside sql engine but if you want to do that, you need to do it in a loop using cursor in a function or stored procedure like so :
DECLARE #merge_field nvarchar(30)
, #user_data nvarchar(MAX)
, #statementid INT = 1
, #text varchar(MAX) = 'Waw, stackoverflow is an amazing library of lost people in the IT hell, and i have the feeling that $$PERC_SAT$$ of the users found a solution, personally I asked $$ASKED$$ questions.'
DECLARE merge_statements CURSOR FAST_FORWARD
FOR SELECT
sm.merge_field
, sm.user_data
FROM dbo.statement_merges AS sm
WHERE sm.statement_id = #statementid
OPEN merge_statements
FETCH NEXT FROM merge_statements
INTO #merge_field , #user_data
WHILE ##FETCH_STATUS = 0
BEGIN
set #text = REPLACE(#text , #merge_field, #user_data )
FETCH NEXT FROM merge_statements
INTO #merge_field , #user_data
END
CLOSE merge_statements
DEALLOCATE merge_statements
SELECT #text
Here is a recursive solution.
SQL Fiddle
MS SQL Server 2017 Schema Setup:
CREATE TABLE [dbo].[store]
(
[id] [int] NOT NULL,
[text] [nvarchar](MAX) NOT NULL
)
CREATE TABLE [dbo].[statement]
(
[id] [int] NOT NULL,
[store_id] [int] NOT NULL
)
CREATE TABLE [dbo].[statement_merges]
(
[statement_id] [int] NOT NULL,
[merge_field] [nvarchar](30) NOT NULL,
[user_data] [nvarchar](MAX) NOT NULL
)
INSERT INTO store (id, text)
VALUES (1, '$$(*)$$, stackoverflow...$$PERC_SAT$$...$$ASKED$$ questions.')
INSERT INTO store (id, text)
VALUES (2, 'Use The #_#')
INSERT INTO statement (id, store_id) VALUES (1, 1)
INSERT INTO statement (id, store_id) VALUES (2, 2)
INSERT INTO statement_merges (statement_id, merge_field, user_data) VALUES (1, '$$PERC_SAT$$', '85%')
INSERT INTO statement_merges (statement_id, merge_field, user_data) VALUES (1, '$$ASKED$$', '12')
INSERT INTO statement_merges (statement_id, merge_field, user_data) VALUES (1, '$$(*)$$', 'Wow')
INSERT INTO statement_merges (statement_id, merge_field, user_data) VALUES (2, ' #_#', 'Flux!')
Query 1:
;WITH Normalized AS
(
SELECT
store_id=store.id,
store.text,
sm.merge_field,
sm.user_data,
RowNumber = ROW_NUMBER() OVER(PARTITION BY store.id,sm.statement_id ORDER BY merge_field),
statement_id = st.id
FROM
store store
INNER JOIN statement st ON st.store_id = store.id
INNER JOIN statement_merges sm ON sm.statement_id = st.id
)
, Recurse AS
(
SELECT
store_id, statement_id, old_text = text, merge_field,user_data, RowNumber,
Iteration=1,
new_text = REPLACE(text, merge_field, user_data)
FROM
Normalized
WHERE
RowNumber=1
UNION ALL
SELECT
n.store_id, n.statement_id, r.old_text, n.merge_field, n.user_data,
RowNumber=r.RowNumber+1,
Iteration=Iteration+1,
new_text = REPLACE(r.new_text, n.merge_field, n.user_data)
FROM
Normalized n
INNER JOIN Recurse r ON r.RowNumber = n.RowNumber AND r.statement_id = n.statement_id
)
,ReverseOnIteration AS
(
SELECT *,
ReverseIteration = ROW_NUMBER() OVER(PARTITION BY statement_id ORDER BY Iteration DESC)
FROM
Recurse
)
SELECT
store_id, statement_id, new_text, old_text
FROM
ReverseOnIteration
WHERE
ReverseIteration=1
Results:
| store_id | statement_id | new_text | old_text |
|----------|--------------|------------------------------------------|--------------------------------------------------------------|
| 1 | 1 | Wow, stackoverflow...85%...12 questions. | $$(*)$$, stackoverflow...$$PERC_SAT$$...$$ASKED$$ questions. |
| 2 | 2 | Use TheFlux! | Use The #_# |
With the help of Randy, I think I've achieved what I wanted to do !
Known the fact that my real case is a contract, in which there are several statements that may be :
free text
stored text without any merges
stored text with one or
several merges
this CTE does the job !
WITH cte_replace_tokens AS (
-- The initial query dont join on merges neither on store because can be a free text
SELECT COALESCE(r.text, s.part_text) AS [final], CAST('' AS NVARCHAR) AS merge_field, s.id, 1 AS i, s.contract_id
FROM statement s
LEFT JOIN store r ON s.store_id = r.id
UNION ALL
-- We loop till the last merge field, output contains iteration to be able to keep the last record ( all fields updated )
SELECT replace(r.final, m.merge_field, m.user_data) as [final], m.merge_field, r.id, r.i + 1 AS i, r.contract_id
FROM cte_replace_tokens r
INNER JOIN statement_merges m ON m.statement_id = r.id
WHERE m.merge_field > r.merge_field AND r.final LIKE '%' + m.merge_field + '%'
-- spare lost replacements by forcing only one merge_field per loop
AND NOT EXISTS( SELECT mm.statement_id FROM statement_merges mm WHERE mm.statement_id = m.statement_id AND mm.merge_field > r.merge_field AND mm.merge_field < m.merge_field)
)
select s.id,
(select top 1 final from cte_replace_tokens t WHERE t.contract_id = s.contract_id AND t.id = s.id ORDER BY i DESC) as res
FROM statement s
where contract_id = 1
If the CTE solution with a cross join is too slow, an alternate solution would be to build a scalar fn dynamically that has every REPLACE required from the token table. One scalar fn call per record then is order(N). I get the same result as before.
The function is simple and likely not to be too long, depending upon how big the token table becomes...256 MB batch limit. I've seen attempts to dynamically create queries to improve performance backfire - moved the problem to compile time. Should not be a problem here.
if not object_id('tempdb..#Raw') is null drop table #Raw
CREATE TABLE #Raw(
[test] [varchar](100) NOT NULL PRIMARY KEY CLUSTERED,
)
if not object_id('tempdb..#Token') is null drop table #Token
CREATE TABLE #Token(
[id] [int] NOT NULL PRIMARY KEY CLUSTERED,
[token] [char](1) NOT NULL,
[value] [char](1) NOT NULL,
)
insert into #Raw values('123456'), ('1122334456')
insert into #Token values(1, '1', 'A'), (2, '2', 'B'), (3, '3', 'C'), (4, '4', 'D'), (5, '5', 'E'), (6, '6', 'F');
DECLARE #sql varchar(max) = 'CREATE FUNCTION dbo.fn_ReplaceTokens(#raw varchar(8000)) RETURNS varchar(8000) AS BEGIN RETURN ';
WITH cte_replace_statement AS (
SELECT a.id, CAST('replace(#raw,''' + a.token + ''',''' + a.value + ''')' as varchar(max)) as [statement]
FROM #Token a
WHERE a.id = 1
UNION ALL
SELECT n.id, CAST(replace(l.[statement], '#raw', 'replace(#raw,''' + n.token + ''',''' + n.value + ''')') as varchar(max)) as [statement]
FROM #Token n
INNER JOIN cte_replace_statement l
ON n.id = l.id + 1
)
select #sql += [statement] + ' END' from cte_replace_statement where id = 6
print #sql
if not object_id('dbo.fn_ReplaceTokens') is null drop function dbo.fn_ReplaceTokens
execute (#sql)
SELECT r.test, dbo.fn_ReplaceTokens(r.test) as [final] FROM [Raw] r

Get hierarchical data is SQL SERVER with fallback logic

Consider the below schema
dbo.Cultures (Id, CultureCode, ParentId)
Culture table stores the data in the parent-child relationship.
Suppose we have below demo data
5 es-ES 3
Now I have another table which stores the multilingual data for the different cultures.
Schema for the table is as following
dbo.LangData(KeyName, CultureId, Value)
here cultureId is the foreign key of dbo.Cultures table.
Suppose this table has following data
Now I require to fetch the data for all the cultures which are in the Culture table and the corresponding value column in the LangData table.
The culture Ids which are not in the LangData table, for those the Value column will the value of the corresponding parent culture Id columns value. I.e. Data will be retrieved using fallback logic
E.g. For the above values the Result set will be following.
5 es-ES Colour_IN
Here for de-DE data is missing in LangData so it's value will be the data in it's parent culture i.e. en-IN, if in case data also not found in en-IN then it will pick the data of it's parent en-US.
Tried Soloution
First I fetch the culture hierarchy using CTE
CREATE FUNCTION [dbo].[ufnGetCultureHierarchyAll] ()
RETURNS #hierarchyResult TABLE(RowNo INT, CultureId INT, ParentCultureId INT)
AS
BEGIN
WITH CultureHierarchy_CTE(RowNo, CultureId, ParentCultureId)
AS (
SELECT 1,
Id,
ParentId
FROM [dbo].Cultures
UNION ALL
SELECT RowNo + 1,
ou.Id,
ou.ParentId
FROM [dbo].Cultures ou
JOIN CultureHierarchy_CTE cte
ON ou.Id = cte.ParentCultureId
)
-- inserting desired records into table and returning
INSERT INTO #hierarchyResult (RowNo,CultureId,ParentCultureId )
SELECT RowNo, CultureId , ParentCultureId FROM CultureHierarchy_CTE
RETURN;
END
This will return the hierarchy of the all the cultures
Now I attempted to apply join of the result set with the LangData table,
DECLARE #cultureHierarchy AS TABLE(
RowNumber INT,
CultureId INT,
ParentCultureId INT
)
--SELECT * FROM master.Cultures
----Get and store culture hierarchy
INSERT INTO #cultureHierarchy
SELECT RowNo, CultureId, ParentCultureId
FROM ufnGetCultureHierarchyAll()
SELECT c.Code AS [CultureCode],
c.CultureId AS [CultureId],
rv.Value
FROM dbo.LangData rv WITH (NOLOCK)
JOIN #cultureHierarchy c ON rv.CultureId = c.CultureId
END
but it is not working.
Is someone have any Idea regarding same.
Solution using Itzik Ben-Gan's hierarchy model. If you can extend the dbo.Cultures table with Hierarchy, Lvl and Root columns and index on Hierarchy, query will be faster. It has to be rewrited in that case though.
drop table if exists dbo.Cultures;
create table dbo.Cultures (
ID int
, Code varchar(50)
, ParentID int
);
insert into dbo.Cultures (ID, Code, ParentID)
values (1, 'en-US', null), (2, 'en-IN', 1), (3, 'de-DE', 2), (4, 'hi-HI', 2)
drop table if exists dbo.LangData;
create table dbo.LangData (
KeyName varchar(100)
, CultureID int
, Value varchar(100)
);
insert into dbo.LangData (KeyName, CultureID, Value)
values ('lblColourName', 1, 'Color'), ('lblColourName', 2, 'Colour-IN');
with cteCultures as (
select
c.ID, c.Code, c.ParentID, 0 as Lvl
, convert(varchar(max), '.' + CONVERT(varchar(50), c.ID) + '.') as Hierarchy
, c.ID as Root
from dbo.Cultures c
where c.ParentID is null
union all
select
c.ID, c.Code, c.ParentID, cc.Lvl + 1 as Lvl
, cc.Hierarchy + convert(varchar(50), c.ID) + '.' as Hierarchy
, cc.Root as Root
from dbo.Cultures c
inner join cteCultures cc on c.ParentID = cc.ID
)
select
ccr.ID
, ccr.Code
, coalesce(ld.Value, ld2.Value) as Value
from cteCultures ccr
left join dbo.LangData ld on ccr.ID = ld.CultureID
outer apply (
select
top (1) tcc.ID
from cteCultures tcc
inner join dbo.LangData tld on tcc.ID = tld.CultureID
where ld.KeyName is null
and ccr.Hierarchy like tcc.Hierarchy + '%'
and ccr.Hierarchy <> tcc.Hierarchy
order by tcc.Lvl desc
) tt
left join dbo.LangData ld2 on tt.ID = ld2.CultureID
If I understand your question:
We just build your hierarchy (SEQ and Lvl are optional) and then perform TWO left joins in concert with a Coalesce().
Example
Declare #Cultures table (id int,ParentId int,Code varchar(50))
Insert into #Cultures values
( 1, NULL,'en-US')
,( 2, 1 ,'en-IN')
,( 3, 2 ,'de-DE')
,( 4, 2 ,'hi-HI')
Declare #LangData table (keyName varchar(50),CultureId int,Value varchar(50))
Insert Into #LangData values
('lblColourName',1,'Color')
,('lblColourName',2,'Color_IN')
;with cteP as (
Select Seq = cast(10000+Row_Number() over (Order by Code) as varchar(500))
,ID
,ParentId
,Lvl=1
,Code
From #Cultures
Where ParentId is null
Union All
Select Seq = cast(concat(p.Seq,'.',10000+Row_Number() over (Order by r.Code)) as varchar(500))
,r.ID
,r.ParentId
,p.Lvl+1
,r.Code
From #Cultures r
Join cteP p on r.ParentId = p.ID)
Select CultureId = A.ID
,A.Code
,Value = Coalesce(C.Value,B.Value)
From cteP A
Left Join #LangData B on (A.ParentId=B.CultureId)
Left Join #LangData C on (A.Id=C.CultureId)
Order By Seq
Returns
CultureId Code Value
1 en-US Color
2 en-IN Color_IN
3 de-DE Color_IN
4 hi-HI Color_IN

SQL Server CTE: How to select the lowest level only

I want to select/display the lowest level of the CTE only. Please help. I am using SQL Server 2016.
Create Table Location
(
Id int
Name varchar(20)
Parent int
)
Insert into location
values (1, Location1, null), (2, Location1child, 1),
(3, Location1grandchild, 2), (4, Location2, null),
(5, Location3, null), (6, Locationchild3, 5)
I need to display only records 3, 4, 6 which is the lowest level.
Update: I already created the query, but record number 4 didn't display. I am expecting record number 4 to be displayed because the record is the lowest level in the group.
With CTE (id, cte_level, cte_name, cte_longname) as
(
Select
A.ID, 1,
cast(A.name as varchar(max)),
cast(A.name as varchar(max))
from
Location A
Union All
Select
A.ID, cte_level + 1,
replicate(' ยท ' , cte_level ) + cast(A.name as varchar(max)),
cte.cte_longname + ' . ' + cast(A.name as varchar(max))
from
Location A
inner join
CTE ON A.Parent = CTE.id
)
select
CTE_2.id,
CTE_2.cte_longname [name]
--, A.cte_name [name]
from
CTE as CTE_1
inner join
CTE as CTE_2 on CTE_1.id = cte_2.id
where
CTE_1.cte_level = 1
And CTE_2.cte_level = (Select MAX(CTE.cte_level) From CTE)
order by
cte_2.cte_longname
It has nothing to do with CTE. Just use LEFT OUTER JOIN with IS NULL check.
SELECT P.*
FROM Location P
LEFT OUTER JOIN Location C ON P.Id = C.Parent
WHERE C.Id IS NULL;

SQL return only distinct IDs from LEFT JOIN

I've inherited some fun SQL and am trying to figure out how to how to eliminate rows with duplicate IDs. Our indexes are stored in a somewhat columnar format and then we pivot all the rows into one with the values as different columns.
The below sample returns three rows of unique data, but the IDs are duplicated. I need just two rows with unique IDs (and the other columns that go along with it). I know I'll be losing some data, but I just need one matching row per ID to the query (first, top, oldest, newest, whatever).
I've tried using DISTINCT, GROUP BY, and ROW_NUMBER, but I keep getting the syntax wrong, or using them in the wrong place.
I'm also open to rewriting the query completely in a way that is reusable as I currently have to generate this on the fly (cardtypes and cardindexes are user defined) and would love to be able to create a stored procedure. Thanks in advance!
declare #cardtypes table ([ID] int, [Name] nvarchar(50))
declare #cards table ([ID] int, [CardTypeID] int, [Name] nvarchar(50))
declare #cardindexes table ([ID] int, [CardID] int, [IndexType] int, [StringVal] nvarchar(255), [DateVal] datetime)
INSERT INTO #cardtypes VALUES (1, 'Funny Cards')
INSERT INTO #cardtypes VALUES (2, 'Sad Cards')
INSERT INTO #cards VALUES (1, 1, 'Bunnies')
INSERT INTO #cards VALUES (2, 1, 'Dogs')
INSERT INTO #cards VALUES (3, 1, 'Cat')
INSERT INTO #cards VALUES (4, 1, 'Cat2')
INSERT INTO #cardindexes VALUES (1, 1, 1, 'Bunnies', null)
INSERT INTO #cardindexes VALUES (2, 1, 1, 'playing', null)
INSERT INTO #cardindexes VALUES (3, 1, 2, null, '2014-09-21')
INSERT INTO #cardindexes VALUES (4, 2, 1, 'Dogs', null)
INSERT INTO #cardindexes VALUES (5, 2, 1, 'playing', null)
INSERT INTO #cardindexes VALUES (6, 2, 1, 'poker', null)
INSERT INTO #cardindexes VALUES (7, 2, 2, null, '2014-09-22')
SELECT TOP(100)
[ID] = c.[ID],
[Name] = c.[Name],
[Keyword] = [colKeyword].[StringVal],
[DateAdded] = [colDateAdded].[DateVal]
FROM #cards AS c
LEFT JOIN #cardindexes AS [colKeyword] ON [colKeyword].[CardID] = c.ID AND [colKeyword].[IndexType] = 1
LEFT JOIN #cardindexes AS [colDateAdded] ON [colDateAdded].[CardID] = c.ID AND [colDateAdded].[IndexType] = 2
WHERE [colKeyword].[StringVal] LIKE 'p%' AND c.[CardTypeID] = 1
ORDER BY [DateAdded]
Edit:
While both solutions are valid, I ended up using the MAX() solution from #popovitsj as it was easier to implement. The issue of data coming from multiple rows doesn't really factor in for me as all rows are essentially part of the same record. I will most likely use both solutions depending on my needs.
Here's my updated query (as it didn't quite match the answer):
SELECT TOP(100)
[ID] = c.[ID],
[Name] = MAX(c.[Name]),
[Keyword] = MAX([colKeyword].[StringVal]),
[DateAdded] = MAX([colDateAdded].[DateVal])
FROM #cards AS c
LEFT JOIN #cardindexes AS [colKeyword] ON [colKeyword].[CardID] = c.ID AND [colKeyword].[IndexType] = 1
LEFT JOIN #cardindexes AS [colDateAdded] ON [colDateAdded].[CardID] = c.ID AND [colDateAdded].[IndexType] = 2
WHERE [colKeyword].[StringVal] LIKE 'p%' AND c.[CardTypeID] = 1
GROUP BY c.ID
ORDER BY [DateAdded]
You could use MAX or MIN to 'decide' on what to display for the other columns in the rows that are duplicate.
SELECT ID, MAX(Name), MAX(Keyword), MAX(DateAdded)
(...)
GROUP BY ID;
using row number windowed function along with a CTE will do this pretty well. For example:
;With preResult AS (
SELECT TOP(100)
[ID] = c.[ID],
[Name] = c.[Name],
[Keyword] = [colKeyword].[StringVal],
[DateAdded] = [colDateAdded].[DateVal],
ROW_NUMBER()OVER(PARTITION BY c.ID ORDER BY [colDateAdded].[DateVal]) rn
FROM #cards AS c
LEFT JOIN #cardindexes AS [colKeyword] ON [colKeyword].[CardID] = c.ID AND [colKeyword].[IndexType] = 1
LEFT JOIN #cardindexes AS [colDateAdded] ON [colDateAdded].[CardID] = c.ID AND [colDateAdded].[IndexType] = 2
WHERE [colKeyword].[StringVal] LIKE 'p%' AND c.[CardTypeID] = 1
ORDER BY [DateAdded]
)
SELECT * from preResult WHERE rn = 1

SQL - Ordering by multiple criteria

I have a table of categories. Each category can either be a root level category (parent is NULL), or have a parent which is a root level category. There can't be more than one level of nesting.
I have the following table structure:
Categories Table Structure http://img16.imageshack.us/img16/8569/categoriesi.png
Is there any way I could use a query which produced the following output:
Free Stuff
Hardware
Movies
CatA
CatB
CatC
Software
Apples
CatD
CatE
So the results are ordered by top level category, then after each top level category, subcategories of that category are listed?
It's not really ordering by Parent or Name, but a combo of the two. I'm using SQL Server.
It seems to me like you are looking to flatten and order your hierarchy, the cheapest way to get this ordering would be to store an additional column in the table that has the full path.
So for example:
Name | Full Path
Free Stuff | Free Stuff
aa2 | Free Stuff - aa2
Once you store the full path, you can order on it.
If you only have a depth of one you can auto generate a string to this effect with a single subquery (and order on it), but this solution does not work that easily when it gets deep.
Another option, is to move this all over to a temp table and calculate the full path there, on demand. But it is fairly expensive.
You could make the table look at itself, ordering by the parent Name then the child Name.
select categories.Name AS DisplayName
from categories LEFT OUTER JOIN
categories AS parentTable ON categories.Parent = parentTable.ID
order by parentTable.Name, DisplayName
Ok, here we go :
with foo as
(
select 1 as id, null as parent, 'CatA' as cat from dual
union select 2, null, 'CatB' from dual
union select 3, null, 'CatC' from dual
union select 4, 1, 'SubCatA_1' from dual
union select 5, 1, 'SubCatA_2' from dual
union select 6, 2, 'SubCatB_1' from dual
union select 7, 2, 'SubCatB_2' from dual
)
select child.cat
from foo parent right outer join foo child on parent.id = child.parent
order by case when parent.id is not null then parent.cat else child.cat end,
case when parent.id is not null then 1 else 0 end
Result :
CatA
SubCatA_1
SubCatA_2
CatB
SubCatB_1
SubCatB_2
CatC
Edit - Solution change inspire from van's order by ! Much simpler that way.
Not entirely sure of your questions but it sounds like PARTITION BY might be useful for you. There's a good introductory post on PARTITION BY here.
Here you have a complete working example using a resursive common table expression.
DECLARE #categories TABLE
(
ID INT NOT NULL,
[Name] VARCHAR(50),
Parent INT NULL
);
INSERT INTO #categories VALUES (4, 'Free Stuff', NULL);
INSERT INTO #categories VALUES (1, 'Hardware', NULL);
INSERT INTO #categories VALUES (3, 'Movies', NULL);
INSERT INTO #categories VALUES (2, 'Software', NULL);
INSERT INTO #categories VALUES (10, 'a', 0);
INSERT INTO #categories VALUES (12, 'apples', 2);
INSERT INTO #categories VALUES (8, 'catD', 2);
INSERT INTO #categories VALUES (9, 'catE', 2);
INSERT INTO #categories VALUES (5, 'catA', 3);
INSERT INTO #categories VALUES (6, 'catB', 3);
INSERT INTO #categories VALUES (7, 'catC', 3);
INSERT INTO #categories VALUES (11, 'aa2', 4);
WITH categories(ID, Name, Parent, HierarchicalName)
AS
(
SELECT
c.ID
, c.[Name]
, c.Parent
, CAST(c.[Name] AS VARCHAR(200)) AS HierarchicalName
FROM #categories c
WHERE c.Parent IS NULL
UNION ALL
SELECT
c.ID
, c.[Name]
, c.Parent
, CAST(pc.HierarchicalName + c.[Name] AS VARCHAR(200))
FROM #categories c
JOIN categories pc ON c.Parent = pc.ID
)
SELECT c.*
FROM categories c
ORDER BY c.HierarchicalName
SELECT
ID,
Name,
Parent,
RIGHT(
'000000000000000' +
CASE WHEN Parent IS NULL
THEN CONVERT(VARCHAR, Id)
ELSE CONVERT(VARCHAR, Parent)
END, 15
)
+ '_' + CASE WHEN Parent IS NULL THEN '0' ELSE '1' END
+ '_' + Name
FROM
categories
ORDER BY
4
The long padding is to account for the fact that SQL Server's INT data type goes from 2,147,483,648 through 2,147,483,647.
You can ORDER BY the expression directly, no need to use ORDER BY 4. It was just to show what it is sorting on.
It is worth noting that this expression cannot use any index. This means sorting a large table will be slow.