Show Null Value from 2 comparable records in MySQL - mysql

I have example data table from attendance machine system like this:
ID Name In
1 John 2015-04-17 08:00:00
1 John 2015-04-17 16:30:00
1 John 2015-04-20 10:01:00
1 John 2015-04-21 10:00:00
1 John 2015-04-21 19:00:00
Here my query:
SELECT a.id AS ID, a.nama AS `Name`, DATE_FORMAT(a.att, '%d-%b-%y') AS `Date`,
DATE_FORMAT(a.att, '%T') as `IN`, DATE_FORMAT(b.att, '%T') AS `OUT`
FROM attendance a
INNER JOIN attendance b
on date(a.att) = date(b.att) AND a.id = b.id
WHERE b.att > a.att
With above query I can split the In (column) time into 2 column become In and out like this:
Id Name Date In Out
1 John 17-Apr-15 08:00:00 16:30:00
1 John 21-Apr-15 10:00:00 19:00:00
The problem is if user John forget to use finger print where he out, for example on date 20 April, the data is not display. So, I want to my output become like this:
Id Name Date In Out
1 John 17-Apr-15 08:00:00 16:30:00
1 John 20-Apr-15 10:01:00 NULL
1 John 21-Apr-15 10:00:00 19:00:00

You should use a LEFT JOIN instead of an INNER JOIN. Or you can use this query instead:
SELECT
Id,
Name,
DATE(att) AS `Date`,
DATE_FORMAT(MIN(att), '%T') AS `In`,
CASE WHEN MAX(att)<>MIN(att) THEN DATE_FORMAT(MAX(att), '%T') END AS `Out`
FROM
attendance
GROUP BY
Id, Name, DATE(att)
Please see a fiddle here.

Related

How to sperate 1 column of date to multiple columns of month in SQL?

I have a scenario in below:
Name
LoginDate
Peter
2020-01-01
Peter
2020-01-02
Mary
2020-01-01
Peter
2020-02-02
Mary
2020-02-05
Chris
2020-02-07
How to write the SQL which can be re-organized in below
Name
Jan
Feb
Peter
2
1
Mary
1
1
Chris
0
1
Appreciated your help!
We can use Month() to do it
create table login_record(
name varchar(100),
login_date date
);
insert into login_record(name,login_date) values
('Peter','2020-01-01'),
('Peter','2020-01-02'),
('Mary','2020-01-01'),
('Peter','2020-02-02'),
('Mary','2020-02-05'),
('Chris','2020-02-07');
SELECT
name,
SUM(IF(month(login_date) = 1,1,0)) AS `Jan`,
SUM(IF(month(login_date) = 2,1,0)) AS `Feb`,
SUM(IF(month(login_date) = 3,1,0)) AS `Mar`
-- sum other month
FROM login_record
group by name
order by name
DB Fiddle Demo

Get all the Attendance and Fullname even no attendance on date with daterange mysql

