How to sum values depending on another column, substract Aliases - mysql

SQL-Noob here.
Having a bunch of items which are either bought or sold.
I would like to Group by item-name and get a sum of purchases and sales.
Also i would like to calculate the stock of items.
I tried a subquery:
SELECT NAME,
(
SELECT SUM(quantity)
FROM transactions
WHERE `type` = "Buy") AS "purchases",
(
SELECT SUM(quantity)
FROM transactions
WHERE `type` = "Sell") AS "sales"
FROM transactions
GROUP BY NAME
which results in wrong values:
After that i tried CASE-Statements
SELECT NAME,
SUM(CASE WHEN `type` = "Buy" THEN quantity END) AS "purchases",
SUM(CASE WHEN `type` = "Sell" THEN quantity END) AS "sales"
FROM transactions
GROUP BY name
which works:
Question 1: Is there a better way to achieve the result?
Question 2: How to calculate the Stock of the items?
Something like purchases - sales AS "Stock" doesn't seem to work.
Thanks alot

Use conditional aggregation:
select name,
sum(case when type = 'Buy' then quantity else 0 end) as purchases,
sum(case when type = 'Sell' then quantity else 0 end) as sales,
sum(case when type = 'Buy' then quantity else - quantity end) as stock
from transactions
where type in ('Buy', 'Sell')
group by name
The where clause is not necessary of there are just those types in the table.
Side notes:
using single quotes for literal strings rather than double quotes; this is standard SQL, that all databases understand
in MySQL, use backticks if you need to quote an identifier rather than double quotes; or better yet, use identifiers that do not require quoting

Related

SQL query to get percentages within a grouping

I've looked over similar questions and I just can't seem to get this right.
I have a table with three columns: ID, Date, and Method. None are unique.
I want to be able to see for any given date, how many rows match a certain pattern on Method.
So, for example, if the table has 100 rows, and 8 of them have the date "01-01-2020" and of those 8, two of them have a method of "A", I would want a return row that says "01-01-2020", "8", "2", and "25%".
My SQL is pretty rudimentary. I have been able to make a query to get me the count of each method by date:
select Date, count(*) from mytable WHERE Method="A" group by Date;
But I haven't been able to figure out how to put together the results that I am needing. Can someone help me out?
You could perform a count over a case expression for that method, and then divide the two counts:
SELECT date,
COUNT(*),
COUNT(CASE method WHEN 'A' THEN 1 END),
COUNT(CASE method WHEN 'A' THEN 1 END) / COUNT(*) * 100
FROM mytable
GROUP BY date
I'm assuming you're interested in all methods rather than just 'A', so you could do the following:
with ptotals as
(
SELECT
thedate,
count(*) as NumRows
FROM
mytable
group by
thedate
)
select
mytable.thedate,
mytable.themethod,
count(*) as method_count,
100 * count(*) / max(ptotals.NumRows) as Pct
from
mytable
inner join
ptotals
on
mytable.thedate = ptotals.thedate
group by
mytable.thedate,
mytable.themethod
You can use AVG() for the ratio/percentage:
SELECT date, COUNT(*),
SUM(CASE WHEN method = 'A' THEN 1 ELSE 0 END),
AVG(CASE WHEN method = 'A' THEN 100.0 ELSE 0 END)
FROM t
GROUP BY date;

MySQL: Using Union to split multiple Select queries into respective tables

