MySQL: Add Default Value to Joined Table when Row not Found - mysql

System info:
$ uname -srvm
Linux 5.15.0-56-generic #62-Ubuntu SMP Tue Nov 22 19:54:14 UTC 2022 x86_64
$ mysql --version
mysql Ver 8.0.31-0ubuntu0.22.04.1 for Linux on x86_64 ((Ubuntu))
I am very inexperienced with MySQL & have been looking for an answer to this for about half a week. I am working with two tables named character_stats & halloffame that I want to join in a query. They look like this:
mysql> SELECT name, level FROM character_stats;
+-----------+-------+
| name | level |
+-----------+-------+
| foo | 0 |
| bar | 0 |
| baz | 3 |
| tester | 4 |
| testertoo | 2 |
+-----------+-------+
mysql> SELECT * from halloffame;
+----+-----------+----------+--------+
| id | charname | fametype | points |
+----+-----------+----------+--------+
| 1 | bar | T | 0 |
| 2 | foo | T | 0 |
| 3 | baz | T | 0 |
| 4 | tester | T | 0 |
| 5 | testertoo | T | 0 |
| 6 | tester | D | 40 |
| 7 | tester | M | 92 |
| 8 | bar | M | 63 |
+----+-----------+----------+--------+
In my query, I want to display all the rows from character_stats & I want to join the points column from halloffame for fametype='M'. If there is no row for fametype='M', I want to set points to 0 for that character name, instead of omitting the entire row as is done in the following:
mysql> SELECT name, level, points FROM character_stats JOIN
-> (SELECT charname, points FROM halloffame WHERE fametype='M')
-> AS hof ON (hof.charname=name);
+--------+-------+--------+
| name | level | points |
+--------+-------+--------+
| tester | 4 | 92 |
| bar | 0 | 63 |
+--------+-------+--------+
So I want it to output this:
+-----------+-------+--------+
| name | level | points |
+-----------+-------+--------+
| foo | 0 | 0 |
| bar | 0 | 63 |
| baz | 3 | 0 |
| tester | 4 | 92 |
| testertoo | 2 | 0 |
+-----------+-------+--------+
I have tried to learn how to use IFNULL, IF-THEN-ELSE, CASE, COALESCE, & COUNT statements from what I have found in documentation & answers on stackoverflow.com. But as I said, I am very inexperienced & don't know how to implement them.
The following works on its own:
SELECT IFNULL((SELECT points FROM halloffame WHERE fametype='M'
AND charname='foo' LIMIT 1), 0) as points;
But I don't know how to join it to the character_stats table. The following would work if I knew how to get the value of character_stats.name before COALESCE is called:
SELECT name, level, 'M' AS fametype, points FROM character_stats
JOIN (SELECT COALESCE((SELECT points FROM halloffame WHERE
fametype='M' AND charname=name LIMIT 1), 0) AS points) AS hof;
According to Adding Default Values on Joining Tables I should be able to use CROSS JOIN, but I am doing something wrong as it still results in Unknown column 'cc.name' in 'where clause':
SELECT name, level, points FROM character_stats
CROSS JOIN (SELECT DISTINCT name FROM character_stats) AS cc
JOIN (SELECT COALESCE((SELECT points FROM halloffame WHERE
fametype='M' AND charname=cc.name LIMIT 1), 0) AS points) AS hof;
Some references I have looked at:
Returning a value even if no result
Usage of MySQL's "IF EXISTS"
Return Default value if no row found
MySQL.. Return '1' if a COUNT returns anything greater than 0
How do write IF ELSE statement in a MySQL query
Simple check for SELECT query empty result
Is there a function equivalent to the Oracle's NVL in MySQL?
MySQL: COALESCE within JOIN
Unknown Column In Where Clause With Join
Adding Default Values on Joining Tables
https://www.tutorialspoint.com/returning-a-value-even-if-there-is-no-result-in-a-mysql-query

I found that I can do the following:
SELECT name, level, COALESCE((SELECT points FROM
halloffame WHERE fametype='M' AND charname=name
LIMIT 1), 0) AS points FROM character_stats;
Though I would still like to know how to do it within a JOIN statement.

Related

MySQL query for data by step size in a given range

