How to retrieve hierarchical data from a SQL Table? - sql-server

I have got the below stored procedure to return the list of Id, parentId and absoluteUrls which works fine:
ALTER PROCEDURE [dbo].[SearchDataManager.HierarchyById]
#currentId AS int
AS
BEGIN
DECLARE #id INT
DECLARE #parentId INT
DECLARE #absoluteUrl NVARCHAR(1000)
DECLARE #Hierarchy TABLE (Id int, ParentId int, AbsoluteUrl nvarchar(1000))
WHILE #currentId != 0
BEGIN
SELECT #id = Id, #parentId = ParentId, #absoluteUrl = AbsoluteUrl
FROM dbo.[SearchDataManager.NiceUrls]
WHERE id = #currentId
INSERT INTO #Hierarchy VALUES (#id, #parentId, #absoluteUrl)
SET #currentId = #parentId
END
SELECT * FROM #Hierarchy
END
The "NiceUrls" table has Id and ParentId. parentId refers to a record in the same table.
it returns like:
----------------------------------
Id | ParentId | AbsoluteUrl
----------------------------------
294 | 5 | url1
5 | 2 | url2
2 | 0 | url3
The above code works fine using a WHILE loop and defining a Table variable but I'm just wondering is there any better way to retrieve hierarchy data from a table?
The problem with the above code is maintainability. If I'd need to return 1 more column of the NiceUrls table then I'd have to define a new variable, add the column to the inline table, etc.
Is there any better way to rewrite the sp?
Thanks,
What's the

with Hierarchy (Id, ParentId, AbsoluteUrl, Level)
AS
(
-- anchor member
SELECT Id,
ParentId,
AbsoluteUrl,
0 AS Level
FROM dbo.[NiceUrls]
WHERE id = #currentId
UNION ALL
-- recursive members
SELECT su.Id,
su.ParentId,
su.AbsoluteUrl,
Level + 1 AS Level
FROM dbo.[NiceUrls] AS su
INNER JOIN Hierarchy ON Hierarchy.ParentId = su.Id
)
SELECT * FROM Hierarchy

Looks like you want all the records from the source table that are related to the original id.
1) Create a CTE that gives you all the ids (see the link Triple noted)
2) Join this CTE to the original table

Related

SQL Server query items with IN query but keep order