I have the following tables:
Product(maker, model, type)
PC(model, speed, ram, hd, price)
Laptop(model, speed, ram, hd, screen, price)
Printer(model, color, type, price)
I need to write a query that will return the average price of all products made by each maker, but only if that average is >= 200, in descending order by price.
I have tried 2 different methods and both get me very close but not exactly what I need:
(SELECT maker, AVG(price) as a
FROM Product NATURAL JOIN PC
WHERE price >= 200
GROUP BY maker)
UNION
(SELECT maker, AVG(price) as b
FROM Product NATURAL JOIN Laptop
WHERE price >= 200
GROUP BY maker)
UNION
(SELECT maker, AVG(price) as c
FROM Product NATURAL JOIN Printer
WHERE price >= 200
GROUP BY maker)
ORDER BY a;
The above gives me the average prices made by each maker for all the products they have made but it is all in one column so you cannot visually tell what product each average is linked to.
SELECT maker,
(SELECT AVG(price)
FROM PC
WHERE price >= 200) as 'Average Cost of PCs',
(SELECT AVG(price)
FROM Laptop
WHERE price >= 200
GROUP BY maker) as 'Average Cost of Laptops',
(SELECT AVG(price)
FROM Printer
WHERE price >= 200
GROUP BY maker) as 'Average Cost of Printers'
FROM Product
GROUP BY maker;
The above successfully gives each type of product its own column and also a column for all the makers, but it gives the average cost for all PCs, Printers, and Laptops in their respective columns instead of the average cost of each made by the maker it is parallel to.
Im not sure which one I am closer to the answer with but I've hit a wall and I'm not sure what to do. If I could get the first code to divide into different columns it would be correct. if I could get the second one to average correctly it would be right.
I am very new to Stack Overflow so I apologize if I did not ask this question in the correct format
You can UNION the "detail" table data, and join to that, and use what is referred to as conditional aggregation (aggregate functions ignore null values) to get your averages:
SELECT p.maker
, AVG(CASE WHEN d.Type = 'PC' THEN d.price ELSE NULL END) AS pcAvg
, AVG(CASE WHEN d.Type = 'Laptop' THEN d.price ELSE NULL END) AS laptopAvg
, AVG(CASE WHEN d.Type = 'Printer' THEN d.price ELSE NULL END) AS printerAvg
FROM Product AS p
LEFT JOIN (
SELECT model, price, 'PC' AS Type FROM PC
UNION SELECT model, price, 'Laptop' AS Type FROM Laptop
UNION SELECT model, price, 'Printer' AS Type FROM Printer
) AS d ON p.model = d.model
GROUP BY p.maker
If you want the average of prices >= 200 you can filter them out in the unioned subqueries or add an AND d.price >= 200 to each WHEN.
If you only want averages >= 200, you need to wrap the query above like so:
SELECT q.maker
, CASE WHEN q.pcAvg >= 200 THEN q.pcAvg ELSE NULL END AS pcAvg
, CASE WHEN q.laptopAvg >= 200 THEN q.laptopAvg ELSE NULL END AS laptopAvg
, CASE WHEN q.printerAvg >= 200 THEN q.printerAvg ELSE NULL END AS printerAvg
FROM (the query above) AS q
;
You cannot omit a column if the value is <= 200, you can only give a different value.
Sidenote: You can actually omit ELSE NULL from a CASE statement, the lack of an else implies else null; I was just being explicit for clarity of example and intent.

How to Perform Subtraction in SQL query based on some conditions?

I want to sum up qty*price where type is 'in' and then subtract the sum of qty*price where type is 'out'. Something like this.
SELECT cid, name, SUM(paid_amt), (SELECT SUM(qty*price) WHERE type = 'in' - SELECT SUM(qty*price) WHERE type = 'out'), type FROMtransactionsGROUP BY cid
Here's is my SQL query.
You can solve your problem by using two different sum expressions, each with a slightly different case statement inside of it. The idea is that your case statement only returns a value to the aggregate sum expression if it's of the correct typing.
select
cid
, sum(case when type = "in" then qty*price else 0 end)
- sum(case when type = "out" then qty*price else 0 end)
from
your_table
group by
cid

Select column(s) corresponding to max/min of another column without joins

