MySQL select based on precedence of a column's values - mysql

Say you have a dynamic view of a table that consists of the following entries
| id | type |
| 1 | cat |
| 2 | dog |
| 3 | bunny|
The catch with this is that sometimes there may not be an id with a type of "cat" it could potentially look like
| id | type |
| 1 | dog |
| 2 | bunny|
How would one write a query for this table to select a single row based on precedence of type.
For example, if the ranking of type was cat > dog > bunny, I would want the first entry from that table with the highest ranking condition.
If we call our view pets, which has ORDER BY id ASC then we would have a query that looks something like this
SELECT *
FROM pets
WHERE type = 'cat' > 'dog' > 'bunny' -- here is where I need help
LIMIT 1
I've tried things like
IF EXISTS (SELECT *
FROM pets
WHERE type = "cat")
THEN SELECT * FROM pets WHERE condition = "cat" LIMIT 1
-- etc... down the ranking
Can't seem to get this to work though. Help is much appreciated.

In MySQL, I would use field():
SELECT *
FROM pets
WHERE type IN ('cat', 'dog', 'bunny')
ORDER BY field(type, 'cat', 'dog', 'bunny')
LIMIT 1
If you don't want to deal with the WHERE clause, you can do:
SELECT *
FROM pets
ORDER BY field(type, 'bunny', 'dog', 'cat') DESC
LIMIT 1
You need to go backwards because non-matches are returned as 0.

You can try to use CASE WHEN make the order number in ORDER BY, do your customer order by then LIMIT 1
CREATE TABLE pets(
ID INT,
type VARCHAR(50)
);
INSERT INTO pets VALUES ( 1 , 'cat');
INSERT INTO pets VALUES ( 2 , 'dog');
INSERT INTO pets VALUES ( 3 , 'bunny');
INSERT INTO pets VALUES ( 4 , 'TST');
Query 1:
SELECT *
FROM pets
ORDER BY CASE
WHEN type = 'cat' THEN 1
WHEN type = 'dog' THEN 2
WHEN type = 'bunny' THEN 3
ELSE 4
END
LIMIT 1
Results:
| ID | type |
|----|------|
| 1 | cat |

Related

Using GROUP BY with MAX()/MIN() giving bad results

