mysql - 如何使用 MySQL 通过 WHERE 来选择第一个可用的 id?

标签 mysql sql performance select

有一个表table,有一列column和另一列userId。 表 table 可以包含任意数量的具有相同 userId 的行。但是,在 SELECT 列、userId FROM table 的集合中,决不应该存在具有重复 (column,userId) 行的列。 这些行将经常被创建、读取、更新、删除和创建。 我希望每个 userId 都有它的本地 column id,如下所示:

+--------+--------+
| column | userId |
+--------+--------+
|      1 |      1 |
|      2 |      1 |
|      3 |      1 |
|      4 |      1 |
|      5 |      1 |
|    ... |    ... |
|      1 |      2 |
|      2 |      2 |
|      3 |      2 |
|      4 |      2 |
|      5 |      2 |
|    ... |    ... |
+--------+--------+

当删除一行时,我想获取某个userId的第一个可用列column id。我会:

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下一个可用的。 另外,表 table 包含大量的创建、更新、删除操作,因此我想要任何能够快速处理数千或数百万行的解决方案。 我认为这个查询没有优化:

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。然后,我将它们全部汇总到一行中,并与查询结果集中的其他行合并,其中一些可以分组,也可以不分组。如果他们被分组,我希望每个玩家的小组部队相对于玩家的其他部队都有唯一的 group_number 。 所以我可以向他们展示:

第一集团军,

第2集团军,

第3集团军,

...

第100军,

等等

这对于应用程序的功能来说并不重要,但我发现有了这样的编号系统,军队更容易记住并且易于识别,然后,比如说,显示一些“随机”长ID

最佳答案

数据库擅长跟踪存在的数据,但不擅长跟踪丢失的数据。

您可以通过以下方式找到间隙:

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) 上的索引来优化它。

这个解决方案非常简单,但它有几个缺陷,当您为给定用户 ID 创建第一行时它不起作用(除非它不返回任何行,否则您知道位置 1 可用) ,随后它永远不会告诉你位置 1 是否是第一个可用的间隙。

另外,请注意 race conditions 。您的查询可能会找到间隙,但在代码插入新行以使用间隙之前,另一个并发请求可能会执行相同的操作,找到相同的间隙并填充它。防止这种情况的唯一方法是:

  • 保证一次不会有多个请求处理给定用户 ID 的数据。
  • 使用 locking read当您 SELECT 间隙时锁定给定用户 ID 的所有行。

目前尚不清楚为什么需要填补这些空白。在大多数情况下,当我看到类似的问题时,应用程序需要更改其设计以避免填补空白的要求。

<小时/>

您在问题中添加了详细信息,您想用它来为军队指定名称:

1-st army, 2-nd army, 3-rd army, ...

您可以考虑创建另一个表“unused_army_names”或其他表。在游戏开始时将每个 user_id 填充 100 行。

当用户创建军队时,执行锁定读取以从该表中选择第一个条目,并在插入时将其从表中删除

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,另一个事务将读取下一个可用的军队名称。

我使用user-defined variable记住军队的名字,这样我就可以删除它。还可以通过三个步骤完成此操作:选择获取军队名称,插入军队表,从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;

我假设此时在代码中,您知道哪支军队丢失了,并且可以将军队名称作为参数传递给两个查询。

关于mysql - 如何使用 MySQL 通过 WHERE 来选择第一个可用的 id?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49842707/

相关文章:

mysql - 在 Google map 上追踪位置路径

mysql - SQLSTATE[23000] : Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails

sql - 带有第二个数据库的 PostgreSQL FOREIGN KEY

mysql - Speedwise,日期时间或时间戳哪个更好?

mysql - Rake DB 迁移 MySQL

php - MySQL获取从现在到去年一个月的记录

mysql - 迁移到 mysql 5.7 后查询速度极慢

Java Mission Control 中的空格是什么意思?

sql - 查询之间的多个值 [PostgreSQL]

mysql获取整月的平均数据