【问题标题】:Oracle SQL - when matched, update AND insertOracle SQL - 匹配时,更新并插入
【发布时间】:2017-05-31 03:47:51
【问题描述】:

我在这里发现了一些类似的问题,但似乎没有一个真正适合我的情况。

我正在处理的表格是这样的,它是一个记录学生在课程中的表现的表格:

| STUDENT_ID | COURSE_ID | ENROLLMENT_TYPE | MARK | STATUS  | VERSION |
|            |           |                 |      |         |         |
| 1234       | 5678      | Mandatory       | 70   | ACTIVE  | 2       |
| 1234       | 5678      | Optional        | 70   | HISTORY | 1       |
| 1234       | 5678      | Optional        | null | HISTORY | 0       |
| 9876       | 4597      | Institutional   | 99   | ACTIVE  | 1       |
| 9876       | 4597      | Institutional   | null | HISTORY | 0       |

我需要将此与另一个表合并,该表根据学生的组跟踪学生的注册情况,以便我可以根据需要插入或更新行:

| GROUP_ID | STUDENT_ID | COURSE_ID | ENROLLMENT_TYPE |
| 4976555  | 1234       | 5678      | Mandatory       |
| 6399875  | 1234       | 9034      | Optional        |
| 6399875  | 9876       | 4597      | Institutional   |

长话短说,我需要检查注册类型是否相同或已更改,因为有些学生根据他们所属的小组注册课程,但小组可能会在一夜之间发生变化。

到这里为止一切都很好,但是如果行匹配,我还需要复制我将要更新的行并将其设置为“HISTORY”,以便我们可以记录所有已进行的更新到某一行。

目前,我有一个典型的

MERGE INTO performance USING group_enrollments
ON performance.STUDENT_ID = group_enrollments.STUDENT_ID 
AND performance.COURSE_ID = group_enrollments.COURSE_ID
WHEN MATCHED THEN UPDATE ......
WHEN NOT MATCHED THEN INSERT ......

在我之前处理这部分代码的人认为,在合并之前将所有行复制为“HISTORY”行是个好主意,但这给我们带来了问题,因为这个过程每晚都会运行并且每次写入超过 150.000 行。 有什么想法吗?

16/01 12.34:更新,有关表格及其关系的更多信息

【问题讨论】:

  • 如果我是你,我会编写一个查询来复制你要更新的行,这样就有一个处于活动状态,一个处于历史状态 - 这样你就可以然后在您的合并语句中使用它来插入历史行并根据需要更新活动行。如果您需要这方面的帮助,请更新您的问题以包括来自您的两个表的数据以及预期的输出。这样,我们就可以自己测试了。
  • 我希望更新更清晰。问题是,在合并表之前,我不知道必须更新哪些行。您的意思是我应该合并它们以查看要复制的内容并再次合并它们以更新活动行吗?
  • 这有帮助 - 谢谢!我添加了我的答案,这表明我首先查询以查找需要更新的行,复制必要的行,然后在合并语句中使用该查询。

标签: sql oracle merge


【解决方案1】:

好的,首先,我将编写一个查询来生成要更新和/或插入的行:

WITH    performance AS (SELECT 1234 student_id, 5678 course_id, 'Mandatory' enrollment_type, 70 mark, 'ACTIVE' status, 2 VERSION FROM dual UNION ALL
                        SELECT 1234 student_id, 5678 course_id, 'Optional' enrollment_type, 70 mark, 'HISTORY' status, 1 VERSION FROM dual UNION ALL
                        SELECT 1234 student_id, 5678 course_id, 'Optional' enrollment_type, NULL mark, 'HISTORY' status, 0 VERSION FROM dual UNION ALL
                        SELECT 9876 student_id, 4597 course_id, 'Institutional' enrollment_type, 99 mark, 'ACTIVE' status, 1 VERSION FROM dual UNION ALL
                        SELECT 9876 student_id, 4597 course_id, 'Institutional' enrollment_type, NULL mark, 'HISTORY' status, 0 VERSION FROM dual),
  group_enrollments AS (SELECT 4976555 group_id, 1234 student_id, 5678 course_id, 'Mandatory2' enrollment_type FROM dual UNION ALL
                        SELECT 6399875 group_id, 1234 student_id, 9034 course_id, 'Optional' enrollment_type FROM dual UNION ALL
                        SELECT 6399875 group_id, 9876 student_id, 4597 course_id, 'Institutional' enrollment_type FROM dual)
