mysql mixing counts from several tables - mysql

I am trying to achieve total number of topics, total number of posts, and last post for given section, please find db structures and query as following...
fcats
| id | title | section |
+----+--------+---------+
| 1 | test | gd |
+----+--------+---------+
ftopics
| id | title | cat_id |
+----+--------+---------+
| 1 | test1 | 1 |
+----+--------+---------+
fposts
| id | post | topic_id | posted_by
+----+-------+----------+---------+
| 1 | post | 1 | user |
+----+-------+----------+---------+
current query
SELECT id,
title ,
(SELECT count(id)
FROM ftopics
WHERE cat_id = id) AS total_topics
FROM fcats
WHERE section = "gd"
by using above query, i could only get total_topics for given section, but i am confused about how to get total number of posts, and last post for given section. please help, thanks.

SELECT A.id section_id,
IFNULL(COUNT(DISTINCT B.id),0) topics_count,
IFNULL(COUNT(C.id),0) post_count,
(SELECT post from fposts where id = MAX(C.id)) last_post
FROM fsections A LEFT JOIN ftopics B
ON A.id = B.cat_id
LEFT JOIN fposts C
ON C.topic_id = B.id
WHERE A.section = "gd"
GROUP BY A.id
Also include the null case if the section doesnot have any post

Maybe something like this:
SELECT
id,
title ,
(
SELECT
count(ftopics.id)
FROM
ftopics
WHERE
ftopics.cat_id = fcats.id
) AS total_topics,
(
SELECT
COUNT(distinct fposts.id)
FROM
ftopics
JOIN fposts
ON ftopics.id=fposts.topic_id
WHERE
ftopics.cat_id = fcats.id
),
(
select
fposts.id
from fposts
inner join ftopics on fposts.topic_id = ftopics.id
inner join fcats c2 on c2.id = ftopics.cat_id
where fcats.id = c2.id
order by fposts.id desc
limit 1
) as last_post_id
FROM fcats
WHERE section = "gd"

For first part of your answer you should use count distinct, and for second part a subquery:
SELECT c.id,
c.title ,
count( distinct t.cat_id) AS total_topics ,
count( distinct p.id) AS total_posts ,
(select p2.id
from ne_forum_posts p2
inner join ne_forum_topics t2 on p2.topic_id = t2.id
inner join ne_forum_sub_cats c2 on c2.id = t2.cat_id
where c2.id = c.id
order by p2.id desc
limit 1) as last_post_id
FROM ne_forum_sub_cats c LEFT OUTER JOIN
ne_forum_topics t on c.id = t.cat_id LEFT OUTER JOIN
ne_forum_posts p on p.topic_id = t.id
WHERE section = "gd"
all typos fixed and tested.

Related

Optimize and reduce size of Union Select Query

