Как выбрать первый доступный идентификатор с помощью ГДЕ с использованием MySQL?

0

В таблице 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-я армия,

и так далее

Это не критично для функциональности приложения, но я обнаружил, что, имея такую систему нумерации, армии более запоминающимися и легко идентифицируемыми, то, скажем, отображение некоторого "случайного" длинного идентификатора

  • 1
    Вы можете получить быстрый запрос, который выполняет ту работу, которую вы хотите, но вы уверены, что вашей бизнес-логике действительно нужна такая функциональность?
  • 0
    Может быть, вы можете сказать нам, почему вам действительно нужно это реализовать. Это будет довольно сложно и много работы, чтобы построить и поддерживать.
Показать ещё 7 комментариев
Теги:
select
performance

1 ответ

0

Базы данных хорошо отслеживают данные, которые присутствуют, но не так хороши при отслеживании данных, которые отсутствуют.

Вы можете найти такой пробел:

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;

Я предполагаю, что на этом этапе кода вы знаете, какая армия потеряна, и вы можете передать имя армии в качестве параметра для обоих запросов.

  • 0
    Спасибо, я проверю это. По причине, проверьте мое обновление.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню