How to do specific MySQL calculations depending on the column value? - mysql

Is there a way to multiply a column with a predefined number based on another column? There are multiple predefined numbers that are used depending on the value in the column.
Example:
Table
Columns: persons_id,activity,scale
Values
1,swimming,4
1,baseball,2
1,basketball,3
2,swimming,6
2,basketball,3
If my predefined numbers are: 6 (swimming), 8 (baseball), 5 (basketball)
The output would look like this
1,swimming,4,24
1,baseball,2,16
1,basketball,2,10
2,swimming,6,36
2,basketball,3,15
Edit: Thank you everyone for contributing. I ended up using the solution from sgeddes.

Sure, you can use CASE:
SELECT Persons_Id, Activity, Scale,
Scale *
CASE
WHEN Activity = 'swimming' THEN 6
WHEN Activity = 'baseball' THEN 8
WHEN Activity = 'basketball' THEN 5
ELSE 1
END Total
FROM YourTable
Good luck.

Have another column called WEIGHT that multiples the SCALE value. Perhaps you can calculate the product using a trigger to populate the column. Otherwise, a simple SELECT will do fine.

you can use this query:
select persons_id, activity, scale,
scale * case when activity = 'swimming' then 6
when activity = 'baseball' then 8
when activity = 'basketball' then 5 end as result
from Table1
but a better solution will be defining a new table Coefficients(activity, coefficient)
so that you can insert rows:
'swimming', 6
'baseball', 8
'basketball', 5
then use something like this:
select persons_id, activity, scale, scale * coefficient as result
from Table1 inner join Coefficients on Table1.activity = Coefficients.activity

You can also use a table that stores the value or create a subquery that will return the multipliers:
select persons_id,
t.activity,
scale,
scale * s.val as result
from yourtable t
inner join
(
select 'swimming' activity, 6 val
union all
select 'baseball' activity, 8 val
union all
select 'basketball' activity, 5 val
) s
on t.activity = s.activity
See SQL Fiddle with Demo
The result is:
| PERSONS_ID | ACTIVITY | SCALE | RESULT |
--------------------------------------------
| 1 | swimming | 4 | 24 |
| 1 | baseball | 2 | 16 |
| 1 | basketball | 3 | 15 |
| 2 | swimming | 6 | 36 |
| 2 | basketball | 3 | 15 |

Related

Optimize the query for a large table in database (SQL)

I am trying to optimize the sql query on a large event table (10 million+ rows) for date range search. I already have unique index on this table which (lid, did, measurement, date).The query below is trying to get the event of three type of measurement (Kilowatts, Current and voltage) for every 2 second interval in date column :
SELECT *, FLOOR(UNIX_TIMESTAMP(date)/2) AS timekey
from events
WHERE lid = 1
and did = 1
and measurement IN ("Voltage")
group by timekey
UNION
SELECT *, FLOOR(UNIX_TIMESTAMP(date)/2) AS timekey
from events
WHERE lid = 1
and did = 1
and measurement IN ("Current")
group by timekey
UNION
SELECT *, FLOOR(UNIX_TIMESTAMP(date)/2) AS timekey
from events
WHERE lid = 1
and did = 1
and measurement IN ("Kilowatts")
group by timekey
This is the table that I am trying to look up to.
=============================================================
id | lid | did | measurement | date
=============================================================
1 | 1 | 1 | Kilowatts | 2020-04-27 00:00:00
=============================================================
2 | 1 | 1 | Current | 2020-04-27 00:00:00
=============================================================
3 | 1 | 1 | Voltage | 2020-04-27 00:00:00
=============================================================
4 | 1 | 1 | Kilowatts | 2020-04-27 00:00:01
=============================================================
5 | 1 | 1 | Current | 2020-04-27 00:00:01
=============================================================
6 | 1 | 1 | Voltage | 2020-04-27 00:00:01
=============================================================
7 | 1 | 1 | Kilowatts | 2020-04-27 00:00:02
=============================================================
8 | 1 | 1 | Current | 2020-04-27 00:00:02
=============================================================
9 | 1 | 1 | Voltage | 2020-04-27 00:00:02
The expected result is retrieve all data that have the date equal to 2020-04-27 00:00:00 and 2020-04-27 00:00:02. The query provided above work as expected. But I am using UNION for look up different measurements on the table, I believe it might not be the optimal way to do it.
Can any SQL expert help me to tone the query that I have to increase the performance?
You have one record every second for each and every measurement, and you want to select one record every two seconds.
You could try:
select *
from events
where
lid = 1
and did = 1
and measurement IN ('Voltage', 'Current')
and extract(second from date) % 2 = 0
This would select records that have an even second part.
Alternatively, if you always have one record every second, another option is row_number() (this requires MySQL 8.0):
select *
from (
select
e.*,
row_number() over(partition by measurement order by date) rn
from events
where
lid = 1
and did = 1
and measurement IN ('Voltage', 'Current')
) t
where rn % 2 = 1
This is a bit less accurate than the previous query though.
Your query is actually three queries combined into one. Luckily they all select rows of data based on similar columns. If you want to make this query run fast you can add the following index:
create index ix1 on events (lid, did, measurement);
In addition to above suggestions, changing the PRIMARY KEY will give you a little more performance:
PRIMARY KEY(lid, did, date, measurement)
and toss id.
Caveat, there could be hiccups if two readings come in at exactly the same "second". This could easily happen if one reading comes in just after the clock ticks, and the next comes in just before the next tick.

Leaderboard position SQL optimization

I'm offering an experience leaderboard for a Discord bot I actively develop with stuff like profile cards showing one's rank. The SQL query I'm currently using works flawlessly, however I notice that this query takes a rather long processing time.
SELECT id,
discord_id,
discord_tag,
xp,
level
FROM (SELECT #rank := #rank + 1 AS id,
discord_id,
discord_tag,
xp,
level
FROM profile_xp,
(SELECT #rank := 0) r
ORDER BY xp DESC) t
WHERE discord_id = '12345678901';
The table isn't too big (roughly 20k unique records), but this query is taking anywhere between 300-450ms on average, which piles up relatively fast with a lot of concurrent requests.
I was wondering if this query can be optimized to increase performance. I've isolated this to this query, the rest of the MySQL server is responsive and swift.
I'd be happy about any hint and thanks in advance! :)
You're scanning 20,000 rows to assign "row numbers" then selecting exactly one row from it. You can use aggregation instead:
SELECT *, (
SELECT COUNT(*)
FROM profile_xp AS x
WHERE xp > profile_xp.xp
) + 1 AS rnk
FROM profile_xp
WHERE discord_id = '12345678901'
This will give you rank of the player. For dense rank use COUNT(DISTINCT xp). Create an index on xp column if necessary.
Not an answer; too long for a comment:
I usually write this kind of thing exactly the same way that you have done, because it's quick and easy, but actually there's a technical flaw with this method - although it only becomes apparent in certain situations.
By way of illustration, consider the following:
DROP TABLE IF EXISTS ints;
CREATE TABLE ints (i INT NOT NULL PRIMARY KEY);
INSERT INTO ints VALUES
(0),(1),(2),(3),(4),(5),(6),(7),(8),(9);
Your query:
SELECT a.*
, #i:=#i+1 rank
FROM ints a
JOIN (SELECT #i:=0) vars
ORDER
BY RAND() DESC;
+---+------+
| i | rank |
+---+------+
| 3 | 4 |
| 2 | 3 |
| 5 | 6 |
| 1 | 2 |
| 7 | 8 |
| 9 | 10 |
| 4 | 5 |
| 6 | 7 |
| 8 | 9 |
| 0 | 1 |
+---+------+
Look, the result set isn't 'random' at all. rank always corresponds to i
Now compare that with the following:
SELECT a.*
, #i:=#i+1 rank
FROM
( SELECT * FROM ints ORDER by RAND() DESC) a
JOIN (SELECT #i:=0) vars;
+---+------+
| i | rank |
+---+------+
| 5 | 1 |
| 2 | 2 |
| 8 | 3 |
| 7 | 4 |
| 4 | 5 |
| 6 | 6 |
| 0 | 7 |
| 1 | 8 |
| 3 | 9 |
| 9 | 10 |
+---+------+
Assuming discord_id is the primary key for the table, and you're just trying to get one entry's "rank", you should be able to take a different approach.
SELECT px.discord_id, px.discord_tag, px.xp, px.level
, 1 + COUNT(leaders.xp) AS rank
, 1 + COUNT(DISTINCT leaders.xp) AS altRank
FROM profile_xp AS px
LEFT JOIN profile_xp AS leaders ON px.xp < leaders.xp
WHERE px.discord_id = '12345678901'
GROUP BY px.discord_id, px.discord_tag, px.xp, px.level
;
Note I have "rank" and "altRank". rank should give you a similar position to what you were originally looking for; your results could have fluctuated for "ties", this rank will always put tied players at their highest "tie". If 3 records tie for 2nd place, those (queried separately with this) will show 2nd place, the next xp down would should 5th place (assuming 1 in 1st, 2,3,4 in 2nd, 5 in 5th). The altRank would "close the gaps" putting 5 in the 3rd place "group".
I would also recommend an index on xp to speed this up further.

How do I combine two queries on the same table to get a single result set in MySQL

I am not very good at sql but I am getting there. I have searched stackoverflow but I can't seem to find the solution and I hope someone out there can help me. I have a table (users) with data like the following. The book_id column is a key to another table that contains a book the user is subscribed to.
|--------|---------------------|------------------|
| id | book_id | name |
|--------|---------------------|------------------|
| 1 | 1 | jim |
| 2 | 1 | joyce |
| 3 | 1 | mike |
| 4 | 1 | eleven |
| 5 | 2 | max |
| 6 | 2 | dustin |
| 7 | 2 | lucas |
|--------|---------------------|------------------|
I have a function in my PHP code that returns two random users from a specific book id (either 1 or 2). Query one returns the result in column 1 and result two returns the results in column 2 like:
|---------------------|------------------|
| 1 | 2 |
|---------------------|------------------|
| jim | max |
| joyce | dustin |
|---------------------|------------------|
I have achieved this by running two separate queries as seen below. I want to know if it's possible to achieve this functionality with one query and how.
$random_users_with_book_id_1 = SELECT name FROM users WHERE book_id=1 LIMIT 2
$random_users_with_book_id_2 = SELECT name FROM users WHERE book_id=2 LIMIT 2
Again, I apologise if it's too specific. The query below has been closest to what I was trying to achieve.:
SELECT a.name AS book_id_1, b.name AS book_id_2
FROM users a, users b
WHERE a.book_id=1 AND b.book_id = 2
LIMIT 2
EDIT: I have created a fiddle to play around with his. I appreciate any help! Thank you!! http://sqlfiddle.com/#!9/7fcbca/1
It is easy actually :)
you can use UNION like this:
SELECT * FROM (
(SELECT * FROM user WHERE n_id=1 LIMIT 2)
UNION
(SELECT * FROM user WHERE n_id=2 LIMIT 2))
collection;
if you read this article about the documentation you can use the () to group the individual queries and the apply the union in the middle. Without the parenthesis it would still LIMIT 2 and show only the two first. Ref. "To apply ORDER BY or LIMIT to an individual SELECT, place the clause inside the parentheses that enclose the SELECT:"
If you want to combine the queries in MySQL, you can just use parentheses:
(SELECT name
FROM users
WHERE n_id = 1
LIMIT 2
) UNION ALL
(SELECT name
FROM users
WHERE n_id = 2
LIMIT 2
);
First, only use UNION if you specifically want to incur the overhead of removing duplicates. Otherwise, use UNION ALL.
Second, this does not return random rows. This returns arbitrary rows. In many cases, this might be two rows near the beginning of the data. If you want random rows, then use ORDER BY rand():
(SELECT name
FROM users
WHERE n_id = 1
ORDER by rand()
LIMIT 2
) UNION ALL
(SELECT name
FROM users
WHERE n_id = 2
ORDER BY rand()
LIMIT 2
);
There are other methods that are more efficient, but this should be fine for up to a few thousand rows.

Problems with a very advanced sql query

I need to do an advanced selection in SQL, but I'm stuck.
I have the following table:
id | user_id | position | value
1 | 1 | 1 | 1
1 | 1 | 2 | 1
1 | 1 | 3 | 3
1 | 2 | 1 | 2
1 | 2 | 2 | 2
1 | 2 | 3 | 2
1 | 3 | 1 | 3
1 | 3 | 2 | 2
1 | 3 | 3 | 1
I need a query that gives me a result set ordered as this:
Total sum for each user (user 1: 5, user 2: 6, user 3: 6)
Value for position 3 for each user (user 1: 3, user 2: 2, user 3: 1)
Val for pos 3 + val for pos 2 for each user (user 1: 4, user 2: 4, user 3: 4)
Val for pos 3 + val for pos 2 + val for pos 1 for each user (user 1: 5, user 2: 6, user 3: 6)
This is just an example, the table can actually contain more positions, so I need a query that is not hard coded on three positions.
NOTE: There is always the same number of positions for each user_id. In this example it's three, but I could as well truncate the table and add data for each user using five positions.
An ugly solution is to assume that there are never no more than ten positions, creating pos1, pos2, and so on as columns and just add them accordingly in the query. If you only use three positions you get a lot of NULL values and you also get stuck with a maximum of ten positions.
I have considered the use of temporary tables, but haven't found a breakthrough there either.
How would you do it?
I need a query that is not hard coded on three positions.
Then you can't output the subtotals in columns. SQL requires that the columns are fixed at the time you prepare the query; you can't write a query that appends more columns dynamically as it discovers how many distinct values are in the data.
You can, however, output a dynamic number of rows.
SELECT t1.user_id, CONCAT(t1.position, '-', MAX(t2.position)) AS position_range,
SUM(t2.value) AS subtotal
FROM MyTable t1
INNER JOIN MyTable t2
ON t1.user_id = t2.user_id AND t1.position <= t2.position
GROUP BY t1.user_id, t1.position;
The output is:
+---------+----------------+----------+
| user_id | position_range | subtotal |
+---------+----------------+----------+
| 1 | 1-3 | 5 |
| 1 | 2-3 | 4 |
| 1 | 3-3 | 3 |
| 2 | 1-3 | 6 |
| 2 | 2-3 | 4 |
| 2 | 3-3 | 2 |
| 3 | 1-3 | 6 |
| 3 | 2-3 | 3 |
| 3 | 3-3 | 1 |
+---------+----------------+----------+
You'll have to write application code to pivot this into columns after you fetch the whole result set.
Sorry, there is no way to write a fully dynamic pivot query in any brand of RDBMS. You have two choices:
Write code to generate the SQL based on data, as shown in #TimLehner's updated answer
Write code to post-process a general-purpose query like the one I show above.
You can potentially do something like this:
select user_id
, sum(value) as value_sum
, (select value from my_table where user_id = t.user_id and position = 3) as pos_3_val
, (select sum(value) from my_table where user_id = t.user_id and position >= 2) as pos_2_3_val
, (select sum(value) from my_table where user_id = t.user_id and position >= 1) as pos_1_2_3_val
from my_table as t
group by user_id
order by user_id
I think this should work in most any RDBMS.
If it has to by dynamic, you could potentially create this query in stored procedure or your application and run it.
You could also dynamically pivot your results from a query like this:
select *
, (
select sum(value)
from my_table
where user_id = t.user_id
and position >= t.position
) as running_total_descending
from my_table t
Please let us know if any of this works, and if you have trouble creating a dynamic version (and which RDBMS).
UPDATE
Now that we know the RDBMS (MySQL) we can have a specific dynamic version:
set #sql = null;
select
group_concat(distinct
concat(
' sum(case when position >= ',
position,
' then value end) as pos_',
position,
'_plus'
)
) into #sql
from my_table;
set #sql = concat('select user_id,', #sql, ' from my_table t group by user_id;');
prepare stmt from #sql;
execute stmt;
deallocate prepare stmt;
SQL Fiddle
Special thanks to #bluefeet for posting this type of solution often.
I should also note that many devs believe this type of pivoting often belongs in the application or front-end. I'm no exception, both for separation of concerns and because your app can generally scale better than your OLTP database.

Mysql join with counting results in another table

I have two tables, one with ranges of numbers, second with numbers. I need to select all ranges, which have at least one number with status in (2,0). I have tried number of different joins, some of them took forever to execute, one which I ended with is fast, but it select really small number of ranges.
SELECT SQL_CALC_FOUND_ROWS md_number_ranges.*
FROM md_number_list
JOIN md_number_ranges
ON md_number_list.range_id = md_number_ranges.id
WHERE md_number_list.phone_num_status NOT IN (2, 0)
AND md_number_ranges.reseller_id=1
GROUP BY range_id
LIMIT 10
OFFSET 0
What i need is something like "select all ranges, join numbers where number.range_id = range.id and where there is at least one number with phone_number_status not in (2, 0).
Any help would be really appreciated.
Example data structure:
md_number_ranges:
id | range_start | range_end | reseller_id
1 | 000001 | 000999 | 1
2 | 100001 | 100999 | 2
md_number_list:
id | range_id | number | phone_num_status
1 | 1 | 0000001 | 1
2 | 1 | 0000002 | 2
3 | 2 | 1000012 | 0
4 | 2 | 1000015 | 2
I want to be able select range 1, because it has one number with status 1, but not range 2, because it has two numbers, but with status which i do not want to select.
It's a bit hard to tell what you want, but perhaps this will do:
SELECT *
from md_number_ranges m
join (
SELECT md_number_ranges.id
, count(*) as FOUND_ROWS
FROM md_number_list
JOIN md_number_ranges
ON md_number_list.range_id = md_number_ranges.id
WHERE md_number_list.phone_num_status NOT IN (2, 0)
AND md_number_ranges.reseller_id=1
GROUP BY range_id
) x
on x.id=m.id
LIMIT 10
OFFSET 0
Is this what you're looking for?
SELECT DISTINCT r.*
FROM md_number_ranges r
JOIN md_number_list l ON r.id = l.range_id
WHERE l.phone_num_status NOT IN (0,2)
SQL Fiddle Demo