Calculations using aliases (from a subquery to the same table) mySQL - mysql

I have a database that stores player kills in CS:GO, I am trying to write a query that can show each player's KD.
I've written a query that will show each player's kills and deaths using aliases.
SELECT
`Name`,
`SteamID` as PlayerID,
count(`EventType`) as kills,
(SELECT count(`EventType`)
FROM `logdata`
WHERE (`EventVariable` = PlayerID AND `EventType` = 'killed')
GROUP BY `EventVariable`
ORDER BY `count(``EventType``)` DESC) as deaths
FROM `logdata`
WHERE `EventType` = 'killed'
GROUP BY `EventType`, `Name`
ORDER BY kills DESC
(results limited to just bots, I didn't want to openly advertise my friends SteamIDs)
To work out KD I just need to divide kills / deaths but you can't do that with aliases, I read that I should be able to wrap the alias e.g. (SELECT kills) / (SELECT deaths) as KD but that doesn't work.
The table looks like this: (Limited to bots again)
I am currently working out KD in PHP using the result of my query but that isn't a great way of doing it. (I am unable to query who has the highest KD for example)
So, my question is, how would I go about calculating the KD if I am unable to make calculations using alias?

I might just write your query using two completely separate subqueries which compute the kills and deaths counts:
SELECT
n.Name,
COALESCE(t1.kill_cnt, 0) AS kills,
COALESCE(t2.death_cnt, 0) AS deaths,
CASE WHEN t2.death_cnt > 0
THEN CAST(t1.kill_cnt / t2.death_cnt AS CHAR(50))
ELSE 'NA' END AS ratio
FROM
( SELECT DISTINCT Name FROM logdata ) n
LEFT JOIN
(
SELECT Name, COUNT(*) AS kill_cnt
FROM logdata
WHERE EventType = 'killed'
GROUP BY Name
) t1
ON
n.Name = t1.Name
LEFT JOIN
(
SELECT EventVariable AS Name, COUNT(*) AS death_cnt
FROM logdata
WHERE EventType = 'killed'
GROUP BY Name
) t2
ON
n.Name = t2.Name
Note that the subquery above which I have aliased as n is just intended to generate a complete list of all users in your database. Ideally, there should be a dedicated user table somewhere. If not, and you don't like my approach, then you will have to come up with some other way to obtain a list of all users.

Thanks to Tim for pointing me in the right direction and providing a query. I have made some changes to get the result I want and I wanted to post the final result.
SELECT
n.SteamID,
COALESCE(t1.kill_cnt, 0) AS kills,
COALESCE(t2.death_cnt, 0) AS deaths,
CASE WHEN t2.death_cnt > 0 THEN CAST(t1.kill_cnt / t2.death_cnt AS CHAR(50))
WHEN t1.kill_cnt = 0 THEN '0'
ELSE 'Infinite' END AS ratio
FROM
( SELECT DISTINCT SteamID FROM logdata ) n
LEFT JOIN
(
SELECT SteamID, COUNT(*) AS kill_cnt
FROM logdata
WHERE EventType = 'killed'
GROUP BY SteamID
) t1
ON
n.SteamID = t1.SteamID
LEFT JOIN
(
SELECT EventVariable AS SteamID, COUNT(*) AS death_cnt
FROM logdata
WHERE EventType = 'killed'
GROUP BY EventVariable
) t2
ON
n.SteamID = t2.SteamID
WHERE t1.kill_cnt > 0 or t2.death_cnt > 0
ORDER BY `ratio` DESC
I attempted to get KD 0 to show as such but that is not all that important at the end of the day, NULL is easy to work with.

Related

SQL: Select records based on comparison of two most recent associated records

Let's say we have a person table and survey table. survey is a set of attributes collected from a person at some point in time. Let's say survey has columns address and marriage_status
How do I select all persons whose address or marriage status has changed in the last survey?
Here's how I would write it if MySQL were able to magically interpret my intention:
SELECT *
FROM person
JOIN
(SELECT *
FROM survey
GROUP BY survey.person_id
ORDER BY survey.timestamp DESC
LIMIT 2 EACH) -- of course this part doesn't actually work. Trying to get last 2 records per person
surveys
ON surveys.person_id = person.id
WHERE surveys[0].address != surveys[1].address
OR surveys[0].marriage_status != surveys[1].marriage_status;
OR
SELECT *
FROM person
JOIN
(SELECT MOST RECENT survey FOR EACH person) latest_survey
ON latest_survey.person_id = person.id
JOIN
(SELECT SECOND MOST RECENT survey FOR EACH person) previous_survey
ON previous_survey.person_id = person.id
WHERE latest_survey.address != previous_survey.address
OR latest_survey.marriage_status != previous_survey.marriage_status;
This seems like a relatively straightforward query, but it's driving me crazy. I suspect I have tunnel vision and I'm not approaching this the right way.
EDIT: I am on MySQL v5. Based on the first couple answers, it seems like this might be the time to migrate to v8 (among other reasons)
So here's how I ended up doing it. It's a little long, but I think it's pretty straightforward? This felt amazing to get working.
(Note that underscores are used as prefixes in table aliases to help keep track of subquery depth)
SELECT person.*
FROM person
JOIN (
-- Join full survey data against each 'most recent' survey timestamp
SELECT s1.*
FROM survey s1
JOIN (
-- get most recent timestamp for each person
SELECT _s1.person_id, MAX(_s1.timestamp) timestamp
FROM survey _s1
GROUP BY person_id
) latest_surveys
ON latest_surveys.person_id = s1.person_id and latest_surveys.timestamp = s1.timestamp
) latest
ON latest.person_id = person.id
JOIN (
-- Join full survey data against each 'SECOND most recent' survey timestamp
select s2.*
from survey s2
JOIN (
-- to get SECOND most recent survey timestamp, do similar query, but exclude latest timestamp
SELECT _s2.person_id, MAX(_s2.timestamp) timestamp
FROM survey _s2
JOIN (
-- get most recent timestamp for each person (again)
SELECT __s2.person_id, MAX(__s2.timestamp) timestamp
FROM survey __s2
GROUP BY person_id
) _latest_surveys
-- Note the *NOT* equal here
ON _latest_surveys.person_id = _s2.person_id and _latest_surveys.timestamp != _s2.timestamp
GROUP BY _s2.person_id
) previous_surveys
ON previous_surveys.person_id = s2.person_id and previous_surveys.timestamp = s2.timestamp
) previous
ON previous.person_id = person.id
WHERE latest.address != previous.address
OR latest.marriage_status != previous.marriage_status;
Analytic functions make your question much more tractable. If you are not yet using MySQL 8+, then now would be a good time to upgrade. Assuming you are using MySQL 8+, we can try:
WITH cte AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY p.id ORDER BY s.timestamp DESC) rn
FROM person p
INNER JOIN survey s ON p.id = s.person_id
)
SELECT id
FROM cte
GROUP BY id
HAVING
MAX(CASE WHEN rn = 1 THEN address END) <> MAX(CASE WHEN rn = 2 THEN address END) OR
MAX(CASE WHEN rn = 1 THEN marriage_status END) <> MAX(CASE WHEN rn = 2 THEN marriage_status END);
The above query uses a pivot trick to isolate the latest, and second latest, addresses and marriage statuses for each person. It retains person id values for those whose latest and second latest addresses or marriage statuses are not identical.
This might be how you can achieve that:
SELECT *
FROM person
JOIN (
SELECT *,
MAX(survey_date) latest_survey,
SUBSTRING_INDEX(SUBSTRING_INDEX(GROUP_CONCAT(survey_date ORDER BY person_id, survey_date ASC),',',-2),',',1) previous_survey,
SUBSTRING_INDEX(GROUP_CONCAT(address ORDER BY person_id, survey_date ASC),',',-1) curadd,
SUBSTRING_INDEX(SUBSTRING_INDEX(GROUP_CONCAT(address ORDER BY person_id, survey_date ASC),',',-2),',',1) prevadd,
SUBSTRING_INDEX(GROUP_CONCAT(marriage_status ORDER BY person_id, survey_date ASC),',',-1) curms,
SUBSTRING_INDEX(SUBSTRING_INDEX(GROUP_CONCAT(marriage_status ORDER BY person_id, survey_date ASC),',',-2),',',1) prevms
FROM survey GROUP BY person_id
HAVING curadd != prevadd OR curms != prevms) A
ON person.id=A.person_id;
Using GROUP_CONCAT and SUBSTRING_INDEX to combine the data value then separate it again and using those to compare at the end. I know there are a bunch of ways to achieve without all these, like your second example is something that I think can be done but when I think about it, it's going to be a very long query. This query however, since you're not using MySQL 8+ is much shorter but the performance of this query is a concern especially on a large table.
It is not given, but I hope you have at least MySQL 8 or similar to have ability to use Common Table Expression. It can simplify the complex query.
The trick part is getting survey records #1 and #2 for each user. I will do it this way: see cte1 and cte2 definition
WITH
cte1 AS (
SELECT MAX(x1.id) AS id, x1.person_id
FROM survey x1
GROUP BY x1.person_id),
cte2 AS (
SELECT MAX(x2.id) AS id, x2.person_id
FROM survey x2
JOIN cte1 ON cte1.person_id = x2.person_id
AND cte1.id > x2.id
GROUP BY x2.person_id)
SELECT
p.*,
s1.address, s2.address address2,
s1.marriage_status, s2.marriage_status marriage_status2
FROM person AS p
JOIN (
cte1 JOIN survey s1 ON s1.id = cte1.id
) ON cte1.person_id = p.id
JOIN (
cte2 JOIN survey s2 ON s2.id = cte2.id
) ON cte2.person_id = p.id
WHERE
(s1.address <> s2.address)
OR (s1.marriage_status <> s2.marriage_status)
https://www.db-fiddle.com/f/hLwdHiZin4MkdUZ4aBz67H/2
Update: Thanks to Ian, I replaced MIN to MAX to get recent records

