My table is like this:
ID Code ParentID
-------------------
1 A01 NULL
2 B83 NULL
3 H92 NULL
15 A013 NULL
23 A018 NULL
33 A01899 NULL
44 B8329 NULL
67 B83293 NULL
What I want is to update ParentID to match the ID of the parent code.
A01 is the parent for A013
A01 is the parent for A018
A018 is the parent for A01899
and so on.
You can see the length of A01 is 3 while the child A013 length is 4, the length of A018 is 4 and the child A01899 length is 6.
I can do that with multiple update statements and repeat that for each case.
UPDATE A
SET ParentID = B.ID
FROM Table A
INNER JOIN Table B ON A.Code like B.Code + '%'
WHERE LEN(A.Code) = 4 AND LEN(B.Code) = 3
But the question is how to do that in a single update statement?
You can first find all the relevant matches - similar to what you have - but also the length of the code it matched to. Then find the one with the longest matched code.
CREATE TABLE #TableA (ID int, Code varchar(10), ParentID int);
INSERT INTO #TableA (ID, Code, ParentID)
VALUES
(1 , 'A01' , NULL),
(2 , 'B83' , NULL),
(3 , 'H92' , NULL),
(15, 'A013' , NULL),
(23, 'A018' , NULL),
(33, 'A01899', NULL),
(44, 'B8329' , NULL),
(67, 'B83293', NULL);
WITH A AS
(SELECT TA.ID, TA.ParentID, TB.ID AS TB_ID,
ROW_NUMBER() OVER (PARTITION BY TA.ID ORDER BY TB.Len_Code DESC) AS rn
FROM #TableA TA
INNER JOIN
(SELECT ID, Code, LEN(CODE) AS Len_Code
FROM #TableA
) TB ON TA.Code LIKE TB.Code + '%'
WHERE TA.ID <> TB.ID
)
UPDATE A
SET A.ParentId = A.TB_ID
WHERE A.rn = 1;
Result
ID Code ParentID
1 A01 NULL
2 B83 NULL
3 H92 NULL
15 A013 1
23 A018 1
33 A01899 23
44 B8329 2
67 B83293 44
Just another option.
Significant Digits can be a risky business in the long run. It has been my experience that they tend to have a rather short shelf-life until an exception needs to be made.
In the example below, we allow for up to three characters in distance. We apply the closest first via the coalesce()
Just to be clear, Left Join D may not be necessary if Parents are within 1 or 2 characters. Conversely, this could be expanded if needed Left Join D ...
Example
Declare #YourTable Table ([ID] int,[Code] varchar(50),[ParentID] int)
Insert Into #YourTable Values
(1,'A01',NULL)
,(2,'B83',NULL)
,(3,'H92',NULL)
,(15,'A013',NULL)
,(23,'A018',NULL)
,(33,'A01899',NULL)
,(44,'B8329',NULL)
,(67,'B83293',NULL)
;with cte as (
Select A.*
,PtNr = coalesce(B.ID,C.ID,D.ID)
From #YourTable A
Left Join #YourTable B on left(A.[Code],len(A.[Code])-1)=B.[Code]
Left Join #YourTable C on left(A.[Code],len(A.[Code])-2)=C.[Code]
Left Join #YourTable D on left(A.[Code],len(A.[Code])-3)=D.[Code]
)
Update cte set ParentID=PtNr
Select * From #YourTable
The Update Table
ID Code ParentID
1 A01 NULL
2 B83 NULL
3 H92 NULL
15 A013 1
23 A018 1
33 A01899 23
44 B8329 2
67 B83293 44
Related
If I have a table in SQL as:
id
code
1
6
1
8
1
4
2
3
2
7
2
4
3
7
3
6
3
7
What I need to do, logically is:
Get the top row of each group when grouped by id, ordered by code
Create a new column to show if the code column contained a 7 anywhere, within the group
Desired result:
id
code
c7
1
4
N
2
3
Y
3
6
Y
I think it needs a "CASE WHEN" statement in the SELECT, but have not worked it out. What query can I execute to get this?
Seems like you can use a MIN and a conditional aggregate:
SELECT id,
MIN(Code) AS Code,
CASE WHEN COUNT(CASE code WHEN 7 THEN 1 END) > 0 THEN 'Y' ELSE 'N' END AS C7
FROM dbo.YourTable
GROUP BY id;
There is probably a better way to do this, but what comes to mind is that first you have to partition the table to get the top one based on whatever that criteria is, then join back against itself to find the ones with the 7
declare #table1 table (id int not null, code int not null)
insert into #table1 (id, code)
values
(1,6),
(1,8),
(1,4),
(2,3),
(2,7),
(2,4),
(3,7),
(3,6),
(3,7)
select id, code, c7
from (
select t.id ,t.code
,(CASE WHEN c.id is null then 'N' else 'Y' END) as c7
,ROW_NUMBER() OVER (PARTITION BY t.id order by t.code) AS p
from #table1 t
left outer join (
select id, code, 'Y' as c7
from #table1
where code = 7) c on c.id = t.id
) sorted
where sorted.p = 1
I have a table like this:
from | to
-----+-----
23 | 24
24 | 25
25 | 27
27 | 30
45 | 46
46 | 47
50 | 52
53 | 60
I need a SQL Server query that detect chain's and return min (from) and max (to) in each chain (also chain's with one record):
from | to
-----+-----
23 | 30
45 | 47
50 | 52
53 | 60
Here's an approach using a recursive CTE.
CREATE TABLE #chainLinks(linkFrom INTEGER, linkTo INTEGER);
INSERT INTO #chainLinks VALUES (23,24);
INSERT INTO #chainLinks VALUES (24,25);
INSERT INTO #chainLinks VALUES (25,27);
INSERT INTO #chainLinks VALUES (27,30);
INSERT INTO #chainLinks VALUES (45,46);
INSERT INTO #chainLinks VALUES (46,47);
INSERT INTO #chainLinks VALUES (50,52);
INSERT INTO #chainLinks VALUES (53,60);
WITH reccte AS
(
/*Recursive Seed*/
SELECT linkFrom AS chainStart,
linkFrom,
linkTo,
0 as links
FROM #chainLinks as chainLinks
WHERE linkFrom NOT IN (SELECT DISTINCT linkTo FROM #chainLinks)
UNION ALL
/*Recursive Term*/
SELECT
reccte.chainStart,
chainLinks.linkFrom,
chainLinks.linkTo,
links + 1
FROM reccte
INNER JOIN #chainLinks as chainLinks ON reccte.linkTo = chainLinks.linkFrom
)
SELECT chainStart, linkTo AS chainEnd
FROM
(
SELECT chainStart, linkFrom, linkTo, links, ROW_NUMBER() OVER (PARTITION BY chainStart ORDER BY links DESC) AS rn
FROM reccte
)subrn
WHERE rn = 1;
A recursive CTE takes two parts
A recursive seed - This is the part above the UNION where we determine which records from our table begin the recursion. Here we want any linkFrom that isn't also a linkTo
A recusrive term - This is the part below the UNION where we join the cte called reccte back to the original table. This part of the CTE iterates over and over again until that join fails.
In here we are also tracking that links which is just a counter of the number of iterations we have gone through to get to that outputted record. We keep the highest number of links for each starting point chainStart.
Here is the working example: https://rextester.com/JWUW57837
If there are branches within the chains it become a little bit more tricky.
In the sample data below, there's a split on From=12.
So the result shows 2 chains starting from 14.
create table yourtable (
[From] int not null,
[To] int not null,
PRIMARY KEY ([From],[To])
)
GO
✓
insert into yourtable
([From],[To]) values
(2,3),(3,5),(5,4)
,(14,12),(12,15),(15,11),(11,10)
,(12,9)
,(21,23)
GO
9 rows affected
;WITH RCTE_CHAINS AS
(
-- seeding with the start of chains
SELECT [From] AS MinFrom, [From], [To], 0 AS Lvl
, CAST(IIF(EXISTS(
SELECT 1 FROM YourTable n
WHERE n.[From] = t.[To]
),1,0) AS BIT) AS hasNext
FROM YourTable t
WHERE NOT EXISTS
(
SELECT 1
FROM YourTable t2
WHERE t2.[To] = t.[From]
)
UNION ALL
-- looping through the childs
SELECT c.MinFrom, t.[From], t.[To], c.Lvl+1
, CAST(IIF(EXISTS(
SELECT 1 FROM YourTable n
WHERE n.[From] = t.[To]
),1,0) AS BIT) AS hasNext
FROM RCTE_CHAINS c
JOIN YourTable t ON t.[From] = c.[To]
)
SELECT MinFrom AS [From], [To]
FROM RCTE_CHAINS
WHERE hasNext = 0
GO
From | To
---: | -:
21 | 23
14 | 9
14 | 10
2 | 4
db<>fiddle here
I couldn't find that via search, so I guess I am not asking it the right way, so help is welcome.
We have a lookup table:
Id Name
------------------
1 "Test"
2 "New"
3 "InProgress"
Table2:
StatusId SomethingElse
1
2
Table 1
ID Other Other StatusId (Fkey to Table2) ...
Then we have a view that selects from several tables and one of the columns is a CASE Statement:
SELECT * FROM Table1 t1 -- has million records
CASE When t1.StatusId = 1 THEN (SELECT Name from LOOKUP table where ID = 1) END --'Test'
CASE When t1.StatusId = 2 THEN (SELECT Name from LOOKUP table where ID = 2) END --'New'
CASE When t3.Date is not null THEN (SELECT Name from LOOKUP table where ID = 3) END --'In Progress'
-- AND ALSO the case look at other tables another 5-6 tables and there are conditions from there
INNER JOIN Table2 t2 on ...
INNER JOIN Table3 t3 on ...
As you see these are really static values.
I want to load them once into variables, e.g.
#LookUp1 = SELECT [NAME] FROM LookUP WHERE Id = 1,
#LookUp2 = SELECT [NAME] FROM LookUP WHERE Id = 2
and replace the select in the CASE statement to this:
When StatusId = 1 THEN #LookUp
When StatusId = 2 THEN #LookUp2
The view loops through millions of records and it gets really slow to do the select from Lookup table for every row.
Why not simply use a join?
SELECT <columns list from main table>, Lt.Name
FROM <main table> As Mt -- Do not use such aliases in real code!
JOIN <SecondaryTable> As St -- this represents your Table3
ON <condition>
[LEFT] JOIN <Lookup table> As Lt
ON Mt.StatusId = Lt.Id
OR (Lt.Id = 3 AND St.Date is not null)
Of course, replace <columns list from main table> with the actual columns list, <main table> with the name of the main table and so on.
The join might be an inner or left join, depending on the nullability of the StatusId column in the main table and if it's nullable, on what you want to get in such cases (either a row with null name or no row at all).
I've put together a little demonstration to show you exactly what I mean.
Create and populate sample tables (Please save us this step in your future questions):
CREATE TABLE LookUp (Id int, Name varchar(10));
INSERT INTO LookUp (Id, Name) VALUES
(1, 'Test'), (2, 'New'), (3, 'InProgress');
CREATE TABLE Table1 (Id int not null, StatusId int null);
INSERT INTO Table1(Id, StatusId)
SELECT n, CASE WHEN n % 3 = 0 THEN NULL ELSE (n % 3) END
FROM
(
SELECT TOP 30 ROW_NUMBER() OVER(ORDER BY ##SPID) As n
FROM sys.objects
) tally
CREATE TABLE Table3
(
Id int not null,
Date date null
)
INSERT INTO Table3 (Id, Date)
SELECT Id, CASE WHEN StatusId IS NULL AND Id % 4 = 0 THEN GetDate() END
FROM Table1
The query:
SELECT Table1.Id,
Table1.StatusId,
Table3.Date,
LookUp.Name
FROM Table1
JOIN Table3
ON Table1.Id = Table3.Id
LEFT JOIN LookUp
ON Table1.StatusId = LookUp.Id
OR (LookUp.Id = 3 AND Table3.Date IS NOT NULL)
Results:
Id StatusId Date Name
1 1 NULL Test
2 2 NULL New
3 NULL NULL NULL
4 1 NULL Test
5 2 NULL New
6 NULL NULL NULL
7 1 NULL Test
8 2 NULL New
9 NULL NULL NULL
10 1 NULL Test
11 2 NULL New
12 NULL 27.06.2019 InProgress
13 1 NULL Test
14 2 NULL New
15 NULL NULL NULL
16 1 NULL Test
17 2 NULL New
18 NULL NULL NULL
19 1 NULL Test
20 2 NULL New
21 NULL NULL NULL
22 1 NULL Test
23 2 NULL New
24 NULL 27.06.2019 InProgress
25 1 NULL Test
26 2 NULL New
27 NULL NULL NULL
28 1 NULL Test
29 2 NULL New
30 NULL NULL NULL
You can also see a live demo on rextester.
Create a SQL function which return Name according to Id.
Create FUNCTION [dbo].[GetLookUpValue]
(
#Id int
)
RETURNS varchar(500)
AS BEGIN
return(Select Name from LOOKUP_table with(nolock) where Id=#Id)
END
Create two table parent and child
Create table parent (ID int, ParentId int,Text varchar(20))
Create table Child (child1Id int, ParentId int, point int)
ID ParentId Text
1 NULL Sony
2 1 phone
3 2 sale
4 2 Rate
child1Id ParentIdId point
100 3 10
200 4 20
I tried something like this
Select sum(b.point),a.ParentId,a.Text from parent A join Child B on a.ID =b.ParentId group by a.ParentId,a.Text
I need output like this.
ID ParentId Text Point
1 NULL Sony null
2 1 phone 30
You can do this with a subquery
SELECT p.ID
, p.parentId
, p.[Text] AS 'Text'
, cld.pointSum AS 'Point'
FROM dbo.Parent AS p
LEFT JOIN (
SELECT c.ParentId
, SUM(c.point) AS 'pointSum'
FROM dbo.Child AS c
GROUP BY c.ParentId
) cld ON (p.ID = cld.ParentId)
I want to create a report which aggregates the number of activities per customer per week.
If there has been no activites on that customer for a given week, 0 should be displayed (i.e week 3 and 4 in the sample below)
CUSTOMER | #ACTIVITIES | WEEKNUMBER
A | 4 | 1
A | 2 | 2
A | 0 | 3
A | 0 | 4
A | 1 | 5
B ...
C ...
The problem is that if there are no activities there is no data to report on and therefor week 3 and 4 in the sample below is not in the report.
What is the "best" way to solve this?
Try this:
DECLARE #YourTable table (CUSTOMER char(1), ACTIVITIES int, WEEKNUMBER int)
INSERT #YourTable VALUES ('A' , 4 , 1)
INSERT #YourTable VALUES ('A' , 2 , 2)
INSERT #YourTable VALUES ('A' , 0 , 3)
INSERT #YourTable VALUES ('A' , 0 , 4)
INSERT #YourTable VALUES ('A' , 1 , 5)
INSERT #YourTable VALUES ('B' , 5 , 3)
INSERT #YourTable VALUES ('C' , 2 , 4)
DECLARE #StartNumber int
,#EndNumber int
SELECT #StartNumber=1
,#EndNumber=5
;WITH AllNumbers AS
(
SELECT #StartNumber AS Number
UNION ALL
SELECT Number+1
FROM AllNumbers
WHERE Number<#EndNumber
)
, AllCustomers AS
(
SELECT DISTINCT CUSTOMER FROM #YourTable
)
SELECT
n.Number AS WEEKNUMBER, c.CUSTOMER, CASE WHEN y.Customer IS NULL THEN 0 ELSE y.ACTIVITIES END AS ACTIVITIES
FROM AllNumbers n
CROSS JOIN AllCustomers c
LEFT OUTER JOIN #YourTable y ON n.Number=y.WEEKNUMBER AND c.CUSTOMER=y.CUSTOMER
--OPTION (MAXRECURSION 500)
OUTPUT:
WEEKNUMBER CUSTOMER ACTIVITIES
----------- -------- -----------
1 A 4
1 B 0
1 C 0
2 A 2
2 B 0
2 C 0
3 A 0
3 B 5
3 C 0
4 A 0
4 B 0
4 C 2
5 A 1
5 B 0
5 C 0
(15 row(s) affected)
I use a CTE to build a Numbers table, but you could build a permanent one look at this question: What is the best way to create and populate a numbers table?. You could Write the Query without a CTE (same results as above):
SELECT
n.Number AS WEEKNUMBER, c.CUSTOMER, CASE WHEN y.Customer IS NULL THEN 0 ELSE y.ACTIVITIES END AS ACTIVITIES
FROM Numbers n
CROSS JOIN (SELECT DISTINCT
CUSTOMER
FROM #YourTable
) c
LEFT OUTER JOIN #YourTable y ON n.Number=y.WEEKNUMBER AND c.CUSTOMER=y.CUSTOMER
WHERE n.Number>=1 AND n.Number<=5
ORDER BY n.Number,c.CUSTOMER
Keep a table of time periods separately, and then outer left join the activities to it.
Like:
select *
from ReportingPeriod as p
left join Activities as a on a.ReportingPeriodId = p.ReportingPeriodId;