基于表列的多个自动增量ID

Posted

技术标签:

【中文标题】基于表列的多个自动增量ID【英文标题】:Multiple autoincrement ids based on table column 【发布时间】:2020-03-09 14:07:36 【问题描述】:

我需要数据库设计方面的帮助。

我有以下表格。

伪代码:

Table order_status 
  id int[pk, increment]
  name varchar


Table order_status_update 
  id int[pk, increment]
  order_id int[ref: > order.id]
  order_status_id int[ref: > order_status.id]
  updated_at datetime


Table order_category 
  id int[pk, increment]
  name varchar


Table file 
  id int[pk, increment]
  order_id int[ref: > order.id]
  key varchar
  name varchar
  path varchar 


Table order 
  id int [pk] // primary key
  order_status_id int [ref: > order_status.id]
  order_category_id int [ref: > order_category.id]
  notes varchar
  attributes json  // no of attributes is not fixed, hence needed a json column

一切正常,但现在我需要为每种类型的 order_category_id 列设置一个自动增量 ID。

例如,如果我有 2 个类别电子产品和玩具,那么我需要电子产品 1、玩具 1、玩具 2、电子产品 2、电子产品 3、玩具 3、玩具 4、玩具 5与order 表的行关联的值。但这是不可能的,因为自动增量是基于每个新行而不是列类型。

换句话说,对于表order 而不是

  id  order_category_id
---------------------
  1       1       
  2       1       
  3       1     
  4       2       
  5       1      
  6       2
  7       1

我需要关注,

 id  order_category_id pretty_ids
----------------------------
  1       1       toy-1
  2       1       toy-2
  3       1       toy-3
  4       2       electronics-1
  5       1       toy-4
  6       2       electronics-2
  7       1       toy-5

我尝试了什么:

我为每个订单类别创建了单独的表格(不是一个理想的解决方案,但目前我有 6 个订单类别,所以现在可以使用)

现在,我有 electronics_ordertoys_order 的表格。列是重复的,但它有效。但是现在我遇到了另一个问题,我与其他表的所有关系都被破坏了。因为electronics_ordertoys_orders 可以有相同的id,所以我不能使用id 列来引用order_status_updateorder_statusfile 表。 我可以在每个表中创建另一列order_category,但这会是正确的方法吗?我在数据库设计方面没有经验,所以我想知道其他人是怎么做的。

我还有一个附带问题。

我需要order_categoryorder_status 的表来存储名称吗?因为这些值不会有太大变化,我可以将它们存储在代码中并保存在order 表的列中。

我知道单独的表有利于灵活性,但在将新行插入order 表之前,我必须查询数据库 2 次以按名称获取 order_statusorder_category。稍后将使用多重连接查询order 表。

--

如果有帮助,我将在后端使用 flask-sqlalchemy,并将 postgresql 作为数据库服务器。

【问题讨论】:

"现在我需要为每个 order_category 设置一个自动递增的 id" - 你在说哪个表,order?然后你会把order_categoryid 一起作为主键?并在fileorder_status_update 表中添加额外的列,以便您可以引用复合键?听起来不是个好主意。完全没有。 或者,您可以使用触发器ON INSERT 操作插入的行(在末尾添加“-n”)。 @Jashwant 我猜你可能需要序列化它。但如果你还要在(id, order_category) 上创建一个UNIQUE 约束,这也应该适用于竞争条件 @Jashwant 是的,它就像一个连接。不要认为它有什么问题。将字符串值直接存储在order 表中的好处是减少磁盘大小和轻松更改名称的能力。不确定“在代码中存储名称”是什么意思。 这可以通过在 select 语句中使用带有 partition by 的 row_number() 轻松实现,但将其存储为表的一部分有点棘手。我已经回答了a question like this for SQL Server,但我不知道如何在 postgreSQL 上做到这一点。希望对您有所帮助。 【参考方案1】:

要回答您的附带问题:是的,出于多种原因,您应该保留带有名称的表格。首先,此类表很小,通常由数据库保存在内存中,因此不使用这些表对性能的好处可以忽略不计。其次,您希望能够使用外部工具来查询数据库并生成报告,并且您希望这些工具可以使用这些标签。第三,您希望最大限度地减少软件与实际数据的耦合,以便它们能够独立发展。添加新类别不需要修改您的软件。

现在,对于主要问题,没有内置工具可用于您想要的那种自动增量。您必须自己构建它。

我建议您将每个类别的序列号保留为类别表中的一列。然后你可以更新它,并在订单表中使用更新后的序列号,like this(这是 PostgreSQL 特有的):

-- set up the tables

create table orders (
  id SERIAL PRIMARY KEY,
  order_category_id int,
  pretty_id VARCHAR
);
create unique index order_category_pretty_id_idx 
  on orders (pretty_id);

create table order_category (
  id SERIAL PRIMARY KEY,
  name varchar NOT NULL,
  seq int NOT NULL default 0
);

-- create the categories
insert into order_category
(name) VALUES
('toy'), ('electronics');


-- create orders, specifying the category ID and generating the pretty ID

WITH 
  new_category_id (id) AS (VALUES (1)), -- 1 here is the category ID for the new order
  pretty AS (
    UPDATE order_category 
    SET seq = seq + 1
    WHERE id = (SELECT id FROM new_category_id)
    RETURNING *
  )
INSERT into orders (order_category_id, pretty_id)
SELECT new_category_id.id, concat(pretty.name, '-', pretty.seq) 
FROM new_category_id, pretty;

您只需在示例中我有1 的位置插入您的类别ID,它将为该类别创建新的pretty_id。第一个类别是 toy-1,下一个是 toy-2,以此类推。

| id  | order_category_id | pretty_id     |
| --- | ----------------- | ------------- |
| 1   | 1                 | toy-1         |
| 2   | 1                 | toy-2         |
| 3   | 2                 | electronics-1 |
| 4   | 1                 | toy-3         |
| 5   | 2                 | electronics-2 |

【讨论】:

我希望 toy-1 和 toy-2 与 order 表的行关联,而不是 order_category 表。我更新了我的问题并添加了pretty_ids 列以更好地解释这一点。您的答案是否仍然相同,我应该存储为 toy-1324 和 toy-9009 ?另外,你能回答我的附带问题吗? @Jashwant 我回答了你的附带问题。不过,我仍然不明白为什么您需要多个版本的“玩具”类别。我也不明白你的变化,因为order 表应该有order_category_id 而不是order_category,漂亮的ID 应该在order_category 表中。 toy-1 和 toy-2 是相同的,您可以/应该只使用 toy,或者它们是不同的,您应该给它们起有意义的不同名称,例如 toy-mechanical 和 toy-electronic。 订单表中是order_category_id。现在修好了。不同类别的订单显示在前端的不同页面上。在每一页上,我都需要顺序 ID,例如toy-1, toy-2, toy-3, toy-4 代替 toy-2, toy-5, toy-6 (作为primary id 1, 3, 4 由不同订单类别的订单获取)。漂亮的 ID 不能在 order_category 表中。 Order_category 表将有固定的行数。 Order table 与order_category table 具有多对一的关系。 @Jashwant 现在我对您要完成的工作有了更好的了解,因此我已经相应地更新了我的答案。我想我已经完全回答了你的问题和附带问题。如果没有,请告诉我。 @OldPro Postgres 中有趣的解决方案,你使用update order_category 作为锁,这意味着它确实降低了数据库服务器的能力,我会在答案中提到它。【参考方案2】:

为了跟踪基于order_category 的增量id,我们可以在另一个表上跟踪这个值。让我们将此表称为:order_category_sequence。为了展示我的解决方案,我刚刚使用order_category 创建了order 表的简化版本。