So basically I have a users table which has a column named "completed_surveys" which holds total number of completed surveys.
I need to create a query which would take step size number and would group them by that range.
Example result which would suit my needs:
+---------+-------------------+
| range | completed_surveys |
+---------+-------------------+
| 0-14 | 4566 |
| 14-28 | 3412 |
| 28-42 | 5456 |
| 42-56 | 33 |
| 56-70 | 31 |
| 70-84 | 441 |
| 84-98 | 576 |
| 98-112 | 23 |
| 112-126 | 12 |
| 126-140 | 1 |
+---------+-------------------+
What I have so far:
select concat(what should i add here??) as `range`,
count(users.completed_surveys) as `completed_surveys` from users WHERE users.completed_surveys > 0 group by 1 order by users.completed_surveys;
I think this query is correct however in the concat function I don't really know how to increase the previous number by 14. Any ideas?
One idea is to first create a helper table with values 0..9 .
CREATE TABLE tmp ( i int );
INSERT INTO tmp VALUES (0) , (1) ... (9);
Then join the two tables:
SELECT concat(i,' - ',(i+1)*7) as `range`,
count(users.completed_surveys) as `completed_surveys` from users
INNER JOIN tmp ON (users.completed_surveys>tmp.i AND users.completed_surveys<=(i+1)*7)
WHERE users.completed_surveys > 0
GROUP BY tmp.i
ORDER BY tmp.i

MySQL bitwise comparison

I have mysql with a user table with answers from a poll saved as a bitwise. How do I find the user with most or least common answers with the reference bitwise?
+------+---------+--+
| User | Answers | |
+------+---------+--+
| A | 1 | |
| B | 5 | |
| C | 10 | |
+------+---------+--+
Assuming by 'reference bitwise' you mean that you have another value that is a bitmask that you are trying to match against the Answers column, something like this should do it for you. In this case, I'm using '4' as the reference bitmask and myTable as the name of your table..
SELECT User, BIT_COUNT(Answers & 4) AS MatchedBits FROM myTable ORDER BY MatchedBits DESC
This returns:
+------+-------------+
| User | MatchedBits |
+------+-------------+
| B | 1 |
| A | 0 |
| C | 0 |
+------+-------------+
You can also add a LIMIT 1 clause to get just the top result, but of course that won't tell you if there is more than one top result with the same number of bits matched.

MySQL: optimize query for scoring calculation

I have a data table that I use to do some calculations. The resulting data set after calculations looks like:
+------------+-----------+------+----------+
| id_process | id_region | type | result |
+------------+-----------+------+----------+
| 1 | 4 | 1 | 65.2174 |
| 1 | 5 | 1 | 78.7419 |
| 1 | 6 | 1 | 95.2308 |
| 1 | 4 | 1 | 25.0000 |
| 1 | 7 | 1 | 100.0000 |
+------------+-----------+------+----------+
By other hand I have other table that contains a set of ranges that are used to classify the calculations results. The range tables looks like:
+----------+--------------+---------+
| id_level | start | end | status |
+----------+--------------+---------+
| 1 | 0 | 75 | Danger |
| 2 | 76 | 90 | Alert |
| 3 | 91 | 100 | Good |
+----------+--------------+---------+
I need to do a query that add the corresponding 'status' column to each value when do calculations. Currently, I can do that adding the following field to calculation query:
select
...,
...,
[math formula] as result,
(select status
from ranges r
where result between r.start and r.end) status
from ...
where ...
It works ok. But when I have a lot of rows (more than 200K), calculation query become slow.
My question is: there is some way to find that 'status' value without do that subquery?
Some one have worked on something similar before?
Thanks
Yes, you are looking for a subquery and join:
select s.*, r.status
from (select s.*
from <your query here>
) s left outer join
ranges r
on s.result between r.start and r.end
Explicit joins often optimize better than nested select. In this case, though, the ranges table seems pretty small, so this may not be the performance issue.

substitute one value with another from a different table in SELECT statement