Double Aggregate Function Mysql

I want to take the maximum value from a series of returned values but I can't figure out a simple way to do it. My query returns all rows so 1/2 way there. I can filter it down with PHP but I'd like to do it all in SQL. I tried with a max subquery but that returned all results still.
DDL:
create table matrix(
count int(4),
date date,
product int(4)
);
create table products(
id int(4),
section int(4)
);
DML:
select max(magic_count), section, id
from (
select sum(count) as magic_count, p.section, p.id
from matrix as m
join products as p on m.product = p.id
group by m.product
) as faketable
group by id, section
Demo with my current try.
Only ids 1 and 3 should be returned from the sample data because they have the highest cumulative count for each of the sections.
Here's a second SQL fiddle that demonstrates the same issue.
Here you go:
select a.id,
a.section,
a.magic_count
from (
select p.id,
p.section,
magic_count
from (
select m.product, sum(count) as magic_count
from matrix m
group by m.product
) sm
join products p on sm.product = p.id
) a
left join (
select p.id,
p.section,
magic_count
from (
select m.product, sum(count) as magic_count
from matrix m
group by m.product
) sm
join products p on sm.product = p.id
) b on a.section = b.section and a.magic_count < b.magic_count
where b.id is null
see a simplified example (and other methods) in the manual entry for The Rows Holding the Group-wise Maximum of a Certain Column
see it working live here
Here you have solution without using JOINs, it has better performance than the other answer, which uses lot of JOINs:
select #rn := 1, #sectionLag := 0;
select id, section, count from (
select id,
case when #sectionLag = section then #rn := #rn + 1 else #rn := 1 end rn,
#sectionLag := section,
section,
count
from (
select id, section, sum(count) count
from matrix m
join products p on m.product = p.id
group by id, section
) a order by section, count desc
) a where rn = 1
Variables at the beginning are used to imitate window functions (LAG and ROW_NUMBER), which are available in MySQL 8.0 or higher (if you are using such version, let me know, so I will give you solution also with window functions).
DEMO
Another demo, where you can compare performance of my and the other query. It contains ~20K rows and my query tends to be almost 2 times faster.

