How to count correctly 2 inner levels count? - mysql

I have a structure for a university DB wherein I have three tables: room, students, possessions
CREATE TABLE `rooms` (
`id` INT(10) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL COLLATE 'utf8_general_ci',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `name` (`name`) USING BTREE
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=3
;
CREATE TABLE `students` (
`id` INT(10) NOT NULL AUTO_INCREMENT,
`ref_room_id` INT(10) NULL DEFAULT NULL,
`student_name` VARCHAR(50) NOT NULL COLLATE 'utf8_general_ci',
PRIMARY KEY (`id`) USING BTREE,
INDEX `FK_students_room` (`ref_room_id`) USING BTREE,
CONSTRAINT `FK_students_room` FOREIGN KEY (`ref_room_id`) REFERENCES `university`.`rooms` (`id`) ON UPDATE CASCADE ON DELETE SET NULL
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=6
;
CREATE TABLE `possessions` (
`id` INT(10) NOT NULL AUTO_INCREMENT,
`ref_student_id` INT(10) NOT NULL,
`name` VARCHAR(50) NOT NULL DEFAULT '' COLLATE 'utf8_general_ci',
PRIMARY KEY (`id`) USING BTREE,
INDEX `FK__students` (`ref_student_id`) USING BTREE,
CONSTRAINT `FK__students` FOREIGN KEY (`ref_student_id`) REFERENCES `university`.`students` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=4
;
INSERT INTO rooms (name) VALUES ('a'),('b');
INSERT INTO students (`ref_room_id`, `student_name`) VALUES
(3,1),
(3,2),
(3,3),
(3,4);
INSERT INTO possessions (ref_student_id, name) VALUES
(7,'a')
(7,'aa'),
(7,'aaa'),
(8,'aaaa'),
(9,'aaaaa'),
(6,'aaaaaa'),
(7,'aaaaaaa');
So, in order to present such a table in MySQL, I created the procedure
CREATE DEFINER=`root`#`localhost` PROCEDURE `get_data`()
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT ''
BEGIN
SELECT r.id AS room_id, r.name, COUNT(s.id) AS student_num, COUNT(p.id) AS possessions_num
FROM rooms r
INNER JOIN students s ON s.ref_room_id = r.id
INNER JOIN possessions p ON p.ref_student_id = s.id
GROUP BY r.name;
END
but what I get is
First of all, it misses the room and secondly instead of 4 students it shows 7...
What am I doing wrong?

Consider the following (and note that my data set may vary slightly from yours...)
DROP TABLE IF EXISTS rooms;
CREATE TABLE `rooms` (
`id` INT(10) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL ,
PRIMARY KEY (`id`)
);
DROP TABLE IF EXISTS students;
CREATE TABLE `students` (
`id` INT(10) NOT NULL AUTO_INCREMENT,
`ref_room_id` INT(10) NULL DEFAULT NULL,
`student_name` VARCHAR(50) NOT NULL,
PRIMARY KEY (`id`)
);
DROP TABLE IF EXISTS possessions;
CREATE TABLE `possessions` (
`id` INT NOT NULL AUTO_INCREMENT,
`ref_student_id` INT NOT NULL,
`name` VARCHAR(50) NOT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO rooms VALUES (1,'a'),(2,'b');
INSERT INTO students VALUES
(11,1,1),
(12,1,2),
(13,1,3),
(14,1,4);
INSERT INTO possessions VALUES
(101,11,'apple'),
(102,11,'banana'),
(103,11,'cherry'),
(104,12,'date'),
(105,15,'elderberry'),
(106,13,'fig'),
(107,14,'huckleberry');
SELECT r.*
, COUNT(DISTINCT s.id) total_students
, COUNT(DISTINCT p.id) total_possessions
FROM rooms r
JOIN students s
ON s.ref_room_id = r.id
JOIN possessions p
ON p.ref_student_id = s.id
GROUP
BY r.id;
+----+------+----------------+-------------------+
| id | name | total_students | total_possessions |
+----+------+----------------+-------------------+
| 1 | a | 4 | 6 |
+----+------+----------------+-------------------+
Or...
SELECT r.*
, COUNT(DISTINCT s.id) total_students
, COUNT(DISTINCT p.id) total_possessions
FROM rooms r
LEFT
JOIN students s
ON s.ref_room_id = r.id
LEFT
JOIN possessions p
ON p.ref_student_id = s.id
GROUP
BY r.id;
+----+------+----------------+-------------------+
| id | name | total_students | total_possessions |
+----+------+----------------+-------------------+
| 1 | a | 4 | 6 |
| 2 | b | 0 | 0 |
+----+------+----------------+-------------------+

Related

how to count same rating from field in sql

I have a problem counting ratings in SQL. This is what my data looks like:
data
CREATE TABLE `restaurant` (
`id_restaurant` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id_restaurant`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1;
insert into `restaurant`(`id_restaurant`,`name`) values (1,'Mc Donald');
insert into `restaurant`(`id_restaurant`,`name`) values (2,'KFC');
CREATE TABLE `user` (
`id_user` int(11) NOT NULL AUTO_INCREMENT,
`userName` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id_user`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;
insert into `user`(`id_user`,`userName`) values (1,'Audey');
CREATE TABLE `factors` (
`factor_id` int(11) NOT NULL AUTO_INCREMENT,
`factor_clean` int(11) NOT NULL DEFAULT '0',
`factor_delicious` int(11) NOT NULL DEFAULT '0',
`id_restaurant` int(11) DEFAULT NULL,
`id_user` int(11) DEFAULT NULL,
PRIMARY KEY (`factor_id`),
KEY `id_restaurant` (`id_restaurant`),
KEY `id_user` (`id_user`),
CONSTRAINT `factors_ibfk_1` FOREIGN KEY (`id_restaurant`) REFERENCES `restaurant` (`id_restaurant`),
CONSTRAINT `factors_ibfk_2` FOREIGN KEY (`id_user`) REFERENCES `user` (`id_user`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=latin1;
insert into `factors`(`factor_id`,`factor_clean`,`factor_delicious`,`id_restaurant`,`id_user`) values (1,1,5,1,1);
insert into `factors`(`factor_id`,`factor_clean`,`factor_delicious`,`id_restaurant`,`id_user`) values (2,0,5,1,1);
insert into `factors`(`factor_id`,`factor_clean`,`factor_delicious`,`id_restaurant`,`id_user`) values (3,1,5,1,1);
insert into `factors`(`factor_id`,`factor_clean`,`factor_delicious`,`id_restaurant`,`id_user`) values (4,3,3,1,1);
And the result should be like this, Show all ratings (1,2,3,4,5) and their count from the fields rating_clean, rating_delicious, and rating_clean
Thanks for your help.
but the result i get
SELECT COUNT(`factor_clean`+`factor_delicious`),'1' AS rating_1 FROM `factors` WHERE 1 GROUP BY `id_restaurant`
result not should like this
the result should not like that,
my question is, how to select just factor_clean and factor_delicious where factor_clean =1 and factor_delicious = 1
Use union all to unpivot the data and then aggregate:
select id_restaurant, rating, count(*)
from ((select r.id_restaurant, r.rating_clean as rating, r.date
from ratings r
) union all
(select r.id_restaurant, r.rating_delicious, r.date
from ratings r
) union all
(select r.id_restaurant, r.rating_clean2, r.date
from ratings r
)
) r
group by id_restaurant, rating
order by id_restaurant, rating;
For example this is solution for table with colums rating_delicious and rating_clean (only one!):
First of all you should create additional table, I called it factors:
CREATE TABLE `factors` (
`factor_id` int(11) NOT NULL AUTO_INCREMENT,
`factor_clean` int(11) NOT NULL DEFAULT '0',
`factor_delicious` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`factor_id`)
)
Next add two records:
INSERT INTO `factors` (`factor_id`, `factor_clean`, `factor_delicious`) VALUES (NULL, '1', '0'), (NULL, '0', '1');
Now you can join this tables and get results:
SELECT x.id_restaurant
, (x.rating_clean * f.factor_clean) + (x.rating_delicious * f.factor_delicious) AS rating
, count(*)
FROM your_table x
JOIN factors f
WHERE 1
GROUP
BY x.id_restaurant
, rating
In order to use next colum (rating_third), you should and column factor_third to factors, insert new row with 1 in this column and finally add something like your_table.rating_third*factors.factor_third to sum in SELECT

How can i optimize this mysql query in social table structure?

Scheme
CREATE TABLE IF NOT EXISTS `content` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`entity_uid` int(11) NOT NULL,
....
PRIMARY KEY (`uid`),
UNIQUE KEY `insert_at` (`insert_at`),
KEY `fk_entity` (`entity_uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `entity` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `entity_comment` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`entity_uid` int(11) NOT NULL,
`user_uid` int(11) NOT NULL,
....
PRIMARY KEY (`uid`),
KEY `fk_entity` (`entity_uid`),
KEY `fk_user` (`user_uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `entity_like` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`entity_uid` int(11) NOT NULL,
`user_uid` int(11) NOT NULL,
....
PRIMARY KEY (`uid`),
KEY `fk_entity` (`entity_uid`),
KEY `fk_user` (`user_uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `entity_share` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`entity_uid` int(11) NOT NULL,
`user_uid` int(11) NOT NULL,
`share_type` int(2) NOT NULL,
....
PRIMARY KEY (`uid`),
KEY `fk_entity` (`entity_uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `entity_view` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`entity_uid` int(11) NOT NULL,
`user_uid` int(11) NOT NULL,
....
PRIMARY KEY (`uid`),
KEY `fk_entity` (`entity_uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `user` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(30) NOT NULL,
....
PRIMARY KEY (`uid`),
UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query 1 - using Left Join [Query took 16.3032 sec]
SELECT c . * , COUNT(DISTINCT ev.uid) AS view_count, COUNT( DISTINCT el.uid ) AS like_count, COUNT( DISTINCT ec.uid ) AS reply_count, COUNT( DISTINCT es.uid ) AS share_count
FROM content AS c
LEFT JOIN entity_view AS ev ON ev.entity_uid = c.entity_uid
LEFT JOIN entity_like AS el ON el.entity_uid = c.entity_uid
LEFT JOIN entity_share AS es ON es.entity_uid = c.entity_uid
LEFT JOIN entity_comment AS ec ON ec.entity_uid = c.entity_uid
GROUP BY c.uid
EDIT - Explain
Query 2 - using Sub query [Query took 0.0069 sec]
SELECT c.*,
(SELECT COUNT(*) FROM entity_view WHERE entity_uid = c.entity_uid) AS view_count ,
(SELECT COUNT(*) FROM entity_like WHERE entity_uid = c.entity_uid) AS like_count ,
(SELECT COUNT(*) FROM entity_comment WHERE entity_uid = c.entity_uid) AS reply_count ,
(SELECT COUNT(*) FROM entity_share WHERE entity_uid = c.entity_uid) AS share_count
FROM content AS c
EDIT - Explain
Result
uid | data of content | view_count | like_count | reply_count | share_count |
-----------------------------------------------------------------------------
1 | ..... | 100 | 10 | 5 | 6 |
-----------------------------------------------------------------------------
2 | ..... | 200 | 20 | 20 | 3 |
-----------------------------------------------------------------------------
3 | ..... | 300 | 10 | 10 | 2 |
-----------------------------------------------------------------------------
Explain
Storage Engine : InnoDB
entity_{action} : Insert occurs when user {action} occurs.(e.g) entity_view is insertion occurs when user sees the content.
Question
How can I optimize more in the above mysql query?
I run the query in two ways and got the results above.
This proved that subquery is much better.
Is there a way to a get better performance than subquery like this table structure? And why is left join so bad?

unique records in mysql one-to-many join without DISTINCT or GROUP BY

Here's the basic query:
SELECT
some_columns
FROM
d
JOIN
m ON d.id = m.d_id
JOIN
s ON s.id = m.s_id
JOIN
p ON p.id = s.p_id
WHERE
some_criteria
ORDER BY
d.date DESC
LIMIT 25
Table m can contain multiple s_ids per each d_id. Here's a super simple example:
+--------+--------+------+
| id | d_id | s_id |
+--------+--------+------+
| 317354 | 291220 | 642 |
| 317355 | 291220 | 32 |
+--------+--------+------+
2 rows in set (0.00 sec)
Which we want. But, obviously, it's producing duplicate d records in this particular query.
These tables have lots of columns, and I need to edit these down due to the sensitive nature of the data, but here's the basic structure as it pertains to this query:
| d | CREATE TABLE `d` (
`id` bigint(22) unsigned NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`),
KEY `date` (`date`)
) ENGINE=InnoDB |
| m | CREATE TABLE `m` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`d_id` bigint(20) NOT NULL,
`s_id` bigint(20) NOT NULL,
`is_king` binary(1) DEFAULT '0',
PRIMARY KEY (`id`),
KEY `d_id` (`d_id`),
KEY `is_king` (`is_king`),
KEY `s_id` (`s_id`)
) ENGINE=InnoDB |
| s | CREATE TABLE `s` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`p_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `p_id` (`p_id`)
) ENGINE=InnoDB |
| p | CREATE TABLE `p` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
) ENGINE=InnoDB |
Now, previously, we had a GROUP BY d.id in place to grab uniques. The data here are now huge, so we can no longer realistically do that. SELECT DISTINCT d.id is even slower.
Any ideas? Everything I come up with creates a problem elsewhere.
Does changing "JOIN m ON d.id = m.d_id" to "LEFT JOIN m ON d.id = m.d_id" accomplish what you're looking for here?
I'm not sure I understand your goal clearly, but "table m contains many rows per each d" immediately has me wondering if you should be using some other type of join to accomplish your ends.

BLOB data returned in MySQL using AES_DECRYPT with ORDER clause

I'm creating a system in which users can store messages via PHP with a MySQL database, and I am using the MySQL AES_ENCRYPT function to encrypt the contents of these messages.
Here is my posts table:
CREATE TABLE IF NOT EXISTS `posts` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user` int(11) DEFAULT NULL,
`group` int(11) DEFAULT NULL,
`body` varbinary(1000) NOT NULL,
`ip` varchar(45) NOT NULL,
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`replyto` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `replyto` (`replyto`),
KEY `user` (`user`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=6 ;
ALTER TABLE `posts`
ADD CONSTRAINT `posts_ibfk_3` FOREIGN KEY (`replyto`) REFERENCES `posts` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,
ADD CONSTRAINT `posts_ibfk_4` FOREIGN KEY (`user`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
And my users table:
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ip` varchar(45) NOT NULL,
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`email` varchar(100) NOT NULL,
`name` varchar(100) NOT NULL,
`hash` varchar(128) NOT NULL,
`salt` varchar(32) NOT NULL,
`guid` varchar(36) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=5 ;
The AES encryption key I'm using for the message body is a SHA-512-hashed concatenation between a prefix and suffix string, and the posting user's GUID in the middle. Thus, I have the following SQL query to select the most recent messages:
SELECT AES_DECRYPT(`posts`.`body`, SHA2(CONCAT('prefix',(`users`.`guid`),'suffix'),512)) AS 'realbody'
FROM `posts`
INNER JOIN `users` ON `posts`.`user` = `users`.`id`
ORDER BY `posts`.`id` DESC
Unfortunately, this does not return the decrypted messages, as you can see in the screenshot:
Instead, I'm getting this BLOB data. However, if I remove the ORDER BY clause from the query:
SELECT AES_DECRYPT(`posts`.`body`, SHA2(CONCAT('prefix',(`users`.`guid`),'suffix'),512)) AS 'realbody'
FROM `posts`
INNER JOIN `users` ON `posts`.`user` = `users`.`id`
Then suddenly, it works:
I really don't know what could be causing this. Does anybody have any ideas?
UPDATED CAST it to CHAR
SELECT `posts`.*, CAST(AES_DECRYPT(`posts`.`body`,SHA2(CONCAT('prefix',`users`.`guid`,'suffix'),512)) AS CHAR) as 'realbody'
FROM `posts` JOIN `users`
ON `posts`.`user` = `users`.`id`
ORDER BY `posts`.`id` DESC
Sample output:
| ID | USER | ... | REALBODY |
---...------------------------
| 2 | 2 | ... | Post 2 |
| 1 | 1 | ... | Post 1 |
Here is SQLFiddle demo

Complex issue with MySQL queries

OK, so I'm facing this extremely complicated issue and since I'm not a guru with MySQL I'd definitely need your input on that.
Let's say we've got a database, created using the code below (I'm pasting the creation code - of just the absolutely-necessary tables - to avoid pasting all the tables) :
DROP TABLE IF EXISTS `Jeweller`.`Orders`;
CREATE TABLE `Jeweller`.`Orders` (
`id` int(11) unsigned NOT NULL,
`date` date DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `Jeweller`.`Product_categories`;
CREATE TABLE `Jeweller`.`Product_categories` (
`id` int(11) unsigned NOT NULL,
`name` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `Jeweller`.`Product_orders`;
CREATE TABLE `Jeweller`.`Product_orders` (
`order_id` int(11) unsigned NOT NULL,
`product_id` int(11) unsigned NOT NULL,
`quantity` int(11),
`value` float,
FOREIGN KEY (`order_id`) REFERENCES `Jeweller`.`Orders`(`id`),
FOREIGN KEY (`product_id`) REFERENCES `Jeweller`.`Products`(`id`),
CHECK (`quantity`>0),
CHECK (`value`>0)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `Jeweller`.`Product_returns`;
CREATE TABLE `Jeweller`.`Product_returns` (
`sale_id` int(11) unsigned NOT NULL,
`product_id` int(11) NOT NULL,
`date` date DEFAULT NULL,
`quantity` int(11),
`value` float,
FOREIGN KEY (`sale_id`) REFERENCES `Jeweller`.`Sales`(`id`),
FOREIGN KEY (`product_id`) REFERENCES `Jeweller`.`Products`(`id`),
CHECK (`quantity`>0),
CHECK (`value`>0)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `Jeweller`.`Product_sales`;
CREATE TABLE `Jeweller`.`Product_sales` (
`sale_id` int(11) unsigned NOT NULL,
`product_id` int(11) NOT NULL,
`quantity` int(11),
`value` float,
FOREIGN KEY (`sale_id`) REFERENCES `Jeweller`.`Sales`(`id`),
FOREIGN KEY (`product_id`) REFERENCES `Jeweller`.`Products`(`id`),
CHECK (`quantity`>0),
CHECK (`value`>0)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `Jeweller`.`Products`;
CREATE TABLE `Jeweller`.`Products` (
`id` int(11) unsigned NOT NULL,
`product_category_id` int(11) NOT NULL,
`seller_id` int(11) NOT NULL,
`name` varchar(100) NOT NULL,
`description` text,
PRIMARY KEY (`id`),
FOREIGN KEY (`product_category_id`) REFERENCES `Jeweller`.`Product_categories`(`id`),
FOREIGN KEY (`seller_id`) REFERENCES `Jeweller`.`Sellers`(`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `Jeweller`.`Sales`;
CREATE TABLE `Jeweller`.`Sales` (
`id` int(11) unsigned NOT NULL,
`date` date DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Now, given that we define Profit as :
Sales - Returns - Orders
How would you go about a query to fetch :
Profit by month AND product_category, only for the year 2013.
For testing purposes, here's the full DB Creation code as well as the DB Population code (with some demo data). (SQLFiddle link)
P.S.
The actual code is kinda different (the above is just an example - though a 100% loyal one)
After several attempts I've managed to just filter 2013 sales/orders/etc... I've even managed to get Profit by product (though it took some endless joins, left outer joins, etc... lol)... However this looks much more complicated. Any ideas?
Here's an approximation of your schema...
DROP TABLE IF EXISTS orders;
CREATE TABLE orders
( order_id int(11) unsigned NOT NULL auto_increment
, date date DEFAULT NULL
, PRIMARY KEY (order_id)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO orders VALUES
(NULL,'2013-01-01'),
(NULL,'2013-01-01'),
(NULL,'2013-02-02'),
(NULL,'2013-02-03'),
(NULL,'2013-03-05'),
(NULL,'2013-06-07');
DROP TABLE IF EXISTS product_orders;
CREATE TABLE product_orders
( order_id int unsigned NOT NULL
, product_id int unsigned NOT NULL
, quantity int NOT NULL DEFAULT 1
, value DECIMAL(5,2)
, PRIMARY KEY(order_id,product_id)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO product_orders VALUES
(1,101,1,100),
(1,102,1,50),
(2,101,2,200),
(3,101,1,100),
(4,102,2,100),
(4,103,3,150),
(5,104,1,300),
(6,102,1,50),
(6,103,2,100),
(6,104,1,300);
DROP TABLE IF EXISTS product_returns;
CREATE TABLE product_returns
( sale_id int unsigned NOT NULL
, product_id int NOT NULL
, date date DEFAULT NULL
, quantity int NOT NULL DEFAULT 1
, value DECIMAL(5,2)
, PRIMARY KEY(sale_id,product_id)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO product_returns VALUES
(21,101,'2013-01-04',2,200),
(22,102,'2013-03-06',1,50),
(22,103,'2013-05-08',1,50),
(23,104,'2013-06-09',1,300);
DROP TABLE IF EXISTS product_sales;
CREATE TABLE product_sales
( sale_id int unsigned NOT NULL
, product_id int NOT NULL
, quantity int NOT NULL
, value DECIMAL(5,2)
, PRIMARY KEY(sale_id,product_id)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO product_sales VALUES
(20,101,1,100),
(20,102,1,50),
(21,101,3,300),
(22,101,1,100),
(22,102,2,100),
(22,103,1,50),
(23,103,2,100),
(23,104,2,600);
DROP TABLE IF EXISTS products;
CREATE TABLE products
( product_id int unsigned NOT NULL AUTO_INCREMENT
, product_category_id int NOT NULL
, name varchar(100) NOT NULL
, description text NULL
, PRIMARY KEY (product_id)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO products VALUES
(101,1,'donuts','Mmm, donuts'),
(102,2,'buzz Cola','Mmm, donuts'),
(103,2,'duff beer','Can\'t get enough'),
(104,1,'Krusty-O\'s','Yum, yum');
DROP TABLE IF EXISTS sales;
CREATE TABLE sales
( sale_id int NOT NULL
, date date DEFAULT NULL
, PRIMARY KEY (sale_id)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO sales VALUES
(20,'2013-01-12'),
(21,'2013-02-15'),
(22,'2013-03-17'),
(23,'2013-05-18');
...and a possible query...
SELECT p.product_category_id
, MONTH(date) month
, SUM(value) profit
FROM
( SELECT product_id,value, date
FROM product_sales ps
JOIN sales s
ON s.sale_id = ps.sale_id
UNION ALL
SELECT product_id,value*-1,date FROM product_returns
UNION ALL
SELECT product_id,value*-1,date
FROM product_orders po
JOIN orders o
ON o.order_id = po.order_id
) x
JOIN products p
ON p.product_id = x.product_id
WHERE YEAR(date) = 2013
GROUP
BY p.product_category_id
, MONTH(date);
+---------------------+-------+---------+
| product_category_id | month | profit |
+---------------------+-------+---------+
| 1 | 1 | -400.00 |
| 1 | 2 | 200.00 |
| 1 | 3 | -200.00 |
| 1 | 5 | 600.00 |
| 1 | 6 | -600.00 |
| 2 | 1 | 0.00 |
| 2 | 2 | -250.00 |
| 2 | 3 | 100.00 |
| 2 | 5 | 50.00 |
| 2 | 6 | -150.00 |
+---------------------+-------+---------+
...and an sqlfiddle of same :http://www.sqlfiddle.com/#!2/22a1d/1
In simplest terms, if restructuring is undesirable, then I would do a simple query to determine the value of orders, returns, and sales seperately and then join those together. This could be done using UNION and subqueries as in the following example : SQLFiddle
I've also taken the liberty of swapping the FLOATs for DECIMALs. There is probably room for improvement on indexes and the like but this should put you on a good track for determining the sums. If you look at the subquery you'll see that ORDER and RETURN selects are selecting a negative value as per your requirement.
One potential pitfall is that any records for which the record from Product has been deleted would not be included. This could be avoided by changing the Product joins into LEFT JOINs and handling the NULL value for product_category_id appropriately. Decided to add this into the latest example, though if the rows from Product are NEVER deleted, then INNER JOIN will suffice
SELECT
d.thisMonth,
d.product_category_id,
SUM(d.sumValue)
FROM (
(
-- Get the order value
SELECT
'order' AS valueType,
MONTH(o.date) AS thisMonth,
p.product_category_id,
SUM(-po.value * po.quantity) AS sumValue
FROM Orders o
INNER JOIN Product_orders po
ON po.order_id = o.id
LEFT JOIN Products p
ON p.id = po.product_id
WHERE o.date BETWEEN '2013-01-01' AND '2013-12-31'
GROUP BY
thisMonth,
product_category_id
) UNION ALL (
-- Get the sales value
SELECT
'sale' AS valueType,
MONTH(s.date) AS thisMonth,
p.product_category_id,
SUM(ps.value * ps.quantity) AS sumValue
FROM Sales s
INNER JOIN Product_sales ps
ON ps.sale_id = s.id
INNER JOIN Products p
ON p.id = ps.product_id
WHERE s.date BETWEEN '2013-01-01' AND '2013-12-31'
GROUP BY
thisMonth,
product_category_id
) UNION ALL (
-- Get the return value
SELECT
'return' AS valueType,
p.product_category_id,
MONTH(pr.date) AS thisMonth,
SUM(-pr.value * pr.quantity) AS sumValue
FROM Product_returns pr
INNER JOIN Products p
ON p.id = pr.product_id
WHERE pr.date BETWEEN '2013-01-01' AND '2013-12-31'
GROUP BY
thisMonth,
product_category_id
)
) d
GROUP BY
d.thisMonth,
d.product_category_id;