关于postgresql:如何在UNIQUE索引中合并两个具有可能NULL值的表?

How to merge two tables with possible NULL values in the UNIQUE index?

如何合并(upsert& delete orphan rows)到tableA

tableA

1
2
3
4
5
6
7
8
9
10
11
12
13
+---------+--------+----------+-------+
| company | OPTION | category | rates |
+---------+--------+----------+-------+
| a       | f      | NULL     | 2.5   |
+---------+--------+----------+-------+
| a       | f      | d        | 2     | *
+---------+--------+----------+-------+
| a       | g      | e        | 3     | **
+---------+--------+----------+-------+
| c       | g      | e        | 4     |
+---------+--------+----------+-------+
| d       | f      | d        | 1     |
+---------+--------+----------+-------+

*表示孤立行*。
**表示要改变的值(3 - > 4)。

仅触摸tableB中存在的公司(示例中a& c,单独留下d)。

tableB

1
2
3
4
5
6
7
8
9
+---------+--------+----------+-------+
| company | OPTION | category | rates |
+---------+--------+----------+-------+
| a       | f      | NULL     | 2.5   |
+---------+--------+----------+-------+
| a       | g      | e        | 4     |
+---------+--------+----------+-------+
| c       | g      | e        | 4     |
+---------+--------+----------+-------+

两个表中的(company, option, category)都有唯一索引。

期望的结果tableA

1
2
3
4
5
6
7
8
9
10
11
+---------+--------+----------+-------+
| company | OPTION | category | rates |
+---------+--------+----------+-------+
| a       | f      | NULL     | 2.5   |
+---------+--------+----------+-------+
| a       | g      | e        | 4     | <-
+---------+--------+----------+-------+
| c       | g      | e        | 4     |
+---------+--------+----------+-------+
| d       | f      | d        | 1     |
+---------+--------+----------+-------+

对于(a,g,e),仅删除了第二行(a,f,d,2),并且rates从3更改为4。

这是一个小提琴:https://rextester.com/QUVC30763

我想先用这个删除孤儿行:

1
2
3
4
5
6
7
8
9
10
11
DELETE FROM tableA
 USING tableB
 WHERE
   -- ignore rows with IDs that don't exist in tableB
   tableA.company = tableB.company
   -- ignore rows that have an exact all-column match in tableB
   AND NOT EXISTS
      (SELECT * FROM tableB
      WHERE tableB.company IS NOT DISTINCT FROM tableA.company
      AND tableB.option IS NOT DISTINCT FROM tableA.option
      AND tableB.category IS NOT DISTINCT FROM tableA.category );

然后用这个upsert:

1
2
3
4
5
6
7
8
9
 INSERT INTO tableA (company, OPTION, category, rates)
   SELECT company, OPTION, category, rates
   FROM   tableB
 ON CONFLICT (company, OPTION, category)
 DO UPDATE
   SET rates= EXCLUDED.rates
 WHERE
      tableA.rates IS DISTINCT FROM
      EXCLUDED.rates;

但upsert函数的问题在于它无法处理可空字段。我必须设置-1来代替null,否则函数将无法知道是否有重复项。我觉得设置-1代替null会在将来创建许多变通方法,所以如果可以,我想避免这样做。

注意:我发现INSERT ... ON CONFLICT ... DO UPDATE可能是要走的路:

  • 在PostgreSQL 9.5上制作MERGE

但我没有看到适合我的情况的查询。而且我不确定是否可以使用可空字段。因此问题是:
是否有一种与可空字段合并的简洁方法?


我认为你走的是正确的道路。但NULLUNIQUE存在设计问题:

optioncategory可以是NULL。在这些情况下,NULL被认为是相等的。您当前的唯一索引不会将NULL值视为相等,因此不会强制执行您的要求。这甚至在您开始合并之前就会产生歧义。 NULL值对于您要实现的内容并不好。解决这个问题会产生更多的工作和额外的失败点。考虑使用特殊值而不是NULL,一切都到位。你在考虑-1。任何对您的实际数据类型和属性的性质自然有意义的东西。

也就是说,DELETE有一个额外的,隐藏的隐藏问题:它会尝试删除孤立行的次数与tableBcompany上的匹配次数一样多。没有什么可以打破,因为多余的尝试什么都不做,但它不必要地昂贵。改为使用EXISTS两次:

1
2
3
4
5
6
7
8
9
10
DELETE FROM tableA a
WHERE  EXISTS (
   SELECT FROM tableB b
   WHERE a.company = b.company
   )
AND    NOT EXISTS (
   SELECT FROM tableB b
   WHERE (a.company, a.option, a.category) IS NOT DISTINCT FROM
         (b.company, b.option, b.category)
   );

如果您坚持使用NULL值,则将UPSERT拆分为UPDATE,然后将INSERT ... ON CONFLICT DO NOTHING拆分为解决方法。如果没有对表的并发写入,则更简单,更便宜。 ON CONFLICT DO NOTHING无需指定冲突目标即可运行,因此您可以使用多个部分索引实现您的需求并使其生效。手册:

For ON CONFLICT DO NOTHING, it is optional to specify a
conflict_target; when omitted, conflicts with all usable constraints
(and unique indexes) are handled. For ON CONFLICT DO UPDATE, a
conflict_target must be provided.

但是,如果您使用工作UNIQUE索引或约束来修复架构,那么您已经拥有的UPSERT可以很好地运行。

并确保没有并发写入表或您可能面临竞争条件和/或死锁,除非您做更多...