UPDATE Table based on the same table with relations to other tables - sql

I have a 2 tables:
Product (Id, RefKey, ParentId)
example data.: (1, 'SX1234', NULL), (2, 'SX4321', NULL)
and
ProductSTAGE (Id, RefKeyCode, ParentCode)
example data: (1, 'SX1234', 'SX4321')
where Product.RefKey = ProductSTAGE.RefKeyCode
How can I update Product table based on these relations to result
Product (Id, RefKey, ParentId)
result data.: (1, 'SX1234', 2)
I used
WITH CTE
AS (
SELECT P.ParentId FROM Product AS P
)
UPDATE CTE SET ParentId = P2.Id
FROM Product AS P2
INNER JOIN ProductSTAGE AS PS ON PS.RefKeyCode = P2.RefKey
WHERE PS.ParentCode IS NOT NULL
but using this my Product.ParentId always is equal Product.Id

I found my CTE problem but CTE is not necessary on this issue. This query works and should be enough for me:
UPDATE P
SET P.ParentId = P2.Id
FROM Product AS P
INNER JOIN ProductSTAGE AS PS ON P.RefKey = PS.RefKeyCode
INNER JOIN Product AS P2 ON P2.RefKey = PS.ParentCode
WHERE PS.ParentCode IS NOT NULL

Related

Transpose data in SQL Server Select