CREATE TABLE order_category (
  id SERIAL PRIMARY KEY,
  name  VARCHAR(100) NULL
); 


CREATE TABLE order_category_sequence (
  id SERIAL PRIMARY KEY,
  order_category_id int NOT NULL,
  current_key  int not null
);

Alter Table order_category_sequence Add Constraint "fk_order_category_id" FOREIGN KEY (order_category_id) REFERENCES order_category (id);
Alter Table order_category_sequence Add Constraint "uc_order_category_id" UNIQUE (order_category_id);


CREATE TABLE "order" (
  id SERIAL PRIMARY KEY,
  order_category_id int NOT NULL,
  pretty_id  VARCHAR(100)  null
);

Alter Table "order" Add Constraint "fk_order_category_id" FOREIGN KEY (order_category_id) REFERENCES order_category (id);

order_category_sequence 表中的order_category_id 列引用order_categorycurrent_key 列保存order 中的最后一个值。

添加新订单行时,我们可以使用触发器从order_category_sequence 读取最后一个值并更新pretty_id。以下触发器定义可用于实现此目的。

--function called everytime a new order is added
CREATE OR REPLACE FUNCTION on_order_created()
  RETURNS trigger AS
$BODY$

DECLARE 
current_pretty_id varchar(100);

BEGIN

-- increment last value of the corresponding order_category_id in the sequence table
Update order_category_sequence
set current_key = (current_key + 1)
where order_category_id = NEW.order_category_id;

--prepare the pretty_id
Select 
oc.name || '-' || s.current_key AS   current_pretty_id 
FROM    order_category_sequence AS s
JOIN order_category AS oc on s.order_category_id = oc.id
WHERE s.order_category_id = NEW.order_category_id
INTO current_pretty_id;

--update order table
Update "order"
set pretty_id = current_pretty_id
where id = NEW.id;


RETURN NEW;
END;
$BODY$ LANGUAGE plpgsql;


CREATE TRIGGER order_created
  AFTER INSERT
  ON "order"
  FOR EACH ROW
  EXECUTE PROCEDURE on_order_created();

如果我们想同步order_categoryorder_category_sequence这两个表,我们可以使用另一个触发器在每次添加新的订单类别时在后一个表中有一行。

//function called everytime a new order_category is added
CREATE OR REPLACE FUNCTION on_order_category_created()
  RETURNS trigger AS
$BODY$

BEGIN
--insert a new row for the newly inserted order_category
Insert into order_category_sequence(order_category_id, current_key)
values (NEW.id, 0);

RETURN NEW;
END;
$BODY$ LANGUAGE plpgsql;


CREATE TRIGGER order_category_created
  AFTER INSERT
  ON order_category
  FOR EACH ROW
  EXECUTE PROCEDURE on_order_category_created();

测试查询和结果:

Insert into order_category(name)
values ('electronics'),('toys');

Insert into "order"(order_category_id)
values (1),(2),(2);


select * from "order";

关于您的附带问题,我更喜欢将诸如 order_status 和 order_category 之类的查找值存储在单独的表中。这样做可以具有上述灵活性,并且在我们有变化时很容易。

【讨论】:

我称之为“子类别”,因为玩具有多个子类别。【参考方案3】:

为了做toys-1 Toys-2 Toys-3你应该重复order_status update的逻辑,跟踪一些status的时间和计数没有区别。 就在order_status update 中,您只需将now() 放入updated_at 就更简单了,例如order_category_track 您将采用最后一个值+ 1 或分别具有不同的序列类别(不建议这样做,因为它将数据库对象与数据库中的数据)。 我会将架构更改为:

在此架构中可能处于不一致状态。但在我看来,在您的应用程序中存在三个不同的实体“order”、“order_status”、“order category track”,它们过着自己的生活。

例如,在没有锁的情况下,几乎不可能为这个任务实现一致的状态。由于下一行依赖于之前与 SQL 相矛盾的内容,此任务变得复杂。

【讨论】:

【参考方案4】:

