Optimizing MySQL query to avoid scanning a lot of rows - mysql

I am running an application that is using tables similar to the below tables. There are one tables for articles and there is another table for tags. I want to get the latest 30 articles for a specific tag order by article id. for example "acer", the below query will do the job but it is not indexed correctly because it will scan a lot of rows if there are a lot of articles related to a specific tag. How to run a query to get the same result without scanning a large number of rows?
EXPLAIN SELECT title
FROM tag, article
WHERE tag = 'acer'
AND tag.article_id = article.id
ORDER BY tag.article_id DESC
LIMIT 0 , 30
Output
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE tag ref tag tag 92 const 220439 Using where; Using index
1 SIMPLE article eq_ref PRIMARY PRIMARY 4 testdb.tag.article_id 1
The flollowing is the tables and sample data:
CREATE TABLE `article` (
`id` int(11) NOT NULL auto_increment,
`title` varchar(60) NOT NULL,
`time_stamp` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1000001 ;
--
-- Dumping data for table `article`
--
INSERT INTO `article` VALUES (1, 'Saudi Apple type D', 1313390211);
INSERT INTO `article` VALUES (2, 'Japan Apple type A', 1313420771);
INSERT INTO `article` VALUES (3, 'UAE Samsung type B', 1313423082);
INSERT INTO `article` VALUES (4, 'UAE Apple type H', 1313417337);
INSERT INTO `article` VALUES (5, 'Japan Samsung type D', 1313398875);
INSERT INTO `article` VALUES (6, 'UK Acer type B', 1313387888);
INSERT INTO `article` VALUES (7, 'Saudi Sony type D', 1313429416);
INSERT INTO `article` VALUES (8, 'UK Apple type B', 1313394549);
INSERT INTO `article` VALUES (9, 'Japan HP type A', 1313427730);
INSERT INTO `article` VALUES (10, 'Japan Acer type C', 1313400046);
CREATE TABLE `tag` (
`tag` varchar(30) NOT NULL,
`article_id` int(11) NOT NULL,
UNIQUE KEY `tag` (`tag`,`article_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
--
-- Dumping data for table `tag`
--
INSERT INTO `tag` VALUES ('Samsung', 1);
INSERT INTO `tag` VALUES ('Acer', 2);
INSERT INTO `tag` VALUES ('Sony', 3);
INSERT INTO `tag` VALUES ('Apple', 4);
INSERT INTO `tag` VALUES ('Acer', 5);
INSERT INTO `tag` VALUES ('HP', 6);
INSERT INTO `tag` VALUES ('Acer', 7);
INSERT INTO `tag` VALUES ('Sony', 7);
INSERT INTO `tag` VALUES ('Acer', 7);
INSERT INTO `tag` VALUES ('Samsung', 9);

What makes you think the query will examine a large number of rows?
The query will scan exactly 30 records using the UNIQUE index on tag (tag, article_id), join the article to each record on PRIMARY KEY and stop.
This is exactly what your plan says.
I just made this test script:
CREATE TABLE `article` (
`id` int(11) NOT NULL auto_increment,
`title` varchar(60) NOT NULL,
`time_stamp` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1000001 ;
CREATE TABLE `tag` (
`tag` varchar(30) NOT NULL,
`article_id` int(11) NOT NULL,
UNIQUE KEY `tag` (`tag`,`article_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT
INTO article
SELECT id, CONCAT('Article ', id), UNIX_TIMESTAMP('2011-08-17' - INTERVAL id SECOND)
FROM t_source;
INSERT
INTO tag
SELECT CASE fld WHEN 1 THEN CONCAT('tag', (id - 1) div 10 + 1) ELSE tag END AS tag, id
FROM (
SELECT tag,
id,
FIELD(tag, 'Other', 'Acer', 'Sony', 'HP', 'Dell') AS fld,
RAND(20110817) AS rnd
FROM (
SELECT 'Other' AS tag
UNION ALL
SELECT 'Acer' AS tag
UNION ALL
SELECT 'Sony' AS tag
UNION ALL
SELECT 'HP' AS tag
UNION ALL
SELECT 'Dell' AS tag
) t
JOIN t_source
) q
WHERE POWER(3, -fld) > rnd;
, where t_source is a table with 1M records in it, and run your query:
SELECT *
FROM tag t
JOIN article a
ON a.id = t.article_id
WHERE t.tag = 'acer'
ORDER BY
t.article_id DESC
LIMIT 30;
It was instant.

try ANSI join syntax:
SELECT title
FROM tag t
INNER JOIN article a
ON t.article_id = a.id
WHERE
t.tag = 'acer'
ORDER BY
tag.article_id DESC
LIMIT 0 , 30
then put an index on tag.tag. Assuming you have enough selectivity on that table, and article.id is a primary key, that should be pretty zippy.

I would suggest modifying the storage engine and schema to utilize foreign keys.
CREATE TABLE `article` (
`id` int(11) NOT NULL auto_increment,
`title` varchar(60) NOT NULL,
`time_stamp` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1000001 ;
CREATE TABLE `tag` (
`id` int(11) NOT NULL auto_increment,
`tag` varchar(30) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `article_tag` (
`id` int(11) NOT NULL auto_increment,
`article_id` int(11) NOT NULL,
`tag_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`article_id`) REFERENCES article(id),
FOREIGN KEY (`tag_id`) REFERENCES tag(id)
) ENGINE=Innodb;
Which results in a query like so:
EXPLAIN
SELECT * FROM article
JOIN article_tag ON article.id = article_tag.id
JOIN tag ON article_tag.tag_id = tag.id
WHERE tag.tag="Acer";
+----+-------------+-------------+--------+----------------+---------+---------+-------------------------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+--------+----------------+---------+---------+-------------------------+------+-------------+
| 1 | SIMPLE | article_tag | ALL | PRIMARY,tag_id | NULL | NULL | NULL | 1 | |
| 1 | SIMPLE | tag | eq_ref | PRIMARY | PRIMARY | 4 | temp.article_tag.tag_id | 1 | Using where |
| 1 | SIMPLE | article | eq_ref | PRIMARY | PRIMARY | 4 | temp.article_tag.id | 1 | |
+----+-------------+-------------+--------+----------------+---------+---------+-------------------------+------+-------------+
3 rows in set (0.00 sec)

Edit: Add this index
UNIQUE KEY tag (article_id,tag)

Related

Query Data from multiple sql tables

I have an ERD above. I want to get the price of the room based on channel and also the status (isInvisible) of the hotel that owns the room mentioned.
Also a RESTful-API endpoint for that, I tried many times on this assignment and can't get it right as I use Nodejs to write an API GET /api/${roomId}/price but the query doesn't work:
SELECT RoomPrice.price, Status.isInvisible
FROM RoomPrice
INNER JOIN Status
ON (RoomPrice.RoomID = Room.Id AND RoomPrice.ChannelID = ChannelId)
AND (Status.HotelID = Hotels.ID AND Status.ChannelID = ChannelID)
I use query below to create my database in WorkBench:
DROP TABLE IF EXISTS `address`;
CREATE TABLE `address` (
`id` int(9) unsigned NOT NULL AUTO_INCREMENT,
`hotel_id` int(9) unsigned NOT NULL,
`address` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY(hotel_id) REFERENCES hotels(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `address` (`hotel_id`, `address`)
VALUES
(1, '7008 Lynch Centers Apt. 596\nLysannemouth, RI 43355'),
(2, '04795 Stanley Mount Apt. 114\nDorrisborough, DC 38070-3542'),
(1, '24586 Eliseo Haven Suite 045\nKossville, WY 17890-7936'),
(2, '639 Toy Corners\nBashirianfort, CA 08964-7258');
DROP TABLE IF EXISTS `channels`;
CREATE TABLE `channels` (
`id` int(9) unsigned NOT NULL AUTO_INCREMENT,
`url` varchar(255) NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT INTO `channels` (`id`, `url`, `name`)
VALUES
(1, 'http://www.beahan.com/', 'quod'),
(2, 'http://www.douglas.com/', 'sit');
DROP TABLE IF EXISTS `hotels`;
CREATE TABLE `hotels` (
`id` int(9) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT INTO `hotels` (`id`, `name`)
VALUES
(1, 'illum'),
(2, 'aliquid');
DROP TABLE IF EXISTS `rooms`;
CREATE TABLE `rooms` (
`id` int(9) unsigned NOT NULL AUTO_INCREMENT,
`hotel_id` int(9) unsigned NOT NULL,
`name` varchar(100) NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY(hotel_id) REFERENCES hotels(id)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
INSERT INTO `rooms` (`id`, `hotel_id`, `name`)
VALUES
(1, 1, 'vel'),
(2, 2, 'fugit'),
(3, 1, 'doloribus'),
(4, 2, 'ut'),
(5, 1, 'et');
DROP TABLE IF EXISTS `room_prices`;
CREATE TABLE `room_prices` (
`room_id` int(9) unsigned NOT NULL,
`channel_id` int(9) unsigned NOT NULL,
`price` decimal(10,2) NOT NULL,
PRIMARY KEY (room_id, channel_id),
FOREIGN KEY(room_id) REFERENCES rooms(id),
FOREIGN KEY(channel_id) REFERENCES channels(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `room_prices` (`room_id`, `channel_id`, `price`)
VALUES
(1, 1, '50687.86'),
(1, 2, '6687.86'),
(2, 1, '10687.86'),
(2, 2, '274739.20'),
(3, 1, '3828.63'),
(3, 2, '12525.86'),
(4, 1, '2623587.86'),
(4, 2, '125151.00'),
(5, 1, '2358704.85'),
(5, 2, '7347473.86');
DROP TABLE IF EXISTS `status`;
CREATE TABLE `status` (
`hotel_id` int(9) unsigned NOT NULL,
`channel_id` int(9) unsigned NOT NULL,
`isInvisible` BOOLEAN NOT NULL,
PRIMARY KEY (hotel_id, channel_id),
FOREIGN KEY(hotel_id) REFERENCES hotels(id),
FOREIGN KEY(channel_id) REFERENCES channels(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `status` (`hotel_id`, `channel_id`, `isInvisible`)
VALUES
(1, 1, false),
(2, 1, true),
(1, 2, true),
(2, 2, true);
I want that once I query to search for the price of roomId (i.e 1) it returns 2 rows (as the mock data only have 2 rows in channels table) that show:
room_id channel_id price isInvisible
1 1 xxxx.xx 0
1 2 xxxx.xx 1
At this moment, I use the query as DRapp help
SELECT
rp.room_id,
rp.channel_id,
rp.price,
s.isInvisible
FROM
room_prices rp
JOIN status s
ON (rp.channel_id = s.channel_id)
JOIN rooms r
ON (rp.room_id = r.id)
JOIN hotels h
ON (r.hotel_id = h.id)
WHERE rp.room_id = 1
It returns 4 rows (instead of 2 rows as expected)
room_id channel_id price isInvisible
1 1 xxxx.xx 0
1 2 xxxx.xx 1
1 1 xxxx.xx 1
1 2 xxxx.xx 1
You need to identify each of the relations as your ERD shows. Each table is joined to its respective context. Dont jump/skip. The only time you can in this scenario is to skip through the channels since the RoomPrice and Status table EACH have a "ChannelId" to qualify the join
SELECT
rp.price,
s.isInvisible
FROM
Room_Prices rp
JOIN Rooms r
on rp.room_id = r.id
JOIN Status s
ON rp.Channel_ID = s.Channel_Id
AND r.hotel_id = s.hotel_id
WHERE
rp.room_id = 1
I had to revise the query. I looked deeper and noticed your CHANNEL table ALSO had the Hotel, so I had to go from the room prices to the room table. From the room table, I can get the hotel. Now that I have the channel from room prices, AND the hotel ID from the rooms table. So NOW I join to the status table on BOTH columns getting the expected single row per room you are expecting.

How to count correctly 2 inner levels count?

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 |
+----+------+----------------+-------------------+

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 to join Between four tables on mysql

i have 4 tables for mysql database, how i join Between them
users:
userid|username
1 | mark
2 | jon
awards_user :
awardid|userid
1 |1
2 |2
cat :
catid|catname
1 | english
2 | computer
awards :
awardid|catid|awardname|awardlink
1 |1 |best1 |pic link
2 |2 |best2 |pic link
resulte :
userid|username|catid|catname|awardid|awardname|awardlink
okay , that is my try :
works but Shows only one result, when there is a member holds a award
، I want to show all the awards, even if there is no one holds them .
$all_awards = $db->query_read(" SELECT * FROM " . TABLE_PREFIX . " users,awards_user,cat,awards
WHERE
awards_user.awardid = awards.awardid
AND
awards_user.userid = users.userid
AND
awards.catid = cat.catid
");
CREATE TABLE `awards` (
`id` int(10) unsigned NOT NULL auto_increment,
`forumid` int(10) unsigned NOT NULL,
`name` varchar(100) NOT NULL default '',
`link` varchar(100) NOT NULL default '',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ;
--
-- Dumping data for table `awards`
--
INSERT INTO `awards` VALUES (1, 2, 'award one', 'http://www.mwadah.com/pict/noway9.gif');
INSERT INTO `awards` VALUES (2, 1, 'award 2', 'http://www.forum-ksa.com/up/uploads/images/forum-ksac59a7122d1.gif');
-- --------------------------------------------------------
--
-- Table structure for table `awards_user`
--
CREATE TABLE `awards_user` (
`id` int(10) unsigned NOT NULL auto_increment,
`awardid` int(10) unsigned NOT NULL,
`userid` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=3 ;
--
-- Dumping data for table `awards_user`
--
INSERT INTO `awards_user` VALUES (2, 2, 2);
-- --------------------------------------------------------
--
-- Table structure for table `forum`
--
CREATE TABLE `forum` (
`forumid` smallint(5) unsigned NOT NULL auto_increment,
`title` varchar(100) NOT NULL default '',
PRIMARY KEY (`forumid`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=2;
--
-- Dumping data for table `forum`
--
INSERT INTO `forum` VALUES (1,'Main Category');
INSERT INTO `forum` VALUES (2,'Main Forum');
-- --------------------------------------------------------
--
-- Table structure for table `user`
--
CREATE TABLE `user` (
`userid` int(10) unsigned NOT NULL auto_increment,
`username` varchar(100) NOT NULL default ''
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=3 ;
--
-- Dumping data for table `user`
--
INSERT INTO `user` VALUES (1,'admin');
INSERT INTO `user` VALUES (1,'mark');
You could structure your JOIN query like this:
SELECT *
FROM users
JOIN awards_user USING(userid)
JOIN awards USING(awardid)
JOIN cat USING(catid)
Then you can add a WHERE clause to filter the result.
SELECT awards.userid,
username,
awards.catid,
catname,
awards.awardid,
awardname,
awardlink
FROM users,
awards_user,
cat,
awards
WHERE awards_user.userid = users.userid
AND awards_user.awardid = awards.awardid
AND cat.catid = awards.catid;

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;