I am wondering if there is a better way to write this query. It achieves the target result but my colleague would prefer it be written without the subselects into temp tables t1-t3. The main "challenge" here is transposing the data from dbo.ReviewsData into a single row along with the rest of the data joined from dbo.Prodcucts and dbo.Reviews.
CREATE TABLE dbo.Products (
idProduct int identity,
product_title varchar(100)
PRIMARY KEY (idProduct)
);
INSERT INTO dbo.Products VALUES
(1001, 'poptart'),
(1002, 'coat hanger'),
(1003, 'sunglasses');
CREATE TABLE dbo.Reviews (
Rev_IDReview int identity,
Rev_IDProduct int
PRIMARY KEY (Rev_IDReview)
FOREIGN KEY (Rev_IDProduct) REFERENCES dbo.Products(idProduct)
);
INSERT INTO dbo.Reviews VALUES
(456, 1001),
(457, 1002),
(458, 1003);
CREATE TABLE dbo.ReviewFields (
RF_IDField int identity,
RF_FieldName varchar(32),
PRIMARY KEY (RF_IDField)
);
INSERT INTO dbo.ReviewFields VALUES
(1, 'Customer Name'),
(2, 'Review Title'),
(3, 'Review Message');
CREATE TABLE dbo.ReviewData (
RD_idData int identity,
RD_IDReview int,
RD_IDField int,
RD_FieldContent varchar(100)
PRIMARY KEY (RD_idData)
FOREIGN KEY (RD_IDReview) REFERENCES dbo.Reviews(Rev_IDReview)
);
INSERT INTO dbo.ReviewData VALUES
(79, 456, 1, 'Daniel'),
(80, 456, 2, 'Love this item!'),
(81, 456, 3, 'Works well...blah blah'),
(82, 457, 1, 'Joe!'),
(84, 457, 2, 'Pure Trash'),
(85, 457, 3, 'It was literally a used banana peel'),
(86, 458, 1, 'Karen'),
(87, 458, 2, 'Could be better'),
(88, 458, 3, 'I can always find something wrong');
SELECT P.product_title as "item", t1.ReviewedBy, t2.ReviewTitle, t3.ReviewContent
FROM dbo.Reviews R
INNER JOIN dbo.Products P
ON P.idProduct = R.Rev_IDProduct
INNER JOIN (
SELECT D.RD_FieldContent AS "ReviewedBy", D.RD_IDReview
FROM dbo.ReviewsData D
WHERE D.RD_IDField = 1
) t1
ON t1.RD_IDReview = R.Rev_IDReview
INNER JOIN (
SELECT D.RD_FieldContent AS "ReviewTitle", D.RD_IDReview
FROM dbo.ReviewsData D
WHERE D.RD_IDField = 2
) t2
ON t2.RD_IDReview = R.Rev_IDReview
INNER JOIN (
SELECT D.RD_FieldContent AS "ReviewContent", D.RD_IDReview
FROM dbo.ReviewsData D
WHERE D.RD_IDField = 3
) t3
ON t3.RD_IDReview = R.Rev_IDReview
EDIT: I have updated this post with the create statements for the tables as opposed to an image of the data (shame on me) and a more specific description of what exactly needed to be improved. Thanks to all for the comments and patience.
As others have said in comments, there is nothing objectively wrong with the query. However, you could argue that it's verbose and hard to read.
One way to shorten it is to replace INNER JOIN with CROSS APPLY:
INNER JOIN (
SELECT D.RD_FieldContent AS 'ReviewedBy', D.RD_IDReview
FROM dbo.ReviewsData D
WHERE D.RD_IDField = 1
) t1
ON t1.RD_IDReview = R.Rev_IDReview
APPLY lets you refer to values from the outer query, like in a subquery:
CROSS APPLY (
SELECT D.RD_FieldContent AS 'ReviewedBy'
FROM dbo.ReviewsData D
WHERE D.RD_IDField = 1 AND D.RD_IDReview = R.Rev_IDReview
) t1
I think of APPLY like a subquery that brings in new columns. It's like a cross between a subquery and a join. Benefits:
The query can be shorter, because you don't have to repeat the ID column twice.
You don't have to expose columns that you don't need.
Disadvantages:
If the query in the APPLY references outer values, then you can't extract it and run it all by itself without modifications.
APPLY is specific to Sql Server and it's not that widely-used.
Another thing to consider is using subqueries instead of joins for values that you only need in one place. Benefits:
The queries can be made shorter, because you don't have to repeat the ID column twice, and you don't have to give the output columns unique aliases.
You only have to look in one place to see the whole subquery.
Subqueries can only return 1 row, so you can't accidentally create extra rows, if only 1 row is desired.
SELECT
P.product_title as 'item',
(SELECT D.RD_FieldContent
FROM dbo.ReviewsData D
WHERE D.RD_IDField = 1 AND
D.RD_IDReview = R.Rev_IDReview) as ReviewedBy,
(SELECT D.RD_FieldContent
FROM dbo.ReviewsData D
WHERE D.RD_IDField = 2 AND
D.RD_IDReview = R.Rev_IDReview) as ReviewTitle,
(SELECT D.RD_FieldContent
FROM dbo.ReviewsData D
WHERE D.RD_IDField = 3 AND
D.RD_IDReview = R.Rev_IDReview) as ReviewContent
FROM dbo.Reviews R
INNER JOIN dbo.Products P ON P.idProduct = R.Rev_IDProduct
Edit:
It just occurred to me that you have made the joins themselves unnecessarily verbose (#Dale K actually already pointed this out in the comments):
INNER JOIN (
SELECT D.RD_FieldContent AS 'ReviewedBy', D.RD_IDReview
FROM dbo.ReviewsData D
WHERE D.RD_IDField = 1
) t1
ON t1.RD_IDReview = R.Rev_IDReview
Shorter:
SELECT RevBy.RD_FieldContent AS 'ReviewedBy'
...
INNER JOIN dbo.ReviewsData RevBy
ON RevBy.RD_IDReview = R.Rev_IDReview AND
RevBy.RD_IDField = 1
The originally submitted query is undoubtedly and unnecessarily verbose. Having digested various feedback from the community it has been revised to the following, working splendidly. In retrospect I feel very silly for having done this with subselects originally. I am clearly intermediate at best when it comes to SQL - I had not realized an "AND" clause could be included in the "ON" clause in a "JOIN" statement. Not sure why I would have made such a poor assumption.
SELECT
P.product_title as 'item',
D1.RD_FieldContent as 'ReviewedBy',
D2.RD_FieldContent as 'ReviewTitle',
D3.RD_FieldContent as 'ReviewContent'
FROM dbo.Reviews R
INNER JOIN dbo.Products P
ON P.idProduct = R.Rev_IDProduct
INNER JOIN dbo.ReviewsData D1
ON D1.RD_IDReview = R.Rev_IDReview AND D1.RD_IDField = 1
INNER JOIN dbo.ReviewsData D2
ON D2.RD_IDReview = R.Rev_IDReview AND D2.RD_IDField = 2
INNER JOIN dbo.ReviewsData D3
ON D3.RD_IDReview = R.Rev_IDReview AND D3.RD_IDField = 3

Dynamically update table with column from another table

I have a table customer like this:
CREATE TABLE tbl_customer (
id INTEGER,
name VARCHAR(16),
voucher VARCHAR(16)
);
and a voucher table like this:
CREATE TABLE tbl_voucher (
id INTEGER,
code VARCHAR(16)
);
Now imagine that the customer table always has rows with id and name filled in, however the voucher needs to be inserted periodically from the tbl_voucher table.
Important: every voucher may only be assigned to one specific customer (i.e. must be unique)
I wrote a query like this:
UPDATE tbl_customer
SET voucher = (
SELECT code
FROM tbl_voucher
WHERE code NOT IN (
SELECT voucher
FROM tbl_customer
WHERE voucher IS NOT NULL
)
LIMIT 1
)
WHERE voucher IS NULL;
However this is not working as expected, since the part that looks for an unused voucher is executed once and said voucher is then applied to every customer.
Any ideas on how I can solve this without using programming structures such as loops?
Also, some example data so you can imagine what I would like to happen:
INSERT INTO tbl_customer VALUES (1, 'Sara', 'ABC');
INSERT INTO tbl_customer VALUES (1, 'Simon', 'DEF');
INSERT INTO tbl_customer VALUES (1, 'Andy', NULL);
INSERT INTO tbl_customer VALUES (1, 'Alice', NULL);
INSERT INTO tbl_voucher VALUES (1, 'ABC');
INSERT INTO tbl_voucher VALUES (2, 'LOL');
INSERT INTO tbl_voucher VALUES (3, 'ZZZ');
INSERT INTO tbl_voucher VALUES (4, 'BBB');
INSERT INTO tbl_voucher VALUES (5, 'CCC');
After the wanted query is executed, I'd expect Andy to have the voucher LOL and Alice should get ZZZ
I am going to guess this is MySQL. The answer is that this is a pain. The following assigns the values in a select:
select c.*, v.voucher
from (select c.*, (#rnc := #rnc + 1) as rn
from tbl_customer c cross join
(select #rnc := 0) params
where c.voucher is null
) c join
(select v.*, (#rnv := #rnv + 1) as rn
from tbl_vouchers v cross join
(select #rnv := 0) params
where not exists (select 1 from tbl_customers c where c.voucher = v.voucher)
) v
on c.rn = v.rn;
You can now use this for the update:
update tbl_customer c join
(select c.*, v.voucher
from (select c.*, (#rnc := #rnc + 1) as rn
from tbl_customer c cross join
(select #rnc := 0) params
where c.voucher is null
) c join
(select v.*, (#rnv := #rnv + 1) as rn
from tbl_vouchers v cross join
(select #rnv := 0) params
where not exists (select 1 from tbl_customers c where c.voucher = v.voucher)
) v
on c.rn = v.rn
) cv
on c.id = cv.id
set c.voucher = cv.voucher;

Recursively sum the nodes of a tree using Postgresql WITH clause

(Using Postgresql 9.1)
I have a tree structure in the database and I need to sum the node's values. There are two caveats:
Not all nodes have a value.
If a parent node has a value, ignore the child values.
While recursing the tree is easy with the powerful recursive WITH clause, it's enforcing these two caveats that is breaking my code. Here's my setup:
CREATE TABLE node (
id VARCHAR(1) PRIMARY KEY
);
INSERT INTO node VALUES ('A');
INSERT INTO node VALUES ('B');
INSERT INTO node VALUES ('C');
INSERT INTO node VALUES ('D');
INSERT INTO node VALUES ('E');
INSERT INTO node VALUES ('F');
INSERT INTO node VALUES ('G');
CREATE TABLE node_value (
id VARCHAR(1) PRIMARY KEY,
value INTEGER
);
INSERT INTO node_value VALUES ('B', 5);
INSERT INTO node_value VALUES ('D', 2);
INSERT INTO node_value VALUES ('E', 0);
INSERT INTO node_value VALUES ('F', 3);
INSERT INTO node_value VALUES ('G', 4);
CREATE TABLE tree (
parent VARCHAR(1),
child VARCHAR(1)
);
INSERT INTO tree VALUES ('A', 'B');
INSERT INTO tree VALUES ('B', 'D');
INSERT INTO tree VALUES ('B', 'E');
INSERT INTO tree VALUES ('A', 'C');
INSERT INTO tree VALUES ('C', 'F');
INSERT INTO tree VALUES ('C', 'G');
This gives me the following tree (nodes and values):
A
|--B(5)
| |--D(2)
| |--E(0)
|
|--C
|--F(3)
|--G(4)
Given the rules above, here are the expected sum values:
A = (5 + 3 + 4) = 12
B = 5
D = 2
E = 0
C = (3 + 4) = 7
F = 3
G = 4
I have written the following SQL, but I can't integrate the recursive UNION and JOIN logic to enforce rule #1 and #2:
WITH recursive treeSum(root, parent, child, total_value) AS (
SELECT tree.parent root, tree.parent, tree.child, node_value.value total_value
FROM tree
LEFT JOIN node_value ON node_value.id = tree.parent
UNION
SELECT treeSum.root, tree.parent, tree.child, node_value.value total_value
FROM tree
INNER JOIN treeSum ON treeSum.child = tree.parent
LEFT JOIN node_value ON node_value.id = tree.parent
)
SELECT root, sum(total_value) FROM treeSum WHERE root = 'A' GROUP BY root
The query returns 10 for root A, but it should be 12. I know the UNION and/or JOIN logic is what's throwing this off. Any help would be appreciated.
EDIT: To clarify, the sum for A is 12, not 14. Given the rules, if a node has a value, grab that value and ignore its children. Because B has a value of 5 we ignore D and E. C has no value, so we grab its children, thus the sum of A = 5(B) + 3(F) + 4(G) = 12. I know it's odd but that's the requirement. Thanks.
EDIT 2: These results will be joined with external datasets so I can't hardcode the root in the WITH clause. For example, I might need something like this:
SELECT root, SUM(total_value) FROM treeSUM GROUP BY root WHERE root = 'A'
This tree is one of many so that means there's multiple roots, specified by calling code--not within the recursive clause itself. Thanks.
EDIT 3: An example of how this will be used in production is the roots will be specified by another table, so I can't hardcode the root into the recursive clause. There might be many roots from many trees.
SELECT id, SUM(COALESCE(value,0)) FROM treeSUM
INNER JOIN roots_to_select rts ON rts.id = treeSUM.id GROUP BY id
SOLUTION (Cleaned up from koriander's answer below)! The following allows roots to be specified by outside sources (either using roots_to_select or WHERE criteria:
WITH recursive roots_to_select AS (
SELECT 'A'::varchar as id
),
treeSum(root, id, value) AS (
select node.id as root, node.id, node_value.value
from node
inner join roots_to_select rts on (node.id = rts.id)
left join node_value on (node.id = node_value.id)
union
select treeSum.root, node.id, node_value.value
from treeSum
inner join tree on (treeSum.id = tree.parent)
inner join node on (tree.child = node.id)
left join node_value on (node.id = node_value.id)
where treeSum.value is null
)
select root, sum(coalesce(value, 0))
from treeSum
group by root
OUTPUT: 12
tested here:
with recursive treeSum(id, value) AS (
select node.id, node_value.value
from node
left join node_value on (node.id = node_value.id)
where node.id = 'A'
union
select node.id, node_value.value
from treeSum
inner join tree on (treeSum.id = tree.parent)
inner join node on (tree.child = node.id)
left join node_value on (node.id = node_value.id)
where treeSum.value is null
)
select sum(coalesce(value, 0)) from treeSum
Edit 1: to combine the result with other table, you can do:
select id, (select sum(coalesce(value, 0)) from treeSum) as nodesum
from node
inner join some_table on (...)
where node.id = 'A'
Edit 2: to support multiple roots based on your Edit 3, you can do (untested):
with recursive treeSum(root, id, value) AS (
select node.id as root, node.id, node_value.value
from node
inner join roots_to_select rts on (node.id = rts.id)
left join node_value on (node.id = node_value.id)
union
select treeSum.root, node.id, node_value.value
from treeSum
inner join tree on (treeSum.id = tree.parent)
inner join node on (tree.child = node.id)
left join node_value on (node.id = node_value.id)
where treeSum.value is null
)
select root, sum(coalesce(value, 0))
from treeSum
group by root

SQL: many-to-many relationship, IN condition

I have a table called transactions with a many-to-many relationship to items through the items_transactions table.
I want to do something like this:
SELECT "transactions".*
FROM "transactions"
INNER JOIN "items_transactions"
ON "items_transactions".transaction_id = "transactions".id
INNER JOIN "items"
ON "items".id = "items_transactions".item_id
WHERE (items.id IN (<list of items>))
But this gives me all transactions that have one or more of the items in the list associated with it and I only want it to give me the transactions that are associated with all of those items.
Any help would be appreciated.
You have to expand out your query for all of the items in the list:
SELECT "transactions".*
FROM "transactions"
WHERE EXISTS (SELECT 1 FROM "items_transactions"
INNER JOIN "items" ON "items".id = "items_transactions".item_id
WHERE "items_transactions".transaction_id = "transactions".id
AND "items".id = <first item in list>)
AND EXISTS (SELECT 1 FROM "items_transactions"
INNER JOIN "items" ON "items".id = "items_transactions".item_id
WHERE "items_transactions".transaction_id = "transactions".id
AND "items".id = <second item in list>)
...
You might also be able to massage it out using IN and COUNT DISTINCT, I'm not sure which would be faster. Something like (completely untested):
SELECT "transactions".*
FROM "transactions"
INNER JOIN (SELECT "items_transactions".transaction_id
FROM "items_transactions"
INNER JOIN "items" ON "items".id = "items_transactions".item_id
WHERE "items".id IN (<list of items>)
GROUP BY "items_transactions".transaction_id
HAVING COUNT(DISTINCT "items".id) = <count of items in list>) matches ON transactions.transaction_id = matches.transaction_id
I think this does what you want.
I would put the list of items you need in to a table (temp one will be fine) and join on to that. Then count the number of distinct items and match the count to the item transactions count.
I've provided the sample DDL & Data that I used.
Create table #trans
(
transId int identity(1,1),
trans varchar(10)
)
Create Table #itemTrans
(
transId int,
itemId int
)
Create table #items
(
itemId int identity(1,1),
item varchar(10)
)
Create table #itemsToSelect
(
itemId int
)
Insert Into #trans
Values ('Trans 1')
Insert Into #trans
Values ('Trans 2')
Insert Into #trans
Values ('Trans 3')
Insert Into #Items
Values ('Item 1')
Insert Into #Items
Values ('Item 2')
Insert Into #Items
Values ('Item 3')
Insert Into #Items
Values ('Item 4')
Insert Into #itemTrans
Values (1, 1)
Insert Into #itemTrans
Values (1, 2)
Insert Into #itemTrans
Values (1, 3)
Insert Into #itemTrans
Values (2, 1)
Insert Into #itemTrans
Values (2, 3)
Insert Into #itemTrans
Values (3, 4)
Insert Into #itemsToSelect
Values (1)
Insert Into #itemsToSelect
Values (2)
Insert Into #itemsToSelect
Values (3)
Select t.transId
From #items i
Join #itemTrans it on i.itemId = it.itemId
Join #trans t on it.transId = t.transId
Join #itemsToSelect its on it.ItemId = its.ItemId
Where it.TransId is not null
Group by t.transId
Having count(distinct(it.itemId)) = (Select count(distinct(itemId)) from #itemsToSelect)
SELECT transactions.*
WHERE (SELECT count(*)
FROM items_transactions
WHERE items_transactions.transaction_id = transactions.transaction_id
AND items_transactions.item_id IN (<list of items>)
) = <number of items>
Although this will probably do a scan of transactions, nesting the correlated subquery for each one... not particularly efficient, so maybe:
SELECT transactions.*
WHERE EXISTS (SELECT 1 FROM items_transactions
WHERE items_transactions.transaction_id = transactions.transaction_id
AND items_transactions.item_id IN (<list of items>)
)
AND
(SELECT count(*)
FROM items_transactions
WHERE items_transactions.transaction_id = transactions.transaction_id
AND items_transactions.item_id IN (<list of items>)
) = <number of items>
or something similar to persuade the DB to find transactions related to at least one of the items first, and then check each transaction is linked against all the items later.
As someone's noted, you can also simply generate join clauses for each item instead, which might well be better if the number of items isn't large.
I haven't executed this, but that should get you the result you want:
SELECT t.* FROM items i
INNER JOIN items_transactions it ON i.id = it.item_id
INNER JOIN transactions t ON it.transaction_id = t.id
WHERE i.id IN (1,2,3)
The final bit of the query looks wrong:
WHERE (items.id IN (<list of items>))
the 'in' statement is like a big OR statement rather than an AND statement, so it is expanded by the optimizer as:
WHERE (items.id = 123 OR items.id = 456 OR items.id = 789)
EDIT
I reckon you need to perform a correlated subquery on the items table.

How do I "pivot" the top 5 reviews for a product into columns?

I need to (safely) handle the case where there is < 5 "reviews" for a product (even none). In addition, being able to define the ordering of the "reviews" is important. I would prefer a solution that would work with both SQL Server and PostgreSQL (with minor modification). I would be willing to use PIVOT as a last resort, but from what I have read it won't do what I want it to do.
Table1
id, product
1, 'Product 1'
2, 'Product 2'
Table2
id, review
1, 'review #1a'
1, 'review #1b'
1, 'review #1c'
1, 'review #1d'
1, 'review #1e'
1, 'review #1f'
2, 'review #2a'
2, 'review #2b'
2, 'review #2c'
2, 'review #2d'
2, 'review #2e'
2, 'review #2f'
Result
1, 'Product 1', 'review #1a', 'review #1b', 'review #1c', 'review #1d', 'review #1e'
2, 'Product 2', 'review #2a', 'review #2b', 'review #2c', 'review #2d', 'review #2e'
Try this (tweaked to refer to the appropriate things):
The idea is... use row_number() to get up to 5 reviews out per product. Then pivot the results, and then do a left join to get product details (including those that don't have any reviews).
with top5reviews as
(
select *
from
(
select
productid, review
, row_number() over (partition by productid order by reviewid) as reviewnum
from reviews
) r
where reviewnum <= 5
)
, pivotted as
(
select productid, [1] as review1, [2] as review2, [3] as review3, [4] as review4, [5] as review5
from top5reviews r
pivot
(max(review) for reviewnum in ([1],[2],[3],[4],[5])) p
)
select *
from products p
left join
pivotted r
on p.productid = r.productid
PIVOT in combination with ROW_NUMBER() will do this. Here's an example from SQL Server's Northwind sample database. If you have any questions about how to adapt it, please ask, and post CREATE TABLE and INSERT statements for the tables and sample data.
I've used a CTE because it's convenient, but it could be rewritten as a derived table.
Note that the use of MAX is only to satisfy the PIVOT syntax requirement of pivoting an aggregate. MIN would work as well.
with Ranked(EmployeeID,Freight,rk) as (
select
EmployeeID,
Freight,
row_number() over (
partition by EmployeeID
order by Freight desc, OrderID
)
from Orders
)
select EmployeeID, [1],[2],[3],[4],[5]
from (select * from Ranked where rk <= 5) as T pivot (
max(Freight) for rk in ([1],[2],[3],[4],[5])
) AS P;
Note that as the OP presents it, the id column in table2 is a foreign key to table1.
alter table table2 add column rank int;
-- better reviews have lower ranks
alter table table2 constraint table2idrank unique( id, rank) ;
-- ranks are unique within a product
-- note, ranks must also be consecutive
create view min_rank_review as
select id, min(rank) as rank
from table2
group by id;
create view product_review_pivot as
select a.product, b.review as r1, c.review as r2, d.review as r3, e.review as r4
from
table1 a left outer join table2 b on (a.id = b.id)
join min_rank_review m on (b.id = m.id and b.rank = m.rank)
left outer join table2 c on (c.id = b.id and c.rank + 1 = b.rank)
left outer join table2 d on (d.id = c.id and d.rank + 1 = c.rank)
left outer join table2 e on (e.id = f.id and e.rank + 1 = d.rank);
select * from product_review_pivot;
On edit: I suppose the down-mods are because this didn't work. But replace left outer join min_rank_review m on (b.id = m.id and b.rank = m.rank) with join min_rank_review m on (b.id = m.id and b.rank = m.rank) (and make a few other minor corrections) and it works.
Sorry, my office mate wanted to go home and woudn't give me time to proof-read it. :)
With the following data inserted:
insert into table1 ( product ) values ('a'), ('b');
insert into table2(id, review, rank) values
(1, 'r1', 1 ), (1, 'r2', 2), (1, 'r3', 3 ),
(1, 'r4', 4), (1, 'r5', 5 ), (2, 'rr2', 1);
I get the following ouput (in MySQL 5):
mysql> select * from product_review_pivot;
+---------+------+------+------+------+
| product | r1 | r2 | r3 | r4 |
+---------+------+------+------+------+
| a | r1 | r2 | r3 | r4 |
| b | rr2 | NULL | NULL | NULL |
+---------+------+------+------+------+
(The solution for non-sequential ranks is left as an exercise.)