Concat rows with the same user - mysql

I have the query:
SELECT e.id_taxe
, u.nm_user
, dt_taxe
, SUM(e.vl_taxe) as vl_taxe
FROM taxe as e
JOIN user as u
ON u.id_user = e.id_user
WHERE id_enterprise = 86
AND dt_taxe BETWEEN '2017-01-01' AND '2017-03-31'
AND lg_cancel = 0
GROUP
BY e.dt_taxe
, e.id_user
ORDER
BY e.id_user
, e.dt_taxe
As result, I have:
id_taxe nm_user dt_taxe vl_taxe
728 Maria 2017-01-01 17091.07048034668
727 Maria 2017-02-01 14091.07048034668
721 Maria 2017-03-01 1021.07048034668
731 Pedro 2017-01-01 16353.569854736328
732 Pedro 2017-02-01 6353.56231239
How I can concat the rows, to get the result:
id_taxe nm_user dt_taxe vl_taxe
728 Maria 2017-01-01 17091.07048034668 ,
2017-02-01 14091.07048034668,
2017-03-01 1021.07048034668
731 Pedro 2017-01-01 16353.569854736328,
2017-02-01 6353.56231239

With a database like PostgreSQL this could be done with window functions but MySQL has no support for window functions so you cannot do that

This type of operation is usually best done at the application layer. Your result set is not very SQLish, because ordering is very important.
However, you can do this with variables:
SELECT (CASE WHEN rn = 1 THEN id_taxe END) as id_taxe,
(CASE WHEN rn = 1 THEN nm_user END) as num_user,
dt_taxe, vl_taxe
FROM (SELECT eu.*,
(#rn := if(#u = e.id_user, #rn + 1,
if(#u := e.id_user, 1, 1)
)
) as rn
FROM (SELECT e.id_taxe, u.nm_user, e.dt_taxe, SUM(e.vl_taxe) as vl_taxe
FROM taxe e INNER JOIN
user u
ON u.id_user = e.id_user
WHERE id_enterprise = 86 AND
dt_taxe BETWEEN '2017-01-01' AND '2017-03-31' AND
lg_cancel = 0
GROUP BY e.dt_taxe, e.id_user
) eu CROSS JOIN
(SELECT #i := -1, #rn := 0)
ORDER BY id_user, dt_taxe
) eu;

Related

How to put all rows at the same level in SQL?

Trying to use this query for a procedure where a client ask for a loan:
select loan_id,
CASE WHEN status = 'document_sent' then date ELSE NULL END as document_sent,
CASE WHEN status = 'document_rejected' then date ELSE NULL END as document_rejected
from table
and status in ('document_sent', 'document_rejected')
order by 2 asc, 3 asc
And this is the result:
loan_id
document_sent
doc_rejected
123
2021-03-01 14:52
123
2021-03-01 14:57
123
2021-03-01 15:33
123
2021-03-01 14:54
123
2021-03-01 15:00
123
2021-03-01 15:39
I would like to have something like this:
loan_id
document_sent
doc_rejected
123
2021-03-01 14:52
2021-03-01 14:54
123
2021-03-01 14:57
2021-03-01 15:00
123
2021-03-01 15:33
2021-03-01 15:39
Is it possible? Thanks
If we don't make any assumptions about the absence/presence of each type of record:
select
coalesce(a.loan_id,b.loan_id) as loan_id
, ds.date as document_sent
, dr.date as document_rejected
from
(select * from tbl where status='document_sent') ds
full outer join
(select * from tbl where status='document_rejected') dr
on ds.loan_id=dr.loan_id
If you know that one or the other status is always present, then it can be a little simplified (can be turned into left join), or if both records are expected to be there it can be simplified to:
select
a.loan_id
, ds.date as document_sent
, dr.date as document_rejected
from
tbl ds
inner join
tbl dr
on ds.loan_id=dr.loan_id
and ds.status='document_sent'
and dr.status='document_rejected'
If `full outer join is not available:
select
coalesce(a.loan_id,b.loan_id) as loan_id
, ds.date as document_sent
, dr.date as document_rejected
from
(select * from tbl where status='document_sent') ds
left join
(select * from tbl where status='document_rejected') dr
on ds.loan_id=dr.loan_id
union all
select
coalesce(a.loan_id,b.loan_id) as loan_id
, ds.date as document_sent
, dr.date as document_rejected
from
(select * from tbl where status='document_sent') ds
right join
(select * from tbl where status='document_rejected') dr
on ds.loan_id=dr.loan_id
where ds.loan_id is null
Thanks #nbk for pointing out.
You could also use MIN/MAX (although we don't want to find min/max of anything, we want to take advantage of their behaviour that 1.they accept dates, and 2. they ignore null values, 3.they are used with GROUP BY to group rows together):
select
loan_id
, min(case when status='document_sent'
then ds.date else NULL end) as document_sent
, min(case when status='document_rejected'
then ds.date else NULL end) as document_rejected
from
tbl
group by id

Calculate two records and join them in the same MySQL row

I have a table that contains the data of an access control of employees, entry and exits from the office in different rows.
The output is based on a query in a date and time range example
ac_date >= '2018-05-12' AND ac_date <= '2018-05-13' AND ac_time >='08:00:00' AND ac_time <= '13:00:00']
Table AC
CREATE TABLE `AC` (
`employee` int(11) NOT NULL,
`ac_date` date NOT NULL,
`ac_time` time NOT NULL,
`ac_event` tinyint(4) NOT NULL,
KEY `index2` (`employee`,`ac_date`,`ac_time`,`ac_event`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Employee ac_date ac_time ac_event
2032 2018-05-12 08:52:00 1
2032 2018-05-12 11:39:33 0
2032 2018-05-12 11:48:06 1
2032 2018-05-12 11:52:54 0
2032 2018-05-12 11:59:54 1
2032 2018-05-12 12:23:40 0
2032 2018-05-13 08:34:43 1
2032 2018-05-13 09:02:25 0
2032 2018-05-13 09:12:16 1
2032 2018-05-13 11:45:21 0
2032 2018-05-13 12:50:40 1
2032 2018-05-13 12:52:16 0
Where ac_ event = 1 is the input and ac event = 0 is the output.
I need to make a query that shows the data in the same row (Entry / Exit) to then take the time the employee lasts outside the office.
Ej:
Output for the first block:
Employee entry_date exit_date entry_date exit_date duration
2032 2018-05-12 2018-05-12 08:52:00 11:39:33 02:47:33
I was able to get the result by doing a query with the same table, but I have duplicate data, so I have to resort to the group by [ac_date, ac_time]. I do not know if the way I'm trying is the right one, so I'd like to see an expert's solution. Thank you!
Update:
http://sqlfiddle.com/#!9/6f36f/3
This is a simplified version of your original query:
select AC.employee, AC.ac_date, AC.ac_time,
min(AC2.ac_time) as exit_time,
timediff(min(AC2.ac_time), AC.ac_time)
from AC
left join AC as AC2
on AC2.ac_date = AC.ac_date
and AC2.ac_time > ac.ac_time
and AC2.ac_event = 0
where AC.ac_event = 1
AND AC.ac_date >= '2018-05-11'
AND AC.ac_date <= '2018-05-13'
AND AC.ac_time >= '00:00:00'
AND AC.ac_time <= '23:59:00'
group by AC.employee, AC.ac_date, AC.ac_time
order by AC.employee, AC.ac_date, AC.ac_time
;
See fiddle
But no matter how you write it, it's always a kind of non-equi-join (i.e. not based on =) and performance might be bad without matching indexes.
Btw, if this was MariaDB or MySQL 8 it would be a simple task for LAG/LEAD.
Try this:
SELECT
*,
TIMEDIFF(exit_time,entry_time) as duration
FROM
(
SELECT
Employee,
ac_date as entry_date,
ac_date as exit_date,
MIN(CASE WHEN ac_event=1 THEN ac_time END) entry_time,
MAX(CASE WHEN ac_event=0 THEN ac_time END) exit_time
FROM
AC
GROUP BY
Employee,
ac_date
) as t
You can use correlated subqueries to get entry / exit pairs:
SELECT employee, ac_date AS entry_date,
(SELECT ac_date
FROM AC AS i
WHERE i.ac_event = 0
AND i.employee = t.employee
AND i.ac_date >= t.ac_date
AND i.ac_time >= t.ac_time
ORDER BY i.ac_date, i.ac_time LIMIT 1) AS exit_date,
ac_time AS entry_time,
(SELECT ac_time
FROM AC AS i
WHERE i.ac_event = 0
AND i.employee = t.employee
AND i.ac_date >= t.ac_date
AND i.ac_time >= t.ac_time
ORDER BY i.ac_date, i.ac_time LIMIT 1) AS exit_time
FROM AC AS t
WHERE t.ac_date BETWEEN '2018-05-12' AND '2018-05-13' AND t.ac_event = 1;
Output:
employee entry_date exit_date entry_time exit_time
-------------------------------------------------------
2032 2018-05-12 2018-05-12 08:52:00 11:39:33
2032 2018-05-12 2018-05-12 11:48:06 11:52:54
2032 2018-05-12 2018-05-12 11:59:54 12:23:40
2032 2018-05-13 2018-05-13 08:34:43 09:02:25
2032 2018-05-13 2018-05-13 09:12:16 11:45:21
2032 2018-05-13 2018-05-13 12:50:40 12:52:16
Then you can use TIMESTAMPDIFF to calculate the time between each entry - exit:
SELECT employee, entry_date, exit_date, entry_time, exit_time,
SEC_TO_TIME(TIMESTAMPDIFF(SECOND,
CONCAT(entry_date, ' ', entry_time),
CONCAT(exit_date, ' ', exit_time))) AS duration
FROM (
SELECT employee, ac_date AS entry_date,
(SELECT ac_date
FROM AC AS i
WHERE i.ac_event = 0
AND i.employee = t.employee
AND i.ac_date >= t.ac_date
AND i.ac_time >= t.ac_time
ORDER BY i.ac_date, i.ac_time LIMIT 1) AS exit_date,
ac_time AS entry_time,
(SELECT ac_time
FROM AC AS i
WHERE i.ac_event = 0
AND i.employee = t.employee
AND i.ac_date >= t.ac_date
AND i.ac_time >= t.ac_time
ORDER BY i.ac_date, i.ac_time LIMIT 1) AS exit_time
FROM AC AS t
WHERE t.ac_date BETWEEN '2018-05-12' AND '2018-05-13' AND t.ac_event = 1) AS x
Demo here
TRY THIS: Just rank data for every entry employee, ac_date, ac_time wise and exit in the separate set then join, it's the one simple way where you can use INNER JOIN instead of LEFT JOIN for the better performance if you want the records with both entry and exit
SELECT en.employee,
en.entry_date,
ex.exit_date,
en.entry_time,
ex.exit_time,
TIMEDIFF(ex.exit_time, en.entry_time) as duration
FROM (
SELECT employee,
ac_date AS entry_date,
ac_time AS entry_time,
#enRank := #enRank + 1 AS rank
FROM AC, (SELECT #enRank := 0) r
WHERE ac_event = 1 ORDER BY employee, ac_date, ac_time) en
LEFT JOIN (
SELECT employee,
ac_date AS exit_date,
ac_time AS exit_time,
#exRank := #exRank + 1 AS rank
FROM AC, (SELECT #exRank := 0) r
WHERE ac_event = 0 ORDER BY employee, ac_date, ac_time) ex ON ex.rank = en.rank
AND ex.employee = en.employee
WHERE en.entry_date >= '2018-05-12' AND ex.exit_date <= '2018-05-13'
ORDER BY en.entry_date
OUTPUT http://sqlfiddle.com/#!9/891d91/18
Another option is to use a double join to the same table in order to get the exit record:
SELECT t1.employee,
t1.ac_date AS entry_date,
t1.ac_time AS entry_time,
t2.ac_date AS exit_date,
t2.ac_time AS exit_time,
SEC_TO_TIME(TIMESTAMPDIFF(SECOND,
CONCAT(t1.ac_date, ' ', t1.ac_time),
CONCAT(t2.ac_date, ' ', t2.ac_time))) AS duration
/* fetches entry records */
FROM AC AS t1
/* fetches exit records that occur after the correlated entry record */
INNER JOIN AC AS t2
ON t1.employee = t2.employee AND t2.ac_event = 0
AND CONCAT(t1.ac_date, ' ', t1.ac_time) <= CONCAT(t2.ac_date, ' ', t2.ac_time)
/* fetches exit records that occur between t1, t2 records */
LEFT JOIN AC AS t3
ON t2.employee = t3.employee AND t3.ac_event = 0
AND CONCAT(t3.ac_date, ' ', t3.ac_time) >= CONCAT(t1.ac_date, ' ', t1.ac_time)
AND CONCAT(t3.ac_date, ' ', t3.ac_time) < CONCAT(t2.ac_date, ' ', t2.ac_time)
WHERE t1.ac_date BETWEEN '2018-05-12' AND '2018-05-13'
AND t1.ac_event = 1
AND t3.employee IS NULL /* There is no record between t1 entry and t2 exit */
Demo here

How to Select same column in 1 table twice in mysql

How can i select 1 column twice using only 1 table, the problem is i have to separate 1 column into 2 return
ex: table Logs
TIMELOGS 11:00 , 12:12 , 13:00 , 17:01 , 17:05 , 17:10
TIMEMODE 0 , 0 , 0 , 1 , 1 , 1
the output should be
IN 11:00 , 12:12 , 13:00
OUT 17:01, 17:05 , 17:10
How can I combine these to queries
Select TIMELOGS as IN FROM table_logs where TIMEMODE = 0;
and
Select TIMELOGS as OUT FROM table_logs where TIMEMODE = 1;
If I understand correctly, you want to enumerate the 0s and the 1s and then group by that:
select max(case when mode = 0 then time end),
max(case when mode = 1 then time end)
from (select l.*,
(#rn := if(#m = mode, #rn + 1,
if(#m := mode, 1, 1)
)
) as rn
from logs l cross join
(select #rn := 0, #m := -1) params
order by mode, time
) l
group by rn;
To output the format you described, (suppose you're using MySQL) you could do:
select
case when TIMEMODE = 0 then 'IN' else 'OUT' end,
group_concat(TIMELOGS)
from table_logs
group by TIMEMODE

SQL - Group Table by entries X amount of time from each other

I need to group together the entries in which the timestamp difference between one and the other is X amount of seconds or less than then average the value for each of them for each of the devices. In the following example I have a Table with this data and I need to group by device with entries between 60 seconds from each other.
Device Timestamp Value
0 30:8c:fb:a4:b9:8b 10/26/2015 22:50:15 34
1 30:8c:fb:a4:b9:8b 10/26/2015 22:50:46 34
2 c0:ee:fb:35:ec:cd 10/26/2015 22:50:50 33
3 c0:ee:fb:35:ec:cd 10/26/2015 22:50:51 32
4 30:8c:fb:a4:b9:8b 10/26/2015 22:51:15 34
5 30:8c:fb:a4:b9:8b 10/26/2015 22:51:47 32
6 c0:ee:fb:35:ec:cd 10/26/2015 22:52:38 38
7 30:8c:fb:a4:b9:8b 10/26/2015 22:54:46 34
This should be the resulting Table
Device First_seen Last_seen Average_value
0 30:8c:fb:a4:b9:8b 10/26/2015 22:50:15 10/26/2015 22:51:47 33,5
1 c0:ee:fb:35:ec:cd 10/26/2015 22:50:50 10/26/2015 22:50:51 32,5
2 c0:ee:fb:35:ec:cd 10/26/2015 22:52:38 10/26/2015 22:52:38 38
3 30:8c:fb:a4:b9:8b 10/26/2015 22:54:46 10/26/2015 22:54:46 34
Thank you very much for your help.
There is an old trick for this!
Mostly based on power of Window functions
Perfectly works for BigQuery!
So, first you "mark" all entries which exceed 60 seconds after previous entry!
Those which exceed getting value 1 and rest getting value 0!
Secondly you define groups by summing all previous marks (of course steps above are done while partitioning by device)
And finally, you just do simple grouping by above defined groups
Three simple steps implemented in one query with few simple sub-selects!
Hope this helps
SELECT device, MIN(ts) AS first_seen, MAX(ts) AS last_seen, AVG(value) AS average_value
FROM (
SELECT device, ts, value, SUM(grp_start) OVER (PARTITION BY device ORDER BY ts) AS grp
FROM (
SELECT device, ts, value,
IF(TIMESTAMP_TO_SEC(TIMESTAMP(ts))-TIMESTAMP_TO_SEC(TIMESTAMP(ts0))>60,1,0) AS grp_start
FROM (
SELECT device, ts, value, LAG(ts, 1) OVER(PARTITION BY device ORDER BY ts) AS ts0
FROM yourTable
)
)
)
GROUP BY device, grp
Here's one way...
DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table
(device CHAR(1) NOT NULL
,timestamp DATETIME NOT NULL
,value INT NOT NULL
,PRIMARY KEY(device,timestamp)
);
INSERT INTO my_table VALUES
('a','2015/10/26 22:50:15',34),
('a','2015/10/26 22:50:46',34),
('b','2015/10/26 22:50:50',33),
('b','2015/10/26 22:50:51',32),
('a','2015/10/26 22:51:15',34),
('a','2015/10/26 22:51:47',32),
('b','2015/10/26 22:52:38',38),
('a','2015/10/26 22:54:46',34);
SELECT m.*
, AVG(n.value) avg
FROM
( SELECT a.device
, a.timestamp start
, MIN(c.timestamp) end
FROM
( SELECT x.*
, CASE WHEN x.device = #prev THEN #i:=#i+1 ELSE #i:=1 END i
, #prev:=device
FROM my_table x
, (SELECT #i:=1,#prev:=null) vars
ORDER
BY device
, timestamp
) a
LEFT
JOIN
( SELECT x.*
, CASE WHEN x.device = #prev THEN #i:=#i+1 ELSE #i:=1 END i
, #prev:=device
FROM my_table x
, (SELECT #i:=1,#prev:=null) vars
ORDER
BY device
, timestamp
) b
ON b.device = a.device
AND b.timestamp > a.timestamp - INTERVAL 60 SECOND
AND b.i = a.i - 1
LEFT
JOIN
( SELECT x.*
, CASE WHEN x.device = #prev THEN #i:=#i+1 ELSE #i:=1 END i
, #prev:=device
FROM my_table x
, (SELECT #i:=1,#prev:=null) vars
ORDER
BY device
, timestamp
) c
ON c.device = a.device
AND c.i >= a.i
LEFT
JOIN
( SELECT x.*
, CASE WHEN x.device = #prev THEN #i:=#i+1 ELSE #i:=1 END i
, #prev:=device
FROM my_table x
, (SELECT #i:=1,#prev:=null) vars
ORDER
BY device
, timestamp
) d
ON d.device = c.device
AND d.i = c.i + 1
AND d.timestamp < c.timestamp + INTERVAL 60 SECOND
WHERE b.i IS NULL
AND c.i IS NOT NULL
AND d.i IS NULL
GROUP
BY a.device
, a.i
) m
JOIN my_table n
ON n.device = m.device
AND n.timestamp BETWEEN start AND end
GROUP
BY m.device
, m.start;
+--------+---------------------+---------------------+---------+
| device | start | end | avg |
+--------+---------------------+---------------------+---------+
| a | 2015-10-26 22:50:15 | 2015-10-26 22:51:47 | 33.5000 |
| a | 2015-10-26 22:54:46 | 2015-10-26 22:54:46 | 34.0000 |
| b | 2015-10-26 22:50:50 | 2015-10-26 22:50:51 | 32.5000 |
| b | 2015-10-26 22:52:38 | 2015-10-26 22:52:38 | 38.0000 |
+--------+---------------------+---------------------+---------+

Concatenating row values sql server 2008 r2

I have two tables register and att_bottom and I want to display only the students at a certain building who have been tardy based on today's date with the periods separated by a comma.
This is the way the data is displayed when joining both tables:
Student ID | Building | Period | Grade
12345 2 1 11
12345 2 5 11
43210 2 1 12
I want this:
Student ID | <u>Building | Period | Grade
12345 2 1,5 11
43210 2 1 12
This is my query:
select r.STUDENT_ID,
r.BUILDING ,
(select ab.attendancePeriod + ','
from att_bottom ab
where ab.STUDENT_ID = r.student_id
and ab.building = '2'
and ab.attendance_c ='T'
and ab.SCHOOL_YEAR =2014
CONVERT(date,ab.attendance_date,102) = convert(date,getdate(),102)
FOR XML PATH ('') ) AS PERIODS,
r.GRADE
FROM register r
where r.CURRENT_STATUS = 'A'
and r.BUILDING ='2'
I'm getting all the students at building 2 and even if they don't have an attedance_c of T; a NULL value for Periods is being retrieved:
Student ID | Building | Period | Grade
12345 2 1 , 5 11
43210 2 1 , 12
95687 2 NULL 09
78417 2 NULL 10
20357 2 NULL 11
I have tried and ab.attendancePeriod is Not NULL and I still get the same results.
Any thoughts?
The outer query doesn't listen to any filters in the subquery; it will return NULL for any rows that aren't matched by the join conditions. You need to filter differently. Here is one way (this also eliminates the errant trailing comma, and avoids comparing dates by converting them expensively to strings):
;WITH x AS
(
SELECT DISTINCT s = r.Student_ID, r.building,
p = ab.attendancePeriod, r.grade
FROM dbo.Register AS r
INNER JOIN dbo.att_bottom AS ab
ON r.Student_ID = ab.Student_ID
AND r.building = ab.building
WHERE ab.building = '2'
AND ab.attendance_c = 'T'
AND ab.SCHOOL_YEAR = 2014
AND ab.attendance_date >= CONVERT(DATE, GETDATE())
AND ab.attendance_date < DATEADD(DAY, 1, CONVERT(DATE, GETDATE()))
AND r.building = '2'
AND r.CURRENT_STATUS = 'A'
)
SELECT DISTINCT
[Student ID] = x.s,
x.building,
Period = STUFF((SELECT ',' + x2.p FROM x AS x2 WHERE x2.s = x.s
FOR XML PATH(''),
TYPE).value(N'./text()[]',N'nvarchar(max)'),1,1,''),
x.grade
FROM x;
Another way:
SELECT DISTINCT
r.Student_ID,
r.building,
Period = STUFF(b.p.value(N'./text()[1]', N'nvarchar(max)'),1,1,''),
r.grade
FROM dbo.Register AS r
CROSS APPLY
(
SELECT p = ',' + ab.attendancePeriod
FROM dbo.att_bottom AS ab
WHERE ab.building = '2'
AND ab.attendance_c = 'T'
AND ab.SCHOOL_YEAR = 2014
AND ab.attendance_date >= CONVERT(DATE, GETDATE())
AND ab.attendance_date < DATEADD(DAY, 1, CONVERT(DATE, GETDATE()))
AND ab.student_id = r.student_id
AND ab.building = r.building
FOR XML PATH(''),TYPE
) AS b(p)
WHERE b.p IS NOT NULL
AND r.building = '2'
AND r.CURRENT_STATUS = 'A';
Move the AS PERIODS select to be an inner join to r.