-- end of mimicking your tables with data in them
SELECT res.student_id,
       res.course_id,
       CASE WHEN dummy.id = 1 THEN res.new_enrollment_type
            WHEN dummy.id = 2 THEN res.old_enrollment_type
       END enrollment_type,
       res.mark,
       CASE WHEN dummy.id = 1 THEN 'ACTIVE'
            WHEN dummy.id = 2 THEN 'HISTORY'
       END status,
       CASE WHEN dummy.id = 1 THEN res.new_version
            WHEN dummy.id = 2 THEN res.old_version
       END VERSION 
FROM   (SELECT ge.student_id,
               ge.course_id,
               ge.enrollment_type new_enrollment_type,
               p.enrollment_type old_enrollment_type,
               p.mark,
               p.status,
               p.version old_version,
               nvl(p.version + 1, 0) new_VERSION
                 -- n.b. this may produce duplicates or unique constraint errors in a concurrent environment
        FROM   group_enrollments ge
               LEFT OUTER JOIN PERFORMANCE p ON ge.student_id = p.student_id
                                                AND ge.course_id = p.course_id
        WHERE  (p.status = 'ACTIVE' OR p.status IS NULL)
        AND    (p.enrollment_type != ge.enrollment_type OR p.enrollment_type IS NULL)) res
        INNER JOIN (SELECT 1 ID FROM dual UNION ALL
                    SELECT 2 ID FROM dual) dummy ON dummy.id = 1
                                                    OR (dummy.id = 2 
                                                        AND res.status = 'ACTIVE');

STUDENT_ID  COURSE_ID ENROLLMENT_TYPE       MARK STATUS     VERSION
---------- ---------- --------------- ---------- ------- ----------
      1234       5678 Mandatory2              70 ACTIVE           3
      1234       9034 Optional                   ACTIVE           0
      1234       5678 Mandatory               70 HISTORY          2

此查询首先查找任何全新的行(即 group_enrollment 表中的行在性能表中没有行)或具有不同的注册类型。这些是需要插入或更新的行。

一旦我们知道了这一点,我们就可以加入一个虚拟的 2 行表,这样无论我们需要插入还是更新,我们总是会连接到第一个虚拟行,但我们只会连接到如果我们需要更新,第二个虚拟行。这意味着我们将只有一行用于插入,而两行用于更新。

