Search code examples
mysqlsubqueryviewdatabase-performance

optimize schema of views joining multiple row count subqueries


I have created a table votes with columns storeid, itemid, userid, and vote which is an INT ranging from 1 to 5.

I want to have three views of this table:

One, store_ratings, with a column storeid and 5 columns onestar, ... fivestars, counting the number of 1s for each store on column onestar, 2s for twostars... and so on.

Another one just the same, but with product_ratings and itemid instead.

A third one, same as the other two, but with user_ratings and userid instead.

As long as I end up with these three views and I do just one insert operation for each vote I can use any schema.

The schema I am using works, but I am worried about its performance. Is there a better way to do this? This is how I am implementing it right now (check out this fiddle to see how it works with some test data):

CREATE TABLE `votes` (
`storeid` INT NOT NULL, 
`itemid` INT NOT NULL, 
`userid` INT NOT NULL, 
`vote` INT NOT NULL, 
PRIMARY KEY (`storeid`, `itemid`, `userid`), 
KEY `storeid` (`storeid`), 
KEY `itemid` (`itemid`), 
KEY `userid` (`userid`), 
KEY `vote` (`vote`)
);

CREATE OR REPLACE VIEW votes_onestar AS
SELECT itemid, storeid, userid, COUNT(*) AS onestar FROM votes WHERE vote = 1 
GROUP BY itemid, storeid, userid;

CREATE OR REPLACE VIEW votes_twostars AS
SELECT itemid, storeid, userid, COUNT(*) AS twostars FROM votes WHERE vote = 2 
GROUP BY itemid, storeid, userid;

CREATE OR REPLACE VIEW votes_threestars AS
SELECT itemid, storeid, userid, COUNT(*) AS threestars FROM votes WHERE vote = 3 
GROUP BY itemid, storeid, userid;

CREATE OR REPLACE VIEW votes_fourstars AS
SELECT itemid, storeid, userid, COUNT(*) AS fourstars FROM votes WHERE vote = 4 
GROUP BY itemid, storeid, userid;

CREATE OR REPLACE VIEW votes_fivestars AS
SELECT itemid, storeid, userid, COUNT(*) AS fivestars FROM votes WHERE vote = 5 
GROUP BY itemid, storeid, userid;

CREATE OR REPLACE VIEW votes_matrix AS
SELECT 
  i1.itemid, i1.storeid, i1.userid, 
  IFNULL (s1.onestar, 0) onestar, 
  IFNULL (s2.twostars, 0) twostars, 
  IFNULL (s3.threestars, 0) threestars, 
  IFNULL (s4.fourstars, 0) fourstars, 
  IFNULL (s5.fivestars, 0) fivestars 
FROM votes i1
LEFT JOIN votes_onestar s1
ON i1.itemid = s1.itemid AND i1.storeid = s1.storeid AND i1.userid = s1.userid
LEFT JOIN votes_twostars s2
ON i1.itemid = s2.itemid AND i1.storeid = s2.storeid AND i1.userid = s2.userid
LEFT JOIN votes_threestars s3
ON i1.itemid = s3.itemid AND i1.storeid = s3.storeid AND i1.userid = s3.userid
LEFT JOIN votes_fourstars s4
ON i1.itemid = s4.itemid AND i1.storeid = s4.storeid AND i1.userid = s4.userid
LEFT JOIN votes_fivestars s5
ON i1.itemid = s5.itemid AND i1.storeid = s5.storeid AND i1.userid = s5.userid;

CREATE OR REPLACE VIEW store_ratings AS
SELECT
  storeid, 
  SUM(onestar) AS onestar, 
  SUM(twostars) AS twostars,
  SUM(threestars) AS threestars, 
  SUM(fourstars) AS fourstars, 
  SUM(fivestars) AS fivestars
FROM votes_matrix
GROUP BY storeid;

CREATE OR REPLACE VIEW user_ratings AS
SELECT
  userid, 
  SUM(onestar) AS onestar, 
  SUM(twostars) AS twostars,
  SUM(threestars) AS threestars, 
  SUM(fourstars) AS fourstars, 
  SUM(fivestars) AS fivestars
FROM votes_matrix
GROUP BY userid;

CREATE OR REPLACE VIEW product_ratings AS
SELECT
  itemid, 
  SUM(onestar) AS onestar, 
  SUM(twostars) AS twostars,
  SUM(threestars) AS threestars, 
  SUM(fourstars) AS fourstars, 
  SUM(fivestars) AS fivestars
FROM votes_matrix
GROUP BY itemid;

The obvious alternative is to store the votes_matrix view as a table and build the votes table as a view from it instead of the opposite (like this). But having every vote take 5 columns where 1 is enough seems like a waste of space and not very practical to update/insert. Plus it also seems less flexible: what if I decide to switch to 0-5 instead of 1-5; or, even worse, 0-100? It seems easier to adapt the 5 views on the first model than the table on the second one. Anyway, I'd appreciate any thoughts or experiences from anybody who has faced this problem before...


Solution

  • Ended up with a much simpler version of the first model, avoiding all the nested views and subqueries, using IF (the equivalent to Oracle's DECODE, although a more standard CASE would also be fine). I think this will do:

    CREATE TABLE `votes` (
    `storeid` INT NOT NULL, 
    `itemid` INT NOT NULL, 
    `userid` INT NOT NULL, 
    `vote` INT NOT NULL, 
    PRIMARY KEY (`storeid`, `itemid`, `userid`), 
    KEY `storeid` (`storeid`), 
    KEY `itemid` (`itemid`), 
    KEY `userid` (`userid`), 
    KEY `vote` (`vote`)
    );
    
    
    CREATE OR REPLACE VIEW store_ratings AS
    SELECT
      storeid, 
      SUM(IF (vote=1,1,0)) AS onestar, 
      SUM(IF (vote=2,1,0)) AS twostars,
      SUM(IF (vote=3,1,0)) AS threestars, 
      SUM(IF (vote=4,1,0)) AS fourstars, 
      SUM(IF (vote=5,1,0)) AS fivestars
    FROM votes
    GROUP BY storeid;
    
    CREATE OR REPLACE VIEW user_ratings AS
    SELECT
      userid, 
      SUM(IF (vote=1,1,0)) AS onestar, 
      SUM(IF (vote=2,1,0)) AS twostars,
      SUM(IF (vote=3,1,0)) AS threestars, 
      SUM(IF (vote=4,1,0)) AS fourstars, 
      SUM(IF (vote=5,1,0)) AS fivestars
    FROM votes
    GROUP BY userid;
    
    CREATE OR REPLACE VIEW product_ratings AS
    SELECT
      itemid, 
      SUM(IF (vote=1,1,0)) AS onestar, 
      SUM(IF (vote=2,1,0)) AS twostars,
      SUM(IF (vote=3,1,0)) AS threestars, 
      SUM(IF (vote=4,1,0)) AS fourstars, 
      SUM(IF (vote=5,1,0)) AS fivestars
    FROM votes
    GROUP BY itemid;
    

    fiddle: http://www.sqlfiddle.com/#!9/3063b/1