mysql 3 queries on 2 tables in one - mysql

I have 2 tables that I need to query
**tbl_jobs**
jobid | description | someinfo
1 foo bar
2 fuu buu
**tbl_invlog**
idinv | jobid | type | ammount
1 1 add 100
2 1 rem 50
3 1 rem 15
4 1 add 8
5 2 add 42
the result should be to make a sum of the inventory "add" and "rem" and give a total of sum(add)-sum(rem) for each jobid, including the rest of the job information.
jobid | description | someinfo | amountadd | amountrem | totaladdrem
1 | foo | bar | 108 | 65 | 43
2 | fuu | buu | 42 | 0 | 42
i have made a quadruple select statement with select * from (select .... ) without using joins or other cool stuff. which is terribly slow. I am quite new to mysql.
I would be glad to an idea on how to solve this.
thanks in advance

This is a query that requires a join and conditional aggregation:
select j.jobid, j.description, j.someinfo,
sum(case when il."type" = 'add' then amount else 0 end) as AmountAdd,
sum(case when il."type" = 'rem' then amount else 0 end) as AmountRem,
(sum(case when il."type" = 'add' then amount else 0 end) -
sum(case when il."type" = 'rem' then amount else 0 end)
) as totaladdrem
from tbl_jobs j left outer join
tbl_invlog il
on j.jobid = il.jobid
group by j.jobid, j.description, j.someinfo;
Note some things. First, the tables have table aliases, defined in the from clause. This allows you to say which table the columns come from. Second, the table aliases are always used for all columns in the query.
MySQL would allow you to just do group by j.jobid, using a feature called "hidden columns". I think this is a bad habit (except in a few cases), so this aggregates by all the columns in the jobs table.
The conditional aggregation is done by putting a condition in the sum() statement.

Related

How to make a query to pivot/aggregate like this?

How would I query this data set:
SessionId | GameMode | Score
----------|----------|-------
1 | 1 | 100
1 | 1 | 90
1 | 2 | 20
1 | 2 | 15
1 | 3 | 5
1 | 3 | 5
2 | 1 | 90
2 | 1 | 80
2 | 2 | 15
2 | 2 | 15
2 | 3 | 2
2 | 3 | 4
to turn it into:
SessionId | GameMode1AvgScore | GameMode2AvgScore | GameMode3AvgScore
----------|--------------------|-------------------|-------------------
1 | 95 | 17.5 | 5
2 | 85 | 15 | 3
To clarify, I'm not asking how to group or aggregate rows, the core of my question is rather how do I pivot the GameMode rows into a column?
With MySQL, to return the specified resultset, we would need a query (SELECT statement) that returns four columns.
SELECT expr1 AS SessionId
, expr2 AS GameMode1AvgScore
, expr3 AS GameMode2AvgScore
, expr4 AS GameMode3AvgScore
FROM ...
GROUP BY SessionId
MySQL does not yet provide "PIVOT" (ala SQL Server and PostgreSQL) or "MODEL" (ala Oracle) features.
So in terms of achieving this result from a SQL SELECT statement, we are left with old-school conditional aggregation,
AVG(CASE WHEN t.GameMode = 1 THEN t.Score ELSE NULL END) AS GameMode1AvgScore
which seems to fall into the "how to group or aggregate rows" bucket you are not asking about. We can use a second SQL statement to help us build the SQL statement we need to run, but seems like that would still fall into the "how to group or aggregate rows". In fact, any SQL statement that produces the specified result is going to need to aggregate rows.
So with that tool excluded from our MySQL toolbelts, that leaves us with one answer: it is not possible to "pivot" GamerMode values into columns.
A statement to return the specified result might be like this:
SELECT t.sessionid AS SessionId
, AVG(CASE WHEN t.GameMode = 1 THEN t.Score ELSE NULL END) AS GameMode1AvgScore
, AVG(CASE WHEN t.GameMode = 2 THEN t.Score ELSE NULL END) AS GameMode2AvgScore
, AVG(CASE WHEN t.GameMode = 3 THEN t.Score ELSE NULL END) AS GameMode3AvgScore
-- ^ ^
FROM t
GROUP BY t.sessionid
ORDER BY t.sessionid
Note how expressions to return the average scores are the same pattern, we're just substutiting in a value of GameMode.
To help us write the query, we could run an initial query to get a distinct list of GameMode values, either from the same table
SELECT q.GameMode FROM t q GROUP BY q.GameMode ORDER BY q.GameMode
or we could query a "dimension" table of GameMode if that exists.
We cycle through the resultset (GameMode values) and generate the expression required to return the average score for each GameMode. And we can use that to construct the SQL statement.

Getting Corresponding Row Values to a GROUP BY with MIN

