在 UPSERT 的 UPDATE 部分,为多列计算一次表达式

In UPDATE part of UPSERT, compute expression once for multiple columns

我在 PostgreSQL 12 数据库中有一个 INSERT ... ON CONFLICT (primary_key) DO UPDATE 查询。

它要求我对更新子句中的多个列进行相同的计算。计算并不太复杂,但我不想在两个地方维护它。计算包含对冲突行和 EXCLUDED 虚拟 table.

的引用

有什么方法可以执行一次此计算并将其结果分配给单个查询表达式中的临时变量?

完整的查询是:

INSERT INTO table_name(id, refreshed_at, tokens) VALUES ('id', CURRENT_TIMESTAMP, 60)
    ON CONFLICT (id) DO UPDATE SET
        tokens = GREATEST(
          -1, 
          LEAST(
            GREATEST(0, table_name.tokens) 
              + EXTRACT(epoch FROM EXCLUDED.refreshed_at) 
              - EXTRACT(epoch FROM table_name.refreshed_at), 
            100
          ) - 1
        ),
        refreshed_at = CASE 
          WHEN (
            GREATEST(0, table_name.tokens) 
              + EXTRACT(epoch FROM EXCLUDED.refreshed_at) 
              - EXTRACT(epoch FROM table_name.refreshed_at)
            ) > 0 THEN EXCLUDED.refreshed_at 
          ELSE table_name.refreshed_at 
          END
        RETURNING tokens >= 0;

tokens 列是根据列的先验值计算的,即查询时间与上次更新列之间的秒数差。 refreshed_at 列只有在更新 tokens 列值时才应更改,因此必须在两个 SET 子句中执行相同的计算。

The refreshed_at column should only be changed if the tokens column value was updated

您最初的想法应该适用于子查询:

INSERT INTO table_name(id, refreshed_at, tokens)
VALUES ('id', CURRENT_TIMESTAMP, 60)
ON CONFLICT (id) DO UPDATE
<b>SET    (tokens, refreshed_at)
     = (SELECT GREATEST(-1, LEAST(GREATEST(0, table_name.tokens) + sub.diff, 100) - 1)
             , CASE WHEN sub.diff > 0 THEN EXCLUDED.refreshed_at ELSE table_name.refreshed_at END
        FROM  (SELECT EXTRACT(epoch FROM EXCLUDED.refreshed_at - table_name.refreshed_at)::int) sub(diff))</b>
RETURNING tokens >= 0;

那是 allowed variant of the UPDATE syntax。这样,以秒为单位的时间戳之间的差异只计算一次。

计算也更便宜,从差异中提取纪元(结果 interval):

EXTRACT(epoch FROM EXCLUDED.refreshed_at - table_name.refreshed_at)

而不是像原来那样提取两次并减去:

EXTRACT(epoch FROM EXCLUDED.refreshed_at) - EXTRACT(epoch FROM table_name.refreshed_at)

但是添加子查询的开销可能比做两次便宜的计算更昂贵!

备选方案

考虑将 WHERE 子句添加到 UPDATE 部分。喜欢:

INSERT INTO table_name(id, refreshed_at, tokens)
VALUES ('id', CURRENT_TIMESTAMP, 60)
ON CONFLICT (id) DO UPDATE
SET     tokens = GREATEST(-1, LEAST(GREATEST(0, table_name.tokens) + EXTRACT(epoch FROM EXCLUDED.refreshed_at - table_name.refreshed_at), 100) - 1)
      , refreshed_at = EXCLUDED.refreshed_at
<b>WHERE   EXTRACT(epoch FROM EXCLUDED.refreshed_at - table_name.refreshed_at)::int > 0</b>
RETURNING tokens >= 0;

这根本不会触及时差低于 1 秒的行。这通常是优越的,因为空更新只会增加成本和膨胀。 (转换 rounds,您可能想要 truncate?)

当然,这也不会 return INSERTUPDATE 部分都没有通过的行。如果这是一个问题,请考虑: