Mapping All Terminal IDs to Previous IDs - sql

I have a table in SQL Server that contains a list of all ID migrations overtime. An individual's ID can change overtime, and this table helps us understand when the change occurs, and what the ID changes from/to. What I'd ultimately like is a way to list all of the previous IDs for the most recent ID (which I'm referring to as the terminal ID). I'm assuming this will require some sort of CTE, but my brain is in a bit of a fog as to how I should set this up.
CREATE TABLE #ExampleIdCrosswalk
(
CurrentId VARCHAR(3)
,PreviousId VARCHAR(3)
,PreviousIdObsoleteDate DATE
)
INSERT INTO #ExampleIdCrosswalk
VALUES
('DEF','ABC','2021-01-01')
,('WVU','ZYX','2021-01-01')
,('MNO','ONM','2021-02-01')
,('PPP','EEE','2021-02-01')
,('GHI','DEF','2021-03-01')
,('TSR','WVU','2021-03-01')
,('NRP','QRS','2021-03-01')
,('JKL','GHI','2021-04-01')
SELECT * FROM #ExampleIdCrosswalk
Ultimately, what I'd like to show is a table with all the terminal ID's along with each of their corresponding previous IDs.
Any help would be appreciated!

You can use a recursive CTE for this:
with cte as (
select currentid, previousid
from ExampleIdCrosswalk ec
where not exists (select 1 from ExampleIdCrosswalk ec2 where ec2.previousId = ec.currentid)
union all
select cte.currentid, ec.previousid
from cte join
ExampleIdCrosswalk ec
on ec.currentId = cte.previousId
)
select *
from cte;
Here is a db<>fiddle.

You can use a recursive CTE, as in:
with
n (last, curr, prev) as (
select currentid, currentid, previousid
from ExampleIdCrosswalk where currentid not in (
select previousid from ExampleIdCrosswalk
)
union all
select n.last, c.currentid, c.previousid
from n
join ExampleIdCrosswalk c on c.currentid = n.prev
)
select last, prev
from n
order by last, prev
Result:
last prev
----- ----
JKL ABC
JKL DEF
JKL GHI
MNO ONM
NRP QRS
PPP EEE
TSR WVU
TSR ZYX
See running example at db<>fiddle.

Related

logic to create more rows in sql

I have a table1 that I wanted to transform into the expected table.
Expected table logic for columns:
cal: comes from cal of table1. ID comes from the ID of table1.
code: this is populated with lp or fp depending upon if we have a value in f_a then we create a new record with fp as code. corresponding to it we check if f_a is populated if yes then we take that date from f_a and put in in the Al column for the same ID. also we check if f_pl is populated if yes then we take the date from that and put it in the pl column.
If the code was lp then we check if l_a is populated then we take that date and place in the date in Al for that code and Id. also, we check if lpl is populated if yes then we take that date and put it in pl.
I am just a starter with SQL so it is a bit overwhelming for me on how to get it started. Please post some solutions.
table1:
ID f_a l_a f_pl lpl cal
CNT 6/20/2018 6/28/2018 6/28/2018 1/31/2020
expected output:
ID Cal code pl Al
CNT 1/31/2020 lp 6/28/2018 6/28/2018
CNT 1/31/2020 fp 6/20/2018
Update:
I have more IDs in the table, so it is not that CNT is the only Id. If I use unpivot then it should follow the same logic for all IDs.
This is a question about how to unpivot columns to rows. In Oracle, I would recommend a lateral join:
select t.id, t.cal, x.*
from mytable t
cross apply (
select 'lp' as code, t.lpl as pl, l_a as al from dual
union all
select 'fp', t.f_pl, t.f_a from dual
) x
This syntax is available in Oracle 12.1 onwards. In earlier versions, you would use union all:
select id, cal, 'lp' as code, lpl as pl, l_a as al from mytable
union all
select id, cal, 'lp' as code, 'fp', f_pl, f_a from mytable
You can use UNPIVOT for multiple columns then do the checks you need on dates:
with a as (
select
'CNT' as ID,
date '2018-06-20' as f_a,
date '2018-06-28' as l_a,
cast(null as date) as f_pl,
date '2018-06-28' as l_pl,
date '2020-01-31' as cal
from dual
)
select *
from a
unpivot(
(pl, al) for code in ((l_pl, l_a) as 'lp', (f_pl, f_a) as 'fp')
) up
ID | CAL | CODE | PL | AL
CNT | 31-JAN-07 | lp | 28-JUN-18 | 28-JUN-18
CNT | 31-JAN-07 | fp | | 20-JUN-18
Working example here.
Please try this script which is not version dependend:
-- Here we select columns foom source table. Please change the names if they are different
with r as (
select
ID,
f_a,
l_a,
f_pl,
lpl, -- Here not sure if example is wrong or no underscore in column deffinition
cal
from table_1 -- Please put real table name here
)
select * from (
select r.id, r.cal, 'lp' as code, r.l_pl as pl, l_a as ai
from r
where r.l_a is not null
union all
select r1.id, r1.cal, 'pl', r1.f_pl, r1.f_a
from r r1
where r1.f_a is not null
)
order by id, cal, code;

Finding updates in a table using Self-Join

I have a table as shown below
tablename - property
|runId|listingId|listingName
1 123 abc
1 234 def
2 123 abcd
2 567 ghi
2 234 defg
As you can see in above code there is a runId and there is a listing Id. I am trying to fetch for a particular runId which are the new listings added (In this case for runId 2 its 4th row with listing id 567 ) and which are the listing Ids that are update (In this case its row 3 and row 5 with listingId 123 and 234 respectively)
I am trying self join and it is working fairly for new updates but new additions are giving me trouble
SELECT p1.* FROM property p1
INNER JOIN property p2
ON p1.listingid = p2.listingid
WHERE p1.runid=456 AND p2.runid!=456
The above query provides me correct updated records in the table. But I am not able to find new listing. I used p1.listingid != p2.listingId , left outer join, still wont work.
I would use the ROW_NUMBER() analytical function for it.
SELECT
T.*
FROM
(
SELECT
T.*,
CASE
WHEN ROW_NUMBER() OVER(
PARTITION BY LISTINGID
ORDER BY
RUNID
) = 1 THEN 'INSERTED'
ELSE 'UPDATED'
END AS OPERATION_
FROM
PROPERTY
)
WHERE
RUNID = 2
-- AND OPERATION_ = 'INSERTED'
-- AND OPERATION_ = 'UPDATED'
This will provide the result as updated if listingid is added in any of the previous runid
Cheers!!
You may try this.
with cte as (
select row_number() over (partition by listingId order by runId) as Slno, * from property
)
select * from property where listingId not in (
select listingId from cte as c where slno>1
) --- for new listing added
with cte as (
select row_number() over (partition by listingId order by runId) as Slno, * from property
)
select * from property where listingId in (
select listingId from cte as c where slno>1
) --- for modified listing
For this, I would recommend exists and not exists. For updates:
select p.*
from property p
where exists (select 1
from property p2
where p2.listingid = p.listingid and
p2.runid < p.runid
);
If you want the result for a particular runid, add and runid = ? to the outer query.
And for new listings:
select p.*
from property p
where not exists (select 1
from property p2
where p2.listingid = p.listingid and
p2.runid < p.runid
);
With an index on property(listingid, runid), I would expect this to have somewhat better performance than a solution using window functions.
Here is a db<>fiddle.

how to combine two result sets without nulls

I have two result sets that I would like to combine , since I would like to send them as a dataset to my ssrs report.
I am currently doing :
Select Sum(Teamsales) as TS, Null as PS from ABC;
union
Select null as TS, Sum(ProdSales) From DEF;
I get an output like this :
123 NULL
NULL 456
Is there a way I can get an output as below ??
123 456
SELECT Sum(Teamsales) as TS,
(
SELECT Sum(ProdSales)
FROM DEF
) AS PS
FROM ABC
Maybe something like this?
select
sum(teamsales) as ts,
Sum(ProdSales) as ps
from
abc cross join
def
(Crossjoins are generally evil... But you could consider maybe to use more strict one.)
Most probably you would be having more than one row in each result set.
As you would apply a dimension in group by clause.
for example
You want to get total Team Sales and total Product sales for each branch then you should write.
Select ABC.BranchID, ABC.TS, DEF.PS
(Select BranchID, Sum(Teamsales) as TS from ABC group by BranchID) ABC
Inner Join
(Select BranchID, Sum(ProdSales) as PS From DEF group by BranchID) DEF
On ABC.BranchID = DEF.BranchID
Since these are both one-liners, you could just cross-join the results, giving you a single line:
SELECT SUM(Teamsales) as TS,
FROM abc
CROSS JOIN (SELECT SUMN(ProdSales) FROM def) t
Try this:
select sum(TS), sum(PS) from (
Select Sum(Teamsales) as TS, Null as PS from ABC
union all
Select null as TS, Sum(ProdSales) as PS From DEF
) as x

How can I SELECT distinct data based on a date field?

I have table that stores a log of changes to objects in another table. Here are my table contents:
ObjID Color Date User
------- ------- ------------------------ --------
1 Red 2010-01-01 12:22:00.000 Joe
1 Blue 2010-01-02 15:22:00.000 Jill
1 Green 2010-01-03 16:22:00.000 Joe
1 White 2010-01-10 09:22:00.000 Mike
2 Red 2010-01-09 10:22:00.000 Mike
2 Blue 2010-01-12 09:22:00.000 Jill
2 Orange 2010-01-12 15:22:00.000 Joe
I want to select the most recent date for each Object, as well as the Color and User on the date of that record.
Bascically, I want this result set:
ObjID Color Date User
------- ------- ------------------------ --------
1 White 2010-01-10 09:22:00.000 Mike
2 Orange 2010-01-12 15:22:00.000 Joe
I'm having trouble wrapping my head around the SQL query I need to write to get this data...
I am retrieving data via ODBC from an iSeries DB2 database (AS/400).
Hey there, I think you want the following (where ColorTable is your table name):
SELECT Color.*
FROM ColorTable as Color
INNER JOIN
(
SELECT ObjID, MAX(Date) as Date
FROM ColorTable
GROUP BY ObjID
) as MaxDateByColor
ON Color.ObjID = MaxDateByColor.ObjID
AND Color.Date = MaxDateByColor.Date
Assuming at least SQL Server 2005
DECLARE #T TABLE (ObjID INT,Color VARCHAR(10),[Date] DATETIME,[User] VARCHAR(50))
INSERT INTO #T
SELECT 1,'Red',' 2010-01-01 12:22:00.000','Joe' UNION ALL
SELECT 1,'Blue','2010-01-02 15:22:00.000','Jill' UNION ALL
SELECT 1,'Green',' 2010-01-03 16:22:00.000','Joe' UNION ALL
SELECT 1,'White',' 2010-01-10 09:22:00.000','Mike' UNION ALL
SELECT 2,'Red',' 2010-01-09 10:22:00.000','Mike' UNION ALL
SELECT 2,'Blue','2010-01-12 09:22:00.000','Jill' UNION ALL
SELECT 2,'Orange','2010-01-12 15:22:00.000','Joe'
;WITH T AS
(
SELECT *,
ROW_NUMBER() OVER (PARTITION BY ObjID ORDER BY Date DESC) AS RN
FROM #T
)
SELECT ObjID,
Color,
[Date],
[User]
FROM T
WHERE RN=1
Or a SQL Server 2000 method from the article linked to in the comments
SELECT ObjID,
CAST(SUBSTRING(string, 24, 33) AS VARCHAR(10)) AS Color,
CAST(SUBSTRING(string, 1, 23) AS DATETIME ) AS [Date],
CAST(SUBSTRING(string, 34, 83) AS VARCHAR(50)) AS [User]
FROM
(
SELECT ObjID,
MAX((CONVERT(CHAR(23), [Date], 126)
+ CAST(Color AS CHAR(10))
+ CAST([User] AS CHAR(50))) COLLATE Latin1_General_BIN) AS string
FROM #T
GROUP BY ObjID) T;
If you have an Objects table and your ObjectHistory table has an index on ObjID and date, then this could perform better than other queries given so far:
SELECT
X.*
FROM
Objects O
CROSS APPLY (
SELECT TOP 1 *
FROM ObjectHistory H
WHERE O.ObjID = O.ObjID
ORDER BY H.[Date] DESC
) X
The performance improvement may only come if you're pulling columns from the Objects table, too, but it's worth a shot.
If you want all Objects regardless of whether they have a history entry, switch to OUTER APPLY (and of course use O.ObjID instead of H.ObjID).
The neat thing about this query is that
It solves for situations where the Date value can have duplicates
It can support an arbitrary number of items per group (say, the top 5 instead of the top 1)
See these two related questions:
SQL/mysql - Select distinct/UNIQUE but return all columns?
And:
How to efficiently determine changes between rows using SQL
SELECT t1.* FROM Table_name as t1
INNER JOIN (
SELECT MAX(Date) as MaxDate, ObjID FROM Table_name
GROUP BY ObjID
) as t2
ON t1.ObjID = t2.ObjID AND t1.Date = t2.MaxDate
You can find out, per object, its most recent change like this:
select objectid, max(changedate) as LatestChange
from LOG
group by objectid
You can then get the color and user columns by linking the set returned above, instantiated as an inline view that has been given an alias, to the same table again:
select color, user, FOO.objectid, FOO.LatestChange
from LOG
inner join
(
select objectid, max(changedate) as LatestChange
from LOG
group by objectid
) as FOO
on LOG.objectid = FOO.objectid and LOG.changedate = FOO.LatestChange
like martin smiths above,
simply just do a row number over partition and pick one of the rows that is most recent
like
SELECT Color,Date,User
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY User ORDER BY [DATE]) AS ROW_NUMBER
FROM [tablename]
) AS ROWS
WHERE
ROW_NUMBER = 2

