Having a tree structure in database (id, parentId, order) where order means some value to sort per parent (it means order in parent list), how one can build full-SQL query to traverse this table in POST-ORDER?
What is post order is described on wiki, though only for binary trees - https://en.wikipedia.org/wiki/Tree_traversal
Not all of them is applyable to custom tree (for example IN-ORDER), but POST-ORDER is actully applyable:
A
/ \
B C
/|\
D E F
output will be:
B D E F C A
The same in SQL data table:
|Id |ParentId | Order
|___|_________|______
|A |null |0
|B |A |0
|C |A |1
|D |C |0
|E |C |1
|F |C |2
I have been struggling with it quite a time, but looks like CTE doesn't allow inner ORDER BY clause (omg, why?!), so this task becomes impossible at my current level without stored procedures.
More as a proof of concept than a usable answer, here's a CTE-based version. It uses STRING_AGG to concatenate the children of each node in order, then recursively replaces each node with its children to build the output string - this means it wouldn't work in situations where node keys are substrings of one another.
DECLARE #t TABLE
(id CHAR(1) NOT NULL PRIMARY KEY,
parentid CHAR(1) NULL,
roworder TINYINT NOT NULL
)
INSERT #t (id, parentid, roworder)
VALUES('A', NULL, 0),
('B','A',0),
('C','A',1),
('D','C',0),
('E','C',1),
('F','C',2),
('G','E',0),-- two extra nodes to prove that this isn't a one-off
('H','E',1)
;WITH aggCTE
AS
(
SELECT parentid, STRING_AGG(CONVERT(VARCHAR(MAX), id), ' ') WITHIN GROUP (ORDER BY Roworder) AS children
FROM #t
GROUP BY parentid
)
,recCTE
AS
(
SELECT a.parentid,
a.children,
CAST(ISNULL(a.parentid,'') AS VARCHAR(MAX)) AS processed, --to prevent loops
0 AS seq, --to pick the right output row
a.children AS firstnode --to disambiguate if the data contains multiple trees
FROM aggCTE AS a
WHERE a.parentid IS NULL
UNION ALL
SELECT a.parentid,
REPLACE(a.children, b.parentid, CONCAT(b.children, ' ', b.parentid)) AS children,
CONCAT(a.processed, b.parentid) AS processed,
a.seq + 1 AS seq,
a.firstnode
FROM recCTE AS a
JOIN aggCTE AS b
ON CHARINDEX(b.parentid, a.children) > 0
AND CHARINDEX(b.parentid, a.processed) = 0
)
,rnCTE
AS
(
SELECT children,
ROW_NUMBER() OVER (PARTITION BY firstnode ORDER BY seq DESC) AS rn
FROM recCTE
)
SELECT children AS post_order_traversal
FROM rnCTE
WHERE rn = 1
Found solution by simply implementing it as is:
If(OBJECT_ID('tempdb..#result') Is Not Null) Drop Table #result;
If(OBJECT_ID('tempdb..#stack') Is Not Null) Drop Table #stack;
create table #result (id int not null identity primary key, [value] bigint null);
create table #stack (id int not null identity primary key, [value] bigint null);
INSERT INTO #stack values(null) --inserting root identifiers here
WHILE EXISTS (SELECT * FROM #stack)
BEGIN
declare #stack_id int, #stack_value bigint
select top 1 #stack_id=id, #stack_value=value from #stack order by id desc
delete from #stack where id=#stack_id
INSERT INTO #stack
-- here comes our query, which should fetch children of specified id and order it
select tos.id
from inputTable as t
where (ISNULL(#stack_value, 0) = 0 AND t.ParentId IS NULL) OR (ISNULL(#stack_value, 0) != 0 AND t.ParentId = #stack_value)
order by t.[order] asc
insert into #result values(#stack_value)
END
select [value] from #result order by id desc
If(OBJECT_ID('tempdb..#result') Is Not Null) Drop Table #result;
If(OBJECT_ID('tempdb..#stack') Is Not Null) Drop Table #stack;
As of now, it seems impossible with usage of CTE.
Related
Objective: I need to fully populate a table with a matrix of values for each column by [PropertyId] grouping. Several [PropertyId] have all the necessary values for each column (Table 1), however, many are missing some values (Table 2). Furthermore, not every [PropertyId] needs these values as they have completely different regional values. Therefore, I need to identify which [PropertyId] both need the values populated and don't have all the necessary values.
Examples:
Table 1. Each identified [PropertyId] grouping should have 23 distinct records for these four columns [ReportingVolumeSettingId],[SpeciesGroupInventoryID],[CropCategoryID],[SortOrder].
Table 2. Here is an example of a PropertyID that is missing a value combination as it only has 22 records:
Both of these example results were queried from the same table [ReportingVolume]. I have not been successful in even identifying which record combination per [PropertyID] are missing. I would like to identify each missing record combination and then insert that record combination into the [ReportingVolume] table.
Problem to Solve -- The SQL Code below is my attempt to 1. Identify the correct List of Values; 2. Identify which properties should have matching values; 3. Identify which properties are missing values; 4. Identify the missing values per property.
;with CORRECT_LIST as
(
select
SpeciesGroupInventoryName, SpeciesGroupInventoryId, CropCategoryName,CropCategoryID, UnitOfMeasure, SortOrder
--*
from [GIS].[RST].[vPropertyDefaultTimberProductAndUnitOfMeasure]
where PropertyId in (1)
)
,
property_list as
(
select distinct rvs.propertyid as Volume_Property, pd.PropertyName, pd.PropertyId from RMS.GIS.ReportingVolumeSetting rvs
right outer JOIN RMS.GIS.PropertyDetail AS pd ON rvs.PropertyId = pd.PropertyId
left outer JOIN RMS.GIS.SpeciesGroupInventory AS sgi ON rvs.SpeciesGroupInventoryId = sgi.SpeciesGroupInventoryId
where sgi.SpeciesGroupInventoryId in (1,2,3)
or pd.PropertyId = 171
)
, Partial_LISTS as
(
select Count(distinct ReportingVolumeSettingId) as CNT_REPORT, pd.PropertyName, pd.PropertyId
from [GIS].[ReportingVolumeSetting] rvs
right outer JOIN property_list AS pd ON rvs.PropertyId = pd.PropertyId
group by pd.propertyId, pd.PropertyName
)
, Add_Props as
(
select propertyName, propertyId, SUM(CNT_REPORT) as CNT_RECORDS from Partial_LISTS
where CNT_REPORT < 23
group by propertyName, propertyId
)
, RVS_RECORDS_PROPS as
(
select addProps.PropertyName, rvs.* from [GIS].[ReportingVolumeSetting] rvs
join Add_Props addProps on addprops.PropertyId = rvs.PropertyID
where rvs.PropertyId in (select PropertyId from Add_Props)
)
select rp.PropertyName, cl.*, rp.SpeciesGroupInventoryId from correct_list cl
left outer join RVS_Records_Props rp
on rp.SpeciesGroupInventoryId = cl.SpeciesGroupInventoryId
and rp.CropCategoryId = cl.CropCategoryID
and rp.SortOrder = cl.SortOrder
Order by rp.PropertyName
How can I modify the code or create a new code block identifies the missing values and inserts them into the table per PropertyId?
I am using SQL SMSS v15.
Thanks so much.
This should identify missing entries. You could simply add an INSERT INTO command on top of this. Keep in mind as the ReportingVolumeSettingId is unique and unknown it's not covered here.
SELECT * FROM (SELECT DISTINCT PropertyId FROM ReportingVolume) rv
CROSS APPLY
(
SELECT DISTINCT SpeciesGroupInventoryId
, CropCategoryId
, SortOrder
FROM ReportingVolume
) x
EXCEPT
SELECT PropertyId, SpeciesGroupInventoryId, CropCategoryId, SortOrder FROM ReportingVolume
I don't have access to your data, so I cannot provide an example specific to your environment, but I can provide you a simple example using SQL Server's EXCEPT operator.
Run the following example in SSMS:
-- Create a list of required values.
DECLARE #Required TABLE ( required_id INT, required_val VARCHAR(50) );
INSERT INTO #Required ( required_id, required_val ) VALUES
( 1, 'Required value 1.' ), ( 2, 'Required value 2.' ), ( 3, 'Required value 3.' ), ( 4, 'Required value 4.' ), ( 5, 'Required value 5.' );
-- Create some sample data to compare against.
DECLARE #Data TABLE ( PropertyId INT, RequiredId INT );
INSERT INTO #Data ( PropertyId, RequiredId ) VALUES
( 1, 1 ), ( 1, 2 ), ( 1, 3 ), ( 2, 1 ), ( 2, 2 ), ( 2, 4 ), ( 2, 5 );
-- Set a property id value to query.
DECLARE #PropertyId INT = 1;
-- Preview #Data's rows for the specified #PropertyId.
SELECT * FROM #Data WHERE PropertyId = #PropertyId ORDER BY PropertyId, RequiredId;
At this point, I've created a list of required values (required_id 1 through 5) and some dummy data to check them against. This initial SELECT shows the current resultset for the specified #PropertyID:
+------------+------------+
| PropertyId | RequiredId |
+------------+------------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
+------------+------------+
You can see that required_id values 4 and 5 are missing for the current property. Next, we can compare #Required against #Data and INSERT any missing required values using the EXCEPT operator and then return the corrected resultset.
-- Insert any missing required values for #PropertyId.
INSERT INTO #Data ( PropertyId, RequiredId )
SELECT #PropertyId, required_id FROM #Required
EXCEPT
SELECT PropertyId, RequiredId FROM #Data WHERE PropertyId = #PropertyId;
-- Preview #Data's revised rows for #PropertyId.
SELECT * FROM #Data WHERE PropertyId = #PropertyId ORDER BY PropertyId, RequiredId;
The updated resultset now looks like the following:
+------------+------------+
| PropertyId | RequiredId |
+------------+------------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 1 | 4 |
| 1 | 5 |
+------------+------------+
You can run this again against #PropertyId = 2 to see a different scenario.
Note the order to using EXCEPT. The required rows comes first, followed by the EXCEPT operator, and then the current rows to be validated. This is important. EXCEPT is saying show me rows from #Required that are not in #Data--which allows for the inserting of any missing required values into #Data.
I know this example doesn't represent your existing data with the 23 rows requirement, but hopefully, it will get you moving with a solution for your needs. You can read more about the EXCEPT operator here.
Here is the complete SSMS example:
-- Create a list of required values.
DECLARE #Required TABLE ( required_id INT, required_val VARCHAR(50) );
INSERT INTO #Required ( required_id, required_val ) VALUES
( 1, 'Required value 1.' ), ( 2, 'Required value 2.' ), ( 3, 'Required value 3.' ), ( 4, 'Required value 4.' ), ( 5, 'Required value 5.' );
-- Create some sample data to compare against.
DECLARE #Data TABLE ( PropertyId INT, RequiredId INT );
INSERT INTO #Data ( PropertyId, RequiredId ) VALUES
( 1, 1 ), ( 1, 2 ), ( 1, 3 ), ( 2, 1 ), ( 2, 2 ), ( 2, 4 ), ( 2, 5 );
-- Set a property id value to query.
DECLARE #PropertyId INT = 1;
-- Preview #Data's rows for the specified #PropertyId.
SELECT * FROM #Data WHERE PropertyId = #PropertyId ORDER BY PropertyId, RequiredId;
-- Insert any missing required values for #PropertyId.
INSERT INTO #Data ( PropertyId, RequiredId )
SELECT #PropertyId, required_id FROM #Required
EXCEPT
SELECT PropertyId, RequiredId FROM #Data WHERE PropertyId = #PropertyId;
-- Preview #Data's revised rows for #PropertyId.
SELECT * FROM #Data WHERE PropertyId = #PropertyId ORDER BY PropertyId, RequiredId;
The table looks like below.
DROP TABLE #TEMP
CREATE TABLE #TEMP
(
UVRID VARCHAR(20),
DynamoNo INT,
FREQHZ INT
)
INSERT #TEMP
SELECT '15AL78',100,10 UNION ALL
SELECT '15AL78',110,20 UNION ALL
SELECT '257T13',100,10 UNION ALL
SELECT '257T13',110,20 UNION ALL
SELECT '257T13',931,30
I am trying to make 1 new column say SuprerFrez whose value is depends on column FREQHZ.
For every UVRID group 2nd value FREQHZ will be 1st value of SuprerFrez
and for last FREQHZ, SuprerFrez value will be zero.
Expected output with 1 new column whose value depends upon FREQHZ column. Order by FREQHZ ASC
UVRID |DynamoNo|FREQHZ|SuprerFrez
'15AL78'|100 |10 |20
'15AL78'|110 |20 |0
'257T13'|100 |10 |20
'257T13'|110 |20 |30
'257T13'|931 |30 |0
You are looking for lead():
select t.*,
lead(FREQhz, 1, 0) over (partition by UVRID order by DynamoNo) as SuprerFrez
from #temp t;
Note this assumes that the ordering is by DynamoNo. If that is not the ordering you have in mind, then you need another column that specifies the ordering. For instance, if you wanted "insert" order, you could use an identity column:
CREATE TABLE #TEMP (
TempID INT IDENTITY(1, 1) PRIMARY KEY,
UVRID VARCHAR(20),
DynamoNo INT,
FREQHZ INT
);
Then the code would look like:
select t.*,
lead(FREQhz, 1, 0) over (partition by UVRID order by TempID) as SuprerFrez
from #temp t;
I have a table say A which has some rows and 'Id' column as a primary key. And other table B and it has the 'TabAId' and references the table A id column.
Want to get a report like shown in the attached image.
Explanation
I 'sql server' database
select One row from the Table A and check the Id in the Table B, If exists then add table B row as next row(If multiple rows are there then also add those number of rows as next rows) else go further.
Tried with case statement which appends to the row, not adds as next row.
With join also happens same only.
It may be easy through programming language like php or scripting like jquery and ajax but I want it through sql server only. It helps me for the further requirements.
So please someone help me.
Edited:
create table tabA(id int not null primary key,
name varchar(20) null,age int null)
insert into tabA values(1,'Sudeep',35),
(2,'Darshan',34)
create table tabB(A_id int not null,nickname varchar(20) null )
insert into tabB values(1,'Kiccha'),
(1,'Nalla'),
(2,'Boss')
output should be like below
Id | name | age |
------------------------
1 | Sudeep | 35 |
------------------------
| *Kichha | |
------------------------
| *Nalla | |
------------------------
2 | Darshan | 34 |
------------------------
| *Boss | |
------------------------
based on the requirement i have done the following.
;
WITH cte
AS (
SELECT *
,DENSE_RANK() OVER (
ORDER BY id
) dn
,ROW_NUMBER() OVER (
PARTITION BY id ORDER BY age DESC
) rn
FROM (
SELECT *
FROM tabA a
UNION ALL
SELECT *
,NULL
FROM tabB b where exists (select 1 from taba a where a.id=b.A_id)
) a
)
SELECT iif(rn = 1, cast(id AS VARCHAR(50)), '') ID
,CONCAT (
iif(rn = 1, '', '*')
,name
) NAME
,iif(rn = 1, cast(age AS VARCHAR(50)), '') AGE
FROM cte
please let me know if anything needs to be added
Edit: as requested display the results based on the offset
if OBJECT_ID('tempdb..#cte_results') is not null
drop table #cte_results
/*
in order to achieve the second goal we need to store in results in a table then use that table to display
results
*/
;
WITH cte
AS (
SELECT *
,DENSE_RANK() OVER (
ORDER BY id
) dn
,ROW_NUMBER() OVER (
PARTITION BY id ORDER BY age DESC
) rn
,ROW_NUMBER() over( order by id asc,age desc) off_set
FROM (
SELECT *
FROM tabA a
UNION ALL
SELECT *
,NULL
FROM tabB b where exists (select 1 from taba a where a.id=b.A_id)
) a
)
SELECT iif(rn = 1, cast(id AS VARCHAR(50)), '') ID
,CONCAT (
iif(rn = 1, '', '*')
,name
) NAME
,iif(rn = 1, cast(age AS VARCHAR(50)), '') AGE,off_set,rn,max(rn) over(partition by id) max_rn,id idrrr
into #cte_results
FROM cte
/*
the following query is used to display the results in the screen dynamically
*/
declare #pre_offset int=0, #post_offset int =2
set #post_offset=( select top 1 max(max_rn)-max(rn)
from #cte_results
where off_Set
between #pre_offset and #post_offset
group by idrrr
order by idrrr desc
)+#post_offset
select id,name,age from #cte_results
where off_Set
between #pre_offset and #post_offset
Results were as follows
I am wanting to create a query that allows me to look up a number that's presented in another table before I add that ID's name to the needed table being called by the query.
An example:
Table1
ID |name |nameID |network |...
----------------------------------
5 |Bob |4 |NPOW |...
6 |Billy |8 |BGER |...
Table2
ID |name |nameID |network |...
----------------------------------
3 |Stan |2 |ERFDT |...
Table3
ID |name |nameID |network |...
----------------------------------
3 |Steve |3 |FRVS |...
Table4 (The table I am inserting the new rows below into)
ID |name |nameID |network |...
----------------------------------
On my page these are the new values I need to add to the table4 above:
Name |ID |network |...
------------------------
Nick |4 |RFGH |...
Tony |3 |ESLO |...
James |2 |HUII |...
Rusty |3 |ERNM |...
Now what I am wanting to do is see what column ID equals X and place that name into Table4.
So doing that the outcome for table4 should look like this after the insert:
ID |name |nameFromID |network |...
--------------------------------------
1 |Nick |Bob |RFGH |...
2 |Tony |Steve |ESLO |...
3 |James |Stan |HUII |...
4 |Rusty |Steve |ERNM |...
So what would the INSERT INTO Table4 query look like for doing this?
You can do this with a view, some joins, and a temp table or some other staging table with session scoping. Here's a temp table approach.
The view to bring all the different tables into a single queryable form:
CREATE VIEW vAllNames
AS
SELECT [Name], NameID FROM Table1 UNION
SELECT [Name], NameID FROM Table2 UNION
SELECT [Name], NameID FROM Table3
The above assumes that NameId cannot be duplicated across these tables, so I've used UNION instead of UNION ALL. Otherwise you'll need to tell us how you want the name to resolve when the id matches successfully to multiple tables. Would you want multiple records inserted into table 4, only the first match, some kind of concatenation, etc.
The temp table so we have something to select from for the insertion unless you want to insert one at a time (you could also do this with a session scoped persistent table or something along those lines):
CREATE TABLE #TempDataToInsert
(
[Name] NVARCHAR(256),
ID INT,
network CHAR(4)
)
Once you've created those you can insert the data from your page directly into TempDataToInsert. Once your data is staged there you can run the final insert into:
INSERT INTO Table4
([Name], nameFromID, network) -- Assumes ID is an identity column
SELECT
tempData.[Name], allNames.[Name], tempData.network
FROM
#TempDataToInsert tempData
LEFT OUTER JOIN vAllNames allNames ON tempData.ID = allNames.NameID -- assumes no match is still valid for insertions
You'll need to clean up the temp table afterwards and I've made some assumptions about your schema but this should point you in the right direction.
CREATE TABLE #Temp0
(
ID INT,
[Name] VARCHAR(100),
NameId INT,
Network VARCHAR(100)
);
INSERT INTO #Temp0
(
ID,
Name,
NameId,
Network
)
VALUES
(5, 'Bob', 4, 'NPOW'),
(6, 'Billy', 8, 'BGER');
CREATE TABLE #Temp1
(
ID INT,
[Name] VARCHAR(100),
NameId INT,
Network VARCHAR(100)
);
INSERT INTO #Temp1
(
ID,
Name,
NameId,
Network
)
VALUES
(3, 'Stan', 2, 'ERFDT');
CREATE TABLE #Temp2
(
ID INT,
[Name] VARCHAR(100),
NameId INT,
Network VARCHAR(100)
);
INSERT INTO #Temp2
(
ID,
Name,
NameId,
Network
)
VALUES
(3, 'Steve', 3, 'FRVS');
SELECT *
INTO #Temp
FROM
(
SELECT [Name],
NameId
FROM #Temp0
UNION
SELECT [Name],
NameId
FROM #Temp1
UNION
SELECT [Name],
NameId
FROM #Temp2
) AS Temp;
CREATE TABLE #Temp3
(
[Name] VARCHAR(100),
Id INT,
Network VARCHAR(100)
);
INSERT INTO #Temp3
(
Name,
Id,
Network
)
VALUES
('Nick', 4, 'RFGH'),
('Tony', 3, 'ESLO'),
('James', 2, 'HUII'),
('RUSTY', 3, 'ERNM');
CREATE TABLE #Table4
(
ID INT IDENTITY(1, 1),
[Name] VARCHAR(100),
NameFormId VARCHAR(100),
Network VARCHAR(100)
);
INSERT INTO #Table4
SELECT T2.Name,
T1.Name,
T2.Network
FROM #Temp T1
JOIN #Temp3 T2
ON T1.NameId = T2.Id;
SELECT *
FROM #Table4;
The following code demonstrates how to match data on NameId between tables. It assumes that the Names being added are unique. If there are multiple NameId matches between input tables then the earliest match is given preference. Multiple NameId matches within a single input table give preference to the lowest Id.
-- Sample data.
declare #Table1 as Table ( Id Int, Name VarChar(10), NameId Int, Network VarChar(10) );
insert into #Table1 ( Id, Name, NameId, Network ) values
( 5, 'Bob', 4, 'NPOW' ), ( 6, 'Billy', 8, 'BGER' );
select * from #Table1;
declare #Table2 as Table ( Id Int, Name VarChar(10), NameId Int, Network VarChar(10) );
insert into #Table2( Id, Name, NameId, Network ) values
( 3, 'Stan', 2, 'ERFDT' );
select * from #Table2;
declare #Table3 as Table ( Id Int, Name VarChar(10), NameId Int, Network VarChar(10) );
insert into #Table3 ( Id, Name, NameId, Network ) values
( 3, 'Steve', 3, 'FRVS' ),
( 4, 'Steven', 3, 'FRVS' ), -- Note duplicate NameId value.
( 9, 'Stanley', 2, 'VOOT' ); -- Note duplicate NameId value.
select * from #Table3;
declare #ValuesToAdd as Table ( Name VarChar(10), NameId Int, Network VarChar(10) );
insert into #ValuesToAdd ( Name, NameId, Network ) values
( 'Nick', 4, 'RFGH' ), ( 'Tony', 3, 'ESLO' ),
( 'James', 2, 'HUII' ), ( 'Rusty', 3, 'ERNM' );
select * from #ValuesToAdd;
-- Process the data.
declare #Table4 as Table ( Id Int Identity, Name VarChar(10), NameId Int,
NameFromId VarChar(10), Network VarChar(10) );
-- Demonstrate the matching.
select Name, NameId, NameFromId, Network
from (
select VTA.Name, VTA.NameId, N.Name as NameFromId, VTA.Network,
Row_Number() over ( partition by VTA.Name order by NameRank, Id ) as RN
from #ValuesToAdd as VTA inner join ( -- Use left outer join if there might not be a match on NameId .
select Id, Name, NameId, 1 as NameRank from #Table1 union all
select Id, Name, NameId, 2 from #Table2 union all
select Id, Name, NameId, 3 from #Table3 ) as N on N.NameId = VTA.NameId ) as PH
--where RN = 1; -- Pick the match from the earliest table.
-- Insert the results.
insert into #Table4
select Name, NameId, NameFromId, Network
from (
select VTA.Name, VTA.NameId, N.Name as NameFromId, VTA.Network,
Row_Number() over ( partition by VTA.Name order by NameRank, Id ) as RN
from #ValuesToAdd as VTA inner join ( -- Use left outer join if there might not be a match on NameId .
select Id, Name, NameId, 1 as NameRank from #Table1 union all
select Id, Name, NameId, 2 from #Table2 union all
select Id, Name, NameId, 3 from #Table3 ) as N on N.NameId = VTA.NameId ) as PH
where RN = 1; -- Pick the match from the earliest table.
select * from #Table4;
We have these tables
CREATE TABLE tbl01
(
[id] int NOT NULL PRIMARY KEY,
[name] nvarchar(50) NOT NULL
)
CREATE TABLE tbl02
(
[subId] int NOT NULL PRIMARY KEY ,
[id] int NOT NULL REFERENCES tbl01(id),
[val] nvarchar(50) NULL,
[code] int NULL
)
If we run this query:
SELECT
tbl01.id, tbl01.name, tbl02.val, tbl02.code
FROM
tbl01
INNER JOIN
tbl02 ON tbl01.id = tbl02.id
we get these results:
-------------------------------
id | name | val | code
-------------------------------
1 | one | FirstVal | 1
1 | one | SecondVal | 2
2 | two | YourVal | 1
2 | two | OurVal | 2
3 | three | NotVal | 1
3 | three | ThisVal | 2
-------------------------------
You can see that each two rows are related to same "id"
The question is: we need for each id to retrieve one record with all val, each val will return in column according to the value of column code
if(code = 1) then val as val-1
else if (code = 2) then val as val-2
Like this:
-------------------------------
id | name | val-1 | val-2
-------------------------------
1 | one | FirstVal | SecondVal
2 | two | YourVal | OurVal
3 | three | NotVal | ThisVal
-------------------------------
Any advice?
Use can use MAX and Group By to achieve this
SELECT id,
name,
MAX([val1]) [val-1],
MAX([val2]) [val-2]
FROM ( SELECT tbl01.id, tbl01.name,
CASE code
WHEN 1 THEN tbl02.val
ELSE ''
END [val1],
CASE code
WHEN 2 THEN tbl02.val
ELSE ''
END [val2]
FROM tbl01
INNER JOIN tbl02 ON tbl01.id = tbl02.id
) Tbl
GROUP BY id, name
Is it the PIVOT operator (http://technet.microsoft.com/en-us/library/ms177410(v=sql.105).aspx) that you are looking for?
You've already got a few answers, but heres one using PIVOT as an alternative. The good thing is this approach is easy to scale if there are additional columns required later
-- SETUP TABLES
DECLARE #t1 TABLE (
[id] int NOT NULL PRIMARY KEY,
[name] nvarchar(50) NOT NULL
)
DECLARE #t2 TABLE(
[subId] int NOT NULL PRIMARY KEY ,
[id] int NOT NULL,
[val] nvarchar(50) NULL,
[code] int NULL
)
-- SAMPLE DATA
INSERT #t1 ( id, name )
VALUES ( 1, 'one'), (2, 'two'), (3, 'three')
INSERT #t2
( subId, id, val, code )
VALUES ( 1,1,'FirstVal', 1), ( 2,1,'SecondVal', 2)
,( 3,2,'YourVal', 1), ( 4,2,'OurVal', 2)
,( 5,3,'NotVal', 1), ( 6,3,'ThisVal', 2)
-- SELECT (using PIVOT)
SELECT id, name, [1] AS 'val-1', [2] AS 'val-2'
FROM
(
SELECT t2.id, t1.name, t2.val, t2.code
FROM #t1 AS t1 JOIN #t2 AS t2 ON t2.id = t1.id
) AS src
PIVOT
(
MIN(val)
FOR code IN ([1], [2])
) AS pvt
results:
id name val-1 val-2
---------------------------------
1 one FirstVal SecondVal
2 two YourVal OurVal
3 three NotVal ThisVal
If there are always only two values, you could join them or even easier, group them:
SELECT tbl01.id as id, Min(tbl01.name) as name, MIN(tbl02.val) as val-1, MAX(tbl02.val) as val-2
FROM tbl01
INNER JOIN tbl02 ON tbl01.id = tbl02.id
GROUP BY tbl02.id
note: this query will always put the lowest value in the first column and highest in the second, if this is not wanted: use the join query:
Join query
If you always want code 1 in the first column and code 2 in the second:
SELECT tbl01.id as id, tbl01.name as name, tbl02.val as val-1, tbl03.val as val-2
FROM tbl01
INNER JOIN tbl02 ON tbl01.id = tbl02.id
ON tbl02.code = 1
INNER JOIN tbl03 ON tbl01.id = tbl03.id
ON tbl03.code = 2
Variable amount of columns
You cannot get an variable amount of columns, only when you do this by building your query in code or t-sql stored procedures.
My advice:
If its always to values: join them in query, if not, let your server-side code transform the data. (or even better, find a way which makes it not nessecery to transform data)
Try this - it uses a pivot function but it also creates creates the dynamic columns dependent on code
DECLARE #ColumnString varchar(200)
DECLARE #sql varchar(1000)
CREATE TABLE #ColumnValue
(
Value varchar(500)
)
INSERT INTO #ColumnValue (Value)
SELECT DISTINCT '[' + 'value' + Convert(Varchar(20),ROW_NUMBER() Over(Partition by id Order by id )) + ']'
FROM Test
SELECT #ColumnString = COALESCE(#ColumnString + ',', '') + Value
FROM #ColumnValue
Drop table #ColumnValue
SET #sql =
'
SELECT *
FROM
(
SELECT
id,name,val,''value'' + Convert(Varchar(20),ROW_NUMBER() Over(Partition by id Order by id ))as [values]
FROM Test
) AS P
PIVOT
(
MAX(val) FOR [values] IN ('+#ColumnString+')
) AS pv
'
--print #sql
EXEC (#sql)