В таблице table
есть столбец column
и другой столбец userId
. Таблица table
может содержать любое количество строк с одинаковым идентификатором пользователя. Однако в коллекции SELECT column, userId FROM table
никогда не должно быть столбцов с повторяющимися (столбцами, userId) строками. Строки будут часто создаваться, считываться, обновляться, удаляться и создаваться. Я хочу, чтобы каждый пользователь имел локальный идентификатор column
, например:
+--------+--------+
| column | userId |
+--------+--------+
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 1 |
| 5 | 1 |
| ... | ... |
| 1 | 2 |
| 2 | 2 |
| 3 | 2 |
| 4 | 2 |
| 5 | 2 |
| ... | ... |
+--------+--------+
И когда строка удалена, я хочу захватить первый доступный идентификатор столбца column
некоторого userId
. Я мог бы:
SELECT AVAILABLE_ID(column)
FROM table WHERE userId = 1 ORDER BY column ASC LIMIT 1
или же
SELECT FIRST_AVAILABLE_ID(column)
FROM table WHERE userId = 1
Итак, если мы увидели это состояние таблицы table
:
+--------+--------+
| column | userId |
+--------+--------+
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 5 | 1 |
+--------+--------+
Я хочу получить:
+--------+
| column |
+--------+
| 4 |
+--------+
И если я вставляю первую строку для некоторого userId, я хочу, чтобы столбец был:
+--------+
| column |
+--------+
| 1 |
+--------+
И если между пробелами нет пробелов, я хочу просто SELECT
следующий доступный column
. Кроме того, таблица table
может быть тяжелой с созданием, обновлением, удалением ops, поэтому я хочу, чтобы любые решения были бы быстрыми с тысячами или миллионами строк. Я думаю, что этот запрос не оптимизирован:
SELECT * FROM (
SELECT t1.column+1 AS Id
FROM table t1
WHERE userId = 1 AND NOT EXISTS(SELECT * FROM table t2 WHERE userId = 1 AND t2.column = t1.column + 1 )
UNION
SELECT 1 AS column
WHERE userId = 1 AND NOT EXISTS (SELECT * FROM table t3 WHERE userId = 1 AND t3.column = 1)) ot
ORDER BY 1 LIMIT 1
Теперь, чтобы объяснить более подробно, зачем мне это нужно: причина чисто косметическая. Я работаю над стратегической игрой, где игроки могут иметь войска. Войска могут иметь два состояния: сгруппированы или не сгруппированы. Если они сгруппированы, несколько строк будут иметь один и тот же group_id. Затем я объединяю их вместе в одну строку и объединяю с другими строками в наборе результатов запроса, некоторые из которых могут быть сгруппированы или нет. Если они сгруппированы, я хочу, чтобы у всех групп игроков группы было уникальное групповое поле по отношению к игрокам других войск. Поэтому я могу показать им, как:
1-я армия,
2-я армия,
3-я армия,
...
100-я армия,
и так далее
Это не критично для функциональности приложения, но я обнаружил, что, имея такую систему нумерации, армии более запоминающимися и легко идентифицируемыми, то, скажем, отображение некоторого "случайного" длинного идентификатора
Базы данных хорошо отслеживают данные, которые присутствуют, но не так хороши при отслеживании данных, которые отсутствуют.
Вы можете найти такой пробел:
select t1.col+1 as avail_col
from mytable as t1
left outer join mytable as t2
on t1.userid = t2.userid and t1.col+1 = t2.col
where t1.userid = 1234 /* whatever userid you search for */
and t2.col is null
order by avail_col limit 1;
Чтобы оптимизировать этот параметр, вам понадобится индекс (userid, col).
Это решение довольно просто, но у него есть несколько недостатков, что он не работает, когда вы создаете первую строку для данного идентификатора пользователя (кроме случаев, когда он не возвращает строк, вы знаете, что позиция 1 доступна), а затем он никогда не скажет вам, является ли положение 1 первым доступным пробелом.
Кроме того, остерегайтесь условий гонки. Ваш запрос может найти пробел, но до того, как ваш код добавит новую строку для использования пробела, другой одновременный запрос может сделать то же самое, найти тот же самый пробел и заполнить его. Единственные способы предотвратить это:
Не понятно, зачем вам заполнять эти пробелы. В большинстве случаев, когда я вижу подобные вопросы, приложение должно изменить свой дизайн, чтобы избежать необходимости заполнения пробелов.
Вы добавили подробности к своему вопросу, которые хотите использовать для присвоения имен армиям:
1-я армия, 2-я армия, 3-я армия,...
Вы могли бы подумать о создании другой таблицы "unused_army_names" или что-то еще. Заполните его в начале игры 100 рядами на user_id.
Когда пользователь создает армию, сделайте запись блокировки, чтобы выбрать первую запись из этой таблицы, и удалите ее из таблицы, когда вы ее вставляете
START TRANSACTION;
INSERT INTO armies (army_name, user_id)
SELECT @army_name := army_name, user_id
FROM unused_army_names
WHERE user_id = 1234
ORDER BY army_name LIMIT 1
FOR UPDATE;
DELETE FROM unused_army_names
WHERE user_id = 1234 AND army_name = @army_name;
COMMIT;
Поскольку я использую FOR UPDATE
, это блокирует строки, которые я выбираю, когда они читают их, поэтому, если другой одновременный запрос пытается сделать то же самое, он остановится и ждет, чтобы приобрести свою собственную блокировку. Как только моя первая транзакция совершается, она освобождает блокировку, а другая транзакция идет. К тому времени я удалил армию 4 из неиспользуемых армейских таблиц, а другая транзакция будет читать следующее доступное название армии.
Я использую пользовательскую переменную, чтобы запомнить имя армии, чтобы я мог ее удалить. Можно также сделать это в три этапа: SELECT, чтобы получить имя армии, INSERT в таблицу армий, DELETE из таблицы unused_army_names.
Используя транзакцию для переноса этих двух изменений (и предполагая, что вы используете InnoDB, который поддерживает транзакции), они гарантированно появятся как одно атомное изменение для других клиентов. Никто не может видеть данные в частично заполненном состоянии.
Затем, когда армия потеряна, верните ее:
START TRANSACTION;
DELETE FROM armies
WHERE user_id = 1234 AND army_name = ?;
INSERT INTO unused_army_names (army_name, user_id) VALUES (?, 1234);
COMMIT;
Я предполагаю, что на этом этапе кода вы знаете, какая армия потеряна, и вы можете передать имя армии в качестве параметра для обоих запросов.