My question is about how to optimize and reduce size of a sql query. I want to join more than 20 multiple queries using UNION, it is giving me the correct result as per the below logic, but I am looking for two things here
something more efficient
I already have 20 UNIONS in my query, and every month I have to add 2-4 UNIONS more which make this query very long so is there any way this query can be rephrased with less code
Select
'343' As 'Manual ID',
'24/07/2020' As 'Date',
A.ID,
O.Order_Name,
C.Customer_Name,
Q.Quantity
From Shipper A
Left Join Order O A.ID = O.ID
Left Join Customer C A.ID = C.ID
Left Join Quantity Q Q.ID = C.ID
where A.ID IN (1)
UNION
Select
'323' As 'Manual ID',
'24/08/2020' As 'Date',
A.ID,
O.Order_Name,
C.Customer_Name,
Q.Quantity
From Shipper A
Left Join Order O A.ID = O.ID
Left Join Customer C A.ID = C.ID
Left Join Quantity Q Q.ID = C.ID
where A.ID IN(2,3,4)
and so on ...
Result
Manual ID | Date | Shipper | Order Name | Customer Name | Qty
343 | 24/07/2020 | 1 | order1 | A | 5
323 | 24/08/2020 | 2 | order2 | B | 2
323 | 24/08/2020 | 3 | order3 | C | 1
323 | 24/08/2020 | 4 | order4 | D | 12
You can try this:
Select
CASE
WHEN A.ID IN(1) THEN '343'
WHEN A.ID IN(2,3,4) THEN '323'
END As 'Manual ID',
CASE
WHEN A.ID IN(1) THEN '24/07/2020'
WHEN A.ID IN(2,3,4) THEN '24/08/2020'
END As 'Date',
A.ID,
O.Order_Name,
C.Customer_Name,
Q.Quantity
From Shipper A
Left Join Order O A.ID = O.ID
Left Join Customer C A.ID = C.ID
Left Join Quantity Q Q.ID = C.ID
Where A.ID IN(1,2,3,4)
First suggestion is to move the parameters in to another table, then join on it. You can even make that an inline view if you don't want to use a real table...
Second suggestion is to use UNION ALL to avoid the costs of deduplication incurred by UNION.
SELECT
params.*,
O.Order_Name,
C.Customer_Name,
Q.Quantity
FROM
(
SELECT '343' As 'Manual ID', '24/07/2020' As 'Date', 1 AS A_ID
UNION ALL SELECT '323' As 'Manual ID', '24/08/2020' As 'Date', 2 AS A_ID
UNION ALL SELECT '323' As 'Manual ID', '24/08/2020' As 'Date', 3 AS A_ID
UNION ALL SELECT '323' As 'Manual ID', '24/08/2020' As 'Date', 4 AS A_ID
)
AS params
INNER JOIN Shipper A ON A.ID = params.A_ID
Left Join Order O ON A.ID = O.ID
Left Join Customer C ON C A.ID = C.ID
Left Join Quantity Q ON Q.ID = C.ID
Alternatively, don't recompute this every month. Write a new query each month, and insert the results into another table?
If you just want to go for query the better way would be to use the case when statement but every now and then you need to keep updating the query adding new cases.
Another, optimized solution will be to create a new table to store
Manual ID, Date, (Common) ID present in Shipper (Table). Then create a view to join all above tables with new Table.
New Table
Manual ID | Date | ID |
343 | 24/07/2020 | 1 |
323 | 24/08/2020 | 2 |
323 | 24/08/2020 | 3 |
323 | 24/08/2020 | 4 |
Then Create a View Joining all Tables including new new table with ID.
In this you just need add new value to new table and you will complete result in view it self.
CREATE VIEW MY_VIEW
AS
SELECT * FROM
(
Select
T.[Manual ID],
T.[Date],
A.ID,
O.Order_Name,
C.Customer_Name,
Q.Quantity
From Shipper A
Left Join Order O A.ID = O.ID
Left Join Customer C A.ID = C.ID
Left Join Quantity Q Q.ID = C.ID
Left Join NewTable T T.ID = A.ID
)
Now just insert value in new table and fetch complete data from MY_VIEW. It will give the same result as you are excepting.

SQL Union query simplification

I don't know if this question is apropriate on this forum.
I have a huge query :
SELECT threshold.id, brand.id, COUNT(brand.id), threshold
FROM current_stock, article, product, brand, delivery, threshold
WHERE current_stock.article_id = article.id
AND article.product_product_code = product.product_code
AND product.brand_id = brand.id
AND article.delivery_id = delivery.id
AND delivery.store_id = 'E260'
AND threshold.brand_id = brand.id
GROUP BY brand.id
HAVING COUNT(brand.id) <= threshold
UNION
SELECT threshold.id, brand.id, 0, threshold
FROM current_stock, article, product, brand, delivery, threshold
WHERE threshold.store_id = 'E260'
AND threshold.brand_id NOT IN (
SELECT brand_id FROM current_stock, article, product, delivery
WHERE current_stock.article_id = article.id
AND article.product_product_code = product.product_code
AND article.delivery_id = delivery.id
AND delivery.store_id = 'E260')
And I think it's possible to do better but after a entire day of try I haven't found a better query giving the same result.
For clarify, I have a stock (with current_stock, article, product and delivery). I also have thresholds. what I want is to check for each thresholds if there is the given minimum amount of stock for the given brand.
My problem is that if there is 0 article of a brand, the first part of the query will not take care about the threshold on this brand. It's why I have added an uggly Union.
Someone have an idea for a better way to do this ?
EDIT
This what I have done after the reading of comments and answers :
SELECT t.id, b.id, t.threshold, count(b.id) stock
FROM threshold t
inner join brand b on b.id = t.brand_id
left join product p on p.brand_id = b.id
inner join article a on a.product_product_code = p.product_code
inner join delivery d on d.id = a.delivery_id
inner join current_stock cs on cs.article_id = a.id
WHERE
t.store_id = 'E260' AND
d.store_id = 'E260'
GROUP BY b.id
HAVING stock <= t.threshold
My problem is that it don't gives all threholds... only ones that have at least one 'current_stock'. I have perhaps don't understand how joins are working.
Here an example of threshold table :
| id | brand_id | threshold |
-----------------------------
| 1 | 86 | 1 |
| 2 | 28 | 1 |
| 3 | 12 | 1 |
What I want as result this :
# with 2 entries in 'current_stock' for the brand id 28, 1 for 12 and 0 for 86
| t.id | b.id | threshold | stock |
-----------------------------------
| 1 | 86 | 1 | 0 |
| 3 | 12 | 1 | 1 |
Guessing a few parts here since you've used implicit joins in your sample. An explicit version would look something like this (provided I guessed correctly for how you are joining the threshold table).
SELECT
t.id,
b.id,
COUNT(b.id),
t.threshold
FROM
current_stock c
inner join article a on a.id = c.article_id
inner join delivery d on d.id = a.delivery_id
inner join product p on p.product_code = a.product_product_code
inner join brand b on b.id = p.brand_id
inner join threshold t on t.brand_id = b.id
WHERE
d.store_id = 'E260'
GROUP BY b.id
HAVING COUNT(b.id) <= t.threshold
Now to get your results to include rows where there aren't any 'articles' you can start switching out the inner joins for left joins. However, you can't simply use left outer join article... in the example above, because the store_id in the WHERE clause will just turn it back into a pseudo inner join.
Instead, is there a different field you can join the delivery table on from current_stock?
EDIT - 07/29/15
I think you're close, you may just have one too many filters and you're counting from 'b' when the wanted outcome suggests you should be counting from 'cs' instead. Try this:
SELECT t.id, b.id, t.threshold, count(cs.id) stock
FROM
threshold t
inner join brand b on b.id = t.brand_id
inner join product p on p.brand_id = b.id
inner join article a on a.product_product_code = p.product_code
left outer join delivery d on d.store_id = t.store_id
left outer join current_stock cs on cs.article_id = a.id
WHERE
t.store_id = 'E260'
GROUP BY b.id
HAVING stock <= t.threshold

