SQL - Comparing against multiple values in the same column? - sql

Ok, so I'm trying to write some SQL and I'm not sure how to tackle this situation. I have a table similiar to what is below. The basic idea is that I need to get the records that are in an 'H' status (easy enough), but I need to exclude records that were in an 'H' status and moved on to an 'A' status at a later date.
So ideally, the results should only return the last two records, IDs 03 and 04. How would you guys do this?
ID STATUS STAT_DATE
01 A 05/01/2013
01 H 05/01/2012
02 A 12/01/2013
02 H 12/01/2012
03 H 03/01/2009
04 H 02/01/2008

You could do it this way:
select *
from t t1
where status='H' and not exists(
select *
from t t2
where t1.id=t2.id and t2.status='A' and t2.stat_date > t1.stat_date)
That will give you all entries of table t with status='H' where there is no entry in t with the same id, a later date, and status='A'.

Related

SQL Server : insert missing rows to be a total of 7 for each BAY

I need to insert missing records with a value of 0 in the column TOP_TIER
to complete a total of 7 records for every LAST_YARD BAY value.
This is my simple query:
SELECT
LAST_YARD_BAY, LAST_YARD_ROW, TOP_TIER
FROM
MAX_TIER;
And this is a part of the result set:
]
As you can see I have 7 records (00A, 00B, 00C, 00D, 00E, 00F, 00G) for BAY 005, same for BAY 009 and 012, but not for BAY 007 (00E, 00F, 00G) and so many others that are not shown in the picture.
My question is: how can I insert the missing records with a value of 0 in TOP_TIER so that I can get the following: (for all Bays not just Bay 007)
007 00A 0
007 00B 0
007 00C 0
007 00D 0
007 00E 05
007 00F 04
007 00G 01
You can cross join the lasr_yard_bays with the last_yard_rows to get the complete set. With a NOT EXISTS you can the filter out only the missing ones and insert them.
INSERT INTO max_tier
(last_yard_bay,
last_yard_row,
top_tier)
SELECT lyb.last_yard_bay,
lyr.last_yard_row,
0 top_tier
FROM (SELECT DISTINCT
last_yard_bay
FROM max_tier) lyb
CROSS JOIN (VALUES ('00A'),
('00B'),
...
('00G')) lyr (last_yard_row)
WHERE NOT EXISTS (SELECT *
FROM max_tier mt
WHERE mt.last_yard_bay = lyb.last_yard_bay
AND mt.last_yard_row = lyr.last_yard_row);
I think this is pretty much the same as your last question, with the table name changed and filtering different. You don't have aggregation, so it is somewhat different, but the solution is very similar:
SELECT lyb.LAST_YARD_BAY, lyr.LAST_YARD_ROW,
COALESCE(mt.LAST_YARD_TIER, 0) as MAX_TIER
FROM (SELECT DISTINCT LAST_YARD_BAY FROM MAX_TIER) lyb CROSS JOIN
(SELECT DISTINCT LAST_YARD_ROW FROM MAX_TIER) lyr LEFT JOIN
MAX_TIER mt
ON mt.LAST_YARD_BAY = lyb.LAST_YARD_BAY AND
mt.LAST_YARD_ROW = lyr.LAST_YARD_ROW
ORDER BY lyr.LAST_YARD_BAY, lyr.LAST_YARD_ROW;
This assumes that all bays and rows you want are in the table. If not, you can use a table constructor (i.e. VALUES()) to generate them.

Join two queries into two columns

I am new to PostgreSQL and I'm learning a lot in a short time but haven't learnt everything yet so forgive me if this is a simple question.
I have searched for this and a JOIN should bring them together but with my setup I can't get it to work.
PROBLEM
I have a WITH statement that has all the criteria for what I want to search for
Then I wish you perform two SELECT queries, one for each user (only 2 users will be searched for at one time)
DATA
The Data consists of time and an the count of the amount, there will be criteria in the with clause where only the username is used further in the queries.
EXAMPLE
HOUR AMOUNT
01 200
02 300
03 500
04 800
Using a UNION I get both queries in the same column
HOUR AMOUNT
01 200
01 75
02 300
02 50
03 500
03 21
04 800
04 300
But I simply want the second query to appear in a new column
HOUR AMOUNT AMOUNT2
01 200 75
02 300 50
03 500 21
04 800 300
I've simplified the SQL so it's not too long:
WITH My_With AS (
...
)
SELECT time, count(time) AS Column_1
FROM My_With
WHERE
username= 'jon'
GROUP BY time
UNION
SELECT time, count(time) AS Column_2
FROM My_With
WHERE
username= 'bob'
GROUP BY time
ORDER BY time
;
I know this is probably really simple but I just can't figure it out, I've managed to put the second set of amounts into a second column but it still generates 2 tows for the time.
I think you can do what you want with conditional aggregation:
SELECT time,
COUNT(*) FILTER (WHERE username = 'jon') as cnt_jon,
COUNT(*) FILTER (WHERE username = 'bob') as cnt_bob
FROM My_With
GROUP BY time;
Or, with less typing:
SELECT time,
SUM( (username = 'jon')::int ) as cnt_jon,
SUM( (username = 'bob')::int ) as cnt_bob
FROM My_With
GROUP BY time;

