SQL Server 2008 R2 fails on cast varchar->time - sql

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

Related

SQL - Replace characters using mapping, without a loop

Is there a way to replace characters in SQL Server from a string using a mapping table and without using a loop.
I have mapping that can go like this:
a => b
b => c
...
z => a
This mapping is not static and can change.
I tried the solution from https://stackoverflow.com/a/45202933/3161817 and https://stackoverflow.com/a/13051989/3161817 but I only end up having a string that are just a, like 'aaaaaaaa'
My current solution is like:
DECLARE #NextChar NCHAR(1)
DECLARE #Position int = 1
DECLARE #StrLength int = LEN(#str)
DECLARE #Result nvarchar(1000) = ''
WHILE (#Position <= #StrLength)
BEGIN
SET #NextChar = SUBSTRING(#str, #Position, 1)
SET #Result = #Result + ISNULL((SELECT ToChar FROM CharMapping
WHERE #NextChar COLLATE Latin1_General_BIN = FromChar COLLATE Latin1_General_BIN
), #NextChar)
SET #Position= #Position + 1
END
but I'm looking for a possible solution without a loop.
DECLARE #t TABLE(
src char
,dest char
)
INSERT INTO #t VALUES
('a', 'b')
,('b', 'c')
,('d', 'e')
DECLARE #TestString nvarchar(100) = 'aabbcdacbezzz';
WITH cte AS(
SELECT 1 lvl, SUBSTRING(#TestString, 1, 1) AS TestPosChar, SUBSTRING(#TestString, 2, LEN(#TestString)-1) AS TestStringRemain
UNION ALL
SELECT lvl + 1, SUBSTRING(TestStringRemain, 1, 1), SUBSTRING(TestStringRemain, 2, LEN(TestStringRemain)-1)
FROM cte
WHERE LEN(TestStringRemain) >= 1
)
SELECT #TestString AS OldString
,SUBSTRING((SELECT ( '' + ISNULL(t.dest, TestPosChar))
FROM cte c
LEFT JOIN #t AS t ON t.src = c.TestPosChar
ORDER BY lvl
FOR XML PATH( '' )
), 1, 1000 ) AS NewString
I made this test :
declare #MyTab table(
letter char
)
declare #MyTab2 table(
letter char
)
insert into #MyTab
select substring(a.b, v.number+1, 1)
from (select 'ABCDEFGHZZZ' b) a
join master..spt_values v on v.number < len(a.b)
where v.type = 'P'
insert into #MyTab2
select NewLetter
from (
select case letter when 'Z' then 'A'
when 'z' then 'a'
else char(ascii(letter)+1) end NewLetter
from #MyTab
) MyView
select stuff(
(select ''+letter from #MyTab2
for xml path('')),1,0,'')
SQL Server 2017 introduces a TRANSLATE function, which is similar to nested REPLACE functions. You didn't specify what version of SQL Server you are using so I don't know if that's an option.
SELECT TRANSLATE(#SourceString, 'abcdefghijklmnopqrstuvwxyz', 'bcdefghijklmnopqrstuvwxyza');
Try (and expand) following query, which use XML PATH, a tally number table and a decode table:
CREATE TABLE #TTMMPP (ORIG CHAR(1), NEWC CHAR(1));
/* add all values to shift */
INSERT INTO #TTMMPP VALUES ('a','b'),('b','c'),('c','d'),('d','e'),('e','f') /*, ....*/
;
/* N as max len of your string */
CREATE TABLE #TTMMPP2 (N smallint);
DECLARE #I INT
DECLARE #ROWS INT
SET #I = 1
SET #ROWS = 1000
WHILE #I < #ROWS
BEGIN
INSERT INTO #TTMMPP2 VALUES (#I)
SET #I = #I + 1
END
----------------------------------------
DECLARE #my_str VARCHAR(100) = 'abcd';
SELECT #my_str AS ORIGINAL,
(
SELECT ''+C.NEWC
FROM (
SELECT N, SUBSTRING( #my_str, N,1) AS X, B.NEWC
FROM #TTMMPP2 A
INNER JOIN #TTMMPP B ON SUBSTRING(#my_str,A.N,1)= B.ORIG
WHERE N<=LEN(#my_str)
) C
FOR XML PATH('')
) AS SHIFTED;
Output:
ORIGINAL SHIFTED
abcd bcde
Updated version: if you want "mark" character not found in decode table you can use this (little changes to query: LEFT JOIN and COALESCE):
DECLARE #my_str VARCHAR(100) = 'abcdefg';
SELECT #my_str AS ORIGINAL,
(
SELECT ''+C.NEWC
FROM (
SELECT N, SUBSTRING( #my_str, N,1) AS X, COALESCE(B.NEWC,'*') AS NEWC
FROM #TTMMPP2 A
LEFT JOIN #TTMMPP B ON SUBSTRING(#my_str,A.N,1)= B.ORIG
WHERE N<=LEN(#my_str)
) C
ORDER BY N
FOR XML PATH('')
) AS SHIFTED;
Output (* substitute character not found in decode table):
ORIGINAL SHIFTED
abcdefg bcde***
New update (as your last comment added):
SELECT #my_str AS ORIGINAL,
(
SELECT ''+C.NEWC
FROM (
SELECT N, SUBSTRING( #my_str, N,1) AS X, COALESCE(B.NEWC,SUBSTRING(#my_str,A.N,1)) AS NEWC
FROM ##TTMMPP2 A
LEFT JOIN #TTMMPP B ON SUBSTRING(#my_str,A.N,1) COLLATE Latin1_General_BIN = B.ORIG COLLATE Latin1_General_BIN
WHERE N<=LEN(#my_str)
) C
ORDER BY N
FOR XML PATH('')
) AS SHIFTED
Output:
ORIGINAL SHIFTED
abcdefgA bcdeefgA

T_SQL Function with 'with' clause

I am wondering why I cannot create the following T-SQL function:
CREATE FUNCTION DaysIncarceratedInYear
--Requires a Patient ID and a year
-- will return the number of days the patient was incarcerated for the year
--incarcerated is stored in a flowsheet
-- the FlowdataID are:
-- Incarceration Start Date: 'ZZZZZ00071
-- Incarceration End Date : 'ZZZZZ00072'
(
#PID varchar(10),
#YEAR int
)
RETURNS int
--DECLARE #PID varchar(10)
--DECLARE #YEAR int
--set #PID = 'ZZZZZ000L6'
--set #YEAR =2012
AS
BEGIN
declare #R int
SELECT #R = Numdays from (
;with startdates as
(
SELECT
dbo.View_PatientFlowValue.FlowValue_Value as val,
ROW_NUMBER() OVER(ORDER BY dbo.View_PatientFlowValue.FlowValue_Value) AS RowNumber
FROM dbo.View_PatientFlowValue
WHERE dbo.View_PatientFlowValue.FlowData_ID = 'ZZZZZ00071'
AND dbo.View_PatientFlowValue.FlowValue_RecordState<>1
AND Patient_ID = #PID
)
,enddates as
(
SELECT
dbo.View_PatientFlowValue.FlowValue_Value val,
ROW_NUMBER() OVER(ORDER BY dbo.View_PatientFlowValue.FlowValue_Value) AS RowNumber
FROM dbo.View_PatientFlowValue
WHERE dbo.View_PatientFlowValue.FlowData_ID = 'ZZZZZ00072'
AND dbo.View_PatientFlowValue.FlowValue_RecordState<>1
AND Person_ID = #PID
)
Select sum(DATEDIFF(d,CalcStart, CalcEnd)) as NumDays
from (
select
case
when DATEDIFF(d, cast(str(#YEAR*10000+1*100+1) as date), s.val) < 1 then '1/1/' + str(#YEAR)
else s.val
end As CalcStart,
ISNULL(e.Val, cast(str((#YEAR+1)*10000+1*100+1) as date)) as CalcEnd,
s.val as realstart, e.val as realend
FROM StartDates s
LEFT OUTER JOIN EndDates e ON s.RowNumber = e.RowNumber
) accountforyear
)
return #R
END
GO
It states there is incorrect syntax near ";" , but if I take the ";" out, it tells me there is incorrect syntax near the keyword "with". What is the proper syntax here?
This query works fine standalone.
I've never seen a CTE as a subquery.
Try re-writing as
;
with startdates as
(
... copy everything
)
Select #R = sum(DATEDIFF(d,CalcStart, CalcEnd))
FROM ...
return #R
You need to put the with statement before the select:
with startdates as
(
SELECT
dbo.View_PatientFlowValue.FlowValue_Value as val,
ROW_NUMBER() OVER(ORDER BY dbo.View_PatientFlowValue.FlowValue_Value) AS RowNumber
FROM dbo.View_PatientFlowValue
WHERE dbo.View_PatientFlowValue.FlowData_ID = 'ZZZZZ00071'
AND dbo.View_PatientFlowValue.FlowValue_RecordState<>1
AND Patient_ID = #PID
)
,enddates as
(
SELECT
dbo.View_PatientFlowValue.FlowValue_Value val,
ROW_NUMBER() OVER(ORDER BY dbo.View_PatientFlowValue.FlowValue_Value) AS RowNumber
FROM dbo.View_PatientFlowValue
WHERE dbo.View_PatientFlowValue.FlowData_ID = 'ZZZZZ00072'
AND dbo.View_PatientFlowValue.FlowValue_RecordState<>1
AND Person_ID = #PID
)
Select #R = sum(DATEDIFF(d,CalcStart, CalcEnd)) as NumDays
from (
select
case
when DATEDIFF(d, cast(str(#YEAR*10000+1*100+1) as date), s.val) < 1 then '1/1/' + str(#YEAR)
else s.val
end As CalcStart,
ISNULL(e.Val, cast(str((#YEAR+1)*10000+1*100+1) as date)) as CalcEnd,
s.val as realstart, e.val as realend
FROM StartDates s
LEFT OUTER JOIN EndDates e ON s.RowNumber = e.RowNumber
) accountforyear;
So, the #R = goes in the select after the with. You don't need a subquery here, so I just added the assignment in.
Define your CTEs before the query:
;with CTE_A( ColA, ColB)
as
(
select
ColA
, ColB
from
SomeTable
)
,CTE_B( ColA, ColC )
as
(
select
ColA
, ColC
from
SomeTable2
)
select
*
from
CTE_A a
inner join CTE_B b
on a.ColA = b.ColB

Convert Comma Delimited String to bigint in SQL Server

I have a varchar string of delimited numbers separated by commas that I want to use in my SQL script but I need to compare with a bigint field in the database. Need to know to convert it:
DECLARE #RegionID varchar(200) = null
SET #RegionID = '853,834,16,467,841,460,495,44,859,457,437,836,864,434,86,838,458,472,832,433,142,154,159,839,831,469,442,275,840,299,446,220,300,225,227,447,301,450,230,837,441,835,302,477,855,411,395,279,303'
SELECT a.ClassAdID, -- 1
a.AdURL, -- 2
a.AdTitle, -- 3
a.ClassAdCatID, -- 4
b.ClassAdCat, -- 5
a.Img1, -- 6
a.AdText, -- 7
a.MemberID, -- 9
a.Viewed, -- 10
c.Domain, -- 11
a.CreateDate -- 12
FROM ClassAd a
INNER JOIN ClassAdCat b ON b.ClassAdCAtID = a.ClassAdCAtID
INNER JOIN Region c ON c.RegionID = a.RegionID
AND a.PostType = 'CPN'
AND DATEDIFF(d, GETDATE(), ExpirationDate) >= 0
AND a.RegionID IN (#RegionID)
AND Viewable = 'Y'
This fails with the following error:
Error converting data type varchar to bigint.
RegionID In the database is a bigint field.. need to convert the varchar to bigint.. any ideas..?
Many thanks in advance,
neojakey
create this function:
CREATE function [dbo].[f_split]
(
#param nvarchar(max),
#delimiter char(1)
)
returns #t table (val nvarchar(max), seq int)
as
begin
set #param += #delimiter
;with a as
(
select cast(1 as bigint) f, charindex(#delimiter, #param) t, 1 seq
union all
select t + 1, charindex(#delimiter, #param, t + 1), seq + 1
from a
where charindex(#delimiter, #param, t + 1) > 0
)
insert #t
select substring(#param, f, t - f), seq from a
option (maxrecursion 0)
return
end
change this part:
AND a.RegionID IN (select val from dbo.f_split(#regionID, ','))
Change this for better overall performance:
AND DATEDIFF(d, 0, GETDATE()) <= ExpirationDate
Your query does not know that those are separate values, you can use dynamic sql for this:
DECLARE #RegionID varchar(200) = null
SET #RegionID = '853,834,16,467,841,460,495,44,859,457,437,836,864,434,86,838,458,472,832,433,142,154,159,839,831,469,442,275,840,299,446,220,300,225,227,447,301,450,230,837,441,835,302,477,855,411,395,279,303'
declare #sql nvarchar(Max)
set #sql = 'SELECT a.ClassAdID, -- 1
a.AdURL, -- 2
a.AdTitle, -- 3
a.ClassAdCatID, -- 4
b.ClassAdCat, -- 5
a.Img1, -- 6
a.AdText, -- 7
a.MemberID, -- 9
a.Viewed, -- 10
c.Domain, -- 11
a.CreateDate -- 12
FROM ClassAd a
INNER JOIN ClassAdCat b ON b.ClassAdCAtID = a.ClassAdCAtID
INNER JOIN Region c ON c.RegionID = a.RegionID
AND a.PostType = ''CPN''
AND DATEDIFF(d, GETDATE(), ExpirationDate) >= 0
AND a.RegionID IN ('+#RegionID+')
AND Viewable = ''Y'''
exec sp_executesql #sql
I use this apporach sometimes and find it very good.
It transfors your comma-separated string into an AUX table (called #ARRAY) and then query the main table based on the AUX table:
declare #RegionID varchar(50)
SET #RegionID = '853,834,16,467,841,460,495,44,859,457,437,836,864,434,86,838,458,472,832,433,142,154,159,839,831,469,442,275,840,299,446,220,300,225,227,447,301,450,230,837,441,835,302,477,855,411,395,279,303'
declare #S varchar(20)
if LEN(#RegionID) > 0 SET #RegionID = #RegionID + ','
CREATE TABLE #ARRAY(region_ID VARCHAR(20))
WHILE LEN(#RegionID) > 0 BEGIN
SELECT #S = LTRIM(SUBSTRING(#RegionID, 1, CHARINDEX(',', #RegionID) - 1))
INSERT INTO #ARRAY (region_ID) VALUES (#S)
SELECT #RegionID = SUBSTRING(#RegionID, CHARINDEX(',', #RegionID) + 1, LEN(#RegionID))
END
select * from your_table
where regionID IN (select region_ID from #ARRAY)
It avoids you from ahving to concatenate the query string and then use EXEC to execute it, which I dont think it is a very good approach.
if you need to run the code twice you will need to drop the temp table
I think the answer should be kept simple.
Try using CHARINDEX like this:
DECLARE #RegionID VARCHAR(200) = NULL
SET #RegionID =
'853,834,16,467,841,460,495,44,859,457,437,836,864,434,86,838,458,472,832,433,142,154,159,839,831,469,442,275,840,299,446,220,300,225,227,447,301,450,230,837,441,835,302,477,855,411,395,279,303'
SELECT 1
WHERE Charindex('834', #RegionID) > 0
SELECT 1
WHERE Charindex('999', #RegionID) > 0
When CHARINDEX finds the value in the large string variable, it will return it's position, otherwise it return 0.
Use this as a search tool.
The easiest way to change this query is to replace the IN function with a string function. Here is what I consider the safest approach using LIKE (which is portable among databases):
AND ','+#RegionID+',' like '%,'+cast(a.RegionID as varchar(255))+',%'
Or CHARINDEX:
AND charindex(','+cast(a.RegionID as varchar(255))+',', ','+#RegionID+',') > 0
However, if you are explicitly putting the list in your code, why not use a temporary table?
declare #RegionIds table (RegionId int);
insert into #RegionIds
select 853 union all
select 834 union all
. . .
select 303
Then you can use the table in the IN clause:
AND a.RegionId in (select RegionId from #RegionIds)
or in a JOIN clause.
I like Diego's answer some, but I think my modification is a little better because you are declaring a table variable and not creating an actual table. I know the "in" statement can be a little slow, so I did an inner join since I needed some info from the Company table anyway.
declare #companyIdList varchar(1000)
set #companyIdList = '1,2,3'
if LEN(#companyIdList) > 0 SET #companyIdList = #companyIdList + ','
declare #CompanyIds TABLE (CompanyId bigint)
declare #S varchar(20)
WHILE LEN(#companyIdList) > 0 BEGIN
SELECT #S = LTRIM(SUBSTRING(#companyIdList, 1, CHARINDEX(',', #companyIdList) - 1))
INSERT INTO #CompanyIds (CompanyId) VALUES (#S)
SELECT #companyIdList = SUBSTRING(#companyIdList, CHARINDEX(',', #companyIdList) + 1, LEN(#companyIdList))
END
select d.Id, d.Name, c.Id, c.Name
from [Division] d
inner join [Company] c on d.CompanyId = c.Id
inner join #CompanyIds cids on c.Id = cids.CompanyId

How to find missing id in the table

I have column looks like below
SID101
SID102
SID103
SID105
SID107
In the above criteria i need to find missed SID numbers. SID104 and SID 106 are missed while ordering.
How can i find the missed id numbers.Could any one help me finding it.
Thanks in advance.
If your table contains gaps with length more than 1 item, you can use this query:
declare #t table(s varchar(20))
insert #t values ('SID101'),('SID102'),('SID103'),('SID105'),('SID108');
with cte as
(
select substring(t.s, 4, len(t.s)) [i]
from #t t
)
select 'SID' + cast(m.number as varchar(20))
from master..spt_values m
left join cte c on c.i = m.number
where [Type] = 'P'
and m.number >= (select min(i) from cte)
and m.number <= (select max(i) from cte)
and c.i is null
Output:
-----------------------
SID104
SID106
SID107
Something like this should work:
DECLARE #i INT;
SET #i = 100;
CREATE TABLE #idsToCheck (checkId varchar(100));
WHILE (#i < 200)
BEGIN
INSERT INTO #idsToCheck VALUES ('SID' + CONVERT(varchar(100), #i));
SET #i = #i + 1;
END
SELECT * FROM #idsToCheck itc
LEFT OUTER JOIN MainTable mt ON itc.checkId = mt.realId
WHERE mt.realId = NULL
DROP TABLE #idsToCheck
... where MainTable is your table containing the SID101, SID102, etc. column values, and MainTable.realId is the column containing those IDs. Modify the #i initial value and number in the while loop condition based on which SIDs you want to check from/to.
It's difficult. With
SELECT COUNT(*),MAX(CAST(REPLACE(y.name,'SID','') AS INT)) AS col_max FROM
sys.objects x INNER JOIN sys.columns y ON x.object_id=y.object_id
WHERE x.name='<TABLE_NAME>'
you should know, how much columns are missing (i.e. COUNT(*) is 5 and col_max is 107)
When you have a table, which contains only one column with all possible IDs from 1 to max (i.e. 100,101,102,103,104,...,132) then you could do
SELECT * FROM (
SELECT CAST(REPLACE(y.name,'SID','') AS INT) AS col_id FROM
sys.objects x INNER JOIN sys.columns y ON x.object_id=y.object_id
WHERE x.name='<TABLE_NAME>'
) a
RIGHT JOIN <TABLE_IDS> b ON a.col_id=b.id
WHERE a.col_id IS NULL AND b.id<=(
SELECT MAX(CAST(REPLACE(y.name,'SID','') AS INT)) AS col_max FROM
sys.objects x INNER JOIN sys.columns y ON x.object_id=y.object_id
WHERE x.name='<TABLE_NAME>'
)
EDIT: sorry, I've seen just now, that these values aren't column names, but values. My solution will find missing column names
 Declare #St int
declare #end int
set #st = CAST( (select RIGHT( max(data),4) from orderno)as int)
set #end = CAST( (select RIGHT( min(data),4) from orderno)as int)
create table #temp(data int)
while(#St <= #end )
begin
insert into #temp values(#St)
set #St = #St +1
end
select * from orderno
select * from #temp
select data from #temp where data not in (select cast(RIGHT(data,4))
declare #t table(s varchar(20))
insert #t values ('SID101'),('SID102'),('SID103'),('SID105'),('SID107');
with cte as
(
select substring(t.s, 4, len(t.s)) [i]
from #t t
)
select 'SID' + cast(t1.i + 1 as varchar(20))
from cte t1
join cte t2 on t2.i > t1.i
and not exists(
select 1
from cte c3
where c3.i > t1.i and c3.i < t2.i
)
where t2.i <> t1.i + 1
Output:
-----------------------
SID104
SID106

Simplest way to do a recursive self-join?

What is the simplest way of doing a recursive self-join in SQL Server? I have a table like this:
PersonID | Initials | ParentID
1 CJ NULL
2 EB 1
3 MB 1
4 SW 2
5 YT NULL
6 IS 5
And I want to be able to get the records only related to a hierarchy starting with a specific person. So If I requested CJ's hierarchy by PersonID=1 I would get:
PersonID | Initials | ParentID
1 CJ NULL
2 EB 1
3 MB 1
4 SW 2
And for EB's I'd get:
PersonID | Initials | ParentID
2 EB 1
4 SW 2
I'm a bit stuck on this can can't think how to do it apart from a fixed-depth response based on a bunch of joins. This would do as it happens because we won't have many levels but I would like to do it properly.
Thanks! Chris.
WITH q AS
(
SELECT *
FROM mytable
WHERE ParentID IS NULL -- this condition defines the ultimate ancestors in your chain, change it as appropriate
UNION ALL
SELECT m.*
FROM mytable m
JOIN q
ON m.parentID = q.PersonID
)
SELECT *
FROM q
By adding the ordering condition, you can preserve the tree order:
WITH q AS
(
SELECT m.*, CAST(ROW_NUMBER() OVER (ORDER BY m.PersonId) AS VARCHAR(MAX)) COLLATE Latin1_General_BIN AS bc
FROM mytable m
WHERE ParentID IS NULL
UNION ALL
SELECT m.*, q.bc + '.' + CAST(ROW_NUMBER() OVER (PARTITION BY m.ParentID ORDER BY m.PersonID) AS VARCHAR(MAX)) COLLATE Latin1_General_BIN
FROM mytable m
JOIN q
ON m.parentID = q.PersonID
)
SELECT *
FROM q
ORDER BY
bc
By changing the ORDER BY condition you can change the ordering of the siblings.
Using CTEs you can do it this way
DECLARE #Table TABLE(
PersonID INT,
Initials VARCHAR(20),
ParentID INT
)
INSERT INTO #Table SELECT 1,'CJ',NULL
INSERT INTO #Table SELECT 2,'EB',1
INSERT INTO #Table SELECT 3,'MB',1
INSERT INTO #Table SELECT 4,'SW',2
INSERT INTO #Table SELECT 5,'YT',NULL
INSERT INTO #Table SELECT 6,'IS',5
DECLARE #PersonID INT
SELECT #PersonID = 1
;WITH Selects AS (
SELECT *
FROM #Table
WHERE PersonID = #PersonID
UNION ALL
SELECT t.*
FROM #Table t INNER JOIN
Selects s ON t.ParentID = s.PersonID
)
SELECT *
FROm Selects
The Quassnoi query with a change for large table. Parents with more childs then 10: Formating as str(5) the row_number()
WITH q AS
(
SELECT m.*, CAST(str(ROW_NUMBER() OVER (ORDER BY m.ordernum),5) AS VARCHAR(MAX)) COLLATE Latin1_General_BIN AS bc
FROM #t m
WHERE ParentID =0
UNION ALL
SELECT m.*, q.bc + '.' + str(ROW_NUMBER() OVER (PARTITION BY m.ParentID ORDER BY m.ordernum),5) COLLATE Latin1_General_BIN
FROM #t m
JOIN q
ON m.parentID = q.DBID
)
SELECT *
FROM q
ORDER BY
bc
SQL 2005 or later, CTEs are the standard way to go as per the examples shown.
SQL 2000, you can do it using UDFs -
CREATE FUNCTION udfPersonAndChildren
(
#PersonID int
)
RETURNS #t TABLE (personid int, initials nchar(10), parentid int null)
AS
begin
insert into #t
select * from people p
where personID=#PersonID
while ##rowcount > 0
begin
insert into #t
select p.*
from people p
inner join #t o on p.parentid=o.personid
left join #t o2 on p.personid=o2.personid
where o2.personid is null
end
return
end
(which will work in 2005, it's just not the standard way of doing it. That said, if you find that the easier way to work, run with it)
If you really need to do this in SQL7, you can do roughly the above in a sproc but couldn't select from it - SQL7 doesn't support UDFs.
Check following to help the understand the concept of CTE recursion
DECLARE
#startDate DATETIME,
#endDate DATETIME
SET #startDate = '11/10/2011'
SET #endDate = '03/25/2012'
; WITH CTE AS (
SELECT
YEAR(#startDate) AS 'yr',
MONTH(#startDate) AS 'mm',
DATENAME(mm, #startDate) AS 'mon',
DATEPART(d,#startDate) AS 'dd',
#startDate 'new_date'
UNION ALL
SELECT
YEAR(new_date) AS 'yr',
MONTH(new_date) AS 'mm',
DATENAME(mm, new_date) AS 'mon',
DATEPART(d,#startDate) AS 'dd',
DATEADD(d,1,new_date) 'new_date'
FROM CTE
WHERE new_date < #endDate
)
SELECT yr AS 'Year', mon AS 'Month', count(dd) AS 'Days'
FROM CTE
GROUP BY mon, yr, mm
ORDER BY yr, mm
OPTION (MAXRECURSION 1000)
DELIMITER $$
DROP PROCEDURE IF EXISTS `myprocDURENAME`$$
CREATE DEFINER=`root`#`%` PROCEDURE `myprocDURENAME`( IN grp_id VARCHAR(300))
BEGIN
SELECT h.ID AS state_id,UPPER(CONCAT( `ACCNAME`,' [',b.`GRPNAME`,']')) AS state_name,h.ISACTIVE FROM accgroup b JOIN (SELECT get_group_chield (grp_id) a) s ON FIND_IN_SET(b.ID,s.a) LEFT OUTER JOIN acc_head h ON b.ID=h.GRPID WHERE h.ID IS NOT NULL AND H.ISACTIVE=1;
END$$
DELIMITER ;
////////////////////////
DELIMITER $$
DROP FUNCTION IF EXISTS `get_group_chield`$$
CREATE DEFINER=`root`#`%` FUNCTION `get_group_chield`(get_id VARCHAR(999)) RETURNS VARCHAR(9999) CHARSET utf8
BEGIN
DECLARE idd VARCHAR(300);
DECLARE get_val VARCHAR(300);
DECLARE get_count INT;
SET idd=get_id;
SELECT GROUP_CONCAT(id)AS t,COUNT(*) t1 INTO get_val,get_count FROM accgroup ag JOIN (SELECT idd AS n1) d ON FIND_IN_SET(ag.PRNTID,d.n1);
SELECT COUNT(*) INTO get_count FROM accgroup WHERE PRNTID IN (idd);
WHILE get_count >0 DO
SET idd=CONCAT(idd,',', get_val);
SELECT GROUP_CONCAT(CONCAT('', id ,'' ))AS t,COUNT(*) t1 INTO get_val,get_count FROM accgroup ag JOIN (SELECT get_val AS n1) d ON FIND_IN_SET(ag.PRNTID,d.n1);
END WHILE;
RETURN idd;
-- SELECT id FROM acc_head WHERE GRPID IN (idd);
END$$
DELIMITER ;