How to find "holes" in a table

I recently inherited a database on which one of the tables has the primary key composed of encoded values (Part1*1000 + Part2).
I normalized that column, but I cannot change the old values.
So now I have
select ID from table order by ID
ID
100001
100002
101001
...
I want to find the "holes" in the table (more precisely, the first "hole" after 100000) for new rows.
I'm using the following select, but is there a better way to do that?
select /* top 1 */ ID+1 as newID from table
where ID > 100000 and
ID + 1 not in (select ID from table)
order by ID
newID
100003
101029
...
The database is Microsoft SQL Server 2000. I'm ok with using SQL extensions.
select ID +1 From Table t1
where not exists (select * from Table t2 where t1.id +1 = t2.id);
not sure if this version would be faster than the one you mentioned originally.
SELECT (ID+1) FROM table AS t1
LEFT JOIN table as t2
ON t1.ID+1 = t2.ID
WHERE t2.ID IS NULL
This solution should give you the first and last ID values of the "holes" you are seeking. I use this in Firebird 1.5 on a table of 500K records, and although it does take a little while, it gives me what I want.
SELECT l.id + 1 start_id, MIN(fr.id) - 1 stop_id
FROM (table l
LEFT JOIN table r
ON l.id = r.id - 1)
LEFT JOIN table fr
ON l.id < fr.id
WHERE r.id IS NULL AND fr.id IS NOT NULL
GROUP BY l.id, r.id
For example, if your data looks like this:
ID
1001
1002
1005
1006
1007
1009
1011
You would receive this:
start_id stop_id
1003 1004
1008 1008
1010 1010
I wish I could take full credit for this solution, but I found it at Xaprb.
from How do I find a "gap" in running counter with SQL?
select
MIN(ID)
from (
select
100001 ID
union all
select
[YourIdColumn]+1
from
[YourTable]
where
--Filter the rest of your key--
) foo
left join
[YourTable]
on [YourIdColumn]=ID
and --Filter the rest of your key--
where
[YourIdColumn] is null
The best way is building a temp table with all IDs
Than make a left join.
declare #maxId int
select #maxId = max(YOUR_COLUMN_ID) from YOUR_TABLE_HERE
declare #t table (id int)
declare #i int
set #i = 1
while #i <= #maxId
begin
insert into #t values (#i)
set #i = #i +1
end
select t.id
from #t t
left join YOUR_TABLE_HERE x on x.YOUR_COLUMN_ID = t.id
where x.YOUR_COLUMN_ID is null
Have thought about this question recently, and looks like this is the most elegant way to do that:
SELECT TOP(#MaxNumber) ROW_NUMBER() OVER (ORDER BY t1.number)
FROM master..spt_values t1 CROSS JOIN master..spt_values t2
EXCEPT
SELECT Id FROM <your_table>
This solution doesn't give all holes in table, only next free ones + first available max number on table - works if you want to fill in gaps in id-es, + get free id number if you don't have a gap..
select numb + 1 from temp
minus
select numb from temp;
This will give you the complete picture, where 'Bottom' stands for gap start and 'Top' stands for gap end:
select *
from
(
(select <COL>+1 as id, 'Bottom' AS 'Pos' from <TABLENAME> /*where <CONDITION*/>
except
select <COL>, 'Bottom' AS 'Pos' from <TABLENAME> /*where <CONDITION>*/)
union
(select <COL>-1 as id, 'Top' AS 'Pos' from <TABLENAME> /*where <CONDITION>*/
except
select <COL>, 'Top' AS 'Pos' from <TABLENAME> /*where <CONDITION>*/)
) t
order by t.id, t.Pos
Note: First and Last results are WRONG and should not be regarded, but taking them out would make this query a lot more complicated, so this will do for now.
Many of the previous answer are quite good. However they all miss to return the first value of the sequence and/or miss to consider the lower limit 100000. They all returns intermediate holes but not the very first one (100001 if missing).
A full solution to the question is the following one:
select id + 1 as newid from
(select 100000 as id union select id from tbl) t
where (id + 1 not in (select id from tbl)) and
(id >= 100000)
order by id
limit 1;
The number 100000 is to be used if the first number of the sequence is 100001 (as in the original question); otherwise it is to be modified accordingly
"limit 1" is used in order to have just the first available number instead of the full sequence
For people using Oracle, the following can be used:
select a, b from (
select ID + 1 a, max(ID) over (order by ID rows between current row and 1 following) - 1 b from MY_TABLE
) where a <= b order by a desc;
The following SQL code works well with SqLite, but should be used without issues also on MySQL, MS SQL and so on.
On SqLite this takes only 2 seconds on a table with 1 million rows (and about 100 spared missing rows)
WITH holes AS (
SELECT
IIF(c2.id IS NULL,c1.id+1,null) as start,
IIF(c3.id IS NULL,c1.id-1,null) AS stop,
ROW_NUMBER () OVER (
ORDER BY c1.id ASC
) AS rowNum
FROM |mytable| AS c1
LEFT JOIN |mytable| AS c2 ON c1.id+1 = c2.id
LEFT JOIN |mytable| AS c3 ON c1.id-1 = c3.id
WHERE c2.id IS NULL OR c3.id IS NULL
)
SELECT h1.start AS start, h2.stop AS stop FROM holes AS h1
LEFT JOIN holes AS h2 ON h1.rowNum+1 = h2.rowNum
WHERE h1.start IS NOT NULL AND h2.stop IS NOT NULL
UNION ALL
SELECT 1 AS start, h1.stop AS stop FROM holes AS h1
WHERE h1.rowNum = 1 AND h1.stop > 0
ORDER BY h1.start ASC