I have to query specific order of string IDs example data:
| ID | RES |
---------------
| A_12 | 1.89 |
| B_27 | 4.53 |
| B_28 | 1.02 |
| C_23 | 2.67 |
A tool generated a specific order which does not follow any standard ordering rule, and I cannot change that order.
I am getting ~20000 of these rows and the RES is misaligned.
I'd like to make a simple query which would collect all needed records by a list IDs and would give me a custom defined ordered list of results.
Something like:
SELECT RES FROM TABLE1 WHERE ID IN ('A_12', 'C_23', 'B_28', 'B_27')
and I'd lke it to return
1.89
2.67
1.02
4.53
I understand IN query would not follow order as under the hood it most likely gets translated to (ID = A OR ID = B OR ID = C) query.
How do I enforce the result of the IN query to maintain my defined order? Do I need to create a temp table with one column for maintaining order? Any good solutions?
Use JOIN instead of using IN and explicitly specify your order:
DECLARE #Test TABLE (
ID VARCHAR(32),
RES DECIMAL(5,2)
)
INSERT #Test (ID, RES)
VALUES
('A_12', 1.89),
('B_27', 4.53),
('B_28', 3.54),
('C_23', 2.67)
SELECT t.ID, t.RES
FROM #Test t
JOIN (
VALUES
('A_12', 1),
('C_23', 2),
('B_28', 3),
('B_27', 4)
) o(ID, OrderId) ON t.ID = o.ID
ORDER BY o.OrderId
Instead of temp table you can use values where you specify the desired order in the additional column, like this:
declare #table1 table(id varchar(10), res decimal(10,2));
insert into #table1 (id, res)
values
('A_12', 1.89),
('B_27', 4.53),
('B_28', 3.54),
('C_23', 2.67);
select t.*
from #table1 t
join (values(1, 'A_12'), (2, 'C_23'), (3, 'B_28'), (4, 'B_27')) v(id,val)
on t.id = v.val
order by v.id;
#Table1 here is a substitute of your physical Table1.
There is no order to keep.
Returns of a select are NOT ORDERED by SQL basic definition, UNLESS YOU DEFINE AN ORDER.
So, there is no order to keep. Period.
If you want to keep one, use a temporary table / table variable for the valeus in IN (and obviously a join) and order by an order you also keep in a second field in said variable.
And no, this is not new - SQL is based on the SET theorem ever since Cobb published his famous paper back in the 1960s or so and never had order in returned results outside of side effects of implementation.
Do I need to create a temp table with one column for maintaining order
This seems to be working:
create table #tmp
(
CustomOrder int,
ID varchar(100)
)
insert into #tmp values (1, 'A_12')
insert into #tmp values (2, 'C_23')
insert into #tmp values (3, 'B_28')
insert into #tmp values (4, 'B_27')
query:
SELECT RES FROM TABLE1 INNER JOIN #tmp ON TABLE1.ID = #tmp.ID WHERE TABLE1.ID IN ('A_12', 'C_23', 'B_28', 'B_27')
ORDER BY #tmp.CustomOrder
output:
1.89
2.67
1.02
4.53
Any better and easier solution?
Just a different approach:
SELECT RES FROM TABLE1 WHERE ID IN ('A_12')
UNION ALL
SELECT RES FROM TABLE1 WHERE ID IN ('C_23')
UNION ALL
SELECT RES FROM TABLE1 WHERE ID IN ('B_28')
UNION ALL
SELECT RES FROM TABLE1 WHERE ID IN ('B_27')
I supposed that the JOIN option is more efficent than this approach. If you want to automatize this option:
DROP TABLE #TABLE1
CREATE TABLE #TABLE1(ID NVARCHAR(4), RES FLOAT)
INSERT INTO #TABLE1 VALUES('A_12',1.89)
INSERT INTO #TABLE1 VALUES('B_27',4.53)
INSERT INTO #TABLE1 VALUES('B_28',1.02)
INSERT INTO #TABLE1 VALUES('C_23',2.67)
DECLARE #ID TABLE(ID NVARCHAR(4) not null);
--HERE HAVE TO INSERT IN ORDER YOU WANT TO RETURN THE RESULTS IN THE QUERY
insert into #ID VALUES('A_12')
insert into #ID VALUES('B_27')
insert into #ID VALUES('B_28')
insert into #ID VALUES('C_23')
DECLARE #UNIONALL NVARCHAR(10) = CHAR(13) + N'UNION ALL'
DECLARE #QUERY NVARCHAR(MAX) = NULL
DECLARE #ID_SEARCH NVARCHAR(4) = NULL
DECLARE C CURSOR FAST_FORWARD FOR SELECT ID FROM #ID
OPEN C
FETCH NEXT FROM C INTO #ID_SEARCH
SET #QUERY = N'SELECT RES FROM #TABLE1 WHERE ID = ''' + #ID_SEARCH + ''' '
FETCH NEXT FROM C INTO #ID_SEARCH
WHILE ##FETCH_STATUS = 0 BEGIN
SET #QUERY = #QUERY + #UNIONALL
SET #QUERY = #QUERY + N' SELECT RES FROM #TABLE1 WHERE ID = ''' + #ID_SEARCH + ''' '
FETCH NEXT FROM C INTO #ID_SEARCH
END
EXECUTE master..sp_executesql #QUERY

Second counter in SQL Server table

I need to have a table like this:
CLIENT_ID ID CONTENT
--------------------
1 1 abc
1 2 abc
1 3 abc
2 1 abc
2 2 abc
2 3 abc
client_id and id are the table's pk. on the application side, only client client_id and content are known, so I thought to have an after insert trigger that updates the id column as needed, but I'm having troubles. any help? Thanks.
My first attempt was:
CREATE TRIGGER test
ON [dbo].[table]
AFTER INSERT
AS
BEGIN
DECLARE #client_id INT;
DECLARE #id INT;
SELECT #client_id = client_id
FROM inserted;
SELECT #id = ISNULL(MAX(clifor_id), 0) + 1
FROM table
WHERE client_id = #client_id
UPDATE [dbo].[table]
SET [id] = #id
WHERE ?????
END
but don't know how to complete it.
ps: sorry for my English!
#Squirrel pointed you in good direction - instead of trigger is a way to go. S.t. like this one:
CREATE TRIGGER test
ON [dbo].[table]
INSTEAD OF INSERT
AS
BEGIN
UPDATE inserted
SET id = (select 1+max(id) from dbo.table where client_id = inserted.client_id)
INSERT INTO dbo.table
SELECT * FROM inserted --note - you should rewrite this block so it will be with explicit list of columns
END
Note that this will fail if you can insert multiple entries for the same client in one query.

