mysql if result does't exist return 0 - mysql

I have query
SELECT FROM_UNIXTIME(1800 * FLOOR(date/1800)) AS period_start,
COUNT(*) AS count
FROM readed_messages
GROUP BY period_start
ORDER BY period_start ASC
How to add clause if/else, if record doesn't exist. I would like to get commands return 0 and commands return count. I didn't know to do this.

Generating missing data is an annoying problem. Usually it's best left to the application level.
Failing that, and if you have to do it in MySQL, the missing data has to come from somewhere. This can be either a dynamic sequence of numbers built from user variables and a union cross join - or if we know our domain values (like we do in this case) we can pre-build some tables to take care of it.
In this case you're operating on dates, so we need a few tables that will help us generate all the values you're interested in. I'd suggest these tables:
calendar_years(y integer)
calendar_months(m integer)
calendar_days(d integer)
calendar_hours(h integer)
calendar_minutes(m integer)
These tables would all be a single column, containing the range of values you're looking at. calendar_minutes can just have 0 and 30 as values because we're just looking at half hour increments.
This is an instance where a cross join is actually what we want.
select unix_timestamp(concat(y, '-', lpad(m, 2, '0'), '-', lpad(d, 2, '0'), ' ', lpad(h, 2, '0'), ':', lpad(mm, 2, '0'), ':', '00')) tt
from calendar_years
cross join calendar_months
cross join calendar_days
cross join calendar_hours
cross join calendar_minutes
Will now give us every possible combination of our table values, or in essence - every period we're interested in looking at.
We build those columns into a timestamp by using concat and lpad to turn them into a valid datetime string, and then convert it with unix_timestamp.
This gives us a set of values to left join against readed_messages, which will give us your final answer:
select from_unixtime(tt), count(id)
from (
select unix_timestamp(concat(y, '-', lpad(m, 2, '0'), '-', lpad(d, 2, '0'), ' ', lpad(h, 2, '0'), ':', lpad(mm, 2, '0'), ':', '00')) tt
from calendar_years
cross join calendar_months
cross join calendar_days
cross join calendar_hours
cross join calendar_minutes
) q
left join readed_messages rm
on (1800 * floor(`date` / 1800)) = q.tt
where tt <> 0 -- this method is brute force, generates some invalid dates
group by tt
Obviously this will give us a lot of values, probably for a period far greater than you're interested in looking at, you can further filter it in the where clause if you wish.
heres a demo

Related

Is it possible to pass several rows of data into a MySQL subquery instead of using a temporary table?

I'd like to know if it's possible to pass rows of data directly into a select subquery, rather than setting up a temporary table and joining on that.
My actual use case is trying to prevent thousands of individual queries, and for architectural reasons adding a temporary table would be a pain (but not impossible, so it's where I may have to go.)
An simplified example of my issue is :
I have a table giving the number plate of the cars in a car park, with each row containing section (a letter), space (a number), and reg_plate (a tinytext).
My boss gives me a list of 3 places and wants the reg number of the car in each (or null if empty).
Now I can do this by creating a temporary table containing the section/space sequence I'm interested in, and then join my carpark table against that to give me each of the rows.
I'm wondering is there a syntax where I could do this in a select, perhaps with a subselect something like this - obviously invalid, but hopefully it shows what I'm getting at:
SELECT targets.section, targets.space, cp.reg_plate
FROM carpark cp
LEFT JOIN (
SELECT field1 AS section, field2 AS space
FROM (
('a', 7), ('c', 14), ('c', 23)
)
) targets ON (cp.section = targets.section AND cp.space = targets.space)
Any ideas gratefully received!
You can use UNION:
SELECT targets.section, targets.space, cp.reg_plate
FROM carpark cp
LEFT JOIN (
SELECT 'a' as section, 7 AS space
UNION ALL
SELECT 'c', 14
UNION ALL
SELECT 'c', 23
) targets ON cp.section = targets.section AND cp.space = targets.space
A followup;
I realised I was hoping to find something analagous to the VALUES used in an insert statement.
It turns out this is available in MySQL 8+, and VALUES is available as a table constructor:
https://dev.mysql.com/doc/refman/8.0/en/values.html
SELECT targets.section, targets.space, cp.reg_plate
FROM carpark cp
LEFT JOIN (
VALUES
ROW ('a', 7), ROW('c', 14), ROW('c', 23)
)
) targets ON (cp.section = targets.column_0 AND cp.space = targets.column_1)
Not available in earlier MySQL (so the UNION method is OK) but great in 8+.

SQL query to add a column that is currently in a different table

