【问题标题】:Postgres SELECT or INSERT in CTE?CTE中的Postgres SELECT或INSERT?
【发布时间】:2020-05-26 06:25:09
【问题描述】:

我使用的是 Postgres 12.2。

我尝试填写一种事件表,其中有一列需要获取某物的 id。

在我创建的示例中,事件存储在 purchases 表中,而 id 需要是公司的 id,取自 companies 表。由于没有固定的公司集,因此公司表需要“随我增长”;)。

所以设置是:

CREATE TABLE 
    companies 
    ( 
        company_id  SERIAL NOT NULL, 
        NAME        CHARACTER VARYING(256) NOT NULL, 
        PRIMARY KEY (company_id), 
        CONSTRAINT companies_unique_name UNIQUE (NAME) 
    )
;
CREATE TABLE 
    purchases
    ( 
        purchase_id    SERIAL NOT NULL, 
        purchase_date  DATE DEFAULT CURRENT_DATE NOT NULL, 
        amount         INTEGER NOT NULL,
        company_id     INTEGER NOT NULL, 
        PRIMARY KEY (purchase_id) 
    )
;

因此,当我现在想要插入事件“从 'company1' 购买了 5 件商品”时,我需要通过在 companies 表中查找“company1”的 id 或通过为companies 中的“company1”。

我可以这样做:

WITH EXISTING_COMPANY AS (
        SELECT company_id FROM companies WHERE name = 'company1'
)
   , NEW_COMPANY AS (
        INSERT INTO companies (name) VALUES ('company1')
        ON CONFLICT(name) DO NOTHING
        RETURNING company_id
)
   , GET_COMPANY_ID AS (
        SELECT COALESCE(
            (SELECT company_id FROM EXISTING_COMPANY),
            (SELECT company_id FROM NEW_COMPANY)
   ) AS company_id
)
INSERT INTO purchases(amount, company_id)
VALUES (5, (select company_id from GET_COMPANY_ID))
;

CTE EXISTING_COMPANY 将为我提供现有“company1”或 null 的 ID。

CTE NEW_COMPANY 会给我一个新创建的公司“company1”或null的ID

CTE GET_COMPANY_ID 最终将通过使用 coalesce 尝试获取现有 id,如果失败,则获取新 id。

虽然这可行,但它的缺点是我需要两次提供公司名称,并且我需要为每家公司提供新的 CTE,因为我不知道如何将公司名称传递给我的 CTE。

  • 有没有办法将公司名称传递给我的 CTE?
  • 还有其他方法可以实现我的目标吗?

【问题讨论】:

    标签: postgresql common-table-expression


    【解决方案1】:

    您可以在另一个 CTE 中为新名称提供 values() 子句。你也不需要existing_company CTE,因为这也可以在get_company cte 内完成:

    WITH input(name) as (
      values ('company1')
    ), new_company AS (
    
      INSERT INTO companies (name) 
      select i.name 
      from input i
      ON CONFLICT (name) DO NOTHING
      RETURNING company_id
    
    ), get_company_id AS (
      select company_id
      from new_company
    
      union all
    
      select company_id
      from companies
      where name in (select name from input)
        and not exists (select * from new_company)
    )
    INSERT INTO purchases(amount, company_id)
    select 5, company_id 
    from get_company_id
    ;
    

    这也可以扩展到处理多个公司和金额:

    WITH input(amount, name) as (
      values 
         (5, 'company1'), 
         (6, 'company2')
    ), new_company AS (
      INSERT INTO companies (name) 
      select name 
      from input
      ON CONFLICT (name) DO NOTHING
      RETURNING company_id, name
    ), get_company_id AS (
    
      select company_id, name
      from new_company
      union all
    
      select c.company_id, c.name
      from companies c
      where c.name in (select i.name from input i)
      and not exists (select * 
                      from new_company nc
                      where nc.company_id = c.company_id)
    )
    INSERT INTO purchases(amount, company_id)
    select i.amount, g.company_id 
    from get_company_id g
      join input i on i.name = g.name
    ;
    

    【讨论】:

    • 如果我错了,请纠正我,但这不是先尝试插入然后再选择吗?由于我希望拥有比新公司更多的“重复出现”公司,反过来不是更有效率吗?这就是我使用 coalesce 的原因,因为当 SELECT 成功时它不会评估 INSERT。
    • INSERT总是执行,不管你是否从其对应的 CTE 中选择。
    • 我通过在companies 表中添加一列并在ON CONFLICT 中更改其值来仔细检查。你说的对。然后似乎我不明白(引用)“COALESCE 函数从左到右评估参数,直到找到第一个非空参数。不评估第一个非空参数的所有剩余参数。”在这里找到:postgresqltutorial.com/postgresql-coalesce
    • @Skeeve:实际上是因为“ WITH 中的数据修改语句只执行一次,并且总是完成,与主查询是否读取它们的所有(或实际上任何)无关输出”(见postgresql.org/docs/current/…
    【解决方案2】:

    我研究了函数,我正在试验这个:

    CREATE OR REPLACE FUNCTION companyID (IN company_name VARCHAR(256))
    RETURNS integer AS
    '
    DECLARE
            cid integer;
    BEGIN
            SELECT company_id INTO cid FROM companies WHERE name = company_name;
            IF cid IS NULL THEN
                    INSERT INTO companies (name) VALUES(company_name)
                    ON CONFLICT(name) DO NOTHING
                    RETURNING company_id INTO cid;
            END IF;
            -- In case of a concurrent insert, cid could be null
            IF cid IS NULL THEN
                    SELECT company_id INTO cid FROM companies WHERE name = company_name;
            END IF;
            RETURN cid;
    END;
    '
    LANGUAGE plpgsql
    ;
    

    插入然后像这样工作:

    INSERT INTO purchases (amount, company_id) VALUES
    (10, companyID('company2')),
    (11, companyID('company1')),
    (42, companyID('company2'))
    ;
    

    这种方法有什么缺点吗?

    【讨论】:

    • 这对于并发插入是不安全的
    • 谢谢。我试图通过忽略插入冲突并在发生此类冲突时检索同时插入的 id 来解决它。这样看起来更好吗?还有其他缺点吗?
    猜你喜欢
    • 2021-12-22
    • 2021-06-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-02
    • 1970-01-01
    • 1970-01-01
    • 2018-08-02
    相关资源
    最近更新 更多