SQL Server 2005: Insert missing records in table that is in another reference table

I need help with the following. I have 2 tables. The first holds data captured by client. example.
[Data] Table
PersonId Visit Tested Done
01 Day 1 Eyes Yes
01 Day 1 Ears Yes
01 Day 2 Eyes Yes
01 Day 3 Eyes Yes
02 Day 1 Eyes Yes
02 Day 2 Ears Yes
02 Day 2 Smell Yes
03 Day 2 Eyes Yes
03 Day 2 Smell Yes
03 Day 3 Ears Yes
and the second table holds info of what needs to be tested.
[Ref] Table
Visit Test
Day 1 Eyes
Day 1 Ears
Day 1 Smell
Day 2 Eyes
Day 2 Ears
Day 2 Smell
Day 3 Eyes
Day 3 Ears
Day 3 Smell
now I'm trying to write an insert query on the [Data] to insert the non-existent tests that needed to be performed. The result I'm looking for example:
[Data] table after:
PersonId Visit Tested Done
01 Day 1 Eyes Yes
01 Day 1 Ears Yes
01 Day 1 Smell No
01 Day 2 Eyes Yes
01 Day 2 Ears No
01 Day 2 Smell No
01 Day 3 Eyes Yes
01 Day 3 Ears No
01 Day 3 Smell No
02 Day 1 Eyes Yes
02 Day 1 Ears No
02 Day 1 Smell No
02 Day 2 Eyes No
02 Day 2 Ears Yes
02 Day 2 Smell Yes
02 Day 3 Eyes No
02 Day 3 Ears No
02 Day 3 Smell No
03 Day 1 Eyes No
03 Day 1 Ears No
03 Day 1 Smell No
03 Day 2 Eyes Yes
03 Day 2 Ears No
03 Day 2 Smell Yes
03 Day 3 Eyes No
03 Day 3 Ears Yes
03 Day 3 Smell No
If needed it will be OK to create a third [results] table.
All help will be much appreciated.
Kind Regards
Jacques
I'm suspicious of the database design if it requires this (along with some other red flags), but the following query should give you what you are asking for:
INSERT INTO Results
(
person_id,
visit,
tested,
done
)
SELECT
P.person_id,
T.visit,
T.test,
'No'
FROM
(SELECT DISTINCT person_id FROM Results) P -- Replace with Persons table if you have one
CROSS JOIN Templates T
LEFT OUTER JOIN Results R ON
R.person_id = P.person_id AND
R.visit = T.visit AND
R.test = T.test
WHERE
R.person_id IS NULL
Or alternatively:
INSERT INTO Results
(
person_id,
visit,
tested,
done
)
SELECT
P.person_id,
T.visit,
T.test,
'No'
FROM
(SELECT DISTINCT person_id FROM Results) P -- Replace with Persons table if you have one
INNER JOIN Templates T ON
NOT EXISTS
(
SELECT *
FROM
Results R
WHERE
R.person_id = P.person_id AND
R.visit = T.visit AND
R.test = T.test
)
I think you'll need a person table with just the personIDs, then you can do a cross join (full outer join) with your test ref table to come up with a schedule of personIDs and expected tests.
Then, with that schedule set, do an outer join with the set of tests performed on personIDs and expect nulls instead of no's.
Then, if you want, you can convert your nulls to 'no'.
This probably isn't the best way, but....What if you were to create a primary key on the [Data] table,
PK: (PersonID, Visit, Tested)
Then you could create a function to insert for each personID, and Day
CREATE PROCEDURE InsertTests
#PersonID int
, #Day nvarchar(10)
Begin
BEGIN TRY
INSERT INTO [Data]
(PersonID, Visit, Tested, Done)
VALUES
(#PersonID, #Day, Eyes, No)
END TRY
BEGIN CATCH
END CATCH
BEGIN TRY
INSERT INTO [Data]
(PersonID, Visit, Tested, Done)
VALUES
(#PersonID, #Day, Ears, No)
END TRY
BEGIN CATCH
END CATCH
BEGIN TRY
INSERT INTO [Data]
(PersonID, Visit, Tested, Done)
VALUES
(#PersonID, #Day, Smell, No)
END TRY
BEGIN CATCH
END CATCH
End
INSERT Data
SELECT P.PersonID, R.Visit, D.Test, 'No'
FROM
Person P -- or (SELECT DISTINCT PersonID FROM Data) P
CROSS JOIN Ref R
WHERE
NOT EXISTS (
SELECT 1
FROM Data D
WHERE
P.PersonID = D.PersonID
AND R.Visit = D.Visit
AND R.Test = D.Test
)
And I can't resist posting a short version of #djacobson's answer:
ALTER TABLE Data ADD CONSTRAINT DF_Data_Done DEFAULT ('No')
INSERT Data (PersonID, Visit, Test)
SELECT P.PersonID, R.Visit, D.Test
FROM Person P CROSS JOIN Ref R
EXCEPT SELECT PersonID, Visit, Test FROM Data
Here's a perhaps-simpler solution using Common Table Expressions:
WITH allTestsForEveryone AS
(
SELECT *
FROM (SELECT DISTINCT PersonID FROM DATA) a
CROSS JOIN REF
),
allMissingTests AS
(
SELECT PersonID,Visit,Test FROM allTestsForEveryone
EXCEPT
SELECT PersonID,Visit,Tested FROM DATA
)
INSERT INTO [DATA] (PersonID, Visit, Tested, Done)
SELECT PersonID, Visit, Test, 0 AS Done FROM allMissingTests;
The first CTE (allTestsForEveryone) gives you a set of all tests needed on all days for all persons. In the second CTE (allMissingTests), we subtract the tests that have been taken using the EXCEPT operator, and add a '0' to represent their not-done status when we insert them (you can replace that with 'No' - when I ran this test I used a bit column). We then insert the results of the second CTE into Data.

Aggregate adjacent only records with T-SQL

I have (simplified for the example) a table with the following data
Row Start Finish ID Amount
--- --------- ---------- -- ------
1 2008-10-01 2008-10-02 01 10
2 2008-10-02 2008-10-03 02 20
3 2008-10-03 2008-10-04 01 38
4 2008-10-04 2008-10-05 01 23
5 2008-10-05 2008-10-06 03 14
6 2008-10-06 2008-10-07 02 3
7 2008-10-07 2008-10-08 02 8
8 2008-10-08 2008-11-08 03 19
The dates represent a period in time, the ID is the state a system was in during that period and the amount is a value related to that state.
What I want to do is to aggregate the Amounts for adjacent rows with the same ID number, but keep the same overall sequence so that contiguous runs can be combined. Thus I want to end up with data like:
Row Start Finish ID Amount
--- --------- ---------- -- ------
1 2008-10-01 2008-10-02 01 10
2 2008-10-02 2008-10-03 02 20
3 2008-10-03 2008-10-05 01 61
4 2008-10-05 2008-10-06 03 14
5 2008-10-06 2008-10-08 02 11
6 2008-10-08 2008-11-08 03 19
I am after a T-SQL solution that can be put into a SP, however I can't see how to do that with simple queries. I suspect that it may require iteration of some sort but I don't want to go down that path.
The reason I want to do this aggregation is that the next step in the process is to do a SUM() and Count() grouped by the unique ID's that occur within the sequence, so that my final data will look something like:
ID Counts Total
-- ------ -----
01 2 71
02 2 31
03 2 33
However if I do a simple
SELECT COUNT(ID), SUM(Amount) FROM data GROUP BY ID
On the original table I get something like
ID Counts Total
-- ------ -----
01 3 71
02 3 31
03 2 33
Which is not what I want.
If you read the book "Developing Time-Oriented Database Applications in SQL" by R T Snodgrass (the pdf of which is available from his web site under publications), and get as far as Figure 6.25 on p165-166, you will find the non-trivial SQL which can be used in the current example to group the various rows with the same ID value and continuous time intervals.
The query development below is close to correct, but there is a problem spotted right at the end, that has its source in the first SELECT statement. I've not yet tracked down why the incorrect answer is being given. [If someone can test the SQL on their DBMS and tell me whether the first query works correctly there, it would be a great help!]
It looks something like:
-- Derived from Figure 6.25 from Snodgrass "Developing Time-Oriented
-- Database Applications in SQL"
CREATE TABLE Data
(
Start DATE,
Finish DATE,
ID CHAR(2),
Amount INT
);
INSERT INTO Data VALUES('2008-10-01', '2008-10-02', '01', 10);
INSERT INTO Data VALUES('2008-10-02', '2008-10-03', '02', 20);
INSERT INTO Data VALUES('2008-10-03', '2008-10-04', '01', 38);
INSERT INTO Data VALUES('2008-10-04', '2008-10-05', '01', 23);
INSERT INTO Data VALUES('2008-10-05', '2008-10-06', '03', 14);
INSERT INTO Data VALUES('2008-10-06', '2008-10-07', '02', 3);
INSERT INTO Data VALUES('2008-10-07', '2008-10-08', '02', 8);
INSERT INTO Data VALUES('2008-10-08', '2008-11-08', '03', 19);
SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS (SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS (SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS (SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish)
OR (T2.Start <= L.Finish AND L.Finish < T2.Finish)));
The output from that query is:
01 2008-10-01 2008-10-02
01 2008-10-03 2008-10-05
02 2008-10-02 2008-10-03
02 2008-10-06 2008-10-08
03 2008-10-05 2008-10-06
03 2008-10-05 2008-11-08
03 2008-10-08 2008-11-08
Edited: There's a problem with the penultimate row - it should not be there. And I'm not clear (yet) where it is coming from.
Now we need to treat that complex expression as a query expression in the FROM clause of another SELECT statement, which will sum the amount values for a given ID over the entries that overlap with the maximal ranges shown above.
SELECT M.ID, M.Start, M.Finish, SUM(D.Amount)
FROM Data AS D,
(SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS (SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS (SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS (SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish)
OR (T2.Start <= L.Finish AND L.Finish < T2.Finish)))) AS M
WHERE D.ID = M.ID
AND M.Start <= D.Start
AND M.Finish >= D.Finish
GROUP BY M.ID, M.Start, M.Finish
ORDER BY M.ID, M.Start;
This gives:
ID Start Finish Amount
01 2008-10-01 2008-10-02 10
01 2008-10-03 2008-10-05 61
02 2008-10-02 2008-10-03 20
02 2008-10-06 2008-10-08 11
03 2008-10-05 2008-10-06 14
03 2008-10-05 2008-11-08 33 -- Here be trouble!
03 2008-10-08 2008-11-08 19
Edited: This is almost the correct data set on which to do the COUNT and SUM aggregation requested by the original question, so the final answer is:
SELECT I.ID, COUNT(*) AS Number, SUM(I.Amount) AS Amount
FROM (SELECT M.ID, M.Start, M.Finish, SUM(D.Amount) AS Amount
FROM Data AS D,
(SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS
(SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS
(SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS
(SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish) OR
(T2.Start <= L.Finish AND L.Finish < T2.Finish)))
) AS M
WHERE D.ID = M.ID
AND M.Start <= D.Start
AND M.Finish >= D.Finish
GROUP BY M.ID, M.Start, M.Finish
) AS I
GROUP BY I.ID
ORDER BY I.ID;
id number amount
01 2 71
02 2 31
03 3 66
Review:
Oh! Drat...the entry for 3 has twice the 'amount' that it should have. Previous 'edited' parts indicate where things started to go wrong. It looks as though either the first query is subtly wrong (maybe it is intended for a different question), or the optimizer I'm working with is misbehaving. Nevertheless, there should be an answer closely related to this that will give the correct values.
For the record: tested on IBM Informix Dynamic Server 11.50 on Solaris 10. However, should work fine on any other moderately standard-conformant SQL DBMS.
Probably need to create a cursor and loop through the results, keeping track of which id you are working with and accumulating the data along the way. When the id changes you can insert the accumulated data into a temporary table and return the table at the end of the procedure (select all from it). A table-based function might be better as you can then just insert into the return table as you go along.
I suspect that it may require iteration of some sort but I don't want to go down that path.
I think that's the route you'll have to take, use a cursor to populate a table variable. If you have a large number of records you could use a permanent table to store the results then when you need to retrieve the data you could process only the new data.
I would add a bit field with a default of 0 to the source table to keep track of which records have been processed. Assuming no one is using select * on the table, adding a column with a default value won't affect the rest of your application.
Add a comment to this post if you want help coding the solution.
Well I decided to go down the iteration route using a mixture of joins and cursors. By JOINing the data table against itself I can create a link list of only those records that are consecutive.
INSERT INTO #CONSEC
SELECT a.ID, a.Start, b.Finish, b.Amount
FROM Data a JOIN Data b
ON (a.Finish = b.Start) AND (a.ID = b.ID)
Then I can unwind the list by iterating over it with a cursor, and doing updates back to the data table to adjust (And delete the now extraneous records from the Data table)
DECLARE CCursor CURSOR FOR
SELECT ID, Start, Finish, Amount FROM #CONSEC ORDER BY Start DESC
#Total = 0
OPEN CCursor
FETCH NEXT FROM CCursor INTO #ID, #START, #FINISH, #AMOUNT
WHILE #FETCH_STATUS = 0
BEGIN
#Total = #Total + #Amount
#Start_Last = #Start
#Finish_Last = #Finish
#ID_Last = #ID
DELETE FROM Data WHERE Start = #Finish
FETCH NEXT FROM CCursor INTO #ID, #START, #FINISH, #AMOUNT
IF (#ID_Last<> #ID) OR (#Finish<>#Start_Last)
BEGIN
UPDATE Data
SET Amount = Amount + #Total
WHERE Start = #Start_Last
#Total = 0
END
END
CLOSE CCursor
DEALLOCATE CCursor
This all works and has acceptable performance for typical data that I am using.
I did find one small issue with the above code. Originally I was updating the Data table on each loop through the cursor. But this didn't work. It seems that you can only do one update on a record, and that multiple updates (in order to keep adding data) revert back to the reading the original contents of the record.

SQL query to calculate correct leadtime in table (MS Access)

I have the following table: log(productId, status, statusDate, department...), where productId, status and statusDate is the primary key.
Example:
id1 01 01/01/2009
id1 02 02/01/2009
id1 03 03/01/2009
id1 01 06/01/2009
id1 02 07/01/2009
id1 03 09/01/2009
id2 01 02/01/2009
id2 02 03/01/2009
id2 01 04/01/2009
id2 03 06/01/2009
id3 01 03/01/2009
id3 02 04/01/2009
I want to make a query that retrieves for each productId in status03, the time that has passed between the first time it reached status01 and status03.Result expected:
id1 2
id1 3
id2 4
Any idea? Thank you
How about:
SELECT t.ID, t.Status, t.SDateTime,
(SELECT Top 1 SDateTime
FROM t t1 WHERE t1.Status = 1
AND t1.ID=t.ID
AND t1.SDateTime<t.SDateTime
AND t1.SDateTime>=
Nz((SELECT Top 1 SDateTime
FROM t t2
WHERE t2.Status=3
AND t2.ID=t.ID
AND t2.SDateTime<t.SDateTime),0)) AS DateStart,
[SDateTime]-[DateStart] AS Result
FROM t
WHERE t.Status=3
I love stuff like this. Looks like it's more complicated than either of the other two answers so far have suggested. Here's a solution that will work. My apologies for the nasty formatting. Also, this will work in SQL Server, but I haven't used Access in forever, so you might need to adjust this a bit to work there. Or it may not work at all if Access doesn't support non-equijoins.
SELECT productId, MAX(tbl.TimeBetween)
FROM
(SELECT status_1.productId as productId, status_1.statusDate as status1Date, MIN(status_3.statusDate) as status3Date, DATEDIFF(m,status_1.statusDate, MIN(status_3.statusDate)) as TimeBetween
FROM
(SELECT productId, status, statusDate
FROM log
WHERE status = '01') status_1
INNER JOIN
(SELECT productId, status, statusDate
FROM log
WHERE status = '03') status_3
ON status_1.productId = status_3.productId AND status_3.statusDate > status_1.statusDate
GROUP BY status_1.productId, status_1.statusDate) tbl
GROUP BY productId, status3Date
ORDER BY productId, TimeBetween
The innermost selects get the records for each status.
Those are then joined to give the '03' status records that are greater than their corresponding '01' records (how often do you get to use a non-equijoin?)
They are then filtered to give the MIN '03' record that is still after the corresponding '01' record.
The outermost select enforces your 'first time it reached '01' status rule' since it may go to '01' status multiple times before reaching '03' status.
If someone else has a more elegant solution, I'd love to see it. I have had to write similar queries to this in the past, and I'd love to see a better solution to this type of problem.
This would work in Sql Server. You'll likely have to translate parts of it for Access:
SELECT l3.productID, DATEDIFF(d,COALESCE(l1.statusDate,l3.statusDate),l3.statusDate) AS LeadTime
FROM log l3
LEFT JOIN log l1 ON l1.productID=l3.productID AND l1.Status='01'
AND l1.statusDate =
(
SELECT TOP 1 statusDate
FROM log lsub
WHERE lsub.Status='01' AND lsub.productID=l1.productID AND l1.statusDate<l3.StatusDate
ORDER BY statusDate DESC
)
WHERE l3.Status='03'