I'm new to SQL. I dabble with it, but get lost easily. Anyhow, I have these two tables that part numbers in them. One of them the part number is the primary key. I guess would have to be the foreign key in the other table. This other table is where the column of information is that I want to query into the first table that has the primary key of part numbers. I can manage to show that part number column easy enough in the first table, but that doesn't do me any good. I need a column called AverageUnitCost, that is directly tied to the cost of each part number.
SELECT
QALog.QALID, QALog.GroupID, QALog.LogDate, QALog.SONumber,
QALog.PartNumber, QALog.PartNotes, QALog.TravelerQty, QALog.EUser,
QALog.ITID, QALog.TrackingNumber, QALog.MDR, QALog.ExpirationDate,
QALog.PONumber, QALog.ReceiptNo, QALog.ReasonID, QALog.RRNo,
Rejections.NumDiscrp, Rejections.RRID, RejectReason.Reason,
ProductGroups.GroupName, Disposition.Disposition, CI_ITEM.itemcode
FROM QALog
INNER JOIN Rejections
ON QALog.QALID = Rejections.QALID
INNER JOIN RejectReason
ON RejectReason.RRID = Rejections.RRID
INNER JOIN Disposition
ON Disposition.DispositionID = Rejections.DispositionID
INNER JOIN ProductGroups
ON ProductGroups.PGID = QALog.GroupID
INNER JOIN CI_ITEM
ON QALog.PartNumber = CI_ITEM.itemcode
WHERE (QALog.LogDate >= DATEADD(year, - 3, GETDATE()))
AND (QALog.ITID = '3')
AND (RejectReason.GroupID = '0')
OR
(QALog.LogDate >= DATEADD(year, - 3, GETDATE()))
AND (QALog.ITID = '3')
AND (RejectReason.GroupID = '3')
ORDER BY QALog.QALID
Your joins look OK, assuming you have the correct table column relationships. But your WHERE condition is wrong, because of the way AND and OR are combined.
But you don't need such a complex mixture, since you have the same conditions on LogDate and ITID in both parts of the OR. So you can simplify it to:
WHERE QALog.LogDate >= DATEADD(year, - 3, GETDATE())
AND QALog.ITID = '3'
AND RejectReason.GroupID IN ('0', '3')
Try in this format
SELECT columnname1, columnname2, columnname3,..... FROM Table1 t, Table2,...... t2 WHERE
t.columnname = t2.columnname ORDER BY ....
Not sure what you are trying to do. Without schema, it is kind of hard to help

Using column value as clause for IN MySql

I don't understand why this doesn't work, I have a column where I store values comma separated in my "Mysql" database then I want to join two tables to give me results. eg:
SELECT *
FROM users u INNER JOIN
groups g
ON u.id IN ( g.ownerId )
WHERE u.active='1' AND g.gid='15';
And the value of g.ownerId in this senerio is '175,178'.
For some reason this only returns the results from the join with ownerId 175. BUT if I manually enter the values ( 175, 178 ) in the IN clause BOTH rows show up. Why isn't it using both values in the ownerId column?
I have tried this to "separate" the values or force a "list" but it didn't work...
SELECT * FROM users u INNER JOIN groups g ON u.id IN ( SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(g.ownerId, ',', 1), ' ', -1) as x,
SUBSTRING_INDEX(SUBSTRING_INDEX(g.ownerId, ',', 2), ' ', -1) as y ) where g.groupId='15'
Has anyone experienced this before or know what to do?
It doesn't work because the in value consists of a list with a single element that happens to have a comma in it. It is equivalent to:
on uid = '175,178'
You can replace the logic with find_in_set():
on find_in_set(uid, g.id) > 0
However, you really should learn about junctions tables and why your data structure is bad, bad, bad:
You are storing numbers as strings.
You have foreign key relationships with no way to declare them.
You are using string operations inappropriately.
Your query cannot make use of an index.
Fix the data structure.

Can't fetch a field with GROUP BY clause

