SQL - for each entry in a table - check for associated row - sql

I have a log table which logs a start row, and a finish row for a particular event.
Each event should have a start row, and if everything goes ok it should have an end row.
But if something goes wrong then the end row may not be created.
I want to SELECT everything in the table that has a start row but not an associated end row.
For example, consider the table like this:
id event_id event_status
1 123 1
2 123 2
3 234 1
4 234 2
5 456 1
6 678 1
7 678 2
Notice that the id column 5 has a start row but no end row. Start is an event_status of 1, end is an event_status of 2.
How can i pull back all the event_ids which have a start row but not an end row>?
This is for mssql.

You could use a not exists subquery to demand that no other row exists that ends the event:
select *
from YourTable t1
where status = 1
and not exists
(
select *
from YourTable t2
where t2.event_id = t1.event_id
and t2.status = 2
)

You can try with left self join as below:
select y1.event_id from #yourevents y1 left join #yourevents y2
on y1.event_id = y2.event_id
and y1.event_status = 1
and y2.event_status = 2
where y2.event_id is null
and y1.event_status = 1

In this particular case you could use one of 3 solutions:
Solution 1. The classic
Check if there is no end status
SELECT *
FROM myTable t1
WHERE NOT EXISTS (
SELECT *
FROM myTable t2
WHERE t1.event_id = t2.event_id AND t2.status=2
)
Solution 2. Make it pretty. Don't do subqueries with so many parentheses
The same check, but in a more concise and pretty manner
SELECT t1.*
FROM myTable t1
LEFT JOIN myTable t2 ON t1.event_id = t2.event_id AND t2.status=2
-- Doesn't exist
WHERE t2.event_id IS NULL
Solution 3. Look for the last status for each event
More flexibility in case the status logic becomes more complicated
WITH last_status AS (
SELECT
id,
event_id,
status,
-- The ROWS BETWEEN ..yadda yadda ... FOLLOWING might be unnecessary. Try, check.
last_value(status) OVER (PARTITION BY event_id ORDER BY status ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_status
FROM myTable
)
SELECT
id,
event_id,
status
FROM last_events
WHERE last_status<>2
There are more, with min/max queries and others. Pick what best suits your need for cleanliness, readability and versatility.

Related

SQL: status flips between 1 and 2; select all statuses, which are 2 since the last time it was 1

I have write only DB log of changes I keep track of (or statuses) and values fluctuate between 1 and 2. In the below table; ID is identity column, STATUS is either 1 or 2 and USER is a user id.
If the latest status (i.e. max ID) for a given user is 1 then my query should return nothing (1 = good). So running the query against the data above would be just that.
Here comes my question: I want to query for all statuses of 2 since the last time it was 1. Here is sample data:
In this case my query should return 2 and 3 (ID) because these have statuses of 2 since the last time it was 1.
This next query should return nothing because the latest status for this user was switched to 1:
And finally this next one should return 5 (because the latest status is 2 since the last time it was 1):
There is no date field in this table, you can only work with MAX(ID) ... GROUP BY ID, USER
How can I do this? I'm using MS SQL 2016.
You can use windowed aggregates to do this
WITH T
AS (SELECT ID,
STATUS,
[USER],
MAX(CASE WHEN Status = 1 THEN ID END) OVER ( PARTITION BY [USER]) AS MaxS1
FROM YourTable)
SELECT *
FROM T
WHERE Status = 2
AND (ID > MaxS1 OR MaxS1 IS NULL)
Remove the OR MaxS1 IS NULL if you don't want the rows returned for users that have 2 and have never had 1 as a status
You can filter with not exists:
select t.*
from mytable t
where
t.status = 2
and not exists (
select 1 from mtyable t1 where t1.user = t.user and t1.id > t.id and t1.status = 1
)
This phrases as: all records with status 2 that have no following record (ie a record with the same user and a greatest id) with status = 1. If there are no records with status = 1 for a given user, all its records will be returned.
This can also be expressed with a left join antipattern:
select t.*
from mytable t
left join mytable t1 on t1.user = t.user and t1.id > t.id and t1.status = 1
where t1.id is null and t.status = 2

How can I remove a record when it has a higher level on column with SQL oracle?

For example:
I have two situations
card cardholder level
1 1 1
1 1 2
1 1 3
card cardholder level
1 1 2
1 1 3
On both situations I only want the first record.
I got something like this, but do not return the expected:
delete from table
where card in (select card from (
select cardholder, card, count(card), count(cardholder) from table
group by cardholder, card
having COUNT (card) > 1))
and level = '3'
;
Use a correlated DELETE comparing with the MIN level value for each card,cardholder combination. I have changed the column level to lvl as level is an Oracle keyword.
DELETE
FROM tab1 t
WHERE EXISTS (
SELECT 1
FROM (
SELECT card
,cardholder
,MIN(lvl) AS lvl
FROM tab1
GROUP BY card
,cardholder
) s
WHERE t.card = s.card
AND t.cardholder = s.cardholder
AND
t.lvl > s.lvl
);
Demo
It is not totally clear what do you mean by
"I only want the first record" .
I have assumed it to be MIN.
If you have an ID PK column and your first record means the one with the least id, then you may use KEEP..DENSE_RANK along with MIN(lvl)
MIN(lvl) KEEP ( DENSE_RANK FIRST ORDER BY ID )
If you want to leave the record with smallest 'level' value, you can use below query:
DELETE FROM table t WHERE EXISTS (SELECT 1 table t2 WHERE t2.level < t.level)

shifting some columns one record back or forward

I have a table with about 8000 rows and 15 columns. After I have inserted the data I saw that my data was wrong after a number of records (let's say 1000) some column values belong to the previous record some thing like this:
A B C (A+B)
==================================
1 1 2
2 2 4
3 3 6
4 4 8
5 5
6 6 10
7 7 12
8 8 14
9 9 16
Now I have to either move some column values a record back or forward and I don't actually have much option testing it I'm afraid I may overwrite some data and ruin the whole table
I should do something like this but for about 7000 records:
update table1
set B = (select B from table1 where id = 1000)
where id = 999
Any ideas?
If you know the ids are sequential with no gaps, you can use a join to look up the value you want:
update t1
set c = tt1.c
from table1 t1 join
table1 t2
on t1.id = t2.id - 1
where t1.id > 1000;
If you cannot trust the ids, you can create the appropriate sequential number without gaps using row_number():
with toupdate as (
select t.*, row_number() over (order by id) as seqnum
from table1
)
update t1
set c = tt1.c
from toupdate t1 join
toupdate t2
on t1.seqnum = t2.seqnum - 1
where t1.id > 1000;
Create another table with the same fields as the table in question. Insert the bad records. Fix the data in the new table. Update the real table from the new one.
First, you should always test your statements before making definate changes to your data. You could start a transaction and only commit when certain it went well or make a copy of your table (select * into x from y) and test on that.
To answer your question, try something like this;
WITH dataToUpdate AS(
SELECT RowNr ,
DATA,
DataFromPreviousRow = FIRST_VALUE(data) OVER (ORDER BY RowNr ROWS 1 PRECEDING)
FROM dbo.test
)
UPDATE dataToUpdate
SET data = dataToUpdate.DataFromPreviousRow;

Complex SQL Query (at least for me)

I'm trying to develop a sql query that will return a list of serial numbers. The table is set up that whenever a serial number reaches a step, the date and time are entered. When it completes the step, another date and time are entered. I want to develop a query that will give me the list of serial numbers that have entered the step, but not exitted the step. They may enter more than once, so I'm only looking for serial numbers that don't have exits after and enter.
Ex.(for easy of use, call the table "Table1")
1. Serial | Step | Date
2. 1 | enter | 10/1
3. 1 | exit | 10/2
4. 1 | enter | 10/4
5. 2 | enter | 10/4
6. 3 | enter | 10/5
7. 3 | exit | 10/6
For the above table, serial numbers 1 and 2 should be retrieved, but 3 should not.
Can this be done in a signle query with sub queries?
select * from Table1
group by Step
having count(*) % 2 = 1
this is when there cannot be two 'enter' but each enter is followed by an 'exit' (as in the example provided)
Personally I think this is something best done through a change in the way the data is stored. The current method cannot be efficient or effective. Yes you can mess around and find a way to get the data out. However, what happens when you have multiple entered steps with no exit for the same serialNO? Yeah it shouldn't happen but sooner or later it will unless you have code written to prevent it (code which coupld get complicated to write). It would be cleaner to have a table that stores both the enter and exit in the same record. Then it become trivial to query (and much faster) in order to find those entered but not exited.
This will give you all 'enter' records that don't have an ending 'exit'. If you only want a list of serial numbers you should then also group by serial number and select only that column.
SELECT t1.*
FROM Table1 t1
LEFT JOIN Table1 t2 ON t2.Serial=t1.Serial
AND t2.Step='Exit' AND t2.[Date] >= t1.[Date]
WHERE t1.Step='Enter' AND t2.Serial IS NULL
I tested this in MySQL.
SELECT Serial,
COUNT(NULLIF(Step,'enter')) AS exits,
COUNT(NULLIF(Step,'exit')) AS enters
FROM Table1
WHERE Step IN ('enter','exit')
GROUP BY Serial
HAVING enters <> exits
I wasn't sure what the importance of Date was here, but the above could easily be modified to incorporate intraday or across-days requirements.
SELECT DISTINCT Serial
FROM Table t
WHERE (SELECT COUNT(*) FROM Table t2 WHERE t.Serial = t2.Serial AND Step = 'exit') <
(SELECT COUNT(*) FROM Table t2 WHERE t.Serial = t2.Serial AND Step = 'enter')
SELECT * FROM Table1 T1
WHERE NOT EXISTS (
SELECT * FROM Table1 T2
WHERE T2.Serial = T1.Serial
AND T2.Step = 'exit'
AND T2.Date > T1.Date
)
If you're sure that you've got matching enter and exit values for the the ones you don't want, you could look for all the serial values where the count of "enter" is not equal to the count of "exit".
If you're using MS SQL 2005 or 2008, you could use a CTE to get the results you're looking for...
WITH ExitCTE
AS
(SELECT Serial, StepDate
FROM #Table1
WHERE Step = 'exit')
SELECT A.*
FROM #Table1 A LEFT JOIN ExitCTE B ON A.Serial = B.Serial AND B.StepDate > A.StepDate
WHERE A.Step = 'enter'
AND B.Serial IS NULL
If you're not using those, i'd try for a subquery instead...
SELECT A.*
FROM #Table1 A LEFT JOIN (SELECT Serial, StepDate
FROM #Table1
WHERE Step = 'exit') B
ON A.Serial = B.Serial AND B.StepDate > A.StepDate
WHERE A.Step = 'enter'
AND B.Serial IS NULL
In Oracle:
SELECT *
FROM (
SELECT serial,
CASE
WHEN so < 0 THEN "Stack overflow"
WHEN depth > 0 THEN "In"
ELSE "Out"
END AS stack
FROM (
SELECT serial, MIN(SUM(DECODE(step, "enter", 1, "exit", -1) OVER (PARTITION BY serial ORDER BY date)) AS so, SUM(DECODE(step, "enter", 1, "exit", -1)) AS depth
FROM Table 1
GROUP BY serial
)
)
WHERE stack = "Out"
This will select what you want AND filter out exits that happened without enters
Several people have suggested rearranging your data, but I don't see any examples, so I'll take a crack at it. This is a partially-denormalized variant of the same table you've described. It should work well with a limited number of "steps" (this example only takes into account "enter" and "exit", but it could be easily expanded), but its greatest weakness is that adding additional steps after populating the table (say, enter/process/exit) is expensive — you have to ALTER TABLE to do so.
serial enter_date exit_date
------ ---------- ---------
1 10/1 10/2
1 10/4 NULL
2 10/4 NULL
3 10/5 10/6
Your query then becomes quite simple:
SELECT serial,enter_date FROM table1 WHERE exit_date IS NULL;
serial enter_date
------ ----------
1 10/4
2 10/4
Here's a simple query that should work with your scenario
SELECT Serial FROM Table1 t1
WHERE Step='enter'
AND (SELECT Max(Date) FROM Table1 t2 WHERE t2.Serial = t1.Serial) = t1.Date
I've tested this one and this will give you the rows with Serial numbers of 1 & 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