Postgres recursive CTE or crosstab function - sql

I try to generate some user statistics from a table that includes logging information.
**TABLE users**
user_id | user_name
-------------------
1 | julia
2 | bob
3 | sebastian
**TABLE logs**
user_id | action | timepoint
------------------------------------
1 | create_quote | 2015-01-01
1 | send_quote | 2015-02-03
1 | create_quote | 2015-02-02
1 | start_job | 2015-01-15
2 | start_job | 2015-02-23
2 | send_quote | 2015-03-04
2 | start_job | 2014-12-02
My desired output is the following table
user_id | username | create_quote | send_quote | start_job
-----------------------------------------------------------
1 | julia |2 | 1 | 1
2 | bob |0 | 1 | 1
3 | sebastian |0 | 0 | 0
It includes all users (even if there was nothing logged), but only the actions between date '2015-01-01' and '2015-05-31'. Actions are counted/grouped by action type and user.
The SQL statement could look someting like
SELECT * FROM myfunction() WHERE to_char(timepoint, 'YY/MM') BETWEEN '15/01' AND '15/05';
Do you have any idea how to manage this? I've been trying around with CTEs and recursion as well as with the crosstab function but could not find any solution.

I think the crosstab function would be a lot more elegant, but in the case that you don't have the extension loaded or, like me, struggle with the syntax, this is a kind of clumsy, brute-force way you could do it:
CREATE OR REPLACE FUNCTION get_stats(
from_date date,
thru_date date)
RETURNS table (
user_id integer,
username text,
create_quote bigint,
send_quote bigint,
start_job bigint
) AS
$BODY$
select
l.user_id, u.username,
sum (case when action = 'create_quote' then 1 else 0 end) as create_quote,
sum (case when action = 'send_quote' then 1 else 0 end) as send_quote,
sum (case when action = 'start_job' then 1 else 0 end) as start_job
from
logs l
join users u on l.user_id = u.user_id
where
l.timepoint between from_date and thru_date
group by
l.user_id, u.username
$BODY$
LANGUAGE sql VOLATILE
COST 100
ROWS 1000;
And then your query would be:
select * from get_stats('2015-01-01', '2015-05-31')
Personally, I would skip the function and just create it as a query, but it's conceivable there are reasons where you would want the function wrapper.
-- EDIT --
Based on an attempted edit, I see you may be okay with a query. Also, you wanted users that have no entries.
With all of that in mind, I think this might work:
select
u.user_id, u.username,
sum (case when action = 'create_quote' then 1 else 0 end) as create_quote,
sum (case when action = 'send_quote' then 1 else 0 end) as send_quote,
sum (case when action = 'start_job' then 1 else 0 end) as start_job
from
users u
left join logs l on
l.user_id = u.user_id and
l.timepoint between '2015-01-01' and '2015-05-31'
group by
u.user_id, u.username

Related

MS-Access Query to PostgreSQL View