Count on multiple tables with missing zero counts

I am running this query to return data with count < 0. It works fine until count is > 0 and < 50. But when count becomes 0, it doesnot return the data. Count is defined by coupons`.`status. On count zero, there will be no data in coupons table with status as 1. This is creating the issue, as it omits the whole row.
SELECT count(*) AS count, clients.title, plans.name
FROM `coupons`
INNER JOIN `clients` ON `coupons`.`client_id` = `clients`.`id`
INNER JOIN `plans` ON `coupons`.`plan_id` = `plans`.`id`
WHERE `coupons`.`status` = 1
GROUP BY `coupons`.`client_id`, `coupons`.`plan_id`
HAVING count < 50
Please help how to fix it.
Table definitions.
coupons (id, client_id, plan_id, customer_id, status, code)
plans (id, name)
clients (id, name...)
client_plans (id, client_id, plan_id)
Basically, a client can have multiple plans and a plan can belong to multiple clients.
Coupons table stores predefined coupons which can be allocated to customers. Non allocated coupons have status as 0, while as allocated coupons get status as 1
Here I am trying to fetch non allocated client wise, plan wise coupon count where either the count is less than 50 or count has reached 0
For example,
If coupons table as 10 rows of client_id = 1 & plan_id = 1 with status as 1, it should return count as 10, but when the table has 0 rows with client_id = 1 and plan_id = 1 with status as 1, it does not return anything in the above query.
Thank you all for your inputs, this worked.
select
sum(CASE WHEN `coupons`.`status` = 1 THEN 1 ELSE 0 END) as count,
clients.title,
plans.name
from
`clients`
left join
`coupons`
on
`coupons`.`client_id` = `clients`.`id`
left join
`plans`
on
`coupons`.`plan_id` = `plans`.`id`
group by
`coupons`.`client_id`,
`coupons`.`plan_id`
having
count < 50
With the inner joins, the query is not going to return any "zero" counts.
If you want to return "zero" counts, you are going to need an outer join somewhere.
But it's not clear what you are actually trying to count.
Assuming that what you are trying to get is a count of rows from coupons, for every possible combination of rows from plans and clients, you could do something like this:
SELECT COUNT(`coupons`.`client_id`) AS `count`
, clients.title
, plans.name
FROM `plans`
CROSS
JOIN `clients`
LEFT
JOIN `coupons`
ON `coupons`.`client_id` = `clients`.`id`
AND `coupons`.`plan_id` = `plans`.`id`
AND `coupons`.`status` = 1
GROUP
BY `clients`.`id`
, `plans`.`id`
HAVING `count` < 50
This is just a guess at result set you are expecting to return. Absent table definitions, example data, and the expected result, we're just guessing.
FOLLOWUP
Based on your comment, it sounds like you want conditional aggregation.
To "count" only the rows in coupons that have status=1, you can do something like this:
SELECT SUM( `coupons`.`status` = 1 ) AS `count`
, clients.title
, plans.name
FROM `coupons`
JOIN `plans`
ON `plans`.`id` = `coupons`.`plan_id`
JOIN `clients`
ON `clients`.`id` = `coupons`.`client_id`
GROUP
BY `clients`.`id`
, `plans`.`id`
HAVING `count` < 50
There are other expressions you can use to get the conditional "count". For example
SELECT COUNT( IF(`coupons`.`status`=1, 1, NULL) ) AS `count`
or
SELECT SUM( IF(`coupons`.`status`=1, 1, 0) ) AS `count`
or, for a more ANSI standards compatible approach
SELECT SUM( CASE WHEN `coupons`.`status` = 1 THEN 1 ELSE 0 END ) AS `count`