I'm trying come up with a SQL statement to print all the duplicate [exported-resource] definition in the Puppet database.
mysql> SELECT id,restype,host_id,source_file_id FROM resources
-> WHERE title IN (SELECT title FROM resources WHERE exported=1
-> GROUP BY title HAVING count(title) > 1) ORDER BY title;
+------+------------------------+---------+----------------+
| id | restype | host_id | source_file_id |
+------+------------------------+---------+----------------+
| 305 | Nagios::Client::Export | 2 | 18 |
| 333 | Nagios_host | 2 | 39 |
| 605 | Nagios_hostextinfo | 6 | 2 |
| 443 | Nagios_hostextinfo | 2 | 39 |
| 499 | Nagios_host | 6 | 2 |
| 770 | Nagios::Client::Export | 6 | 18 |
......
......
Which is working just fine, but how can I retrieve/print hosts.name from hosts table in stead of the host_id. I just can't get my head around with rewriting the above SQL statement. The hosts table looks like this:
mysql> SELECT id,name FROM hosts;
+----+-----------------------------------------+
| id | name |
+----+-----------------------------------------+
| 2 | controller-dns-01.sdas.cloud.com |
| 6 | controller-monitoring-01.sdas.cloud.com |
| 1 | controller-puppet.sdas.cloud.com |
| 13 | talend-admin-01.sdas.cloud.com |
| 15 | talend-jobserver-01.sdas.cloud.com |
| 14 | talend-jobserver-02.sdas.cloud.com |
+----+-----------------------------------------+
Also, is there a way to print only the first part of the hostname (i.e. only controller-dns-01) in stead of the complete string? Any suggestion from any one greatly appreciated. Cheers!!
Update:
This is my final command: Just in case if someone else also looking for a way to find out the Puppet Exported resources duplicate definitions
mysql> CREATE INDEX index_resources_on_restypetitle ON resources (restype(12),title(12));
mysql> SELECT r.id, r.restype, r.title, SUBSTRING_INDEX(h.name,'.',1) AS 'host_name',
-> SUBSTRING_INDEX(s.filename,'puppet/',-1) AS 'file_name', r.line FROM resources r
-> LEFT JOIN hosts h ON r.host_id = h.id LEFT JOIN source_files s ON r.source_file_id = s.id
-> WHERE MD5(CONCAT(restype,title,host_id))
-> IN (SELECT MD5(CONCAT(restype,title,host_id)) FROM resources
-> WHERE exported=1 GROUP BY MD5(CONCAT(restype,title,host_id))
-> HAVING COUNT(MD5(CONCAT(restype,title,host_id))) > 1) ORDER BY title;
the SUBSTRING_INDEX(s.filename....) bit may needs readjusting according to the configuration. A big thank to thiella for helping me out.
You need to JOIN your resources table with your hosts table, using SUBSTRING_INDEX to show the part of the string at the left of the dot:
SELECT
r.id, r.restype, r.host_id, r.source_file_id,
SUBSTRING_INDEX(h.name, '.', 1)
FROM
resources r LEFT JOIN hosts h
ON r.host_id = h.id
WHERE
r.title IN (SELECT title
FROM resources
WHERE export=1
GROUP BY title
HAVING count(title) > 1)
ORDER BY
r.title;

In MYSQL, how do I get a LEFT JOIN to return every row in one table, and a flag if there were any matching rows in another table?

Basically, I have two tables, admin_privilege and admin_roll_privilege. I'm trying to write a query to get every row from admin_privilege, and if there is a row in admin_roll_privilege with a matching admin_privilege_id AND a matching admin_roll_id, to set a new column to 1. So far, I have this:
SELECT ap.*,
IF(arp.admin_privilege_id IS NULL,0,1) AS has_privilege
FROM admin_privilege ap LEFT JOIN admin_roll_privilege arp
ON ap.admin_privilege_id=arp.admin_privilege_id
WHERE arp.admin_roll_id=3
OR arp.admin_roll_id IS NULL;
This works in every case except where there are no matching rows admin_roll_privilege.
See Example:
+---------------+--------------------+
| admin_roll_id | admin_privilege_id |
+---------------+--------------------+
| 1 | 2 |
| 1 | 3 |
+---------------+--------------------+
+--------------------+------------------------+
| admin_privilege_id | admin_privilege_name |
+--------------------+------------------------+
| 1 | Access Developer Tools |
| 4 | Edit System Settings |
| 2 | Edit User Profiles |
| 3 | Resolve Challenges |
+--------------------+------------------------+
Querying for WHERE admin roll id=1 works as expected:
+--------------------+------------------------+---------------+
| admin_privilege_id | admin_privilege_name | has_privilege |
+--------------------+------------------------+---------------+
| 1 | Access Developer Tools | 0 |
| 4 | Edit System Settings | 0 |
| 2 | Edit User Profiles | 1 |
| 3 | Resolve Challenges | 1 |
+--------------------+------------------------+---------------+
But, if i query for admin_roll_id=3, i only get two rows returned:
+--------------------+------------------------+---------------+
| admin_privilege_id | admin_privilege_name | has_privilege |
+--------------------+------------------------+---------------+
| 1 | Access Developer Tools | 0 |
| 4 | Edit System Settings | 0 |
+--------------------+------------------------+---------------+
How can I get this query to return all 4?
Edit: This is what ended up working, moving the condition to the on clause:
SELECT ap.*,
IF(arp.admin_privilege_id IS NULL,0,1) AS has_privilege
FROM admin_privilege ap LEFT JOIN admin_roll_privilege arp
ON (ap.admin_privilege_id=arp.admin_privilege_id AND arp.admin_roll_id=1)
Move the appropriate conditions from the WHERE clause to the ON clause.
You are not returning all rows by using the WHERE clause on the entire statement.
Turn the LEFT JOIN into a subselect on wich you can add the WHERE clause you need.
SELECT ap.admin_privilege_id
, ap.admin_privilege_name
, IF(arp.admin_privilege_id IS NULL,0,1) AS has_privilege
FROM admin_privilege ap
LEFT OUTER JOIN (
SELECT admin_privilege_id
FROM admin_roll_privilege arp
WHERE arp.admin_roll_id = 3
) arp ON arp.admin_privilege_id = ap.admin_privilege_id