然后根据 dummy.id 输出正确的值是一件容易的事(第一个虚拟行的新值,第二个虚拟行的旧值。

完成此操作后,我们就知道需要将哪些数据合并到性能表中,因此现在合并语句将如下所示:

merge into performance tgt
  using (SELECT res.student_id,
                res.course_id,
                CASE WHEN dummy.id = 1 THEN res.new_enrollment_type
                     WHEN dummy.id = 2 THEN res.old_enrollment_type
                END enrollment_type,
                res.mark,
                CASE WHEN dummy.id = 1 THEN 'ACTIVE'
                     WHEN dummy.id = 2 THEN 'HISTORY'
                END status,
                CASE WHEN dummy.id = 1 THEN res.new_version
                     WHEN dummy.id = 2 THEN res.old_version
                END VERSION 
         FROM   (SELECT ge.student_id,
                        ge.course_id,
                        ge.enrollment_type new_enrollment_type,
                        p.enrollment_type old_enrollment_type,
                        p.mark,
                        p.status,
                        p.version old_version,
                        nvl(p.version + 1, 0) new_VERSION
                          -- n.b. this may produce duplicates or unique constraint errors in a concurrent environment
                 FROM   group_enrollments ge
                        LEFT OUTER JOIN PERFORMANCE p ON ge.student_id = p.student_id
                                                         AND ge.course_id = p.course_id
                 WHERE  (p.status = 'ACTIVE' OR p.status IS NULL)
                 AND    (p.enrollment_type != ge.enrollment_type OR p.enrollment_type IS NULL)) res
                 INNER JOIN (SELECT 1 ID FROM dual UNION ALL
                             SELECT 2 ID FROM dual) dummy ON dummy.id = 1
                                                             OR (dummy.id = 2 
                                                                 AND res.status = 'ACTIVE')) src
    ON (tgt.student_id = src.student_id AND tgt.course_id = src.course_id AND tgt.status = src.status)
WHEN MATCHED THEN
  UPDATE SET tgt.enrollment_type = src.enrollment_type,
             tgt.version = src.version
WHEN NOT MATCHED THEN
  INSERT (tgt.student_id, tgt.course_id, tgt.enrollment_type, tgt.mark, tgt.status, tgt.version)
  VALUES (src.student_id, src.course_id, src.enrollment_type, src.mark, src.status, src.version);

为了清楚起见,这里有一个非常简单的条件复制行示例(我们也可以将其称为部分交叉连接,因为一个表中的所有行都连接到另一个表中的至少一行):

WITH sample_data AS (SELECT 100 ID, NULL status FROM dual UNION ALL -- expect only one row
                     SELECT 101 ID, 'A' status FROM dual UNION ALL -- expect two rows
                     SELECT 102 ID, 'B' status FROM dual -- expect only one row
                    )
SELECT dummy.id dummy_row_id,
       sd.id,
       sd.status
FROM   sample_data sd
       INNER JOIN (SELECT 1 ID FROM dual UNION ALL
                   SELECT 2 ID FROM dual) dummy ON dummy.id = 1
                                                   OR (dummy.id = 2 
                                                       AND sd.status = 'A')
ORDER BY sd.id, dummy.id;

DUMMY_ROW_ID         ID STATUS
------------ ---------- ------
           1        100 
           1        101 A
           2        101 A
           1        102 B

您可以看到,对于 sample_data “表”中的 id=101 行,我们有两行,但其他两个 id 各只有一行。

希望这可以为您澄清事情吗?

【讨论】:

  • 我对 SQL 还是很陌生,所以我认为我没有得到你的意思。当它找到匹配的行时如何创建一个新行?
  • 您是否看到我将 res 子查询加入到其中的虚拟子查询?这是一个两行结果集。连接条件dummy.id = 1 确保我们始终将虚拟子查询中的第一行连接到 res 子查询中的每一行。 or (dummy.id = 2 and res.status = 'ACTIVE') 条件意味着我们仅在 res.status = 'ACTIVE' 时才加入第二个虚拟行。 res.status 列仅在我们已经在性能表中有该 student_id 和 course_id 的行时才会填充,因此我们知道我们需要为此设置两行 - 更新的行和历史行。有意义吗?
  • 我还在答案的末尾添加了一个更简单的示例,希望能把事情弄清楚一点。
  • 对不起,我还是不明白。我们实际上是在代码的哪一部分插入行?合并仍然会导致匹配更新(现有行)和不匹配插入时,对吗?虚拟子查询是否修改了原点以使行不匹配并插入另一个?对不起,无知的问题,但我真的很困惑......顺便说一下,我没有指定,因为我认为它会更容易,但我正在研究一个视图,其中所有行的状态处于活动状态。
  • 谢谢,我终于搞定了,并且成功了!查询有点繁重(在我的数据库上运行它最多需要 3 分钟),但它应该不是问题,因为它会在夜间每天运行一次。我真的非常感谢你,这是解决我的问题的绝妙方法,也是了解更多 SQL 知识的好机会!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-18
  • 1970-01-01
相关资源
最近更新 更多