Is this select field really necessary if its not labeled/referenced? - sql

In our code stack I came across this stored proc:
ALTER PROCEDURE [dbo].[MoveNodes]
(
#id bigint,
#left bigint,
#right bigint,
#parentid bigint,
#offset bigint,
#caseid bigint,
#userid bigint
)
AS
BEGIN
WITH q AS
(
SELECT id, parent, lft, rgt, title, type, caseid, userid, 0 AS level,
CAST(LEFT(CAST(id AS VARCHAR) + REPLICATE('0', 10), 10) AS VARCHAR) AS bc
FROM [dbo].DM_FolderTree hc
WHERE id = #id and caseid = #caseid
UNION ALL
SELECT hc.id, hc.parent, hc.lft, hc.rgt, hc.title, hc.type, hc.caseid, hc.userid, level + 1,
CAST(bc + '.' + LEFT(CAST(hc.id AS VARCHAR) + REPLICATE('0', 10), 10) AS VARCHAR)
FROM q
JOIN [dbo].DM_FolderTree hc
ON hc.parent = q.id
)
UPDATE [dbo].DM_FolderTree
SET lft = ((-lft) + #offset), rgt = ((-rgt) + #offset), userid = #userid
WHERE id in (select id from q)
AND lft <= (-(#left)) AND rgt >= (-(#right))
AND caseid = #caseid;
UPDATE [dbo].DM_FolderTree
SET parent = #parentid, userid = #userid
WHERE id = #id AND caseid = #caseid;
END
Please, note the field labeled bc and how that variable is used below in the second CAST function, which we'll call for this discussion the unlabeled field, UF.
Two question pop out at me from the above.
UF is not used anywhere in the CTE, q, other than to select the field...does it serve any function?
Since bc is only used to generate UF, is bc necessary at all, either?

Looks like several fields are not being used.
I would question if the update is doing what is really needed or if the query could be simplified and give the same results in the fields you are actually using. There is no point in doing work you don't need to do. But only you can only know which one is wrong based on your local business requirements. All I can do is tell you that likely one of them is wrong.
If you want to check if your simplified version of the CTE give the same results in the fields you use, do this:
Declare #id bigint,
#left bigint,
#right bigint,
#parentid bigint,
#offset bigint,
#caseid bigint,
#userid bigint
-- then set the variables to typical values
WITH q AS
(
SELECT id, parent, lft, rgt, title, type, caseid, userid, 0 AS level,
CAST(LEFT(CAST(id AS VARCHAR) + REPLICATE('0', 10), 10) AS VARCHAR) AS bc
FROM [dbo].DM_FolderTree hc
WHERE id = #id and caseid = #caseid
UNION ALL
SELECT hc.id, hc.parent, hc.lft, hc.rgt, hc.title, hc.type, hc.caseid, hc.userid, level + 1,
CAST(bc + '.' + LEFT(CAST(hc.id AS VARCHAR) + REPLICATE('0', 10), 10) AS VARCHAR)
FROM q
JOIN [dbo].DM_FolderTree hc
ON hc.parent = q.id
)
SELECT dft.id, dft.lft, q.lft + #offset), dft.rgt,((-q.rgt) + #offset),
FROM [dbo].DM_FolderTree dft
JOIN q on q.id = dft.id
WHERE lft <= (-(#left))
AND rgt >= (-(#right))
AND caseid = #caseid;
WITH q2 AS
(
SELECT id, parent, lft, rgt
FROM [dbo].DM_FolderTree hc
WHERE id = #id and caseid = #caseid
UNION ALL
SELECT hc.id, hc.parent, hc.lft, hc.rgt,
FROM q2
JOIN [dbo].DM_FolderTree hc
ON hc.parent = q2.id
)
SELECT dft.Id, dft.lft, q2.lft + #offset), dft.rgt,((-q2.rgt) + #offset),
FROM [dbo].DM_FolderTree dft
JOIN q2 on q2.id = dft.id
WHERE lft <= (-(#left))
AND rgt >= (-(#right))
AND caseid = #caseid;
If your updated CTE gives the same results in the queries then the change is good. I used the id field and the values that are currently in the dft table and what you are replacing them with in the update to do the check. This can also tell you if the update results were what you were expecting. You can return any other fields that might help you make that determination if you want. Not knowing your data meaning, I don't know what else might be useful to you.
It's just me but I would not use the correlated subquery in the update either, I would rather see it as a join similar to what I used in the queries to check results.

bc is an alias in the "anchor" level, not a variable, of the recursive CTE. Basically, the ID being forced into a 10 character ID.
1 = 100000000
217 = 2170000000
etc.
In the recursive level of the cte, this ID, bc, is being concatenated to itself separated with a period (.).
1000000000.1000000000
2170000000.2170000000
The reason it is unnamed (UF) is that the field takes the name created in the first SELECT of a UNION. If you ran the CTE on its own, you'd see it labeled bc.
It doesn't look to provide any useful purpose other than to designate the anchor record. This might be a holdover from development or part of a design feature that was never fleshed out.
I'd move the comma onto the same line and just comment it out for now. Be careful removing too many fields though. Unlike this one, the rest may be required to provide a distinct enough record for the recursive count to work properly.
SELECT id, parent, lft, rgt, title, type, caseid, userid, 0 AS level
--, CAST(LEFT(CAST(id AS VARCHAR) + REPLICATE('0', 10), 10) AS VARCHAR) AS bc
FROM [dbo].DM_FolderTree hc
WHERE id = #id and caseid = #caseid
UNION ALL
SELECT hc.id, hc.parent, hc.lft, hc.rgt, hc.title, hc.type, hc.caseid, hc.userid, level + 1
--, CAST(bc + '.' + LEFT(CAST(hc.id AS VARCHAR) + REPLICATE('0', 10), 10) AS VARCHAR)
FROM q
JOIN [dbo].DM_FolderTree hc

Related

Conversion of CTE to temp table in SQL Server to get All Possible Parents

I have one user table in which I maintain parent child relationship and I want to generate the result with all user id along with its parentid and all possible hierarchical parents as comma separated strings.
My table structure is as follows.
CREATE TABLE [hybarmoney].[Users](
[ID] [bigint] IDENTITY(1,1) NOT NULL,
[USERID] [nvarchar](100) NULL,
[REFERENCEID] [bigint] NULL
)
and I am getting the result using the below CTE
;WITH Hierarchy (
ChildId
,ChildName
,ParentId
,Parents
)
AS (
SELECT Id
,USERID
,REFERENCEID
,CAST('' AS VARCHAR(MAX))
FROM hybarmoney.Users AS FirtGeneration
WHERE REFERENCEID = 0
UNION ALL
SELECT NextGeneration.ID
,NextGeneration.UserID
,Parent.ChildId
,CAST(CASE
WHEN Parent.Parents = ''
THEN (CAST(NextGeneration.REFERENCEID AS VARCHAR(MAX)))
ELSE (Parent.Parents + ',' + CAST(NextGeneration.REFERENCEID AS VARCHAR(MAX)))
END AS VARCHAR(MAX))
FROM hybarmoney.Users AS NextGeneration
INNER JOIN Hierarchy AS Parent ON NextGeneration.REFERENCEID = Parent.ChildId
)
SELECT *
FROM Hierarchy
ORDER BY ChildId
OPTION (MAXRECURSION 0)
But I have the limitation of MAXRECURSION and when I googled, I got to know that temp tables are an alternative solution but I was not able to do the same
and also i don't want to get all possible top parents, for my purpose I want to find 15 levels of hierarchical parents for each users. Is it possible to use temp tables for my purpose if possible how.
What you could do, in order to get only N levels of your CTE is to create an additional column where you keep track of each level.
;WITH Hierarchy (
ChildId
,ChildName
,ParentId
,LEVEL
,Parents
)
AS (
SELECT Id
,USERID
,REFERENCEID
,0 AS LEVEL
,CAST('' AS VARCHAR(MAX))
FROM hybarmoney.Users AS FirtGeneration
WHERE REFERENCEID = 0
UNION ALL
SELECT NextGeneration.ID
,NextGeneration.UserID
,Parent.ChildId
,LEVEL+1 AS LEVEL
,CAST(CASE
WHEN Parent.Parents = ''
THEN (CAST(NextGeneration.REFERENCEID AS VARCHAR(MAX)))
ELSE (Parent.Parents + ',' + CAST(NextGeneration.REFERENCEID AS VARCHAR(MAX)))
END AS VARCHAR(MAX))
FROM hybarmoney.Users AS NextGeneration
INNER JOIN Hierarchy AS Parent ON NextGeneration.REFERENCEID = Parent.ChildId
)
SELECT *
FROM Hierarchy
WHERE LEVEL <= 15
ORDER BY ChildId
OPTION (MAXRECURSION 0)
This works, assuming I understood correctly your following statement: "for my purpose I want to find 15 levels of hierarchical parents for each users", where you actually meant 15 levels of hierarchical parents for a single user (in your case REFERENCEID=0).
If you want this to generate a list of 15 hierarchical parents for each user in your hybarmoney.Users table, then move your CTE in a table valued function and implement a similar solution as explained here.
I got the same result using the flowing query if there may be better solution
Create PROC UspUpdateUserAndFiftenLevelIDs (#UserID BIGINT)
AS
BEGIN
DECLARE #REFERENCEIDString NVARCHAR(max)
SET #REFERENCEIDString = ''
DECLARE #ReferenceID BIGINT
SET #ReferenceID = #UserID
DECLARE #Count INT
SET #Count = 0
WHILE (#count < 15)
BEGIN
SELECT #ReferenceID = U.REFERENCEID
,#REFERENCEIDString = #REFERENCEIDString + CASE
WHEN #REFERENCEIDString = ''
THEN (CAST(U.REFERENCEID AS VARCHAR(100)))
ELSE (',' + CAST(U.REFERENCEID AS VARCHAR(MAX)))
END
FROM hybarmoney.Users U
WHERE U.ID = #ReferenceID
SET #Count = #Count + 1
END
SELECT #UserID
,#REFERENCEIDString
END

Reference CTE inside CTE?

I found this stored proc in our codebase:
ALTER PROCEDURE [dbo].[MoveNodes]
(
#id bigint,
#left bigint,
#right bigint,
#parentid bigint,
#offset bigint,
#caseid bigint,
#userid bigint
)
AS
BEGIN
WITH q AS
(
SELECT id, parent, lft, rgt, title, type, caseid, userid, 0 AS level,
CAST(LEFT(CAST(id AS VARCHAR) + REPLICATE('0', 10), 10) AS VARCHAR) AS bc
FROM [dbo].DM_FolderTree hc
WHERE id = #id and caseid = #caseid
UNION ALL
SELECT hc.id, hc.parent, hc.lft, hc.rgt, hc.title, hc.type, hc.caseid, hc.userid, level + 1,
CAST(bc + '.' + LEFT(CAST(hc.id AS VARCHAR) + REPLICATE('0', 10), 10) AS VARCHAR)
FROM q
JOIN [dbo].DM_FolderTree hc
ON hc.parent = q.id
)
UPDATE [dbo].DM_FolderTree
SET lft = ((-lft) + #offset), rgt = ((-rgt) + #offset), userid = #userid
WHERE id in (select id from q) AND lft <= (-(#left)) AND rgt >= (-(#right)) AND caseid = #caseid;
UPDATE [dbo].DM_FolderTree SET parent = #parentid, userid = #userid WHERE id = #id AND caseid = #caseid;
END
where you'll notice that the CTE q is being used called on the UNION. What exactly are we calling here? Everything before the UNION, the whole CTE? What exactly is happening here.
I'm assuming that this code is legal, since its been in production for quite some time (FLW, I know). But still, I have no idea what's happening here.
This is a recursive query. It calls the CTE again and again until all ID's and CaseIDs have walked the tree.
Think about nesting of folders in a directory. This query simply walks all the directors to get final the "file path" for all files in all folders.
Notice how Level starts at 0 and then gets added to. The second time though level is now 1 and becomes 2 and then 3 and so on.
To better understand:
Grab the select cte portion (with q as...) and replace the update with Select * from q and run it. Just so you can see what it does. Bit rough learning to start with but walking though an example by doing the above will help.
Specific answers to questions:
What exactly are we calling here?
Your building a baseline which denotes the all roots you wish to start with and then traversing all the levels under that root/folder. So in essense you're crawling the entire structure for hc.parent = q.id
Everything before the UNION, the whole CTE?
The whole cte. Recursion powerfully cool stuff!

SQL Server 2008 R2 fails on cast varchar->time

I encountered a weird error in our SQL Server 2008 R2 server. The cast of a varchar to time fails depending on what other columns are used in SELECT clause of the top level statement. Code to reproduce the issue
CREATE FUNCTION [dbo].[explode]
(
#haystack varchar(max),
#separator varchar(8000)
)
RETURNS
#ret TABLE
(
orderCol int identity(1,1),
value varchar(max)
)
AS
BEGIN
declare #index bigint
set #index = charindex(#separator,#haystack)
while #index > 0
begin
insert into #ret (value) values (substring(#haystack,1,#index-1))
set #haystack = substring(#haystack,#index+len(#separator),len(#haystack)-#index-len(#separator)+1)
set #index = charindex(#separator,#haystack)
end
insert into #ret (value) select #haystack
RETURN
END
And the query:
declare #s varchar(1000) = 'a,2015-10-08,1451,1,2,3,4,5,6,7,8,9,10,11;a,2015-10-08,1721,12,13,14,15,16,17,18,19,20,21,22'
declare #units varchar(1000) = 'l1,l2,l3,l4,l5,l6,l7,l8,l9,l10,l11'
set #units = '#label,#date,#hour,'+#units
;with cte as (select b.value,c.value as unit, a.orderCol as ri from dbo.explode(#s,';') a
cross apply dbo.explode(value,',') b
inner join dbo.explode(#units,',') c
on b.orderCol = c.orderCol),
topCte as (
select c4.unit as unit
,convert(varchar,(case
when len(c3.value) <= 3 then '0' + substring(c3.value,1,1) + ':' + substring(c3.value,2,2)
else (substring(c3.value,1,2) + ':' + substring(c3.value,3,2))
end+':00'),108) as [time]
,c1.value as label
,c2.value as [Date]
,c4.value
from cte c1
inner join cte c2
on c1.ri = c2.ri and c1.unit = '#label' and c2.unit = '#date'
inner join cte c3
on c1.ri = c3.ri and c3.unit = '#hour'
inner join cte c4
on c1.ri = c4.ri and c4.unit not in ('#label','#date','#hour')
)
select unit, label, [date], value, cast([time] as time)
from topCte
This will fail with:
Msg 241, Level 16, State 1, Line 5
Conversion failed when converting date and/or time from character string.
However when I change the last two lines into any of these, it works correctly:
select unit, label, [date], value, [time]
from topCte
select unit, label, [date], cast([time] as time)
from topCte
I would like to stress that I'm fully aware that this code is sub optimal and I know how to rework this so to avoid the error by rewriting the code still fullfilling business requirement. However this error shouldn't occur in this way and I'm very curious what is triggering it.
I believe there is something wrong in how SQL Server runs the upper query.
I would store the result into a Temporary Table and then cast the result as desired
create table #table_name
(
unit varchar(3),
label varchar(10),
[date] varchar(10),
value varchar(10),
[time] varchar(10),
)
declare #s varchar(1000) = 'a,2015-10-08,1451,1,2,3,4,5,6,7,8,9,10,11;a,2015-10-08,1721,12,13,14,15,16,17,18,19,20,21,22'
declare #units varchar(1000) = 'l1,l2,l3,l4,l5,l6,l7,l8,l9,l10,l11'
set #units = '#label,#date,#hour,'+#units
;with cte as (select b.value,c.value as unit, a.orderCol as ri from dbo.explode(#s,';') a
cross apply dbo.explode(value,',') b
inner join dbo.explode(#units,',') c
on b.orderCol = c.orderCol),
topCte as (
select c4.unit as unit
,(case
when len(c3.value) <= 3 then '0' + substring(c3.value,1,1) + ':' + substring(c3.value,2,2)
else (substring(c3.value,1,2) + ':' + substring(c3.value,3,2))
end+':00') as [time]
,c1.value as label
,c2.value as [Date]
,c4.value
from cte c1
inner join cte c2
on c1.ri = c2.ri and c1.unit = '#label' and c2.unit = '#date'
inner join cte c3
on c1.ri = c3.ri and c3.unit = '#hour'
inner join cte c4
on c1.ri = c4.ri and c4.unit not in ('#label','#date','#hour')
)
insert into #table_name(unit, label, [date], value, [time])
select unit, label, [date], value, [time]
from topCte
select unit, label, [date], value, cast([time] as time)
from #table_name
The problem is your case expression for the [time] column. If you remove the cast in the select statement you will see values like 14:51:00 which is invalid for time. You are initially converting to a varchar but you don't specify the size which is another issue. There really is no need to convert this to a varchar because it is already a varchar value.
Here is a working version of your code.
declare #s varchar(1000) = 'a,2015-10-08,1451,1,2,3,4,5,6,7,8,9,10,11;a,2015-10-08,1721,12,13,14,15,16,17,18,19,20,21,22'
declare #units varchar(1000) = 'l1,l2,l3,l4,l5,l6,l7,l8,l9,l10,l11'
set #units = '#label,#date,#hour,'+#units
;with cte as (select b.value,c.value as unit, a.orderCol as ri from dbo.explode(#s,';') a
cross apply dbo.explode(value,',') b
inner join dbo.explode(#units,',') c
on b.orderCol = c.orderCol),
topCte as (
select c4.unit as unit
,
case
when len(c3.value) <= 3 then '0' + substring(c3.value,1,1) + ':' + substring(c3.value,2,2)
else (substring(c3.value,1,2) + ':' + substring(c3.value,3,2))
end
as [time]
,c1.value as label
,c2.value as [Date]
,c4.value
from cte c1
inner join cte c2
on c1.ri = c2.ri and c1.unit = '#label' and c2.unit = '#date'
inner join cte c3
on c1.ri = c3.ri and c3.unit = '#hour'
inner join cte c4
on c1.ri = c4.ri and c4.unit not in ('#label','#date','#hour')
)
select unit, label, [date], value, cast([time] as time)
from topCte

SQL: How Do you Declare multiple paramaters as one?

I am attempting to do the following
1. Link two tables via a join on the same database
2. Take a column that exists in both FK_APPLICATIONID(with a slight difference,
where one = +1 of the other I.e. Column 1 =1375 and column 2 = 1376
3. In one of the tables exist a reference number (QREF1234) and the other
contains 11 phonenumbers
4. I want to be able to enter the Reference number, and it returns all 11
phonenumbers as a single declarable value.
5. use "Select * from TableD where phonenum in (#Declared variable)
Here is what I have so far,
Use Database 1
DECLARE #Result INT;
SELECT #Result = D.PhoneNum1,
FROM Table1
JOIN TABLE2 D on D.FK_ApplicationID= D.FK_ApplicationID
where TABLE1.FK_ApplicationID = D.FK_ApplicationID + 1
and QREF = 'Q045569/2'
Use Database2
Select * from Table3 where PhoneNum = '#result'
I apologise to the people below who didn't understand what I was trying to achieve, and I hope this clears it up.
Thanks
There are a few options but the best answer depends on what you are really trying to achieve.
There is a SQL trick whereby you can concatenate values into a variable, for example;
create table dbo.t (i int, s varchar(10))
insert dbo.t values (1, 'one')
insert dbo.t values (2, 'two')
insert dbo.t values (3, 'three')
go
declare #s varchar(255)
select #s = isnull(#s + ', ', '') + s from t order by i
select #s
set #s = null
select #s = isnull(#s + ', ', '') + s from t order by i desc
select #s
Alternatively, if you just want one value then you can use the TOP keyword, for example;
select top 1 #s = s from t order by i
select #s
select top 1 #s = s from t order by i desc
select #s
Alternatively, you can use three-part-naming and just join across the databases, something like;
SELECT T.*
FROM DB1.dbo.Table1
JOIN DB1.dbo.Table2 D
ON D.FK_ApplicationID = D.FK_ApplicationID
JOIN DB2.dbo.Table T
ON T.PhoneNum = RIGHT(D.PhoneNum1, 11)
WHERE DB1.dbo.FK_ApplicationID = D.dbo.FK_ApplicationID + 1
AND Hidden = 'VALUE'
Hope this helps,
Rhys

How to combine the values of the same field from several rows into one string in a one-to-many select?

Imagine the following two tables:
create table MainTable (
MainId integer not null, -- This is the index
Data varchar(100) not null
)
create table OtherTable (
MainId integer not null, -- MainId, Name combined are the index.
Name varchar(100) not null,
Status tinyint not null
)
Now I want to select all the rows from MainTable, while combining all the rows that match each MainId from OtherTable into a single field in the result set.
Imagine the data:
MainTable:
1, 'Hi'
2, 'What'
OtherTable:
1, 'Fish', 1
1, 'Horse', 0
2, 'Fish', 0
I want a result set like this:
MainId, Data, Others
1, 'Hi', 'Fish=1,Horse=0'
2, 'What', 'Fish=0'
What is the most elegant way to do this?
(Don't worry about the comma being in front or at the end of the resulting string.)
There is no really elegant way to do this in Sybase. Here is one method, though:
select
mt.MainId,
mt.Data,
Others = stuff((
max(case when seqnum = 1 then ','+Name+'='+cast(status as varchar(255)) else '' end) +
max(case when seqnum = 2 then ','+Name+'='+cast(status as varchar(255)) else '' end) +
max(case when seqnum = 3 then ','+Name+'='+cast(status as varchar(255)) else '' end)
), 1, 1, '')
from MainTable mt
left outer join
(select
ot.*,
row_number() over (partition by MainId order by status desc) as seqnum
from OtherTable ot
) ot
on mt.MainId = ot.MainId
group by
mt.MainId, md.Data
That is, it enumerates the values in the second table. It then does conditional aggregation to get each value, using the stuff() function to handle the extra comma. The above works for the first three values. If you want more, then you need to add more clauses.
Well, here is how I implemented it in Sybase 13.x. This code has the advantage of not being limited to a number of Names.
create proc
as
declare
#MainId int,
#Name varchar(100),
#Status tinyint
create table #OtherTable (
MainId int not null,
CombStatus varchar(250) not null
)
declare OtherCursor cursor for
select
MainId, Name, Status
from
Others
open OtherCursor
fetch OtherCursor into #MainId, #Name, #Status
while (##sqlstatus = 0) begin -- run until there are no more
if exists (select 1 from #OtherTable where MainId = #MainId) begin
update #OtherTable
set CombStatus = CombStatus + ','+#Name+'='+convert(varchar, Status)
where
MainId = #MainId
end else begin
insert into #OtherTable (MainId, CombStatus)
select
MainId = #MainId,
CombStatus = #Name+'='+convert(varchar, Status)
end
fetch OtherCursor into #MainId, #Name, #Status
end
close OtherCursor
select
mt.MainId,
mt.Data,
ot.CombStatus
from
MainTable mt
left join #OtherTable ot
on mt.MainId = ot.MainId
But it does have the disadvantage of using a cursor and a working table, which can - at least with a lot of data - make the whole process slow.