Reputation: 141827
Each of my user's have a set number of slots for storage. They can delete from any slot and there is a maximum number of slots (undetermined yet but will be somewhere in the range of 30 - 100). So if a user has an item in their first 5 slots, and then they delete from slots 4 and 2. They will have items still in slots 0, 1, and 3
. I want to find their first empty slot, so in this example: slot 2.
I discovered a way to do this, but it seems kind of hackish and could possibly be optimized a lot.
This is how I currently do it:
// Make set of ints from 0 to max size
$slots = 'SELECT 0 as `x`';
for($i = 1; $i < $max; $i++)
$slots .= ' UNION SELECT '.$i.' as `x`';
$q = $db->prepare('
SELECT MIN(`x`)
FROM (
'.$slots.'
) as `slots`
WHERE
`x` NOT IN (
SELECT `slot`
FROM `'.$table.'`
WHERE
`user` = ?
)
');
$q->setFetchMode(PDO::FETCH_NUM);
$q->execute(array($user));
So if MAX = 5 the query would become:
SELECT MIN(`x`)
FROM (
SELECT 0 as `x` UNION
SELECT 1 as `x` UNION
SELECT 2 as `x` UNION
SELECT 3 as `x` UNION
SELECT 4 as `x`
) as `slots`
WHERE
`x` NOT IN (
SELECT `slot`
FROM `my_table`
WHERE
user = 1
)
Basically I am generating a subquery to create te equivalent of PostgreSQL's generate_series(0, MAX - 1) Like I previously said, MAX will be between 30 and 100.
Some other ideas I had to do this are to either:
x
) and a row for each integer between 0 and 99, and use that in place of the subquery, orAre either of those better, or is there another way that is better?
Upvotes: 1
Views: 573
Reputation: 44152
You can find the first empty slot by joining the table to itself, with the condition that the second table instance has slot numbers one higher than the first:
Select a.slot+1
From my_table a
Left Join my_table b
On (a.user = 1 and b.user = 1 and a.slot = b.slot - 1)
Where b.slot is null and a.slot < 100
Order by a.slot
Limit 1
Replace the constants 1 and 100 with the actual user id and maximum slot number, and this will give you the first available slot for that user, or no rows if every slot is full.
Upvotes: 3
Reputation: 35333
UNTESTED: But the concept seems correct...
Select min(a.`rNum`)
FROM (Select `slot`,@rownum:=@rownum+1 as rNum,userID
from `slots`, (SELECT @rownum:=0)
order by `slot`) as A
where A.userID=1 and A.`slot`<> a.rNum
Inner select would return
1 1 1
3 2 1
5 3 1
So outer would return
2
Updated to address rowNum = null taken from: http://blog.gomilko.com/2007/04/28/mysql-rownum-imitation
Upvotes: 0
Reputation: 434685
Don't use an "inline" table like this:
FROM (
SELECT 0 as `x` UNION
SELECT 1 as `x` UNION
SELECT 2 as `x` UNION
SELECT 3 as `x` UNION
SELECT 4 as `x`
) as `slots`
Build a real table called slots
and populate it once with values from 1 to 100 (or whatever). Then you can do things like this:
select min(s.slot)
from slots s
left outer join your_table t on s.slot = t.slot and t.user = 1
where t.slot is null
to get the first open slot in the sub-table of your_table
where user = 1
.
Upvotes: 2
Reputation: 60413
I think maybe you are over thinking this. Why keep track of a sequence just rely on the total number of slots used/unused and do your operations based on the primary key. If youre worried about a count query everytime then store the number used in the user table.
Upvotes: 0