2

我想创建一个 user_widgets 表,该表由 user_id 和 user_widget_id 主键,其中 user_widget_id 像序列一样工作,除了它从每个用户 1 个开始。

对此有通用或实用的解决方案吗?我正在使用 PostgreSQL,但也可以使用不可知论的解决方案。

示例表:user_widgets

 |  user_id  |  user_widget_id  |  user_widget_name    |
 +-----------+------------------+----------------------+
 |  1        |  1               | Andy's first widget  |
 +-----------+------------------+----------------------+
 |  1        |  2               | Andy's second widget |
 +-----------+------------------+----------------------+
 |  1        |  3               | Andy's third widget  |
 +-----------+------------------+----------------------+
 |  2        |  1               | Jake's first widget  |
 +-----------+------------------+----------------------+
 |  2        |  2               | Jake's second widget |
 +-----------+------------------+----------------------+
 |  2        |  3               | Jake's third widget  |
 +-----------+------------------+----------------------+
 |  3        |  1               | Fred's first widget  |
 +-----------+------------------+----------------------+

编辑:

我只是想包括这种设计的一些原因。

1. 减少信息泄露,而不仅仅是“通过默默无闻的安全”

在用户不应该知道彼此的系统中,他们也不应该知道彼此的widget_id。如果这是一张库存表、奇怪的商业机密、发票或其他更敏感的东西,他们就可以开始为这些小部件拥有自己未受影响的 ID 集。除了明显的例行安全检查之外,这还添加了一个隐式安全层,其中必须通过小部件 ID用户 ID 过滤表。

2. 数据导入

应该允许用户从其他系统导入他们的数据,而不必丢弃他们所有的旧 ID(如果他们有整数 ID)。

3. 清洁度

与我的第一点并没有太大的不同,但我认为创建的内容比其他用户少的用户可能会对他们的小部件 ID 的显着跳跃感到困惑或烦恼。这当然比功能更肤浅,但仍然很有价值。

一个可能的解决方案

其中一个答案表明应用程序层会处理这个问题。我可以在该用户的表上存储一个递增的 next_id 列。或者甚至可能只计算每个用户的行数,并且不允许删除记录(使用删除/停用标志代替)。这可以通过触发器函数,甚至是存储过程而不是在应用程序层中完成吗?

4

2 回答 2

6

如果你有一张桌子:

CREATE TABLE user_widgets (
  user_id int
 ,user_widget_name text  --should probably be a foreign key to a look-up table
  PRIMARY KEY (user_id, user_widget_name)
)

您可以动态分配user_widget_id和查询:

WITH x AS (
   SELECT *, row_number() OVER (PARTITION BY user_id
                                ORDER BY user_widget_name) AS user_widget_id 
   FROM   user_widgets
   )
SELECT *
FROM   x
WHERE  user_widget_id = 2;

user_widget_id在这种情况下,每个用户按字母顺序应用并且没有间隙,添加、更改或删除条目显然会导致更改。

手册中有关窗口功能的更多信息。


更稳定(但不完全):

CREATE TABLE user_widgets (
  user_id int
 ,user_widget_id serial
 ,user_widget_name
  PRIMARY KEY (user_id, user_widget_id)
)

和:

WITH x AS (
   SELECT *, row_number() OVER (PARTITION BY user_id
                                ORDER BY user_widget_id) AS user_widget_nr 
   FROM   user_widgets
   )
SELECT *
FROM   x
WHERE  user_widget_nr = 2;

解决问题更新

可以实施一种制度来计算每个用户的现有小部件。但是你将很难使它对并发写入防弹。您将不得不锁定整个表或使用SERIALIZABLE事务模式——这两者都是性能下降的真正因素,并且需要额外的代码。

但是,如果您保证不会删除任何行,您可以使用我的第二种方法 - 一个user_widget_id跨表的序列,这会给您一个“原始” ID。序列是一种经过验证的并发加载解决方案,保留了相对顺序user_widget_id并且速度很快。您可以使用一个视图来提供对表的访问,该视图将“原始”动态替换为上面user_widget_id的相应user_widget_nr查询。

您可以(此外)通过在下班时间user_widget_id替换它或由您选择的事件触发来“实现”无缝。user_widget_nr

为了提高性能,我会user_widget_id从一个非常高的数字开始序列。似乎每个用户只能有几个小部件。

SELECT setval(user_widgets_user_widget_id_seq', 100000);

如果没有数字足够高以保证安全,请添加一个标志。使用条件WHERE user_widget_id > 100000快速识别“原始”ID。如果您的表很大,您可能希望使用条件添加部分索引(它会很小)。用于CASE声明中提到的视图。在此声明中“实现”ID:

UPDATE user_widgets w
SET    user_widget_id = u.user_widget_nr
FROM (
   SELECT user_id, user_widget_id
         ,row_number() OVER (PARTITION BY user_id
                             ORDER BY user_widget_id) AS user_widget_nr 
   FROM   user_widgets
   WHERE  user_widget_id > 100000
   ) u
WHERE  w.user_id = u.user_id
AND    w.user_widget_id = u.user_widget_id;

REINDEX可能会在下班时间或什VACUUM FULL ANALYZE user_widgets至在下班时间跟进。考虑FILLFACTOR低于 100,因为列将至少更新一次。

我当然不会把这个留给应用程序。这引入了多个额外的故障点。

于 2012-09-10T20:53:28.920 回答
1

我将加入,质疑具体要求。通常,如果您尝试订购此类物品,最好留给应用程序处理。如果你认识我,你会意识到这真的是在说些什么。我担心的是,我能想到的每个案例都可能需要应用程序重新排序,否则这些数字将无关紧要。

所以我只想:

CREATE TABLE user_widgets (
      user_id int references users(id),
      widget_id int,
      widget_name text not null,
      primary key(user_id, widget_id)
);

我会把它留在那里。

现在根据您的理由,这解决了您所有的问题(进口)。然而,我曾经有过很长一段时间不得不做类似的事情。我的用例是当地税务管辖区要求装箱单(!)按顺序编号,没有间隙,与发票分开。计数记录,顺便说一句,不会满足您的进口要求。

我们所做的是创建一个表,每个序列有一行并使用它,然后将其与触发器绑定。

于 2012-09-11T06:47:31.590 回答