Need information in rows into columns - sql

Currently I have a view which gets user, date, session id, activity and hostname.
User logins to a system and a session id is created, same session id gets updated for the logoff as well.
View data:
user
date
session_id
activity
hostname
X
2023-02-07T11:02
45
Login
XYZ
X
2023-02-07T11:06
45
Logout
XYZ
Y
2023-02-07T10:02
67
Login
ABC
Y
2023-02-07T10:32
67
Logout
ABC
X
2023-02-06T11:02
48
Login
XYZ
X
2023-02-06T11:06
48
Logout
XYZ
I want the data to come out as below,
user
Hostname
login
logout
X
XYZ
2023-02-07T11:02
2023-02-07T11:06
Y
ABC
2023-02-07T10:02
2023-02-07T10:32
X
XYZ
2023-02-06T11:02
2023-02-06T11:06
I have written a query using pivot
select * from ( select user, date, session_id, activity, hostname from view)
pivot ( max(date) for activity in ('login','logoff')) view
I am getting the results as expected but I don't want session_id to come up in the results and also the column name for login & logoff is as 'login' and 'logoff', how can I rename them?

If you do not want certaing columns displayed then do not SELECT them (naming the ones you do want to display rather than using SELECT *) and if you do not want the default column aliases then explicitly provide your own aliases:
SELECT username,
hostname,
login,
logoff
FROM (
SELECT username,
date_column,
session_id,
activity,
hostname
FROM view_name
)
PIVOT (
MAX(date_column) FOR activity IN (
'login' AS login,
'logout' AS logoff
)
);
or, if you do not want to group by the session id:
SELECT username,
hostname,
login,
logoff
FROM (
SELECT username,
date_column,
activity,
hostname
FROM view_name
)
PIVOT (
MAX(date_column) FOR activity IN (
'login' AS login,
'logout' AS logoff
)
);
fiddle

Here's one option:
Sample data:
SQL> with test (cuser, datum, session_id, activity, hostname) as
2 (select 'x', to_date('07.02.2023 11:02', 'dd.mm.yyyy hh24:mi'), 45, 'Login' , 'xyz' from dual union all
3 select 'x', to_date('07.02.2023 11:06', 'dd.mm.yyyy hh24:mi'), 45, 'Logout', 'xyz' from dual union all
4 select 'y', to_date('07.02.2023 10:02', 'dd.mm.yyyy hh24:mi'), 67, 'Login' , 'abc' from dual union all
5 select 'y', to_date('07.02.2023 10:32', 'dd.mm.yyyy hh24:mi'), 67, 'Logout', 'abc' from dual union all
6 select 'x', to_date('06.02.2023 11:02', 'dd.mm.yyyy hh24:mi'), 48, 'Login' , 'xyz' from dual union all
7 select 'x', to_date('06.02.2023 11:06', 'dd.mm.yyyy hh24:mi'), 48, 'Logout', 'xyz' from dual
8 )
Query:
9 select cuser, hostname,
10 max(case when activity = 'Login' then datum end) login,
11 max(case when activity = 'Logout' then datum end) logout
12 from test
13 group by cuser, hostname, session_id
14 order by cuser, login;
C HOS LOGIN LOGOUT
- --- ---------------- ----------------
x xyz 06.02.2023 11:02 06.02.2023 11:06
x xyz 07.02.2023 11:02 07.02.2023 11:06
y abc 07.02.2023 10:02 07.02.2023 10:32
SQL>

Related

check for logout status in oracle sql