I have a table (id, employee_id, device_id, logged_time) [simplified] that logs attendances of employees from biometric devices.
I generate reports showing the first in and last out time of each employee by date.
Currently, I am able to fetch the first in and last out time of each employee by date, but I also need to fetch the first in and last out device_ids of each employee. The entries are not in sequential order of the logged time.
I do not want to (and probably cannot) use joins as in one of the reports the columns are dynamically generated and can lead to thousands of joins. Furthermore, these are subqueries and are joined to other queries to get further details.
A sample setup of the table and queries are at http://sqlfiddle.com/#!9/3bc755/4
The first one just shows lists the entry and exit time by date of every employee
select
attendance_logs.employee_id,
DATE(attendance_logs.logged_time) as date,
TIME(MIN(attendance_logs.logged_time)) as entry_time,
TIME(MAX(attendance_logs.logged_time)) as exit_time
from attendance_logs
group by date, attendance_logs.employee_id
The second one builds up an attendance chart given a date range
select
`attendance_logs`.`employee_id`,
DATE(MIN(case when DATE(`attendance_logs`.`logged_time`) = '2017-09-18' THEN `attendance_logs`.`logged_time` END)) as date_2017_09_18,
MIN(case when DATE(`attendance_logs`.`logged_time`) = '2017-09-18' THEN `attendance_logs`.`logged_time` END) as entry_2017_09_18,
MAX(case when DATE(`attendance_logs`.`logged_time`) = '2017-09-18' THEN `attendance_logs`.`logged_time` END) as exit_2017_09_18,
DATE(MIN(case when DATE(`attendance_logs`.`logged_time`) = '2017-09-19' THEN `attendance_logs`.`logged_time` END)) as date_2017_09_19,
MIN(case when DATE(`attendance_logs`.`logged_time`) = '2017-09-19' THEN `attendance_logs`.`logged_time` END) as entry_2017_09_19,
MAX(case when DATE(`attendance_logs`.`logged_time`) = '2017-09-19' THEN `attendance_logs`.`logged_time` END) as exit_2017_09_19
/*
* dynamically generated columns for dates in date range
*/
from `attendance_logs`
where `attendance_logs`.`logged_time` >= '2017-09-18 00:00:00' and `attendance_logs`.`logged_time` <= '2017-09-19 23:59:59'
group by `attendance_logs`.`employee_id`;
Tried:
Similar to max and min logged_time of each date using case, tried to select the device_id where logged_time is max/min.
```MIN(case
when
`attendance_logs.logged_time` = MIN(
case when DATE(`attendance_logs`.`logged_time`)
= '2017-09-18' THEN `attendance_logs`.`logged_time` END
)
then `attendance_logs`.`device_id` end) as entry_device_2017_09_18 ```
This results in invalid use of group by
A quick hack for your query to pick the device id for in and out by using GROUP_CONCAT with in SUBSTRING_INDEX
SUBSTRING_INDEX(GROUP_CONCAT(case when DATE(`l`.`logged_time`) = '2017-09-18' THEN `l`.`device_id` END ORDER BY `l`.`device_id` desc),',',1) exit_device_2017_09_18,
Or if device id will be same for each in and its out then simply it can be written with GROUP_CONCAT only
GROUP_CONCAT(DISTINCT case when DATE(`l`.`logged_time`) = '2017-09-18' THEN `l`.`device_id` END)
DEMO
To avoid joins I suggest you try "correlated subqueries" instead:
select
employee_id
, logdate
, TIME(entry_time) entry_time
, (select MIN(l.device_id)
from attendance_logs l
where l.employee_id = d.employee_id
and l.logged_time = d.entry_time) entry_device
, TIME(exit_time) exit_time
, (select MAX(l.device_id)
from attendance_logs l
where l.employee_id = d.employee_id
and l.logged_time = d.exit_time) exit_device
from (
select
attendance_logs.employee_id
, DATE(attendance_logs.logged_time) as logdate
, MIN(attendance_logs.logged_time) as entry_time
, MAX(attendance_logs.logged_time) as exit_time
from attendance_logs
group by
attendance_logs.employee_id
, DATE(attendance_logs.logged_time)
) d
;
see: http://sqlfiddle.com/#!9/06e0e2/3
Note: I have used MIN() and MAX() on those subqueries only to avoid any possibility that these return more than one value. You could use limit 1 instead if you prefer.
Note also: I do not normally recommend correlated subqueries as they can cause performance issues, but they do supply the data you need.
oh, and please try to avoid using date as a column name, it isn't good practice.

Using sum() to mimic count() of rows

I want to count the number of rows where a particular field = 'Q1'.
I usually use count(particular_field), but this does not allow me to count only when that field = 'Q1'.
Does the query SUM(particular_field = 'Q1') work for this matter? Or am I able to do count(particular_field = 'Q1')?
you can either do
select count(*)
from table
where particular_field = 'Q1'
or
select sum(case when particular_field ='Q1' then 1 else 0 end)
from table
You should be able to use a CASE statement with your SUM() (See SQL Fiddle)
SELECT SUM(CASE WHEN particular_field = 'Q1' THEN 1 ELSE 0 END) yourCount
FROM yourTable
Or (See SQL Fiddle) - this will give you a list of the count and each field. If you only want the one value, then use a WHERE clause to filter:
select count(*), particular_field
from yourTable
group by particular_field