I'm trying to create a simple query that will find a person with highest average marks and display some basic information about them. It's retrieving the proper record, but I can't make MySQL display students.classId field. The error I'm getting in LibreOffice Base is
Not in aggregate function or group by clause.
Query with error:
SELECT CONCAT(`students`.`surname`, CONCAT(' ', `students`.`name`)) AS `Student`,
AVG(CAST(`marks`.`mark` AS DECIMAL (10, 2))) AS `Average`,
`students`.`classId`
FROM `students`, `marks`, `subjects`
WHERE `marks`.`subjectId` = `subjects`.`subjectId`
AND `students`.`studentId` = `marks`.`markId`
GROUP BY `students`.`surname`, `students`.`name`
ORDER BY `Average` DESC LIMIT 1;
Query without error:
SELECT CONCAT(`students`.`surname`, CONCAT(' ', `students`.`name`)) AS `Student`,
AVG(CAST(`marks`.`mark` AS DECIMAL (10, 2))) AS `Average`
FROM `students`, `marks`, `subjects`
WHERE `marks`.`subjectId` = `subjects`.`subjectId`
AND `students`.`studentId` = `marks`.`markId`
GROUP BY `students`.`surname`, `students`.`name`
ORDER BY `Average` DESC LIMIT 1;
I'm not really experienced with SQL, but I think that posting table definitions isn't necessary in this case. If I am wrong, please leave a note in the comments, I'll update the question as soon as possible.
Please note that it is not a homework.
The problematic item is this:
`students`.`classId`
Since the GROUP BY query produces a single row for one or more rows of the joined tables, that single row may correspond to more than one students.classId value.
That is what SQL is asking you to fix: it wants to know which of potentially many items of students.classId you want it to return. The two choices are adding an aggregate function, say
MIN(`students`.`classId`) AS StudentClassId
or using students.classId in the GROUP BY clause:
GROUP BY `students`.`surname`, `students`.`name`, `students`.`classId`
Note that if you go with the later choice, the aggregation would be per student / class pair, not per student.

Mysql: Select most recent entry by user and case number

I have a table whose data looks like this:
INSERT INTO `cm_case_notes` (`id`, `case_id`, `date`, `time`, `description`, `username`, `supervisor`, `datestamp`) VALUES
(45977, '1175', '2010-11-19 16:27:15', 600, 'Motion hearing...Denied.', 'bjones', 'jharvey,', '2010-11-19 21:27:15'),
(46860, '1175', '2010-12-11 16:11:19', 300, 'Semester Break Report', 'bjones', 'jharvey,', '2010-12-11 21:11:19'),
(48034, '1175', '2011-05-04 17:30:03', 300, 'test', 'bjones', 'jharvey,', '2011-05-04 22:30:03'),
(14201, '1175', '2009-02-06 00:00:00', 3600, 'In court to talk to prosecutor, re: the file', 'csmith', 'sandrews', '2009-02-07 14:33:34'),
(14484, '1175', '2009-02-13 00:00:00', 6300, 'Read transcript, note taking', 'csmith', 'sandrews', '2009-02-16 17:22:36');
I'm trying to select the most recent case note (by date) on each case by each user. The best I've come up with is:
SELECT * , MAX( `date` ) FROM cm_case_notes WHERE case_id = '1175' GROUP BY username
This, however, doesn't give the most recent entry, but the first one for each user. I've seen several similar posts here, but I just can't seem to get my brain around them. Would anybody take pity on the sql-deficient and help?
If you want only the dates of the most recent case note for every user and every case, you can use this:
--- Q ---
SELECT case_id
, username
, MAX( `date` ) AS recent_date
FROM cm_case_notes
GROUP BY case_id
, username
If you want all the columns from these row (with most recent date) follow the Quassnoi link for various solutions (or the other provided links). The easiest to write would be to make the above query into a subquery and join it to cm_case_notes:
SELECT cn.*
FROM
cm_case_notes AS cn
JOIN
( Q ) AS q
ON ( q.case_id, q.username, q.recent_date )
= ( cn.case_id, cn.username, cn.`date` )
If you just want the lastet case note but only for a particular case_id, then you could add the where condition in both cn and Q (Q slightly modified):
SELECT cn.*
FROM
cm_case_notes AS cn
JOIN
( SELECT username
, MAX( `date` ) AS recent_date
FROM cm_case_notes
WHERE case_id = #particular_case_id
GROUP BY username
) AS q
ON ( q.username, q.recent_date )
= ( cn.username, cn.`date` )
WHERE cn.case_id = #particular_case_id
the reason why you don't get what would like to fetch from the database is the use of SELECT * together with GROUP.
In fact, only the results of aggregate functions and / or the group field(s) itself can be safely SELECTed. selecting anything else leads to unexpected results. (the exact result depends on order, query optimization and such).
What you are trying to achieve is called fetching a "groupwise maximum". This is a common problem / common task in SQL, you can read a nice writeup here:
http://jan.kneschke.de/projects/mysql/groupwise-max/
or in the MySQL manual here:
http://dev.mysql.com/doc/refman/5.1/en/example-maximum-column-group-row.html
or a detailed long explanation by stackoverflow user Quassnoi here:
http://explainextended.com/2009/11/24/mysql-selecting-records-holding-group-wise-maximum-on-a-unique-column/
Have you considered a DESC ordering and simply limiting 1?