I am converting a microsoft access query into a postgresql view. The query has obvious components that I have found reasonable answers to. However, I am still stuck on getting the final result:
SELECT All_Claim_Data.Sec_ID,
Sum(IIf([Type]="LODE",IIf([Status]="Active",1,0),0)) AS LD_Actv,
Sum(IIf([Type]="LODE",IIf([Loc_Date]>#8/31/2017#,IIf([Loc_Date]<#9/1/2018#,1,0),0),0)) AS LD_stkd_17_18,
Sum(IIf([Type]="LODE",IIf([Loc_Date]>#8/31/2016#,IIf([Loc_Date]<#9/1/2017#,1,0),0),0)) AS LD_stkd_16_17,
Sum(IIf([Type]="LODE",IIf([Loc_Date]<#1/1/1910#,IIf(IsNull([Clsd_Date]),1,(IIf([Clsd_Date]>#1/1/1900#,1,0))),0),0)) AS Actv_1900s,
Sum(IIf([Type]="LODE",IIf([Loc_Date]<#1/1/1920#,IIf(IsNull([Clsd_Date]),1,(IIf([Clsd_Date]>#1/1/1910#,1,0))),0),0)) AS Actv_1910s,
FROM All_Claim_Data.Sec_ID,
GROUP BY All_Claim_Data.Sec_ID,
HAVING (((Sum(IIf([casetype_txt]="LODE",1,0)))>0));
Realizing I need to use CASE SUM WHEN, here is what I have worked out so far:
CREATE OR REPLACE VIEW hgeditor.vw_test AS
SELECT All_Claim_Data.Sec_ID,
SUM (CASE WHEN(Type='LODE' AND WHEN(Status='Active',1,0),0)) AS LD_Actv,
SUM (CASE WHEN(Type='LODE' AND WHEN(Loc_Date>'8/31/2017' AND Loc_Date<'9/1/2018',1,0),0),0)) AS LD_stkd_17_18,
SUM (CASE WHEN(Type='LODE' AND WHEN(Loc_Date<'1/1/1910' AND (IsNull(Clsd_Date),1,(WHEN([Clsd_Date]>'1/1/1900',1,0))),0),0)) AS Actv_1900s
FROM All_Claim_Data.Sec_ID,
GROUP BY All_Claim_Data.Sec_ID,
HAVING (((SUM(IIf(Type='LODE',1,0)))>0));
The goal is to count the number of instances in which the Sec_ID has the following:
has (Type = LODE and Status = Active) = SUM integer
has (Type = LODE and Loc_Date between 8/31/2017 and 9/1/2018) = SUM Integer
My primary issue is getting a SUM integer to populate in the new columns
Case expressions are the equivalent to the Access IIF() functions, but WHEN isn't a function so it isn't used by passing a set of parameters. Think of it as being a tiny where clause instead, it evaluates one or more predicates to determine what to do, and the action taken is established by what you specify after THEN
CREATE OR REPLACE VIEW hgeditor.vw_test AS
SELECT
All_Claim_Data.Sec_ID
, SUM( CASE
WHEN TYPE = 'LODE' AND
STATUS = 'Active' THEN 1
ELSE 0
END ) AS LD_Actv
, SUM( CASE
WHEN TYPE = 'LODE' AND
Loc_Date > to_date('08/31/2017','mm/dd/yyyy') AND
Loc_Date < to_date('09/1/2018','mm/dd/yyyy') THEN 1
ELSE 0
END ) AS LD_stkd_17_18
, SUM( CASE
WHEN TYPE = 'LODE' AND
Loc_Date < to_date('1/1/1910','mm/dd/yyyy') AND
[Clsd_Date] > to_date('1/1/1900','mm/dd/yyyy') THEN 1
ELSE 0
END ) AS Actv_1900s
FROM All_Claim_Data.Sec_ID
GROUP BY
All_Claim_Data.Sec_ID
HAVING COUNT( CASE
WHEN Type = 'LODE' THEN 1
END ) > 0
;
By the way, you should NOT be relying on MM/DD/YYYY as dates in Postgres
nb: Aggregate functions ignore NULL, take this example:
+----------+
| id value |
+----------+
| 1 x |
| 2 NULL |
| 3 x |
| 4 NULL |
| 5 x |
+----------+
select
count(*) c_all
, count(value) c_value
from t
+-------+----------+
| c_all | c_value |
+-------+----------+
| 5 | 3 |
+-------+----------+
select
sum(case when value IS NOT NULL then 1 else 0 end) sum_case
, count(case when value IS NOT NULL then 1 end) count_case
from t
+----------+-------------+
| sum_case | count_case |
+----------+-------------+
| 3 | 3 |
+----------+-------------+

Transpose row-columns and Counting number of instances per column in SQLITE

I have a table structured as
|creationDate|rule. |position|
|01.01.2018 |squid:S1132|12 |
|01.01.2018 |squid:S1132|14 |
|01.01.2018 |squid:S1132|19 |
|01.01.2018 |squid:S1121|12 |
|01.01.2018 |squid:S1121|14 |
|01.02.2018 |squid:S1130|12 |
My goal is to count the number of rules per date, reporting them in different columns.
|creationDate| S1132 | S1121 | S1130 |
|01.01.2018 | 3 |2 | 0 |
|01.02.2018 | 0 |0 | 1 |
I have a total of 180 rules...
Is it possible to make it in a single query?
Running this query
select creationDate , count("creationDate") as "squid:S1132"
from SONAR_ISSUES
where rule='squid:S1132' group by creationDate
I obtain this result
|creationDate|S1132 |
|01.01.2018 |3 |
I can do a similar query for each rule, but then, I am not able to merge them...
try by using case when
select creationDate ,count(case when rule='squid:S1132' then 1 end) as S1132,
count(case when rule='squid:S1121' then 1 end) as S1121,
count(case when rule='squid:S1130' then 1 end) as S1130
from SONAR_ISSUES
group by
creationDate
You can try using conditional aggregation
DEMO
select
creationDate,
count(case when rule='squid:S1132' then "creationDate" end) as "squid:S1132",
count(case when rule='squid:S1121' then "creationDate" end) as "squid:S1121" ,
count(case when rule='squid:S1130' then "creationDate" end) as "squid:S1130"
from SONAR_ISSUES
group by creationDate

How can I convert rows to columns in SQL

I've been referencing this question a lot, but my case is a little different so I haven't quite figured it out.
I have a set of data that looks something like this:
--------------------------------------
| Id | Answer| Question | EntryBy
--------------------------------------
| 1 |John |Name? | User1 |
| 2 |2.4 |Raiting? | User1 |
| 3 |ZH1E4A |UserId? | User1 |
| 4 |Paul |Name? | User1 |
| 5 |2.3 |Raiting? | User1 |
| 6 |Ron |Name? | User2 |
| 7 |857685 |UserId? | User2 |
----------------------------
I need to pivot the data so that it's structured like so:
----------------------------------------------------------
| Category | Name? | Raiting? | UserId? | EntryBy |
----------------------------------------------------------
| Category1| John | 2.4 | ZH1E4A | User1 |
| Category1| Paul | 2.3 | NULL | User1 |
| Category1| Ron | NULL | 857685 | User2 |
As you can see, there are multiple "Questions" but they don't always have an answer/value. I know the exact number of questions that may be asked/answered so I'm assuming that may help if I used a CASE expression?
Note: The 'Category' column in the last table is just another value similar to 'EntryBy' in the first. I've attempted the pivot approach in the cited question, but the results I get are not correct. I also tried the CASE statement but it resulted in an error since the Questions are titled the same.
Being 2008, we lose the sum() over function, but we can simulate it via a cross apply to create a Grp indicator.
This also assumes the ID is sequential (risky) and Name? is the Group Key.
Also, check the spelling of RAITING
Also, I have no idea where Category is coming from
Example
Select [Name?] = max(case when Question = 'Name?' then Answer end)
,[Raiting?] = max(case when Question = 'Raiting?' then Answer end)
,[UserId?] = max(case when Question = 'UserId?' then Answer end)
,[EntryBy?] = max([EntryBy])
From (
Select A.*
,B.Grp
From YourTable A
Cross Apply (Select Grp=count(*) from YourTable where Question='Name?' and ID<=A.ID) B
) A
Group By Grp
Returns
Name? Raiting? UserId? EntryBy?
John 2.4 ZH1E4A User1
Paul 2.3 NULL User1
Ron NULL 857685 User2
This only does a single parse of the table (or "Values Table Expression") for this one, compared to John's, which does 2:
WITH VTE AS (
SELECT *
FROM (VALUES
(1,'John ','Name? ','User1'),
(2,'2.4 ','Raiting?','User1'),
(3,'ZH1E4A','UserId? ','User1'),
(4,'Paul ','Name? ','User1'),
(5,'2.3 ','Raiting?','User1'),
(6,'Ron ','Name? ','User2'),
(7,'857685','UserId? ','User2'),
(8,'Steve ','Name? ','User3'),
(9,'2.5 ','Raiting?','User3'),
(10,'Jane ','Name? ','User3'),
(11,'GA18S1','UserId? ','User3'),
(12,'2.3 ','Raiting?','User3'),
(13,'ABH12D','UserId? ','User3')) V(ID, Answer, Question, EntryBy)),
Groups AS(
SELECT *,
ROW_NUMBER() OVER (ORDER BY ID ASC) -
ROW_NUMBER() OVER (PARTITION BY CASE WHEN Question = 'Name?' THEN 0 ELSE 1 END ORDER BY ID ASC) AS Grp
FROM VTE)
SELECT 'Category1' AS Category,
MAX(CASE Question WHEN 'Name?' THEN Answer ELSE NULL END) AS [Name?],
MAX(CASE Question WHEN 'Raiting?' THEN Answer ELSE NULL END) AS [Raiting?],
MAX(CASE Question WHEN 'UserID?' THEN Answer ELSE NULL END) AS [UserID?],
EntryBy
FROM Groups
GROUP BY CASE Grp WHEN 0 THEN Grp + 1 ELSE Grp END,
EntryBy
ORDER BY CASE Grp WHEN 0 THEN Grp + 1 ELSE Grp END;
I also added a few extra values to display what happens if the sequencing goes wrong.
Result set:
Category Name? Raiting? UserID? EntryBy
--------- ------- -------- ------- -------
Category1 John 2.4 ZH1E4A User1
Category1 Paul 2.3 NULL User1
Category1 Ron NULL 857685 User2
Category1 Steve 2.5 NULL User3
Category1 Jane 2.3 GA18S1 User3

SQL left join for users table

I have the following example table (simplified from what I really have as there are a few more joins in there)
|permission|username|
| perm1 | u1 |
| perm2 | u1 |
| perm3 | u1 |
| perm1 | u2 |
| perm4 | u2 |
I have many users and many groups in this table
I am having trouble with user permissions where some users can do things in my application and others cannot. At the moment I am having to manually go through and figure out what the differences are.
I want to crate a query from the above table that gives me an answer like this
|permission| u1 | u2 |
|perm1 | 1 | 1 |
|perm2 | 1 | 0 |
|perm3 | 1 | 0 |
|perm4 | 0 | 1 |
I do not really want 1 and 0 under the users but I want something to I can easily visualise that user 1 has these permissions and user2 does not. This would be really useful for me to ensure that users have the same permissions.
I have done the SQL to get the top table so I can see the permissions.
I was thinking that some sort of left join on itself might the answer there but everything I try seems to end up as invalid SQL.
Any hints please? Im happy to experiment if I get a starting point :)
Thanks in advance
What you're looking for is called "pivoting". One way to do that is a group by, like:
select permission
, count(case when username = 'u1' then 1 end) as User1
, count(case when username = 'u2' then 1 end) as User2
, count(case when username = 'u3' then 1 end) as User3
, ...
from YourTable
group by
permission
It looks like Oracle supports the PIVOT keyword. You can use it to accomplish the same thing:
select *
from YourTable
pivot (
count(*) as HasPermission
for (username) in ('u1', 'u2', 'u3', ...)
);
Example for both approaches at SQL Fiddle.

How to retrieve data from different rows of the same table based on different criteria

I'm trying to write a plain SQL statement for building an Oracle report but I'm stuck at some point. x_request table stores the requests made and different tasks related to specific requests that have been done are stored in x_request_work_log. To summarize the structure of these tables:
X_request
-id
-name
-requester
-request_date
x_request_work_log
-id
-request_id (foreign key)
-taskId
-start_date
-end_date
Now let's assume that these tables are filled with sample data as follows:
x_request
id name requester request_date
1 firstReq John 01/01/2012
2 secondReq Steve 21/01/2012
x_request_work_log
id requestId taskId startDate endDate
1 1 0 01/01/2012 03/01/2012
2 1 1 04/01/2012 04/01/2012
3 1 2 05/01/2012 15/01/2012
4 2 0 24/01/2012 02/02/2012
The template of my report is as follows:
requestName timeSpent(task(0)) timeSpent(task(1)) timeSpent(task(2))
| | | | | | | |
So, that's where I'm stuck. I need a Sql Select statement that will return each row in the formatted way as described above. How can i retrieve and display the start and end dates of different tasks. Btw timeSpent = endDate(task(x)) - startDate(task(x))
Note: Using different select subqueries for each spent time calculation is not an option due to performance constraints. There must be another way.
It sounds like you just want something like
SELECT r.name request_name,
SUM( (CASE WHEN l.taskId = 0
THEN l.endDate - l.StartDate
ELSE 0
END) ) task0_time_spent,
SUM( (CASE WHEN l.taskId = 1
THEN l.endDate - l.StartDate
ELSE 0
END) ) task1_time_spent,
SUM( (CASE WHEN l.taskId = 2
THEN l.endDate - l.StartDate
ELSE 0
END) ) task2_time_spent
FROM x_request_work_log l
JOIN x_request r ON (l.requestId = r.Id)
GROUP BY r.name
If you happen to be using 11g, you could also use the PIVOT operator.
If you need to display all members of a group in one row, you can accomplish this in MySQL with the GROUP_CONCAT operator (I don't know what the equivalent is in Oracle):
> SELECT requestID,
GROUP_CONCAT(DATEDIFF(endDate,startDate)) AS length
FROM request_work_log
GROUP BY requestId;
+-----------+--------+
| requestID | length |
+-----------+--------+
| 1 | 2,0,10 |
| 2 | 9 |
+-----------+--------+
(and then add in the inner join to your other table to replace requestID with the request name)