SQL subquery logic confusion

I'm writing an SQL query but I'm stuck at a point and can't figure out how to solve this issue. First have a look at the query below:
SELECT user_id, COUNT(*) as count
FROM hwc_attend
WHERE at_id IN
(SELECT evdet_id
FROM eve_detail
WHERE evdet_id IN (SELECT at_id FROM hwc_attend WHERE attendstate=1 )
AND location <> ''
AND evdet_id > 999
AND location NOT IN (SELECT ASIN FROM pReviews )
)
GROUP BY user_id
This query is working fine but giving lesser results than required because the part SELECT ASIN FROM pReviews should be like SELECT ASIN FROM pReviews where cID={place current value of "location" field from table eve_detail here.
For a better understanding, here's the errornous query:
SELECT user_id, COUNT(*) as count
FROM hwc_attend
WHERE at_id IN
(SELECT evdet_id **, location**
FROM eve_detail
WHERE evdet_id IN (SELECT at_id FROM hwc_attend WHERE attendstate=1)
AND location <> ''
AND evdet_id > 999
AND location NOT IN (SELECT ASIN FROM pReviews where cID=**location**)
)
GROUP BY user_id
It's hard to explain.. In short, I have to remove "location" values from the result fetched from table "eve_detail" that also exist in table "pReviews" in column cID.
Additionally, it would be nice if someone can covert it into joins. I would need both queries for learning.
Translating it to a join would use something like this. Using a LEFT OUTER JOIN and checking for NULL instead of NOT IN. I am assuming that hwc_attend has a unique column called id which is used in the count to get distinct rows.
SELECT ha1.user_id, COUNT(DISTINCT ha1.id) as count
FROM hwc_attend ha1
INNER JOIN eve_detail ed ON ha1.at_id = ed.evdet_id
INNER JOIN hwc_attend ha2 ON ed.evdet_id = ha2.at_id
LEFT OUTER JOIN pReviews pr ON ed.location = pr.ASIN AND cID = **location**
WHERE ha2.attendstate = 1
AND ed.location <> ''
AND ed.evdet_id > 999
AND pr.ASIN IS NULL
GROUP BY ha1.user_id
Change
AND location NOT IN (SELECT ASIN FROM pReviews )
To
AND NOT EXISTS (SELECT ASIN FROM pReviews WHERE eve_detail.location = ASIN.cID )

Fixing SQL Query so it will become more Efficient

I've got 3 tables:
mobile_users - with id,phone_type,...
2+3. iphone_purchases AND android_purchases - with id,status,user_id,..
I am trying to get all of the users who made 2 or more purchases.
successful purchase is identified by status > 0.
Also I am tring to get the total amount of users in the mobile_users table in the same query.
this is the query I came up with:
SELECT COUNT(*) AS `users`,
( SELECT COUNT(*)
FROM `mobile_users`
) AS `total`
FROM `mobile_users`
WHERE `mobile_users`.`phone_type` = 'iphone'
AND ( SELECT COUNT(*)
FROM ( SELECT `status`,
`user_id`
FROM `iphone_purchases`
UNION
SELECT `status`,
`user_id`
FROM `android_purchases`
) AS `purchase_list`
WHERE `purchase_list`.`status` > 0
AND `purchase_list`.`user_id` = `mobile_users`.`id`
) >= 2
It's very slow, and I have to find a way to improve it.
Any help would be appreciated!
Edit:
Also you should take in consideration that i'm building this query with sub-queries in PHP.
I'm building it with more conditions on the WHERE statment.
Your query is just returning counts of users, not each user.
The following restructures your query. It counts the number of purchases for iphones and androids separately, and then combines them using left outer join. The where clause simply combines the counts:
select mu.*, i.cnt as iphones, a.cnt as androids
from mobile_users mu left outer join
(SELECT `user_id`, count(*) as cnt
FROM `iphone_purchases`
where `status` > 0
group by user_id
) i
on i.user_id = mu.id left outer join
(SELECT `user_id`, count(*) as cnt
FROM `android_purchases`
where `status` > 0
group by user_id
) a
on a.user_id = mu.id
where coalesce(i.cnt, 0) + coalesce(a.cnt, 0) >= 2;