I've got data that's something like this:
+------------+---------+-------+
| Name | Time | Flag |
+------------+---------+-------+
| Bob | 401 | 1 |
| Bob | 204 | 0 |
| Dan | 402 | 1 |
| Dan | 210 | 0 |
| Jeff | 204 | 0 |
| Fred | 407 | 1 |
| Mike | 415 | 1 |
| Mike | 238 | 0 |
+------------+---------+-------+
I want to get each person's best time, but if the "flag" is set, their time should be divided by 2.
For example, Bob's best time would be 200.5
Now I can do a relatively simple query to get this data like this:
SELECT userid,
MIN(CASE WHEN flag = 1 THEN time / 2 ELSE time END) AS convertedTime,
time,
flag
FROM times t
GROUP BY userid
ORDER BY convertedtime ASC
The problem here is that is doesn't return the proper corresponding data for the time and flag, getting this data:
Bob 200.5 204 0
instead of the correct data
Bob 200.5 401 1
Of course I see the issue with the previous query and I've fixed it with this:
SELECT userid,
MIN(convertedtime) AS convertedTime,
(SELECT time
FROM times
WHERE MIN(convertedtime) = CASE
WHEN flag = 1 THEN time / 2
ELSE time
end
LIMIT 1) AS time,
(SELECT flag
FROM times
WHERE MIN(convertedtime) = CASE
WHEN flag = 1 THEN time / 2
ELSE time
end
LIMIT 1) AS flag
FROM (SELECT userid,
CASE
WHEN flag = 1 THEN time / 2
ELSE time
end AS convertedTime,
time,
flag
FROM times t) AS t
GROUP BY userid
ORDER BY convertedtime ASC
SQLFiddle
This does work, but I feel like there has to be a better and more efficient way of doing this. In my actual query, the part where I'm dividing the time by 2 is a much longer formula and I've got thousands of rows so it's very slow.
So the question is, is there a better/more efficient query for this?
Use a common technique from SQL select only rows with max value on a column but add the flag check when comparing the values in the ON condition.
SELECT t1.username, t2.convertedtime, t1.time, t1.flag
FROM times AS t1
JOIN (SELECT username,
MIN(IF(flag = 1, time/2, time) AS convertedtime
FROM times
GROUP BY username) AS t2
ON t1.username = t2.username
AND t1.time = IF(t1.flag = 1, t2.convertedtime * 2, t2.convertedtime)
This probably won't be very efficient -- I don't think it will be able to use an index for the conditional comparison.
This is what I ended up using (couldn't multiply by 2 to revert the number in this case due to my more complicated actual query having rounding in it), but Barmar did pretty much the same thing as I'm doing so I marked his as the answer.
SELECT times.userID, times2.convertedTime, times.time, times.flag
FROM times JOIN
(
SELECT userid,
MIN(CASE WHEN flag = 1 THEN time / 2 ELSE time END) AS convertedTime,
time,
flag
FROM times t
GROUP BY userid
ORDER BY convertedtime ASC
) as times2 ON times.userid = times2.userid AND
(
(times.time / 2 = times2.convertedTime AND times.flag = 1) OR
(times.time = times2.convertedTime AND times.flag = 0)
)

Get data from one table and count matching records from another

I'm not sure if this is possible. I have one table members and a second table transactions.
I need to get the name of the member from the members table, but also count the number of transactions that member has made from another table. Is this even possible in a JOIN statement, or do I need to write two statements?
SELECT
m.first_name,
m.last_name,
COUNT(t.giver_id),
COUNT(t.getter_id)
FROM
members AS m
JOIN
transactions AS t
ON
m.id = t.giver_id
WHERE
m.id = $i
I should add that it's possible a member has not made any transactions and would therefore not appear in the transactions table.
When I run this code, it returns all NULL columns. When I add the EXPLAIN statement, MySql says "Impossible WHERE noticed after reading const table..."
Is this possible? If so, then what am I doing wrong? Thanks in advance.
EDIT:
Sample data structure and expected output:
members
id | first_name | last_name
_______________________________
1 | Bill | Smith
2 | Joe | Jones
transactions table
id | giver_id | getter_id | status
________________________________________
1 | 1 | 2 | complete
2 | 1 | 2 | complete
So running my query should return:
1 | Bill | Smith | 2 | 0
2 | Joe | Jones | 0 | 2
Simple LEFT JOIN should suffice:
SELECT
m.first_name,
m.last_name,
SUM(CASE WHEN m.id = t.giver_id THEN 1 END) AS giver_count,
SUM(CASE WHEN m.id = t.getter_id THEN 1 END) AS getter_count
FROM members AS m
LEFT JOIN transactions AS t ON m.id = t.giver_id OR m.id = t.getter_id
GROUP BY m.first_name, m.last_name
Do not forget adding GROUP BY when using aggregate functions. Just because MySQL allows the query to go through without it, it doesn't mean it is advised. MySQL will pick up random row values for unaggregated columns which can be problematic. Avoid this anti-pattern.

Mysql increment value on multiple cases to order by

I have a table with products, and by answering a set of questions (Could be 2 or could be six, it's not always the same) I want to get results. Each answer has rules to it like color = green or noise > 69.
Now I don't want to use these rules as a WHERE to refine my searchresults, but I want to increment a variable on CASE so I can ORDER on the amount of true conditions. I still get all values, but the ones that better suit my customers needs are on top.
I tried a lot already, something like:
SELECT a.*, b.*,COUNT(CASE WHEN (b.noise >= 1600) THEN 1 ELSE 0 END) as condition_true
But I cant get it to work with multiple CASES.
SAMPLE DATA:
Products table:
id | title
1 | washing machine X
2 | Washing Machine Y200
3 | Even cooler washing machine
Productinfo table
id | noise | color | locked | product_id
1 | 40 | white | 1 | 1
2 | 68 | green | 0 | 2
3 | 72 | green | 1 | 3
Possible rules I will use in the output table
b.noise > 42
b.color = "green"
b.locked = 1
I would love an output table like this
product_id | title | condition_true
3 | Even cooler washing machine | 3
2 | Washing Machine Y200 | 2
1 | washing machine X | 1
CASE only returns one value, and COUNT requires multiple rows. You could just add them.
SELECT a.*, b.*, (CASE WHEN b.noise >= 1600 THEN 1 ELSE 0 END) +
(CASE WHEN color = 'green' THEN 1 ELSE 0 END) AS relevance
FROM table1
ORDER BY relevance DESC
Or, abbreviated for MySQL:
SELECT a.*, b.*, (b.noise >= 1600) +
(color = 'green') AS relevance
FROM table1
ORDER BY relevance DESC
COUNT is not what you want, that works to aggregate results from multiple rows when using GROUP BY.
I would do this using a series of IF() statements, returning 1 or 0 based on the condition (you could also return a bigger number, for more important columns or something):
SELECT IF(b.noise >= 1600,1,0)+IF(b.ocl1 <= 3,1,0)+...;

mysql sum() two joined tables, multiplies result

i have two tables; invoices & invoiceitems.
invoiceitems contains the items on each invoice
eg:
invoices
----------------------------------
| id |status| net | tax | total |
----------------------------------
| 72 |paid | 100 | 120 | 220 |
| 73 |unpaid| 50 | 5 | 55 |
| 74 |paid | 400 | 45 | 445 |
| 75 |paid | 250 | 67 | 317 |
invoiceitems
-------------------------------
| invoiceid |itemdescription |
-------------------------------
| 72 | apples |
| 72 | pears |
| 72 | oranges |
| 73 | lemons |
| 73 | oranges |
as you can see, in the example invoice number 72 has 3 items
i want to search my invoices for certain things, and display a count of certain fields.
but my problem is that the sum value seems to get multiplied by the number of fields there are in the second table.
$sql = "SELECT COUNT(DISTINCT invoices.id) AS num,
SUM(CASE invoices.status WHEN 'Paid' THEN 1 ELSE 0 END) AS numpaid,
SUM(CASE invoices.status WHEN 'Paid' THEN invoices.total ELSE 0 END) AS sumtotal,
FROM invoices
LEFT JOIN invoiceitems ON invoices.id=invoiceitems.invoiceid
WHERE invoices.id LIKE :invoiceid
AND IFNULL(opcinvoiceitems.itemdescription, '') LIKE :itemdescription
AND invoices.net LIKE :net
AND invoices.tax LIKE :tax
AND invoices.total LIKE :total
AND ......"
so using the above, the total for invoice 72 would be multiplied by 3
i'm really sorry, i know this is really badly explained but i cant explain it any other way, been searching for ages but cant find a solution. hope someone can help. thanks
One way to do what you want is to pre-aggregate the invoiceItems table before joining:
SELECT COUNT(i.id) AS num,
SUM(CASE i.status WHEN 'Paid' THEN 1 ELSE 0 END) AS numpaid,
SUM(CASE i.status WHEN 'Paid' THEN i.total ELSE 0 END) AS sumtotal,
FROM invoices i LEFT JOIN
(select ii.invoiceid, sum(. . .) as . . .
from invoiceitems ii
where IFNULL(ii.itemdescription, '') LIKE :itemdescription AND
group by ii.invoiceid
) ii
ON i.id = ii.invoiceid
WHERE i.id LIKE :invoiceid AND
i.net LIKE :net AND
i.tax LIKE :tax AND
i.total LIKE :total AND .....
Your query doesn't actually use invoiceitems in the from clause, so it is hard to provide a more detailed example.
When you do a join, you produce records created by matching up ones from the original tables. Thus, you will have 3 records for invoice #72, each created by matching up the single invoices record for #72 with each of the invoice items for #72. Each combined record will have the same total (in this case, 220), and thus the sum would be 3 times that.
It sounds like you just want total, then; you could just use total directly, or you could take your sum and divide it by the count (which you appear to also be computing).