Query: Select and display next highest value as new column

I have this query which lists out IDs from "pages" on our site.
SELECT mdl_page.id
FROM mdl_page, mdl_log, mdl_user
WHERE mdl_log.module = "page"
AND mdl_log.action = "view"
AND mdl_user.id = mdl_log.userid
AND mdl_log.info = mdl_page.id
AND mdl_log.course = 178
The result is simple:
| ID |
|-----|
| 3 |
| 4 |
| 7 |
| 11 |
Notice the jumps in the count. I'm trying to get something like this:
| ID | NEXT ID |
|-----|---------|
| 3 | 4 |
| 4 | 7 |
| 7 | 11 |
| 11 | 12 |
Can anyone point in me in the right direction for this?
UPDATE
One twist, the system (not my own) I have to run the query through only allows queries that begin with 'SELECT'.
Two ways i can think of use a co-related subquery,in your sub query compare the value from main query and sorts it in ascending manner and limit the result to one
SELECT
p.id ,
(SELECT
p1.id
FROM mdl_page p1
JOIN mdl_log l1 ON (l1.info = p1.id)
JOIN mdl_user u1 ON (u1.id = l1.userid)
WHERE l1.module = "page"
AND l1.action = "view"
AND l1.course = 178
AND p1.id > p.id
ORDER BY p1.id ASC LIMIT 1) NEXT_ID
FROM mdl_page p
JOIN mdl_log l ON (l.info = p.id)
JOIN mdl_user u ON (u.id = l.userid)
WHERE l.module = "page" AND l.action = "view" AND l.course = 178
ORDER BY p.id
and use a rank query, in rank query i am left joining the same query with the less than condition ON (t.id< t1.id) so it will result in multiple rows like (3,4),(3,7),(3,11) so i need to pick the first combination of 3,4 for this i have used a rank query to give the rank to the items that belong to same group, in parent where i am just restricting the result set to show the first pair for each group
SELECT t3.id,t3.NEXT_ID FROM (
SELECT t.id id, t1.id NEXT_ID ,
#r:= CASE WHEN #g = t.id THEN #r +1 ELSE 1 END rownum,
#g:= t.id
FROM
(SELECT
p.id
FROM
mdl_page p
JOIN mdl_log l ON (l.info = p.id)
JOIN mdl_user u ON (u.id = l.userid)
WHERE l.module = "page"
AND l.action = "view"
AND l.course = 178
ORDER BY p.id
) t
LEFT JOIN
(SELECT
p.id
FROM
mdl_page p
JOIN mdl_log l ON (l.info = p.id)
JOIN mdl_user u ON (u.id = l.userid)
WHERE l.module = "page"
AND l.action = "view"
AND l.course = 178
ORDER BY p.id ) t1 ON (t.`id` < t1.id)
CROSS JOIN (SELECT #g:=0,#r:=0) t2
ORDER BY t.`ID` , t1.ID
) t3
WHERE t3.rownum = 1
resutset you will get as null for 11 if there is no more record exist which have an id greater than 11 ,or in other words the last record will have a null in next_id column
ID NEXT_ID
3 4
4 7
7 11
11 NULL
Perhaps you should create a temporary table, which is pretty much the same as the query you're running and erase the first line?
Then run your query and join it with the temporary table?

Mysql GROUPing with ORDER based on auto increment column

I have to group my anime index according to their AniDB ID and show the values in a DESCENDING order according to file auto increment id.
Here's what I did currently:
SELECT
f.id, f.category, f.anidb, f.mal_id, COUNT( * ) AS dupes, f.filename,
a.titles, a.synopsis, a.episodes, a.image, a.rating,
c.name as cat_name, c.id as categoryid
FROM table_files f
LEFT JOIN table_anidb a ON a.id = f.anidb
LEFT JOIN table_categories c ON c.id = f.category
GROUP BY a.id ORDER BY f.id DESC
PROBLEM:
I have Naruto 8 episodes. episode 8's ID is 204. And ep.1 has ID 160. The query return like this:
id | anidb | filename | dupes | cat_name
--------------------------------------------------------
201 | 8692 | SAO | 1 | Series
200 | 9251 | RYO | 1 | Movie
.....
.......
160 | 239 | Naruto ep.1 | 8 | Series
But I want Naruto Episode 8 to be showed in the top of the results instead of episode 1 in the last.
How do I group by anidb and mal_id at the same time with an OR logic? So that the grouping can be done even if there is not any anidb ID provided.
Ad. 1.
Since id, anidb and filename are all in one table i'm afraid you can't get away from doing a subquery join:
SQLFiddle
SELECT f.id, f.anidb, f.filename
FROM files f
JOIN
(SELECT MAX(id) as id FROM files GROUP BY anidb) AS f2
ON f2.id = f.id
ORDER BY f.id DESC
(data flattened for the sake of readibility but you can get the general idea)
Ad. 2.
As for the second problem, you really just have to add second grouping column to the above joined subquery:
SQLFiddle
SELECT f.id, f.anidb, f.mal_id, f.filename
FROM files f
JOIN
(SELECT MAX(id) as id FROM files GROUP BY anidb, mal_id) AS f2 on f2.id = f.id
ORDER BY f.id DESC
The NULL's are distinct from each other (e.g. NULL != NULL) so there's no fear that grouping would melt all the nulled anidb rows into one.
For the first problem you can use ORDER BY dupes
SELECT
f.id, f.category, f.anidb, f.mal_id, COUNT( * ) AS dupes, f.filename,
a.titles, a.synopsis, a.episodes, a.image, a.rating,
c.name as cat_name, c.id as categoryid
FROM table_files f
LEFT JOIN table_anidb a ON a.id = f.anidb
LEFT JOIN table_categories c ON c.id = f.category
GROUP BY a.id ORDER BY dupes DESC
For the second problem you can use CASE to check if f.anidb is null
SELECT
f.id, f.category, f.anidb, f.mal_id, COUNT( * ) AS dupes, f.filename,
a.titles, a.synopsis, a.episodes, a.image, a.rating,
c.name as cat_name, c.id as categoryid
FROM table_files f
LEFT JOIN table_anidb a ON a.id = f.anidb
LEFT JOIN table_categories c ON c.id = f.category
GROUP BY
(CASE WHEN f.anidb IS NULL THEN f.mal_id ELSE f.anidb END )
ORDER BY dupes DESC

SELECT distinct values for multiple rows of same ID

I have a table that looks like this:
ID | FIELD_NAME | VALUE
23 | sign_up | yes
23 | first_name | Fred
23 | street | Barber Lane
24 | sign_up | no
24 | first_name | Steve
24 | street | Camaro St.
25 | sign_up | yes
25 | first_name | Larry
25 | street | Huckleberry Ave
I want to run a query that will select unique ID's and the values as named columns so it would appear like so:
ID | SIGN_UP | FIRST_NAME | STREET |
23 | yes | Fred | Barber Lane |
24 | no | Steve | Camaro St. |
25 | yes | Larry | Huckleberry Ave. |
Any help would be much appreciated!!
You can use this simple solution:
SELECT DISTINCT
a.id,
b.value AS SIGN_UP,
c.value AS FIRST_NAME,
d.value AS STREET
FROM tbl a
LEFT JOIN tbl b ON a.id = b.id AND b.field_name = 'sign_up'
LEFT JOIN tbl c ON a.id = c.id AND c.field_name = 'first_name'
LEFT JOIN tbl d ON a.id = d.id AND d.field_name = 'street'
Just to be safe, I made the joins LEFT JOIN's because I do not know if an id can have missing fields, in which case they will show up as NULL in our derived columns.
SQL-Fiddle Demo
You could also try pivoting with the help of grouping and conditional aggregating:
SELECT
ID,
MAX(CASE FIELD_NAME WHEN 'sign_up' THEN VALUE END) AS SIGN_UP,
MAX(CASE FIELD_NAME WHEN 'first_name' THEN VALUE END) AS FIRST_NAME,
MAX(CASE FIELD_NAME WHEN 'street' THEN VALUE END) AS STREET
FROM atable
GROUP BY
ID
;
Adapted from another answer by me:
SELECT ids.ID AS ID,
sign_up.VALUE AS SIGN_UP,
first_name.VALUE AS FIRST_NAME,
street.VALUE AS STREET
FROM (SELECT DISTINCT ID FROM tableName) AS ids
LEFT JOIN tableName AS sign_up
ON (sign_up.ID = ids.ID AND
sign_up.FIELD_NAME = 'sign_up')
LEFT JOIN tableName AS first_name
ON (first_name.ID = ids.ID AND
first_name.FIELD_NAME = 'first_name')
LEFT JOIN tableName AS street
ON (street.ID = ids.ID AND
street.FIELD_NAME = 'street')
The left joins will ensure that missing values will result in NULL cells, instead of an omission of the whole row. Not sure whether that is important in your application. If it is not, you can use an inner join and in particular get rid of the subquery to select all unique IDs. See my original answer from which I derived this.
Am not sure there is a Pivot/Unpivot feature in MySQL.
Try this:
SELECT a.ID,
c.FIELD_NAME AS SIGN_UP,
a.FIELD_NAME AS FIRST_NAME,
b.FIELD_NAME AS STREET
FROM <YOUR-TABLE> a LEFT JOIN <YOUR-TABLE> b
ON a.ID = b.ID
AND a.FIELD_NAME = 'first_name'
AND b.FIELD_NAME = 'street' LEFT JOIN <YOUR-TABLE> c
ON c.ID = a.ID
AND c.FIELD_NAME = 'sign_up'
One approach is to use a correlated subquery to return each field value as a column,
SELECT t.id
, (SELECT f1.value FROM mytable f1
WHERE f1.id = t.id AND f1.field_name = 'sign_up'
ORDER BY f1.value LIMIT 1
) AS SIGN_UP
, (SELECT f2.value FROM mytable f2
WHERE f2.id = t.id AND f2.field_name = 'first_name'
ORDER BY f2.value LIMIT 1
) AS FIRST_NAME
, (SELECT f3.value FROM mytable f3
WHERE f3.id = t.id AND f3.field_name = 'street'
ORDER BY f3.value LIMIT 1
) AS STREET
FROM (SELECT s.id
FROM mytable s
GROUP BY s.id
ORDER BY s.id
) t
This isn't the only way, but it's a workable approach, especially if you are concerned that you will get exactly four columns returned, and that they will be returned in a specific sequence.
Note that this approach works when a particular field_name is "missing" for a particular ID (it will return a NULL in place of a value). It also works if there are multiple occurrences of the same field_name for a particular ID. (This query will return only one of them, and disregard the other.)
This same result set can also be obtained with a query written like this:
SELECT t.id AS ID
, f1.sign_up AS SIGN_UP
, f2.first_name AS FIRST_NAME
, f3.street AS STREET
FROM (SELECT s.id
FROM mytable s
GROUP BY s.id
ORDER BY s.id
) t
LEFT
JOIN (SELECT s1.id
, MIN(s1.value) AS sign_up
FROM mytable s1
WHERE s1.field_name = 'sign_up'
AND s1.value IS NOT NULL
GROUP BY s1.id
) f1
ON f1.id = t.id
LEFT
JOIN (SELECT s2.id
, MIN(s2.value) AS first_name
FROM mytable s2
WHERE s2.field_name = 'first_name'
AND s2.value IS NOT NULL
GROUP BY s2.id
) f2
ON f2.id = t.id
LEFT
JOIN (SELECT s3.id
, MIN(s3.value) AS street
FROM mytable s3
WHERE s3.field_name = 'street'
AND s3.value IS NOT NULL
GROUP BY s3.id
) f3
ON f3.id = t.id
With other queries, ensure you are getting the desired behavior when a field_name is "missing" for a given ID, or when there are duplicate field_name for a given ID, or when there are additional field_name values in the table which you are not concerned with.