Join two queries into two columns - sql

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;

Related

Two select count queries and then calculate percentage

I'd like to combine two queries
SELECT COUNT(*) FROM abc;
SELECT COUNT(Status) FROM ABC WHERE Status='Active';
And then calculate the percentage (by taking the 2nd query divided by first query). I'd like to achieve this in one single query. What i've attempted so far:
SELECT COUNT(*) AS A FROM abc
UNION
SELECT COUNT(Status) AS B FROM ABC WHERE Status='Active';
UNION
SELECT(COUNT(Status)*100/SELECT COUNT(*) FROM abc)) AS %ofAB FROM abc WHERE Status='Active'
What I get:
A
--
31
36
86,11111111
What I want:
A | B | %ofAB
---------------------
36 | 31 | 86,1111111%
This should give you what you want:
SELECT
COUNT(*) AS TotalCount,
SUM(IIF(Status = 'Active', 1, 0)) AS ActiveCount,
ROUND((SUM(IIF(Status = 'Active', 1, 0)) * 100/ COUNT(*)),2) AS PctActive
FROM
Abc
EDIT: Didn't notice that this was for Access. I don't know if CAST is available in Access, so you may need to use an equivalent function to make sure that the integers don't simply yield 1 or 0. It's possible that Access will convert a division into a decimal automatically, but in SQL Server it does not.

Summing two tables into one table in Jet SQL

Another SQL question if I may.
I have two tables (generated from TRANSFORM - PIVOT queries in Jet SQL)
Category ID Account Jan Feb ... Dec
1 1 Cash 10 20 30
1 2 Card 100 200 300
1 3 Savings 200 400 600
and
Category ID Account Jan Feb ... Dec
1 1 Cash -5 -10 -20
1 2 Card -100 -200
1 3 Savings -100 -400
Category, ID and Account will always be the same in two tables. There will be no accounts that occur in one table that don't occur in others. There may be NULL values in either table but there will always be a matching cell in each table.
What I would like is
Category ID Account Jan Feb ... Dec
1 1 Cash 5 10 10
1 2 Card 100 100 100
1 3 Savings 100 400 200
I have played around with UNION and JOIN queries but can't get there.
Thanks again,
Andy
Edit - The original queries are
TRANSFORM Sum(Items.amount) AS total
SELECT Accounts.accCategory, Accounts.ID, Accounts.comment AS Account
FROM Accounts INNER JOIN Items ON Accounts.ID = Items.accFrom
WHERE (((Year([idate]))=2013) AND ((Items.category)<>3 Or (Items.category) Is Null) AND ((Accounts.accCategory)=6 OR (Accounts.accCategory)=7) AND ((Accounts.curr)=1))
GROUP BY Accounts.accCategory, Accounts.ID, Accounts.comment
PIVOT Format(idate,'mmm') IN ('Jan','Feb','Mar','Apr', 'May','Jun','Jul','Aug','Sep','Oct','Nov','Dec');
and
TRANSFORM Sum(Items.amount) AS total
SELECT Accounts.accCategory, Accounts.ID, Accounts.comment AS Account
FROM Accounts INNER JOIN Items ON Accounts.ID = Items.accFrom
WHERE (((Year([idate]))=2013) AND ((Items.category)=3) AND ((Items.comment)='Monthly') AND ((Accounts.accCategory)=6) AND ((Accounts.curr)=1))
GROUP BY Accounts.accCategory, Accounts.ID, Accounts.comment
PIVOT Format(idate,'mmm') In ('Jan','Feb','Mar','Apr', 'May','Jun','Jul','Aug','Sep','Oct','Nov','Dec');
One approach using the join of the 2 tables deb and cred is as follows:
SELECT cred.accCategory, cred.ID, cred.Account, [deb].[Jan]+[cred].[Jan] AS tot_Jan
FROM cred INNER JOIN deb ON (cred.Account = deb.Account) AND (cred.ID = deb.ID) AND (cred.accCategory = deb.accCategory);
This screenshot shows how the query is constructed:
And here are the results (excuse the table formatting):
accCategory ID Account tot_Jan
1 1 Cash 5
1 2 Card 100
1 3 Savings 100
Be warned that the JOIN is an equi-JOIN so it means the same number of records must exist in BOTH tables else you will find that credits where a debit does not exist will be dropped! So make sure that both input tables are consistent with the same key values (the 3 GROUP BY fields in your earlier PIVOT query).
The alternative approach is the UNION with a GROUP BY to calculate the total from it. I will paste that shortly.
========== EDIT ============
You can build this up in stages. First I did SELECT * FROM cred UNION SELECT * FROM deb and looked at in Design View.
Went back into the SQL view and made sure the UNION query has brackets around it and SELECT sub.* FROM before the brackets and AS SUB after the brackets.
Then I modified the query to include the GROUP BY and Sum(Jan) bits. The final SQL is here and a screenshot follows.
SELECT sub.accCategory, sub.ID, sub.Account, Sum(sub.Jan) AS SumOfJan
FROM (SELECT * FROM cred UNION SELECT * FROM deb) AS sub
GROUP BY sub.accCategory, sub.ID, sub.Account;

SQL - Comparing against multiple values in the same column?

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'.

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.