I have written below query to retrieve the values,
select * from abc where status in ('login,'logout');
Status
Time
Login
2021-08-29 10:00:00
Logout
2021-08-29 10:30:00
Login
2021-08-29 11:00:00
In the table above, I have to check if the latest logout status is empty or present. But logout is not done yet.
I want to use IF condition to check if logout status is empty or present and it should use only the latest values. For example, the above query retrieves all values, but I want only the latest value ,that is
Login is at 2021-08-29 11:00:00 , but logout is not done yet.
I want only last record as output. Suppose if logout has been done,
Status
Time
Login
2021-08-29 10:00:00
Logout
2021-08-29 10:30:00
Login
2021-08-29 11:00:00
Logout
2021-08-29 11:30:00
Then I should get,
Status
Time
Login
2021-08-29 11:00:00
Logout
2021-08-29 11:30:00
as output
If the correct sequence is maintained by the logging application (Login before Logout and no consecutive duplicates of the same status), then you need to select the last two statuses and decide, whether to show the time of Logout (if it is the last), or not (if it is followed by Login status).
insert into t(status, time)
select 'Login', timestamp '2021-08-29 10:00:00' from dual union all
select 'Logout', timestamp '2021-08-29 10:30:00' from dual union all
select 'Login', timestamp '2021-08-29 11:00:00' from dual
create view v_test as
with last_ as (
select t.*
, row_number() over(partition by status order by time desc) as rn
/*There should be nothing after last logout*/
, lead(status) over(order by time) as next_status
from t
where status in ('Login', 'Logout')
)
select
status
/*
Show the time of the last status (regardless of its type),
And the time of the Login status if it's followed by Logout.
*/
, decode(next_status, 'Logout', time, null, time) as time
from last_
where rn = 1
select *
from v_test
STATUS | TIME
:----- | :------------------
Login | 2021-08-29 11:00:00
Logout | null
insert into t(status, time)
values('Logout', timestamp '2021-08-29 11:30:00')
select *
from v_test
STATUS | TIME
:----- | :------------------
Login | 2021-08-29 11:00:00
Logout | 2021-08-29 11:30:00
db<>fiddle here
The following approach uses window functions to determine the last login time and the logout time after the last login time
SELECT
status, time
FROM (
select
status,time ,
CASE
WHEN status='Login' AND
(time = MAX(time) OVER (PARTITION BY status)) THEN 1
ELSE 0
END as is_last_login,
CASE
WHEN status='Logout' AND
(time > MAX(CASE WHEN status='Login' THEN time END) OVER ()) THEN 1
ELSE 0
END as is_logout_after_last_login
from
abc
where
status in ('Login','Logout')
) t
WHERE
t.is_last_login=1 OR t.is_logout_after_last_login=1;
or
WITH last_login_time AS (
select max(time) last_login from abc where status='Login'
)
select
status,
time
from
abc
where
(
status='Login' AND time = (select last_login from last_login_time)
) OR
(
status='Logout' AND time > (select last_login from last_login_time)
)
Working Demo Db Fiddle
Edit 1
Left joining on a temporary table as shown in the example query below will also provide a null result for Logout if there is no logout entry as yet
WITH last_login_time AS (
select max(time) last_login from abc where status='Login'
),
login_logout_times AS (
select
status,
time
from
abc
where
(
status='Login' AND time = (select last_login from last_login_time)
) OR
(
status='Logout' AND time > (select last_login from last_login_time)
)
)
select
d.status,
l.time
from (
select 'Login' as status from dual
union all
select 'Logout' as status from dual
) d
left join login_logout_times l on l.status=d.status
Working Demo Db Fiddle
Let me know if this works for you.

Oracle: getting an average by week for the timespan of available data

I have some data that shows daily logins by clients on every available date they logged in that streches back a few years.
date month clientId loginCount
------------ --------- ---------- ------------
01/01/2021 01-2021 1234 234
02/01/2021 01-2021 1234 978
01/02/2021 02-2021 6547 45
01/02/2021 02-2021 345 86
....
For each client, I would like to generate the average number of times they login every week for however long they have corresponding date entries in the table :
clientId avgWeeklyLoginCount
---------- ---------------------
1234 125
6547 26
345 48
I understand 'IW' could be used in the TO_CHAR function to do this, e.g.
SELECT
TO_CHAR(date,'IW'),
clientId,
SUM(loginCount) as summedCount
FROM
logins
GROUP BY
TO_CHAR(date,'IW')
but not sure how to get an average by client id from this. any help will be appreciated!
You can using it as example. It can be looks like unnecessary overcomplicated:
ceil((in_date - trunc(to_date('06.01.0001', 'dd.MM.yyyy'), 'IW'))/7)
It means number of week since 1 CE. If your dates contain within single year you can use TO_CHAR(date,'IW') or TO_CHAR(date,'WW') instead of.
with logins(in_date, clientId, loginCount) as (
select to_date('01/01/2021 01:00:00', 'dd/MM/yyyy HH:MI:SS'), 1234, 234 from dual union all
select to_date('02/01/2021 01:00:00', 'dd/MM/yyyy HH:MI:SS'), 1234, 978 from dual union all
select to_date('01/02/2021 01:00:00', 'dd/MM/yyyy HH:MI:SS'), 6547, 45 from dual union all
select to_date('01/02/2021 01:00:00', 'dd/MM/yyyy HH:MI:SS'), 345, 86 from dual union all
select to_date('31/12/2020 01:00:00', 'dd/MM/yyyy HH:MI:SS'), 347, 1 from dual union all
select to_date('01/01/2021 01:00:00', 'dd/MM/yyyy HH:MI:SS'), 347, 1 from dual
)
select
clientId, avg(loginCount) avgLoginCountPerWeek
from (
select
week_number, clientId, sum(loginCount) loginCountPerWeek
from (
select
ceil((in_date - trunc(to_date('06.01.0001', 'dd.MM.yyyy'), 'IW'))/7) week_number, clientId, loginCount
from
logins
) t
group by
week_number, clientId
)
group by
clientId
You can use an aggregation query and count(distinct):
select clientid,
count(*) / count(distinct trunc(in_date, 'WW')) as avg_per_week
from logins
group by clientid;

Display Entry and Exit sql records in one row and calculate the time between them

I have the following table (transactions table) and I am using MS SQL:
Now what I need to do is for each cardholder show the entry and the matching exit on the same row. So that they can see for how long the person was inside the building.
I have been trying to approach this in various ways but I can't get the information. Any suggestions as to how I can approach this will be greatly appreciated. I have been looking into the CTE approach but my issue is that I am having difficulties getting this to work.
This demonstrates how it might be done in Oracle. The trans with clause would of course be a real table in a real world scenario. For this SQL to work in other RDBMSes, you probably have to remove all from dual and change how the time strings are parsed and calculated on to get seconds for each visit. (Subtraction on two DATE date type values in Oracle leaves a floating point number for the number of days between them, hence *24*3600 to get seconds).
with
trans as (
select 4 id, 'CT1' name, 54 tmhisid, 'Entry' caption, '2017-10-16 14:31:06' dt from dual union
select 5 id, 'CT2' name, 55 tmhisid, 'Entry' caption, '2017-10-16 14:31:14' dt from dual union
select 4 id, 'CT1' name, 56 tmhisid, 'Exit' caption, '2017-10-16 14:47:00' dt from dual union
select 5 id, 'CT2' name, 57 tmhisid, 'Exit' caption, '2017-10-16 14:47:05' dt from dual union
select 4 id, 'CT1' name, 58 tmhisid, 'Entry' caption, '2017-10-16 15:05:24' dt from dual union
select 5 id, 'CT2' name, 59 tmhisid, 'Entry' caption, '2017-10-16 15:05:33' dt from dual union
select 4 id, 'CT1' name, 60 tmhisid, 'Exit' caption, '2017-10-16 15:10:25' dt from dual union
select 5 id, 'CT2' name, 61 tmhisid, 'Exit' caption, '2017-10-16 15:10:29' dt from dual
),
trans2 as ( select trans.*, to_date(dt,'YYYY-MM-DD HH24:MI:SS') d from trans ),
entry as ( select * from trans2 where caption='Entry' ),
exit as ( select * from trans2 where caption='Exit' ),
visit as (
select en.id, en.name, en.d entry, ex.d exit
from entry en
join exit ex on ex.id=en.id
where ex.d>=en.d
and not exists (select 1 from exit ex2 where id=en.id and ex2.d>en.d and ex2.d<ex.d)
)
select visit.*, (exit-entry)*24*3600 sec from visit
order by entry;
Result:
ID NAM ENTRY EXIT SECONDS
---- --- ------------------- ------------------- --------
4 CT1 2017-10-16 14:31:06 2017-10-16 14:47:00 954
5 CT2 2017-10-16 14:31:14 2017-10-16 14:47:05 951
4 CT1 2017-10-16 15:05:24 2017-10-16 15:10:25 301
5 CT2 2017-10-16 15:05:33 2017-10-16 15:10:29 296

create id column based on activity data

I have a table EVENTS
USER EVENT_TS EVENT_TYPE
abc 2016-01-01 08:00:00 Login
abc 2016-01-01 08:25:00 Stuff
abc 2016-01-01 10:00:00 Stuff
abc 2016-01-01 14:00:00 Login
xyz 2015-12-31 18:00:00 Login
xyz 2016-01-01 08:00:00 Logout
What I need to do is produce a session field for each period of activity for each user. In addition, if the user has been idle for a period equal to or longer than p_timeout (1 hour in this case) then a new session starts at the next activity. Users don't always log out cleanly, so the logout isn't walways there...
Notes:
Logout always terminates a session
There doesn't have to be a logout or a login (because software)
Login is always a new session
Output like
USER EVENT_TS EVENT_TYPE SESSION
abc 2016-01-01 08:00:00 Login 1
abc 2016-01-01 08:25:00 Stuff 1
abc 2016-01-01 10:00:00 Stuff 2
abc 2016-01-01 14:00:00 Login 3
xyz 2015-12-31 18:00:00 Login 1
xyz 2016-01-01 08:00:00 Logout 1
Any thoughts on how to acheive this?
I think this may do what you need. I changed "user" to "usr" in the input, and "session" to "sess" in the output - I don't ever use reserved Oracle words for object names.
Note: as Boneist pointed out below, my solution will assign a session number of 0 to the first session, if it is a Logout event (or a succession of Logouts right at the top). If this situation can occur in the data, and if the desired behavior is to start session counts at 1 even in that case, then the definition of flag must be tweaked - for example, by making flag = 1 when lag(event_ts) over (partition by usr order by event_ts) is null as well.
Good luck!
with
events ( usr, event_ts, event_type ) as (
select 'abc', to_timestamp('2016-01-01 08:00:00', 'yyyy-mm-dd hh24:mi:ss'), 'Login' from dual union all
select 'abc', to_timestamp('2016-01-01 08:25:00', 'yyyy-mm-dd hh24:mi:ss'), 'Stuff' from dual union all
select 'abc', to_timestamp('2016-01-01 10:00:00', 'yyyy-mm-dd hh24:mi:ss'), 'Stuff' from dual union all
select 'abc', to_timestamp('2016-01-01 14:00:00', 'yyyy-mm-dd hh24:mi:ss'), 'Login' from dual union all
select 'xyz', to_timestamp('2015-12-31 18:00:00', 'yyyy-mm-dd hh24:mi:ss'), 'Login' from dual union all
select 'xyz', to_timestamp('2016-01-01 08:00:00', 'yyyy-mm-dd hh24:mi:ss'), 'Logout' from dual
),
start_of_sess ( usr, event_ts, event_type, flag ) as (
select usr, event_ts, event_type,
case when event_type != 'Logout'
and
( event_ts >= lag(event_ts) over (partition by usr
order by event_ts) + 1/24
or event_type = 'Login'
or lag(event_type) over (partition by usr
order by event_ts) = 'Logout'
)
then 1 end
from events
)
select usr, event_ts, event_type,
count(flag) over (partition by usr order by event_ts) as sess
from start_of_sess
;
Output (timestamps use my current NLS_TIMESTAMP_FORMAT setting):
USR EVENT_TS EVENT_TYPE SESS
--- --------------------------------- ---------- ------
abc 01-JAN-2016 08.00.00.000000000 AM Login 1
abc 01-JAN-2016 08.25.00.000000000 AM Stuff 1
abc 01-JAN-2016 10.00.00.000000000 AM Stuff 2
abc 01-JAN-2016 02.00.00.000000000 PM Login 3
xyz 31-DEC-2015 06.00.00.000000000 PM Login 1
xyz 01-JAN-2016 08.00.00.000000000 AM Logout 1
6 rows selected
I think this will do the trick:
WITH EVENTS AS (SELECT 'abc' usr, to_date('2016-01-01 08:00:00', 'yyyy-mm-dd hh24:mi:ss') event_ts, 'login' event_type FROM dual UNION ALL
SELECT 'abc' usr, to_date('2016-01-01 08:25:00', 'yyyy-mm-dd hh24:mi:ss') event_ts, 'Stuff' event_type FROM dual UNION ALL
SELECT 'abc' usr, to_date('2016-01-01 10:00:00', 'yyyy-mm-dd hh24:mi:ss') event_ts, 'Stuff' event_type FROM dual UNION ALL
SELECT 'abc' usr, to_date('2016-01-01 14:00:00', 'yyyy-mm-dd hh24:mi:ss') event_ts, 'login' event_type FROM dual UNION ALL
SELECT 'xyz' usr, to_date('2015-12-31 18:00:00', 'yyyy-mm-dd hh24:mi:ss') event_ts, 'login' event_type FROM dual UNION ALL
SELECT 'xyz' usr, to_date('2016-01-01 08:00:00', 'yyyy-mm-dd hh24:mi:ss') event_ts, 'Logout' event_type FROM dual UNION ALL
SELECT 'def' usr, to_date('2016-01-01 08:00:00', 'yyyy-mm-dd hh24:mi:ss') event_ts, 'Logout' event_type FROM dual UNION ALL
SELECT 'def' usr, to_date('2016-01-01 08:15:00', 'yyyy-mm-dd hh24:mi:ss') event_ts, 'Logout' event_type FROM dual)
SELECT usr,
event_ts,
event_type,
SUM(counter) OVER (PARTITION BY usr ORDER BY event_ts) session_id
FROM (SELECT usr,
event_ts,
event_type,
CASE WHEN LAG(event_type, 1, 'Logout') OVER (PARTITION BY usr ORDER BY event_ts) = 'Logout' THEN 1
WHEN event_type = 'Logout' THEN 0
WHEN event_ts - LAG(event_ts) OVER (PARTITION BY usr ORDER BY event_ts) > 1/24 THEN 1
WHEN event_type = 'login' THEN 1
ELSE 0
END counter
FROM EVENTS);
USR EVENT_TS EVENT_TYPE SESSION_ID
--- ------------------- ---------- ----------
abc 2016-01-01 08:00:00 login 1
abc 2016-01-01 08:25:00 Stuff 1
abc 2016-01-01 10:00:00 Stuff 2
abc 2016-01-01 14:00:00 login 3
def 2016-01-01 08:00:00 Logout 1
def 2016-01-01 08:15:00 Logout 2
xyz 2015-12-31 18:00:00 login 1
xyz 2016-01-01 08:00:00 Logout 1
This solution relies on the logic-short circuiting that takes place in the CASE expression and the fact that the event_type is not null. It also assumes that multiple logouts in a row are counted as separate sessions:
If the previous row was a logout row (and if there is no previous row - i.e. for the first row in the set - treat it as if a logout row was present), we want to increase the counter by one. (Logouts terminate the session, so we always have a new session following a logout.)
If the current row is a logout, then this terminates the existing session. Therefore, the counter shouldn't be increased.
If the time of the current row is greater than an hour from the previous row, increase the counter by one.
If the current row is a login row, then it's a new session, so increase the counter by one.
For any other case, we don't increase the counter.
Once we've done that, it's just a matter of doing a running total on the counter.
For completeness (for users with Oracle 12 or above), here is a solution using MATCH_RECOGNIZE:
select usr, event_ts, event_type, sess
from events
match_recognize(
partition by usr
order by event_ts
measures match_number() as sess
all rows per match
pattern (strt follow*)
define follow as event_type = 'Logout'
or ( event_type != 'Login'
and prev(event_type) != 'Logout'
and event_ts < prev(event_ts) + 1/24
)
)
;
Here I cover an unusual case: a Logout event following another Logout event. In such cases, I assume all consecutive Logouts, no matter how many and how far apart in time, belong to the same session. (If such cases are guaranteed not to occur in the data, so much the better.)
Please see also the Note I added to my other answer (for Oracle 11 and below) regarding the possibility of the very first event for a usr being a Logout (if that is even possible in the input data).

SQL select case group by

I have a table 'LIST_USERS'.
Table Description -
USER_ID NUMBER(8)
LOGIN_ID VARCHAR2(8)
CREATE_DATE TIMESTAMP(6)
LOGIN_DATE TIMESTAMP(6)
Table data -
USER_ID LOGIN_ID CREATE_DATE LOGIN_DATE
---------------------------------------------------
101 test1 04/24/2016 null
102 test1 04/24/2016 04/29/2016
103 test2 04/25/2016 null
104 test2 04/26/2016 null
105 test3 04/27/2016 04/28/2016
106 test3 04/27/2016 04/29/2016
107 test4 04/28/2016 04/29/2016
987 test5 04/29/2016 null
109 test5 04/29/2016 null
108 test5 04/29/2016 04/29/2016
Condition - I need to fetch USER_ID, and LOGIN_ID from 'LIST_USERS' table based of max LOGIN_DATE. If LOGIN_DATE is null, I need to get the record based on max CREATE_DATE.
I need to get the below result -
USER_ID LOGIN_ID
---------------------
102 test1
104 test2
106 test3
107 test4
108 test5
I am using the below query. But it will give me only LOGIN_ID, and 'Login_Or_Create_Date' but I need USER_ID, and LOGIN_ID. Is there way I can get USER_ID as well as in the result shown above?
select LOGIN_ID,
(case when max(LOGIN_DATE) is null then max(CREATE_DATE)
else max(LOGIN_DATE) end) as Login_Or_Create_Date
from LIST_USERS;
Try this:
SELECT USER_ID, LOGIN_ID
FROM (
SELECT USER_ID, LOGIN_ID,
ROW_NUMBER() OVER (PARTITION BY LOGIN_ID
ORDER BY COALESCE(LOGIN_DATE, CREATE_DATE) DESC) AS rn
FROM LIST_USERS) t
WHERE t.rn = 1
Sounds like a job for keep dense_rank:
select min(user_id) keep (dense_rank last order by coalesce(login_date, create_date))
as user_id,
login_id
from list_users
group by login_id
order by user_id;
The last keeps the record with the latest login/create date; the coalesce() takes the login date first and falls back to the create date if that is null (or you could use nvl() instead of course). You could also do first and order by desc - the result is the same (if there are no nulls anyway, and it looks like there shouldn't be), but last feels more intuitive when you want the latest date I think.
Demo using your data in a CTE:
with list_users(user_id, login_id, create_date, login_date) as (
select 101, 'test1', date '2016-04-24', null from dual
union all select 102, 'test1', date '2016-04-24', date '2016-04-29' from dual
union all select 103, 'test2', date '2016-04-25', null from dual
union all select 104, 'test2', date '2016-04-26', null from dual
union all select 105, 'test3', date '2016-04-27', date '2016-04-28' from dual
union all select 106, 'test3', date '2016-04-27', date '2016-04-29' from dual
union all select 107, 'test4', date '2016-04-28', date '2016-04-29' from dual
)
select min(user_id) keep (dense_rank last order by coalesce(login_date, create_date))
as user_id,
login_id
from list_users
group by login_id
order by user_id;
USER_ID LOGIN
---------- -----
102 test1
104 test2
106 test3
107 test4
And with your modified data:
with list_users(user_id, login_id, create_date, login_date) as (
select 101, 'test1', date '2016-04-24', null from dual
union all select 102, 'test1', date '2016-04-24', date '2016-04-29' from dual
union all select 103, 'test2', date '2016-04-25', null from dual
union all select 104, 'test2', date '2016-04-26', null from dual
union all select 105, 'test3', date '2016-04-27', date '2016-04-28' from dual
union all select 106, 'test3', date '2016-04-27', date '2016-04-29' from dual
union all select 107, 'test4', date '2016-04-28', date '2016-04-29' from dual
union all select 987, 'test5', date '2016-04-29', null from dual
union all select 109, 'test5', date '2016-04-29', null from dual
union all select 108, 'test5', date '2016-04-29', date '2016-04-29' from dual
)
select min(user_id) keep (dense_rank last order by coalesce(login_date, create_date))
as user_id,
login_id
from list_users
group by login_id
order by user_id;
USER_ID LOGIN
---------- -----
102 test1
104 test2
106 test3
107 test4
108 test5