count in dynamic name SQL Server

I create a stored procedure in SQL Server:
ALTER PROCEDURE [dbo].[testChildren]
#Parent INT
AS
BEGIN
SET NOCOUNT ON;
WITH EntityChildren AS
(
SELECT
nname, nodeid, level, ncode, nparent
FROM
GrpItm
WHERE
nodeid = #Parent
UNION ALL
SELECT
e.nname, e.nodeid, e.level, e.ncode, e.nparent
FROM
GrpItm e
INNER JOIN
EntityChildren e2 ON e.nparent = e2.nodeid
)
SELECT COUNT(Level) AS [level]
FROM EntityChildren
END
How can I make each count level in row and named the row by level value like this:
| level 1 | level 2 | level 3 |
+---------+---------+---------+
| 2 | 3 | 1 |
If you want something closer to...
level | count
-------+-------
1 | 2
2 | 3
3 | 1
Then it would just be something like...
SELECT
level,
COUNT(*) AS row_count
FROM
EntityChildren
GROUP BY
level
But that's simpler than what you're already accomplished, so I may be missing the point of your question and comment?
This is only regarding to #MatBailie answer. If you have a table like his, you can pivot your data and get it as you request.
Table Created with Levels and counts
You need a script to populate this table with the values you need
create table dbo.myt (levels int, counts int)
insert into dbo.myt
values
(1 , 2),
(2 , 3),
(3 , 1)
SQL Code
DECLARE #ALIASNAME nvarchar(50) = 'Levels'
DECLARE #Str NVARCHAR(MAX);
DECLARE #Str2 NVARCHAR(MAX);
SELECT #Str = STUFF(
(
SELECT DISTINCT
','+'[Levels'+cast(levels as nvarchar(50))+']'
FROM dbo.myt FOR XML PATH('')
), 1, 1, '');
PRINT #Str
SET #str2 = N'select * from (
select cast('''+#ALIASNAME+'''+ cast(Levels as nvarchar(50)) as nvarchar(50)) as Levels,counts,ROW_NUMBER() over(PARTITION by levels order by counts) as rn
from dbo.myt
)x
PIVOT
(Max(counts) FOR Levels in ('+#Str+')
) as p';
PRINT #Str2;
EXEC (#Str2);
Your Stored proc
ALTER PROCEDURE [dbo].[testChildren]
#Parent INT
AS
BEGIN
SET NOCOUNT ON;
WITH EntityChildren AS
(
SELECT
nname, nodeid, level, ncode, nparent
FROM
GrpItm
WHERE
nodeid = #Parent
UNION ALL
SELECT
e.nname, e.nodeid, e.level, e.ncode, e.nparent
FROM
GrpItm e
INNER JOIN
EntityChildren e2 ON e.nparent = e2.nodeid
)
INSERT INTO dbo.myt (Levels,Counts)
SELECT
level, COUNT(*) AS row_count
FROM
EntityChildren
GROUP BY level
/* INSERT PIVOT SCRIPT AND INSERT INTO A NEW TABLE */
END
Result

How to update N rows when N is in a from (select N from #myVar)

I'm developing this stored procedure on SQL Server 2012.
The stored procedure will update Quantity rows in EXTERNAL_CODES table for each row in #newBatches parameter. It's like a loop, I will need to create a new row in BATCHES table for each row in #newBatches parameter.
And then, I have to update Quantity rows in EXTERNAL_CODES table with each batchId created.
CREATE PROCEDURE [dbo].[CreateBatchAndKeepExternalCodes]
#newBatches as dbo.CreateBatchList READONLY,
#productId int
AS
set nocount on;
declare #lowestCodeLevel tinyint;
-- ======== VALIDATION ==========
if ((select count(name) from #newBatches) = 0)
return -112;
-- ====== CODE ========
-- Get lowest aggregation level.
set #lowestCodeLevel =
(select min(c.application_code)
from CHINA_CODES_HEADER c, PRODUCTS p
where p.Id = #productId and c.DRUG_TEN_SEATS = p.PRODUCT_CODE);
begin transaction;
insert into BATCHES (PRODUCT_ID, NAME, CREATED)
select #productId, Name, CAST(SYSDATETIMEOFFSET() as nvarchar(50))
from #newBatches;
update top(t.Quantity) EXTERNAL_CODES
set BATCH_ID = (select ID from BATCHES where NAME = t.Name)
, USED = 1
from (select Name, Quantity from #newBatches) t
where PRODUCT_ID = #productId and CODE_LEVEL = #lowestCodeLevel;
commit transaction;
RETURN 0
I get an error on this update:
update top(t.Quantity) EXTERNAL_CODES
set BATCH_ID = (select ID from BATCHES where NAME = t.Name)
, USED = 1
from (select Name, Quantity from #newBatches) t
where PRODUCT_ID = #productId and CODE_LEVEL = #lowestCodeLevel;
The error is here: update top(t.Quantity). It can't find t.Quantity.
dbo.CreateBatchList is:
CREATE TYPE [dbo].[CreateBatchList] AS TABLE
(
Name nVARCHAR(20),
Quantity int
)
My problem is that I can't set to update Quantity rows. Any idea?
The error (or warning) message is:
SQL71005: The reference to the column t.Quantity could not be resolved.
Maybe I could use MERGE.
Your update statement is quite confusing. If for instance #newBatches table has multiple lines, then you are saying, pick all the Quantity from #newBatches in Top?
Anyway, I think the solution is to use a loop to use each line from #newBatches to update. I have modified your code in order to test it on my side, and have replaced all the tables with Table Variables. You may find it helpful.
But still without any Order By clause and without knowing actual business logic, I can't say this solution is correct.
DECLARE #productID int;
DECLARE #lowestCodeLevel int;
DECLARE #EXTERNAL_CODES table(BATCH_ID varchar(100), USED bit, PRODUCT_ID int, CODE_LEVEL int);
DECLARE #BATCHES table(ID int, NAME varchar(100));
DECLARE #newBatches table(Name nVARCHAR(20), Quantity int);
-- we don't know at this point whether #newBatches has some column
-- through which we can uniquely identify a row
-- that is why we are creating this new table in which we have Row_ID column
-- through which we can extract each line
DECLARE #newBatchesWithRowID table(Row_ID int not null identity, Name nVarchar(20), Quantity int);
INSERT INTO #newBatchesWithRowID(Name, Quantity)
SELECT Name, Quantity
FROM #newBatches;
DECLARE #prvRow_ID int;
-- loop to iterate in #newBatchesWithRowID table
WHILE(1 = 1)
Begin
DECLARE #row_ID int = NULL;
DECLARE #Name varchar(100);
DECLARE #Quantity int;
SELECT TOP 1 #row_ID = Row_ID
, #Quantity = Quantity
, #Name = Name
FROM #newBatchesWithRowID
WHERE Row_ID > #prvRow_ID OR #prvRow_ID IS NULL
ORDER BY Row_ID;
If #row_ID IS NULL Break;
SET #prvRow_ID = #row_ID;
update top(#Quantity) #EXTERNAL_CODES
set BATCH_ID = (select ID from #BATCHES where NAME = #Name)
, USED = 1
where PRODUCT_ID = #productId and CODE_LEVEL = #lowestCodeLevel;
END

Is there anyway to reset the identity of a Table Variable?

Say I have a table variable:
DECLARE #MyTableVar TABLE (ID INT IDENTITY(1,1), SomeData NVARCHAR(300))
After I have inserted 250 rows, I need to "Start Over" with the table. I do this:
DELETE FROM #MyTableVar
Is there anything I can do to the table variable so that this:
insert into #MyTableVar Values("TestData")
select * from #MyTableVar
will return this:
_______________________________
| ID | SomeData |
|___________|_________________|
| | |
| 1 | TestData |
|___________|_________________|
instead of this:
_______________________________
| ID | SomeData |
|___________|_________________|
| | |
| 251 | TestData |
|___________|_________________|
Instead relying on an Identity, why not use the new ranking functions such as Row_Number
Insert #MyTableVar( Id, Value )
Select Row_Number() Over ( Order By Value )
, Value
From SomeOtherTable
Instead of re-seeding the IDENTITY, why not just delete from the #table variable, then use ROW_NUMBER() against the input? e.g. instead of the lazy
SELECT * FROM #MyTableVar;
...use...
SELECT ID = ROW_NUMBER() OVER (ORDER BY ID), SomeData FROM #MyTableVar;
Now you don't need to care what the seed is, whether it starts at 1, whether there are any gaps, etc.
unfortunately there is no function to reseed identity column in table variable, I know this question is very old, but in case other people encountered the same problem, I would like to share my method to solve this problem.
/* declare another table variable with same structure and perform select insert*/
DECLARE #MyTableVar1 TABLE (ID INT IDENTITY(1,1), SomeData NVARCHAR(300))
insert into #MyTableVar1
select someData from #MyTableVar
However, If you want to perform dynamic reseeding inside a loop, I would suggest using a table object
You can't reseed the identity value on a Table Variable but you can do the same thing with a Temp Table:
CREATE TABLE #TAB(ID INT IDENTITY,VALUE VARCHAR(10))
DECLARE #RESEED INT = 32
DBCC CHECKIDENT(#TAB,RESEED,#RESEED)
INSERT INTO #TAB
SELECT 'TEST'
SELECT * FROM #TAB
Since you are re-using your table, if I got it right, how about you do not initialize your counters to 1 and instead use this as an example?
DECLARE #ctr INT
IF #ctr IS NULL or #ctr <= 0 --this part is to control #ctr value on loops
SET #ctr = 1
ELSE
SELECT #ctr = MIN(id) FROM #tbl
This way, you are not restarting your loop to 1 nor is there a need for you to truncate the table.
Is it possible to have another int column on your table variable and update that column with modulo after the insert is finished?
declare #Mytablevar table
(
id int identity(1,1)
,id1 int
somedata nvarchar(300)
)
-- insert your data as you would. After insert is finished, do the following:
update #mytablevar set id1 = case when id > 250 then id % 250 else id end
I tried it on net but i am not able to get any solution on reset identity for table variable.
If you are able to use temp table #MyTableVar instead of table #MyTableVar variable then it is possible to reset identity value
DBCC CHECKIDENT('TableName', RESEED, NewValue)
DBCC CHECKIDENT(#MyTableVar, RESEED, 0)
Newvalue must be one less than the newIdentiyValue
NewValue= NewIdentity-1;
If you still want to learn more you can refer my blog
http://tryconcepts.blogspot.in/2012/08/reset-identity-column-to-new-id.html
I just had this idea and it works!!! :
declare #TableVariable table (
IdentityColumn int identity(1,1),
SomeOtherValue int,
DesiredResult int
)
declare #FirstIdentityValueEachTimeYouLoadDataToTable int
declare #Count int
set #Count = 1
while #Count <= 5
begin
delete #TableVariable
insert into #TableVariable (SomeOtherValue) select 45
insert into #TableVariable (SomeOtherValue) select 90
insert into #TableVariable (SomeOtherValue) select 2
select #FirstIdentityValueEachTimeYouLoadDataToTable = min(IdentityColumn) from #TableVariable
Update #TableVariable set DesiredResult = IdentityColumn - #FirstIdentityValueEachTimeYouLoadDataToTable + 1
select * from #TableVariable
set #Count = #Count + 1
end
Can you use temporary table?
This is a sample how to do this with a temp table.
CREATE TABLE #MyTableVar (ID INT IDENTITY(1,1), SomeData NVARCHAR(300))
insert #MyTableVar(SomeData) values ('test1'), ('test2')
---doesn't work
DELETE FROM #MyTableVar
insert #MyTableVar(SomeData) values ('test3'), ('test4')
select * from #MyTableVar
--resets the identity
truncate table #MyTableVar
insert #MyTableVar(SomeData) values ('test3'), ('test4')
select * from #MyTableVar
Regards
Piotr
you should truncate your table instead of deleting all rows from it.
but note that truncate will not work for some tables, (listed from MSDN):
You cannot use TRUNCATE TABLE on tables that:
Are referenced by a FOREIGN KEY constraint. (You can truncate a table that has a foreign key that references itself.)
Participate in an indexed view.
Are published by using transactional replication or merge replication.
and added myself:
You cannot truncate a table variable.
syntac of truncate is:
TRUNCATE TABLE
[ { database_name .[ schema_name ] . | schema_name . } ]
table_name
[ ; ]
EDIT: I didn't notice that you are questioning about table variables.
as far as I know there is no way to reset an identity column in a table variable. you can use a temp table instead.
DELETE FROM does not reset identity.
TRUNCATE does.
If you are using SQL Server then use this DBCC CHECKIDENT('Customer', RESEED, 0) Where Customer is a table name. When you insert records into table after this command your primery key column value will be start from 1 again.
Read this
http://codedotnets.blogspot.in/2012/09/how-to-reset-identity-in-sql-server.html

Resources