我建议将类别分为 2 级层次结构:类别(玩具、电子)和子类别(玩具 1、玩具 2、电子 1 等):

因此您可以使用列 order_subcategory.full_name 包含已编译的“toy-1”值,或者您可以创建视图以动态创建此字段:

select oc.name || "-" || os.number
from order_category as oc 
join order_subcategory as os on oc.id = os.category_id

https://dbdiagram.io/d/5dd6a132edf08a25543e34f8

关于您的问题“我需要 order_category 和 order_status 的表来存储名称吗?”: 最好将此类数据存储为单独的字典表。它为您提供一致性和可靠性。对于 RDBMS 而言,查询这些表非常快速和容易,因此请随意使用。

【讨论】:

是的,这是要走的路,我会将表 order_subCategory 重命名为 Itemsorder_category => itemCategory。只是在阅读时更有意义:) 您将在 RDBMS 中添加计算数据。我认为这将在未来对进化/维护造成伤害。我认为架构很好。但是不要把全名放进去;)(但是如果你在 PG12 上:也许你可以使用生成的列。更少的人为控制,更多的可维护性。 如我所说,这个字段可以在视图中编译,如果这样更容易使用的话。否则,使用过程填充表应该可以解决问题。【参考方案5】:

我将只关注您展示的 3 个表格:orderorder_statusorder_category。 为新记录创建新表不是正确的方法。作为您的解释,我认为您尝试使用orderorder_category 表作为多对多关系。如果是这样,您需要的是这样的数据透视表:

我目前在订单表中添加order_status 列, 您可以根据需要在这些表格之一中添加此列。附带问题: 对于order_status,如果订单状态是固定的,(比如只有ACTIVE,INACTIVE,以后不会有更多值了)最好使用ENUM类型的列。

【讨论】:

【参考方案6】:

简单的答案是直接回答您的问题。但我认为在这种情况下这不是一件好事。所以我会不这样做。 我认为也许整个概念是错误的。

首要任务:澄清您的业务需求和主张。

一个订单可以有多个类别

一个类别可以涉及多个订单

一个订单一次只能有一个状态,但可以有多个通过时间

一个状态可以被多个订单使用

一个订单对应一个文件(可能是账单证明)

一个文件只涉及一个订单

第二:建议

有少量的保留关键字,您不能在生产环境中使用。 (https://www.postgresql.org/docs/current/sql-keywords-appendix.html)。例如,我将“命令”一词替换为“命令”。 在生产前必须回答的剩余问题:为什么“订单”表中的属性属性?这里有不尊重正常形式的风险。 (https://www.geeksforgeeks.org/normal-forms-in-dbms/)

第三:受孕解决方案

这通常足以让您有一个良好的开端。但我想玩得更开心一点:) 所以......

第四:对所需性能的询问

按顺序估计每天/每月的负载(每月一千万行?)

第五:物理解决方案

在另一个表空间中存档(取消或终止时触发 => 存档) 另一个表空间中的索引(您的 dba 会感谢您) 订单表可能的分区(https://pgxn.org/dist/pg_partman/doc/pg_partman.html,https://www.postgresql.org/docs/current/ddl-partitioning.html) 硬件和选项选择(高可用性?灾难管理?如果是:详细说明需要进一步研究,但很少) 数据转置(真的需要吗?如果需要:阐述需要进一步研究,但很少)

finaaaaaal 代码降级! (伴随着美妙的音乐)

-- as a postgres user
CREATE DATABASE command_system;
CREATE SCHEMA in_prgoress_command;
CREATE SCHEMA archived_command;
--DROP SCHEMA public;
-- create tablespaces on other location than below
CREATE TABLESPACE command_indexes_tbs location 'c:/Data/indexes';
CREATE TABLESPACE archived_command_tbs location 'c:/Data/archive';
CREATE TABLESPACE in_progress_command_tbs location 'c:/Data/command';

CREATE TABLE in_prgoress_command.command
(
    id bigint /*or bigserial if you use a INSERT RETURNING clause*/ primary key
    , notes varchar(500)
    , fileULink varchar (500)
)
TABLESPACE in_progress_command_tbs;

CREATE TABLE archived_command.command
(
    id bigint /*or bigserial if you use a INSERT RETURNING clause*/ primary key
    , notes varchar(500)
    , fileULink varchar (500)
)
TABLESPACE archived_command_tbs;

CREATE TABLE in_prgoress_command.category
(
    id int primary key
    , designation varchar(45) NOT NULL
)
TABLESPACE in_progress_command_tbs;
INSERT INTO in_prgoress_command.category 
VALUES (1,'Toy'), (2,'Electronic'), (3,'Leather'); --non-exaustive list

CREATE TABLE in_prgoress_command.status
(
    id int primary key
    , designation varchar (45) NOT NULL
)
TABLESPACE in_progress_command_tbs;

INSERT INTO in_prgoress_command.status 
VALUES (1,'Shipping'), (2,'Cancel'), (3,'Terminated'), (4,'Payed'), (5,'Initialised'); --non-exaustive list

CREATE TABLE in_prgoress_command.command_category
(
    id bigserial primary key
    , idCategory int 
    , idCommand bigint
)
TABLESPACE in_progress_command_tbs;

ALTER TABLE in_prgoress_command.command_category
ADD CONSTRAINT fk_command_category_category FOREIGN KEY (idCategory) REFERENCES in_prgoress_command.category(id);

ALTER TABLE in_prgoress_command.command_category
ADD CONSTRAINT fk_command_category_command FOREIGN KEY (idCommand) REFERENCES in_prgoress_command.command(id);

CREATE INDEX idx_command_category_category ON in_prgoress_command.command_category USING BTREE (idCategory) TABLESPACE command_indexes_tbs;
CREATE INDEX idx_command_category_command ON in_prgoress_command.command_category USING BTREE (idCommand) TABLESPACE command_indexes_tbs;

CREATE TABLE archived_command.command_category
(
    id bigserial primary key
    , idCategory int 
    , idCommand bigint
)
TABLESPACE archived_command_tbs;

ALTER TABLE archived_command.command_category
ADD CONSTRAINT fk_command_category_category FOREIGN KEY (idCategory) REFERENCES in_prgoress_command.category(id);

ALTER TABLE archived_command.command_category
ADD CONSTRAINT fk_command_category_command FOREIGN KEY (idCommand) REFERENCES archived_command.command(id);

CREATE INDEX idx_command_category_category ON archived_command.command_category USING BTREE (idCategory) TABLESPACE command_indexes_tbs;
CREATE INDEX idx_command_category_command ON archived_command.command_category USING BTREE (idCommand) TABLESPACE command_indexes_tbs;

CREATE TABLE in_prgoress_command.command_status
(
    id bigserial primary key
    , idStatus int 
    , idCommand bigint
    , change_timestamp timestamp --anticipate if you can the time-zone problematic
)
TABLESPACE in_progress_command_tbs;

ALTER TABLE in_prgoress_command.command_status
ADD CONSTRAINT fk_command_status_status FOREIGN KEY (idStatus) REFERENCES in_prgoress_command.status(id);

ALTER TABLE in_prgoress_command.command_status
ADD CONSTRAINT fk_command_status_command FOREIGN KEY (idCommand) REFERENCES in_prgoress_command.command(id);

CREATE INDEX idx_command_status_status ON in_prgoress_command.command_status USING BTREE (idStatus) TABLESPACE command_indexes_tbs;
CREATE INDEX idx_command_status_command ON in_prgoress_command.command_status USING BTREE (idCommand) TABLESPACE command_indexes_tbs;
CREATE UNIQUE INDEX idxu_command_state ON in_prgoress_command.command_status USING BTREE (change_timestamp, idStatus, idCommand) TABLESPACE command_indexes_tbs;

CREATE OR REPLACE FUNCTION sp_trg_archiving_command ()
    RETURNS TRIGGER
language plpgsql
as $function$
DECLARE
BEGIN
    -- Copy the data
    INSERT INTO archived_command.command
    SELECT *
    FROM in_prgoress_command.command
    WHERE new.idCommand = idCommand;    

    INSERT INTO archived_command.command_status (idStatus, idCommand, change_timestamp)
    SELECT idStatus, idCommand, change_timestamp
    FROM in_prgoress_command.command_status
    WHERE idCommand = new.idCommand;    

    INSERT INTO archived_command.command_category (idCategory, idCommand)
    SELECT idCategory, idCommand
    FROM in_prgoress_command.command_category
    WHERE idCommand = new.idCommand;    

    -- Delete the data
    DELETE FROM in_prgoress_command.command_status
    WHERE idCommand = new.idCommand;    
    DELETE FROM in_prgoress_command.command_category
    WHERE idCommand = new.idCommand;    
    DELETE FROM in_prgoress_command.command
    WHERE idCommand = new.idCommand;    
END;
$function$;

DROP TRIGGER IF EXISTS t_trg_archiving_command ON in_prgoress_command.command_status;
CREATE TRIGGER t_trg_archiving_command
AFTER INSERT
ON in_prgoress_command.command_status
FOR EACH ROW
WHEN (new.idstatus = 2 or new.idStatus = 3)
EXECUTE PROCEDURE sp_trg_archiving_command();

CREATE TABLE archived_command.command_status
(
    id bigserial primary key
    , idStatus int 
    , idCommand bigint
    , change_timestamp timestamp --anticipate if you can the time-zone problematic
)
TABLESPACE archived_command_tbs;

ALTER TABLE archived_command.command_status
ADD CONSTRAINT fk_command_command_status FOREIGN KEY (idStatus) REFERENCES in_prgoress_command.category(id);

ALTER TABLE archived_command.command_status
ADD CONSTRAINT fk_command_command_status FOREIGN KEY (idCommand) REFERENCES archived_command.command(id);

CREATE INDEX idx_command_status_status ON archived_command.command_status USING BTREE (idStatus) TABLESPACE command_indexes_tbs;
CREATE INDEX idx_command_status_command ON archived_command.command_status USING BTREE (idCommand) TABLESPACE command_indexes_tbs;
CREATE UNIQUE INDEX idxu_command_state ON archived_command.command_status USING BTREE (change_timestamp, idStatus, idCommand) TABLESPACE command_indexes_tbs;

结论:

在许多情况下,当您担心钥匙的处置方式时,是因为它们不在合适的位置。汽车也一样! :D 不要将任何解决方案视为预言解决方案:对其进行基准测试。

【讨论】:

客户经常在订单中添加/删除字段。这些需要保存在订单表中。这些字段并非对所有订单类别都通用,有些有,有些没有。我不能一次又一次地增加表中的列。因此,我将它们保存在 1 JSON 列中。它的模式较少,因此对我有用。其他解决方案是为这些字段创建一个表并将字段名称和订单ID传递给它,但它是 postgresql ,为什么不使用 JSON ? RDBMS 概念中的第一条规则是在数据库中拥有原子数据。但是,如果这是客户可以添加列的数据,我想您没有其他选择;)(很多人在 json 列中放置任何内容而不考虑正常形式。这就是我发表评论的原因;))跨度>

以上是关于基于表列的多个自动增量ID的主要内容,如果未能解决你的问题,请参考以下文章

MySQL 序列与 id 列的自动增量

如何从 SQLite 表中检索最后一个自动增量 ID?

如果一个表是自动增量的,你怎么能从一个 sqlite 表的元数据中得到?

仅当自动增量数据相等时才从另一个表列更新 mysql 列

带有标识(自动增量)列的 BULK INSERT

Hive 自动增量 UDF 没有给出想要的结果