How to do this in Mysql to get all users even no records or absent on that selected date range?
attendance_tbl
ID
user_id
time_in
time_out
created_at
1
001
2022-01-01 08:00:00
2022-01-01 17:00:00
2022-01-03 08:00:00
2
002
2022-01-01 08:15:24
2022-01-01 17:00:00
2022-01-03 08:15:24
3
003
2022-01-02 08:44:55
2022-01-02 17:00:00
2022-01-04 08:44:55
4
004
2022-01-03 08:40:22
2022-01-03 17:00:00
2022-01-04 08:40:22
users_tbl
ID
user_id
f_name
1
001
John Doe
2
002
Jane Doe
3
003
Ronal Black
4
004
Lucy White
Expected Output Daterange : from 2022-01-01 to 2022-01-03
Will get all the Users Fullname
ID
user_id
Date
f_name
time_in
time_out
created_at
1
001
Jan 1 2022
John Doe
2022-01-01 08:00:00
2022-01-01 17:00:00
2022-01-03 08:00:00
2
002
Jan 1 2022
Jane Doe
2022-01-01 08:15:24
2022-01-01 08:15:24
2022-01-03 08:00:00
3
003
Jan 1 2022
Ronal Black
4
004
Jan 1 2022
Lucy White
5
001
Jan 2 2022
John Doe
6
002
Jan 2 2022
Jane Doe
7
003
Jan 2 2022
Ronal Black
2022-01-02 17:00:00
2022-01-02 17:00:00
2022-01-02 17:00:00
8
004
Jan 2 2022
Lucy White
9
001
Jan 3 2022
John Doe
10
002
Jan 3 2022
Jane Doe
11
003
Jan 3 2022
Ronal Black
12
004
Jan 3 2022
Lucy White
2022-01-04 17:00:00
2022-01-04 17:00:00
2022-01-04 17:00:00
Given that you want to include the absent data, we need to start by getting the date range for the desired period. Using a user variable to store and increment a counter value is a performant way of doing this -
SELECT
'2022-01-01' + INTERVAL #row_number DAY `date`,
#row_number := #row_number + 1
FROM `attendance_tbl`, (SELECT #row_number := 0) AS `x`
LIMIT 31 /* 31 days in January */
If you have a table with a contiguous integer sequence (auto-incremented PK without deletes), you could use that instead -
SELECT '2022-01-01' + INTERVAL (`id` - 1) DAY `date`
FROM `attendance_tbl`
WHERE `id` <= 31 /* 31 days in January */
ORDER BY `id` ASC
We then add a cross join to build the full set of dates and users -
SELECT *
FROM (
SELECT '2022-01-01' + INTERVAL (`id` - 1) DAY `date`
FROM `attendance_tbl`
WHERE `id` <= 31
ORDER BY `id` ASC
) d
CROSS JOIN `users_tbl` `u`
By cross joining between these two tables we get the cartesian product (all combinations of the two sets). We then just take it a step further by using a left join to the attendance data -
SELECT
`u`.`user_id`,
DATE_FORMAT(`d`.`date`, '%b %e %Y') `date`,
`u`.`f_name`,
`a`.`time_in`,
`a`.`time_out`
FROM (
SELECT
'2022-01-01' + INTERVAL (`id` - 1) DAY `date`,
(SELECT TIMESTAMP(`date`, '00:00:00')) `begin`,
(SELECT TIMESTAMP(`date`, '23:59:59')) `end`
FROM `attendance_tbl`
WHERE `id` <= 31
ORDER BY `id` ASC
) d
CROSS JOIN `users_tbl` `u`
LEFT JOIN `attendance_tbl` `a`
ON `u`.`user_id` = `a`.`user_id`
AND `a`.`time_in` BETWEEN `d`.`begin` AND `d`.`end`
ORDER BY `d`.`date`, `u`.`user_id`
If your attendance_tbl can have more than 1 row per user per day then you will need to add GROUP BY d.date, u.user_id and aggregate_functions in the select list.
I have added begin and end to the derived table. This is to allow for index use for the join. This is not important while the attendance_tbl is small but will matter more as the table grows. Adding an index on (user_id, time_in) will make a huge difference to performance in the longer term.
Here's a db<>fiddle for you to play with.
To run this from PHP using PDO you could do something like this -
<?php
$pdo = new PDO($dsn, $user, $password);
$sql = "SELECT
`u`.`user_id`,
DATE_FORMAT(`d`.`date`, '%b %e %Y') `date`,
`u`.`f_name`,
`a`.`time_in`,
`a`.`time_out`
FROM (
SELECT
:START_DATE + INTERVAL (`id` - 1) DAY `date`,
(SELECT TIMESTAMP(`date`, '00:00:00')) `begin`,
(SELECT TIMESTAMP(`date`, '23:59:59')) `end`
FROM `attendance_tbl`
WHERE `id` <= :DAYS_RANGE
ORDER BY `id` ASC
) d
CROSS JOIN `users_tbl` `u`
LEFT JOIN `attendance_tbl` `a`
ON `u`.`user_id` = `a`.`user_id`
AND `a`.`time_in` BETWEEN `d`.`begin` AND `d`.`end`
ORDER BY `d`.`date`, `u`.`user_id`";
$stmt = $pdo->prepare($sql);
$startDate = new DateTime('2022-01-01');
$endDate = new DateTime('2022-02-01');
$interval = $startDate->diff($endDate, true);
$daysRange = $interval->days + 1;
// Execute the statement
$stmt->execute([
':START_DATE' => $startDate->format('Y-m-d'),
':DAYS_RANGE' => $daysRange]
);
$attendance = $stmt->fetchAll(PDO::FETCH_OBJ);
Check this. In here I call the attendance_tbl twice, one for creating a list of date and users and the other for fetching the data (time in and time out). And by using BETWEEN as #nnichols suggested to filter the selected range you prefer which I just realized earlier.
select u.`user_id`, date(a.time_in) as `date`, u.`f_name`, b.`time_in`, b.`time_out`, b.created_at from attendance_tbl a
join users_tbl u
left join attendance_tbl b on b.`user_id`=u.`user_id` and date(b.`time_in`)=date(a.`time_in`)
WHERE DATE(a.time_in) BETWEEN '2022-01-01' AND '2022-01-31'
GROUP BY `date`, u.user_id;
RESULT
user_id date f_name time_in time_out created_at
------- ---------- ----------- ------------------- ------------------- ---------------------
001 2022-01-01 John Doe 2022-01-01 08:00:00 2022-01-01 17:00:00 2022-01-03 08:00:00
002 2022-01-01 Jane Doe 2022-01-01 08:15:24 2022-01-01 17:00:00 2022-01-03 08:15:24
003 2022-01-01 Ronal Black (NULL) (NULL) (NULL)
004 2022-01-01 Lucy White (NULL) (NULL) (NULL)
001 2022-01-02 John Doe (NULL) (NULL) (NULL)
002 2022-01-02 Jane Doe (NULL) (NULL) (NULL)
003 2022-01-02 Ronal Black 2022-01-02 08:44:55 2022-01-02 17:00:00 2022-01-04 08:44:55
004 2022-01-02 Lucy White (NULL) (NULL) (NULL)
001 2022-01-03 John Doe (NULL) (NULL) (NULL)
002 2022-01-03 Jane Doe (NULL) (NULL) (NULL)
003 2022-01-03 Ronal Black (NULL) (NULL) (NULL)
004 2022-01-03 Lucy White 2022-01-03 08:40:22 2022-01-03 17:00:00 2022-01-04 08:40:22
For the ID column just create a table with AUTO_INCREMENT id and insert your selected data.
To format your date (if you really really need to) like the one in your example result, just change the DATE(a.time_in) to DATE_format(a.time_in, '%b %d %Y').
SQL Fiddle Example

Want to generate a row_number variable in MySQL 5.6 - but not using #variables

I have a table (all code is on fiddle).
id nom bal bal_dt val_dt
1 Bill 75.00 2019-11-01 2020-03-31
1 Bill 100.00 2020-04-01 2020-07-31
1 Bill 500.00 2020-08-01 2021-11-11 -- record goes over New Year 2021
2 Ben 5.00 2019-11-01 2020-03-31
2 Ben 10.00 2020-04-01 2020-07-31
2 Ben 100.00 2020-08-01 2021-11-11 -- record goes over New Year 2021
6 rows
The primary key is (id, bal_dt) - only one deposit/day.
I want to get the last record before the New Year 2021 (or <= 2021-01-01 00:00:00).
I try code from here as follow.
select a2.id, a2.nom, a2.val_dt,
(select count(*) from account a1 where a1.id < a2.id) AS rn
from account a2
where a2.val_dt <= '2021-01-01 00:00:00'
order by val_dt desc;
But result is not good.
id nom val_dt rn
1 Bill 2020-07-31 0
2 Ben 2020-07-31 3
1 Bill 2020-03-31 0
2 Ben 2020-03-31 3
I want something like
id nom rn val_dt bal
1 Bill 1 2020-08-01 500.00
2 Ben 1 2020-08-01 100.00
so I choose record for Bill and Ben. Any helps please?
note - I don't requier #variables and not assume 3 only records or only 2 accounts and not same dates and not only last date!
You can use NOT EXISTS and a correlated subquery that checks for the absence of a younger timestamp within the desired period.
SELECT a1.id,
a1.nom,
a1.val_dt
FROM account a1
WHERE a1.val_dt < '2021-01-01 00:00:00'
AND NOT EXISTS (SELECT *
FROM account a2
WHERE a2.val_dt < '2021-01-01 00:00:00'
AND a2.val_dt > a1.val_dt
AND a2.id = a1.id);
Note that 2021-01-01 00:00:00 already is in 2021, so the operator needs to actually be < not <=.
Solved it (see fiddle)!
select
tab.id, tab.md, a2.bal
from account a2
join
(
select
a1.id, max(a1.bal_dt) AS md
from account a1
where a1.bal_dt <= '2021-01-01 00:00:00'
group by a1.id
) as tab
on a2.id = tab.id and a2.bal_dt = tab.md;
and.
id md bal
1 2020-08-01 500.00
2 2020-08-01 100.00

Adding values that occur within same month: mysql

I have a table like this:
Date Name Qty
2016-09-13 00:00:00 John 2
2016-09-15 00:00:00 Matt 3
2016-09-21 00:00:00 Rich 1
2016-09-23 00:00:00 Matt 1
2016-10-05 00:00:00 John 1
2016-10-07 00:00:00 Matt 3
2016-10-12 00:00:00 Rich 0
2016-10-23 00:00:00 Matt 2
How can I do, using MySQL, to retrieve the addition of all the Qty values that corresponds to the same month and place that info on a view?
SELECT sum(Qty) as sum, month(date) as month, year(date) as year FROM table_name GROUP BY month(date), year(date)
will return
sum month year
4 12 2015
10 12 2016
What you probably want could be one of the following queries:
SELECT YEAR(`Date`) AS yr, MONTH(`Date`) AS mnt, SUM(Qty) AS Qty
FROM `table1`
GROUP BY YEAR(`Date`), MONTH(`Date`)
or
SELECT EXTRACT(YEAR_MONTH FROM `Date`) AS mnt, SUM(Qty) as Qty
FROM `table1`
GROUP BY EXTRACT(YEAR_MONTH FROM `Date`)
This query should produce something like this:
mnt | qty
---------+-----
2016-09 | 7
2016-10 | 6
The MySQL function EXTRACT() is able to return only some components of a date.

Finding MAX and MIN values for each same start and end week

There is a query I am trying to implement in which I am not having much success with in trying to find the MAX and MIN for each week.
I have 2 Tables:
SYMBOL_DATA (contains open,high,low,close, and volume)
WEEKLY_LOOKUP (contains a list of weeks(no weekends) with a WEEK_START and WEEK_END)
**SYMBOL_DATA Example:**
OPEN, HIGH, LOW, CLOSE, VOLUME
23.22 26.99 21.45 22.49 34324995
WEEKLY_LOOKUP (contains a list of weeks(no weekends) with a WEEK_START and WEEK_END)
**WEEKLY_LOOKUP Example:**
WEEK_START WEEK_END
2016-01-25 2016-01-29
2016-01-18 2016-01-22
2016-01-11 2016-01-15
2016-01-04 2016-01-08
I am trying to find for each WEEK_START and WEEK_END the high and low for that particular week.
For instance, if the WEEK is WEEK_START=2016-01-11 and WEEK_END=2016-01-15, I would have
5 entries for that particular symbol listed:
DATE HIGH LOW
2016-01-15 96.38 93.54
2016-01-14 98.87 92.45
2016-01-13 100.50 95.21
2016-01-12 99.96 97.55
2016-01-11 98.60 95.39
2016-01-08 100.50 97.03
2016-01-07 101.43 97.30
2016-01-06 103.77 100.90
2016-01-05 103.71 101.67
2016-01-04 102.24 99.76
For each week_ending (2016-01-15) the HIGH is 100.50 on 2016-01-13 and the LOW is 92.45 on 2016-01-14
I attempted to write a query that gives me a list of highs and lows, but when I tried adding a MAX(HIGH), I had only 1 row returned back.
I tried a few more things in which I couldn't get the query to work (some sort of infinite run type). For now, I just have this that gives me a list of highs and lows for every day instead of the roll-up for each week which I am not sure how to do.
select date, t1.high, t1.low
from SYMBOL_DATA t1, WEEKLY_LOOKUP t2
where symbol='ABCDE' and (t1.date>=t2.START_DATE and t1.date<=t2.END_DATE)
and t1.date<=CURDATE()
LIMIT 30;
How can I get for each week (Start and End) the High_Date, MAX(High), and Low_Date, MIN(LOW) found each week? I probably don't need a
full history for a symbol, so a LIMIT of like 30 or (30 week periods) would be sufficient so I can see trending.
If I wanted to know for example each week MAX(High) and MIN(LOW) start week ending 2016-01-15 the result would show
**Result:**
WEEK_ENDING 2016-01-15 100.50 2016-01-13 92.45 2016-01-14
WEEK_ENDING 2016-01-08 103.77 2016-01-06 97.03 2016-01-08
etc
etc
Thanks to all of you with the expertise and knowledge. I greatly appreciate your help very much.
Edit
Once the Week Ending list is returned containing the MAX(HIGH) and MIN(LOW) for each week, is it possible then on how to find the MAX(HIGH) and MIN(LOW) from that result set so it return then only 1 entry from the 30 week periods?
Thank you!
To Piotr
select part1.end_date,part1.min_l,part1.max_h, s1.date, part1.min_l,s2.date from
(
select t2.start_date, t2.end_date, max(t1.high) max_h, min(t1.low) min_l
from SYMBOL_DATA t1, WEEKLY_LOOKUP t2
where symbol='FB'
and t1.date<='2016-01-22'
and (t1.date>=t2.START_DATE and t1.date<=t2.END_DATE)
group by t2.start_date, t2.end_date order by t1.date DESC LIMIT 1;
) part1, symbol_data s1, symbol_data s2
where part1.max_h = s1.high and part1.min_l = s2.low;
You will notice that the MAX and MIN for each week is staying roughly the same and not changing as it should be different for week to week for both the High and Low.
SQL Fiddle
I have abbreviated some of your names in my example.
Getting the high and low for each week is pretty simple; you just have to use GROUP BY:
SELECT s1.symbol, w.week_end, MAX(s1.high) AS weekly_high, MIN(s1.LOW) as weekly_low
FROM weeks AS w
INNER JOIN symdata AS s1 ON s1.zdate BETWEEN w.week_start AND w.week_end
GROUP BY s1.symbol, w.week_end
Results:
| symbol | week_end | weekly_high | weekly_low |
|--------|---------------------------|-------------|------------|
| ABCD | January, 08 2016 00:00:00 | 103.77 | 97.03 |
| ABCD | January, 15 2016 00:00:00 | 100.5 | 92.45 |
Unfortunately, getting the dates of the high and low requires that you re-join to the symbol_data table, based on the symbol, week and values. And even that doesn't do the job; you have to account for the possibility that there might be two days where the same high (or low) was achieved, and decide which one to choose. I arbitrarily chose the first occurrence in the week of the high and low. So to get that second level of choice, you need another GROUP BY. The whole thing winds up looking like this:
SELECT wl.symbol, wl.week_end, wl.weekly_high, MIN(hd.zdate) as high_date, wl.weekly_low, MIN(ld.zdate) as low_date
FROM (
SELECT s1.symbol, w.week_start, w.week_end, MAX(s1.high) AS weekly_high, MIN(s1.low) as weekly_low
FROM weeks AS w
INNER JOIN symdata AS s1 ON s1.zdate BETWEEN w.week_start AND w.week_end
GROUP BY s1.symbol, w.week_end) AS wl
INNER JOIN symdata AS hd
ON hd.zdate BETWEEN wl.week_start AND wl.week_end
AND hd.symbol = wl.symbol
AND hd.high = wl.weekly_high
INNER JOIN symdata AS ld
ON ld.zdate BETWEEN wl.week_start AND wl.week_end
AND ld.symbol = wl.symbol
AND ld.low = wl.weekly_low
GROUP BY wl.symbol, wl.week_start, wl.week_end, wl.weekly_high, wl.weekly_low
Results:
| symbol | week_end | weekly_high | high_date | weekly_low | low_date |
|--------|---------------------------|-------------|---------------------------|------------|---------------------------|
| ABCD | January, 08 2016 00:00:00 | 103.77 | January, 06 2016 00:00:00 | 97.03 | January, 08 2016 00:00:00 |
| ABCD | January, 15 2016 00:00:00 | 100.5 | January, 13 2016 00:00:00 | 92.45 | January, 14 2016 00:00:00 |
To get the global highs and lows, just remove the weekly table from the original query:
SELECT wl.symbol, wl.high, MIN(hd.zdate) as high_date, wl.low, MIN(ld.zdate) as low_date
FROM (
SELECT s1.symbol, MAX(s1.high) AS high, MIN(s1.low) as low
FROM symdata AS s1
GROUP BY s1.symbol) AS wl
INNER JOIN symdata AS hd
ON hd.symbol = wl.symbol
AND hd.high = wl.high
INNER JOIN symdata AS ld
ON ld.symbol = wl.symbol
AND ld.low = wl.low
GROUP BY wl.symbol, wl.high, wl.low
Results:
| symbol | high | high_date | low | low_date |
|--------|--------|---------------------------|-------|---------------------------|
| ABCD | 103.77 | January, 06 2016 00:00:00 | 92.45 | January, 14 2016 00:00:00 |
The week table seems entirely redundant...
SELECT symbol
, WEEK(zdate)
, MIN(low) min
, MAX(high) max_high
FROM symdata
GROUP
BY symbol, WEEK(zdate);
This is a simplified example. In reality, you might use DATE_FORMAT or something like that instead.
http://sqlfiddle.com/#!9/c247f/3
Check if following query produces desired result:
select part1.end_date,part1.min_l,part1.max_h, s1.date, part1.min_l,s2.date from
(
select t2.start_date, t2.end_date, max(t1.high) max_h, min(t1.low) min_l
from SYMBOL_DATA t1, WEEKLY_LOOKUP t2
where symbol='ABCDE'
and (t1.date>=t2.START_DATE and t1.date<=t2.END_DATE)
group by t2.start_date, t2.end_date
) part1, symbol_data s1, symbol_data s2
where part1.max_h = s1.high and part1.min_l = s2.low
and (s1.date >= part1.start_date and part1.end_date)
and (s2.date >= part1.start_date and part1.end_date)