The table
The query
SELECT
id, MAX(fecha_hora_carga) AS fecha_hora_carga
FROM
calibraciones_instrumentos
GROUP BY
instrumento_id
The result
Its returning the most recent fecha_hora_carga dates, but the ids are 24 and 28...i think they should be 27 and 29!
Why are the ids not corresponding with the date?
The problem is MySQL does not make much sense when grouping by a max value.
It grabs the max column and then the other columns in that table you selected by whatever order you sort them by.
To get what you want, you have to use subqueries to pull the data you want.
Here is an example:
SELECT
t1.id,
t1.fecha_hora_carga
FROM
calibraciones_instrumentos AS t1
JOIN(
SELECT MAX(fecha_hora_carga) AS fecha_hora_carga,
instrument_id
FROM
calibraciones_instrumentos
GROUP BY
instrument_id
) AS t2
ON (t1.fecha_hora_carga = t2.fecha_hora_carga AND
t1.instrument_id = t2.instrument_id
);
Because you are misusing SQL. You have one column in the GROUP BY clause and that column isn't even being selected!
In most databases -- including the most recent versions of MySQL -- your query would generate a syntax error because id is neither in the GROUP BY nor an argument to an aggregation function such as MIN().
So, MySQL is providing just an arbitrary id. I would expect an aggregation query to look like this:
SELECT instrumento_id, MAX(fecha_hora_carga) AS fecha_hora_carga
FROM calibraciones_instrumentos
GROUP BY instrumento_id;
Or, if you want the row with the maximum fecha_hora_carga for each instrumento_id, use filtering:
select ci.*
from calibraciones_instrumentos ci
where ci.fecha_hora_carga = (select max(ci2.fecha_hora_carga)
from calibraciones_instrumentos ci2
where ci2.instrumento_id = ci.instrumento_id
);
This is because your query is incorrect
The MAX is an aggregate function and gets the max. value from the fecha_hora_carga, this won't give you the corresponding id too it just gets the maximum value stored in the fecha_hora_carga column, not a row.
See the following sample:
mysql>CREATE TABLE test_group_by (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, val1 INT, val2 INT);`
mysql>INSERT INTO test_group_by (val1, val2) VALUES(10,1), (6, 1), (18, 1), (22, 2), (4, 2);
mysql> SELECT * FROM test_group_by;
+----+------+------+
| id | val1 | val2 |
+----+------+------+
| 1 | 10 | 1 |
| 2 | 6 | 1 |
| 3 | 18 | 1 |
| 4 | 22 | 2 |
| 5 | 4 | 2 |
+----+------+------+
mysql> SELECT id, MAX(val1) FROM test_group_by GROUP BY val2;
+----+-----------+
| id | MAX(val1) |
+----+-----------+
| 1 | 18 |
| 4 | 22 |
+----+-----------+
As you can see in the example, that is a simplified representation of your table.
The MAX function does not retrieves a entry, just the max. value of all the entries in the table. But your query also asks for a ID, it just makes one up (which ID is returned cannot be said for sure).

In MySQL, select one row out of multiple rows based on condition, otherwise select first row and exclude the rest

I am interested in MySQL of writing a query that looks through a list consisting of IDs and locations. Each ID represents a unique person who can be tied to multiple locations.
I have the following table to simplify things:
+----+----------+
| ID | Location |
+----+----------+
| 1 | Bldg#1 |
| 1 | Bldg#2 |
| 2 | Bldg#3 |
+----+----------+
I am looking to deduplicate the above table to only end up with ONE row per ID, but I would also like to add a conditional that preferences Bldg#1 for any given ID. In other words, given a table of multiple rows with potentially the same ID and multiple locations, I would like to write a query that outputs 1 row per ID, and if any of the rows associated with that ID also have a location of Bldg#1, I want to keep that row and drop the rest. Otherwise, I just want to keep one arbitrary location row for that ID.
For the above table, I would like the following as output:
+----+----------+
| ID | Location |
+----+----------+
| 1 | Bldg#1 |
| 2 | Bldg#3 |
+----+----------+
You can group by id and use conditional aggregation:
select id,
case
when max(location = 'Bldg#1') then 'Bldg#1'
else any_value(location)
end location
from tablename
group by id
See the demo.
Results:
| id | location |
| --- | -------- |
| 1 | Bldg#1 |
| 2 | Bldg#3 |
You can use row_number() with a case expression:
select id, location
from (select t.*,
row_number() over (partition by id
order by (case location when 'Bldg#1' then 1 when 'Bldg#2' then 2 when 'Bldg#3' then 3 else 4 end)
) as seqnum
from t
) t
where seqnum = 1;
This does not assume any particular ordering -- such as alphabetical ordering.
Is this you looking for?
Exmaple:
Query:
DROP TABLE IF EXISTS #TEST
CREATE TABLE #TEST (ID INT, Location NVARCHAR(10))
INSERT INTO #TEST
SELECT 1,'Bldg#1'
UNION
SELECT 1,'Bldg#2'
UNION
SELECT 2,'Bldg#3'
SELECT ID,Location FROM (
SELECT ID,Location, ROW_NUMBER() OVER(PARTITION BY ID ORDER BY ID) AS RNM
FROM #TEST) T
WHERE RNM = 1
inner query will make sure the location is in order so that Bldg#1 is always the first for each id, so then the outer group by will pick the first record
SELECT * FROM
(
SELECT id, location
FROM location
ORDER BY id, location ASC
)a
GROUP BY id

How to find the count of a particular number that can exist in 2 columns

I have a table that consist of a table that describes calls. Hence there is a to column and a from column. The problem is that I want the total messages sent by each number, which can be from or to. Refer to the table above for visuals.
I want the final table to be somethng that shows A : 3 , B: 2 , C:1 and D:1.
How do u count the numbers in 2 columns and sum them up?
One solution would be to first UNION ALL two aggregate queries to gather the count of occurences of each value in the two different columns, and them sum the results in an outer query, like:
SELECT val, SUM(cnt) cnt
FROM (
SELECT `from` val, COUNT(*) cnt FROM mytable GROUP BY `from`
UNION ALL
SELECT `to`, COUNT(*) FROM mytable GROUP BY `to`
) x
GROUP BY val
This demo on DB Fiddle with your sample data returns:
| val | cnt |
| --- | --- |
| A | 3 |
| B | 2 |
| C | 1 |
| D | 1 |
| E | 1 |
You can unpivot the data and aggregate:
select person, count(*) as num_calls
from ((select from as person from t) union all
(select to as person from t
) c
group by person;
Note that from and to are really, really bad names for columns because they are SQL keywords. I haven't escaped them in the query, because that just clutters the query and I assume the real columns have better names.

SQL Order by the number at the end of the string

I have a table with some columns and I want to order the selection by the column 'post_id'. The rows are like this: 'rgpost0', 'rgpost1','rcpost2', ...
How can I order the selection by the number at the end of the value of the column 'post_id' descending?
This code is not working: SELECT * FROM posts_info ORDER BY post_id+0 DESC
I don't want to change the column type to number. I just want to order by number at the end of the string.
You can use reverse() twice:
SELECT * FROM posts_info
ORDER BY reverse(reverse(post_id) + 0) + 0 DESC
For this table:
create table posts_info(id int, post_id varchar(100));
insert into posts_info(id, post_id) values
(1, 'bob52'),
(2, 'alice634'),
(3, 'john12'),
(4, 'mark7'),
(5, 'mary001');
Results:
| id | post_id |
| --- | -------- |
| 2 | alice634 |
| 1 | bob52 |
| 3 | john12 |
| 4 | mark7 |
| 5 | mary001 |
See the demo.
If you are using MySQL 8+ then we can use REGEXP_SUBSTR here:
SELECT *
FROM posts_info
ORDER BY
CAST(REGEXP_SUBSTR(post_id, '[0-9]+$') AS UNSIGNED) DESC,
post_id DESC;
I added a second sorting level in case two post_id happen to end in the same number. In this case, I order descending by the entire post_id.
Try this
SELECT * FROM posts_info ORDER BY CAST(SUBSTR(post_id, 7) AS UNSIGNED) DESC;

MySQL conditionally populate column 3 based on DISTINCT involving 2 other columns in one table

Had a good read through similar topics but I can't quite a) find one to match my scenario, or b) understand others enough to fit / tailor / tweek to my situation.
I have a table, the important fields being;
+------+------+--------+--------+
| ID | Name | Price |Status |
+------+------+--------+--------+
| 1 | Fred | 4.50 | |
| 2 | Fred | 4.50 | |
| 3 | Fred | 5.00 | |
| 4 | John | 7.20 | |
| 5 | John | 7.20 | |
| 6 | John | 7.20 | |
| 7 | Max | 2.38 | |
| 8 | Max | 2.38 | |
| 9 | Sam | 21.00 | |
+------+------+--------+--------+
ID is an auto-incrementing value as records get added throughout the day.
NAME is a Primary Key field, which can repeat 1 to 3 times in the whole table.
Each NAME will have a PRICE value, which may or may not be the same per NAME.
There is also a STATUS field that need to be populated based on the following, which is actually the part I am stuck on.
Status = 'Y' if each DISTINCT name has only one price attached to it.
Status = 'N' if each DISTINCT name has multiple prices attached to it.
Using the table above, ID's 1, 2 and 3 should be 'N', whilst 4, 5, 6, 7, 8 and 9 should be 'Y'.
I think this may well involve some form of combination of JOINs, GROUPs, and DISTINCTs but I am at a loss on how to put that into the right order for SQL.
In order to get the count of distinct Price values per name, we must use a GROUP BY on the Name field, but since you also want to display all names ungrouped but with an additional Status field, we must first create a subselect in the FROM clause which groups by the name and determines whether the name has multiple price values or not.
When we GROUP BY Name in the subselect, COUNT(DISTINCT price) will count the number of distinct price values for each particular name. Without the DISTINCT keyword, it would simply count the number of rows where price is not null.
In conjunction with that, we use a CASE expression to insert N into the Status column if there is more than one distinct Price value for the particular name, otherwise, it will insert Y.
The subselect only returns one row per Name, so to get all names ungrouped, we join that subselect to the main table on the condition that the subselect's Name = the main table's Name:
SELECT
b.ID,
b.Name,
b.Price,
a.Status
FROM
(
SELECT Name, CASE WHEN COUNT(DISTINCT Price) > 1 THEN 'N' ELSE 'Y' END AS Status
FROM tbl
GROUP BY Name
) a
INNER JOIN
tbl b ON a.Name = b.Name
Edit: In order to facilitate an update, you can incorporate this query using JOINs in the UPDATE like so:
UPDATE
tbl a
INNER JOIN
(
SELECT Name, CASE WHEN COUNT(DISTINCT Price) > 1 THEN 'N' ELSE 'Y' END AS Status
FROM tbl
GROUP BY Name
) b ON a.Name = b.Name
SET
a.Status = b.Status
Assuming you have an unfilled Status column in your table.
If you want to update the status column, you could do:
UPDATE mytable s
SET status = (
SELECT IF(COUNT(DISTINCT price)=1, 'Y', 'N') c
FROM (
SELECT *
FROM mytable
) s1
WHERE s1.name = s.name
GROUP BY name
);
Technically, it should not be necessary to have this:
FROM (
SELECT *
FROM mytable
) s1
but there is a mysql limitation that prevents you to select from the table you're updating. By wrapping it in parenthesis, we force mysql to create a temporary table and then it suddenly is possible.