Doing this with the correlated subquery is indeed inefficient, because MySQL has to run the subquery for every row of the outer query, just to decide if the row in the outer query meets the conditions. That's a lot of overhead.
Here's a method using no subquery:
SELECT t1.*
FROM tablename t1
JOIN tablename t2 ON t1.source = t2.source and t1.relationship = t2.relationship
AND t1.weight <= t2.weight
WHERE t1.source = 'cat'
GROUP BY t1.id
HAVING COUNT(*) <= 2;
And here's a method using neither subquery, nor joins/group by:
SELECT *
FROM (
SELECT tablename.*, IF(@r = relationship, @n:=@n+1, @n:=1) AS _n,
@r:=relationship AS _r
FROM (SELECT @r:=null, @n:=1) _init, tablename
WHERE source = 'cat'
ORDER BY relationship, weight DESC
) AS _t
WHERE _n <= 2;
These solutions also need some tiebreaker in case there are multiple rows with the same top weights. But that applies to all the solutions.
The simpler solution, which wouldn't require special gymnastics or tiebreakers, is to use SQL window functions like ROW_NUMBER() OVER (PARTITION BY relationship)
, but MySQL does not support these.