如何重新排序表中的项目

kan*_*e77 10 sql oracle

我有一个表(称之为my_table),可以像这样简化:NAME,SEQ_NO,LOCKED.

项目被删除并添加,我想重新排序它们(修改SEQ_NO),顺序总是从1到COUNT(*),并且被锁定的项保留它们的SEQ_NO,没有未锁定的项目将获得该数字.仅使用新的SEQ_NO更新未锁定的项目.

例:

这个

NAME  SEQ_NO    LOCKED
Foo   1         N
Bar   3         Y
Abc   4         Y
Baz   5         N
Cde   7         N

会导致:

NAME  SEQ_NO    LOCKED
Foo   1         N
Baz   2         N
Bar   3         Y
Abc   4         Y
Cde   5         N

我怎么能这样做?

Jon*_*ler 8

有时你的1..COUNT(*)编号和'不重新编号锁定的行'的目标导致无法解决的冲突.例如:

NAME  SEQ_NO    LOCKED
Foo    1        N
Bar   13        Y
Abc   14        Y
Baz    5        N
Cde    7        N
Run Code Online (Sandbox Code Playgroud)

我将假设此场景所需的输出是:

NAME  SEQ_NO    LOCKED
Foo    1        N
Baz    2        N
Cde    3        N
Bar   13        Y
Abc   14        Y
Run Code Online (Sandbox Code Playgroud)

您的示例显示解锁数据保持其原始序列号顺序,并且锁定数据显然不会获得新数字.

我假设原始数据中没有重复的序列号.


快速摘要

这是一个有趣且棘手的问题.重新排序数据的关键是知道放置未锁定行的位置.在示例数据中:

NAME  OLD_SEQ   LOCKED   NEW_SEQ
Foo   1         N        1
Bar   3         Y        3
Abc   4         Y        4
Baz   5         N        2
Cde   7         N        5
Run Code Online (Sandbox Code Playgroud)

我们可以给解锁的行一个从1..3开始计数的序列号,因此我们最终得到一对ord:old序列A {1:1,2:5,3:7}.我们可以为结果集1..5生成一个槽列表.我们从那个插槽列表中删除由锁定行保存的插槽,将{1,2,5}作为重新排序列表中未锁定行占用的插槽列表.然后我们按顺序编号,留下对ord:new B {1:1,2:2,3:5}.然后我们可以在第一个字段上加入这两个列表A和B,并抛弃排序,留下一对新的:旧的插槽号C {1:1,2:5,5:7}.锁定的行产生一组新的:旧值,其中new = old,因此D {3:3,4,4:4}.最终结果是C和D的并集,因此结果集包含:

  • 新序列号1中的旧序列号1;
  • 新2中的旧5;
  • (新3中的旧3);
  • (新4中的旧4); 和
  • 新5中的老7.

这适用于锁定行也具有序列号13和14的情况; 解锁的行被分配新的序列号1,2,3,并且锁定的行保持不变.该问题的一条评论询问"1个锁定,5个解锁,10个锁定"; 这会产生'1个锁定,2个解锁,10个锁定'.

在SQL中实现这一点需要相当多的SQL.掌握OLAP功能的人可能比我的代码更快地到达那里.将SELECT结果转换为UPDATE语句也很棘手(并没有完全解决).但能够以正确的结果顺序获取数据是至关重要的,解决这个问题的关键是列表A和B所代表的排序步骤.


TDQD - 测试驱动的查询设计

与任何复杂的SQL查询操作一样,秘诀是逐步构建查询.如上所述,我们需要以不同方式处理锁定和未锁定的行.在这种情况下,目标最终是UPDATE语句,但我们需要知道如何为UPDATE生成数据,因此我们首先执行SELECT.

Renumberable行

-- Query 1
SELECT Name, Seq_No
  FROM My_Table
 WHERE Locked = 'N'
 ORDER BY Seq_No;

NAME  SEQ_NO
Foo   1
Baz   5
Cde   7
Run Code Online (Sandbox Code Playgroud)

在适当的情况下,这些可以使用ORDER BY子句进行排序,但子查询通常不允许使用ORDER BY子句,我们需要生成一个数字.使用OLAP函数,您可以更紧凑地执行此操作.在Oracle中,您可以使用ROWNUM生成行号.有一个技巧可以在任何DBMS中使用,虽然它不是特别快.

假设没有来自锁定行的干扰,重新编号的行

-- Query 2
SELECT m1.Name, m1.Seq_No AS Old_Seq, COUNT(*) AS New_Seq
  FROM My_Table m1
  JOIN My_Table m2
    ON m1.Seq_No >= m2.Seq_No
 WHERE m1.Locked = 'N' AND m2.Locked = 'N'
 GROUP BY m1.Name, m1.Seq_No
 ORDER BY New_Seq;

NAME  Old_Seq   New_Seq
Foo   1         1
Baz   5         2
Cde   7         3
Run Code Online (Sandbox Code Playgroud)

这是一个非等值的,这使得这不是特别快的操作.

不可思议的行

-- Query 3
SELECT Name, Seq_No
  FROM My_Table
 WHERE Locked = 'Y'
 ORDER BY Seq_No;

NAME  Seq_No
Bar   3
Abc   4
Run Code Online (Sandbox Code Playgroud)

新序列号

假设我们设法得到一个数字列表,1..N(样本数据中N = 5).我们从该列表中删除锁定的条目(3,4),离开(1,2,5).当这些排名(1 = 1,2 = 2,3 = 5)时,我们可以使用未锁定记录新序列加入排名,但使用另一个数字作为记录的最终序列号.这让我们有一些小问题需要解决.首先,生成每个数字1..N; 我们可以做其中一个可怕的小非等值技巧,但应该有一个更好的方法:

-- Query 4
SELECT COUNT(*) AS Ordinal
  FROM My_Table AS t1
  JOIN My_Table AS t2
    ON t1.Seq_No >= t2.Seq_No
 GROUP BY t1.Seq_No
 ORDER BY Ordinal;

Ordinal
1
2
3
4
5
Run Code Online (Sandbox Code Playgroud)

然后我们可以从此列表中删除锁定的序列号:

-- Query 5
SELECT Ordinal
  FROM (SELECT COUNT(*) AS ordinal
          FROM My_Table t1
          JOIN My_Table t2
            ON t1.Seq_No <= t2.Seq_No
         GROUP BY t1.Seq_No
       ) O
 WHERE O.Ordinal NOT IN (SELECT Seq_No FROM My_Table WHERE Locked = 'Y')
 ORDER BY Ordinal;

 Ordinal
 1
 2
 5
Run Code Online (Sandbox Code Playgroud)

现在我们需要对这些进行排名,这意味着另一个自连接,但这一次是在表达式上.是时候使用'Common Table Expressions'或CTE,也称为'WITH子句':

-- Query 6
WITH HoleyList AS
    (SELECT ordinal
       FROM (SELECT COUNT(*) ordinal
               FROM My_Table t1
               JOIN My_Table t2
                 ON t1.seq_no <= t2.seq_no
              GROUP BY t1.seq_no
            ) O
      WHERE O.Ordinal NOT IN (SELECT Seq_No FROM My_Table WHERE Locked = 'Y')
    )
SELECT H1.Ordinal, COUNT(*) AS New_Seq
  FROM HoleyList H1
  JOIN HoleyList H2
    ON H1.Ordinal >= H2.Ordinal
 GROUP BY H1.Ordinal
 ORDER BY New_Seq;

Ordinal  New_Seq
1        1
2        2
5        3
Run Code Online (Sandbox Code Playgroud)

整理起来

因此,现在我们需要将结果与查询2连接以获取未锁定行的最终数字,然后将其与查询3的并集以获得所需的输出.当然,我们必须在输出中获得Locked的正确值.仍然是逐步的:

-- Query 7
WITH
Query2 AS
   (SELECT m1.Name, m1.Seq_No AS Old_Seq, COUNT(*) AS New_Seq
      FROM My_Table m1
      JOIN My_Table m2 ON m1.Seq_No <= m2.Seq_No
     WHERE m1.Locked = 'N' AND m2.Locked = 'N'
     GROUP BY m1.Name, m1.Seq_No
   ),
HoleyList AS
   (SELECT ordinal
      FROM (SELECT COUNT(*) AS ordinal
              FROM My_Table t1
              JOIN My_Table t2
                ON t1.seq_no <= t2.seq_no
             GROUP BY t1.seq_no
           ) O
      WHERE O.Ordinal NOT IN (SELECT Seq_No FROM My_Table WHERE Locked = 'Y')
    ),
Reranking AS    
   (SELECT H1.Ordinal, COUNT(*) AS New_Seq
      FROM HoleyList H1
      JOIN HoleyList H2
        ON H1.Ordinal >= H2.Ordinal
     GROUP BY H1.Ordinal
   )
SELECT r.Ordinal, r.New_Seq, q.Name, q.Old_Seq, 'N' Locked
  FROM Reranking r
  JOIN Query2    q
    ON r.New_Seq = q.New_Seq
 ORDER BY r.New_Seq;

Ordinal  New_Seq  Name  Old_Seq  Locked
1        1        Cde   7        N
2        2        Baz   5        N
5        3        Foo   1        N
Run Code Online (Sandbox Code Playgroud)

这需要与查询3的变体结合使用:

-- Query 3a
SELECT Seq_No Ordinal, Seq_No New_Seq, Name, Seq_No Old_Seq, Locked
  FROM My_Table
 WHERE Locked = 'Y'
 ORDER BY New_Seq;

Ordinal  New_Seq  Name  Old_Seq  Locked
3        3        Bar   3        Y
4        4        Abc   4        Y
Run Code Online (Sandbox Code Playgroud)

结果集

结合这些产量:

-- Query 8
WITH
Query2 AS
   (SELECT m1.Name, m1.Seq_No AS Old_Seq, COUNT(*) AS New_Seq
      FROM My_Table m1
      JOIN My_Table m2 ON m1.Seq_No <= m2.Seq_No
     WHERE m1.Locked = 'N' AND m2.Locked = 'N'
     GROUP BY m1.Name, m1.Seq_No
   ),
HoleyList AS
   (SELECT ordinal
      FROM (SELECT COUNT(*) AS ordinal
              FROM My_Table t1
              JOIN My_Table t2
                ON t1.seq_no <= t2.seq_no
             GROUP BY t1.seq_no
           ) O
      WHERE O.Ordinal NOT IN (SELECT Seq_No FROM My_Table WHERE Locked = 'Y')
    ),
Reranking AS    
   (SELECT H1.Ordinal, COUNT(*) AS New_Seq
      FROM HoleyList H1
      JOIN HoleyList H2
        ON H1.Ordinal >= H2.Ordinal
     GROUP BY H1.Ordinal
   ),
Query7 AS
   (SELECT r.Ordinal, r.New_Seq, q.Name, q.Old_Seq, 'N' Locked
      FROM Reranking r
      JOIN Query2    q
        ON r.New_Seq = q.New_Seq
   ),
Query3a AS
   (SELECT Seq_No Ordinal, Seq_No New_Seq, Name, Seq_No Old_Seq, Locked
      FROM My_Table
     WHERE Locked = 'Y'
   )
SELECT Ordinal, New_Seq, Name, Old_Seq, Locked
  FROM Query7
UNION
SELECT Ordinal, New_Seq, Name, Old_Seq, Locked
  FROM Query3a
 ORDER BY New_Seq;
Run Code Online (Sandbox Code Playgroud)

这给出了结果:

Ordinal  New_Seq  Name  Old_Seq  Locked
1        1        Cde   7        N
2        2        Baz   5        N
3        3        Bar   3        Y
4        4        Abc   4        Y
5        3        Foo   1        N
Run Code Online (Sandbox Code Playgroud)

因此,编写一个正确排序数据的SELECT语句是可能的(尽管很容易).

转换为UPDATE操作

现在我们必须找到一种方法将monstrosity转换为UPDATE语句.留给我自己的设备,我会考虑一个事务,它将查询8的结果选入一个临时表,然后从源表(My_Table)中删除所有记录,并将查询结果的相应项目插入到原始表然后提交.

Oracle似乎不支持动态创建的"每个会话"临时表; 只有全局临时表.并且有充分的理由不使用它们,因为它们都是SQL Standard.不过,它会在这里诀窍,我不知道还有什么工作:

与此工作分开:

CREATE GLOBAL TEMPORARY TABLE ReSequenceTable
(
    Name     CHAR(3) NOT NULL,
    Seq_No   INTEGER NOT NULL,
    Locked   CHAR(1) NOT NULL
)
ON COMMIT DELETE ROWS;
Run Code Online (Sandbox Code Playgroud)

然后:

-- Query 8a
BEGIN;   -- May be unnecessary and/or unsupported in Oracle
INSERT INTO ReSequenceTable(Name, Seq_No, Locked)
    WITH
    Query2 AS
       (SELECT m1.Name, m1.Seq_No AS Old_Seq, COUNT(*) AS New_Seq
          FROM My_Table m1
          JOIN My_Table m2 ON m1.Seq_No <= m2.Seq_No
         WHERE m1.Locked = 'N' AND m2.Locked = 'N'
         GROUP BY m1.Name, m1.Seq_No
       ),
    HoleyList AS
       (SELECT ordinal
          FROM (SELECT COUNT(*) AS ordinal
                  FROM My_Table t1
                  JOIN My_Table t2
                    ON t1.seq_no <= t2.seq_no
                 GROUP BY t1.seq_no
               ) O
          WHERE O.Ordinal NOT IN (SELECT Seq_No FROM My_Table WHERE Locked = 'Y')
        ),
    Reranking AS    
       (SELECT H1.Ordinal, COUNT(*) AS New_Seq
          FROM HoleyList H1
          JOIN HoleyList H2
            ON H1.Ordinal >= H2.Ordinal
         GROUP BY H1.Ordinal
       ),
    Query7 AS
       (SELECT r.Ordinal, r.New_Seq, q.Name, q.Old_Seq, 'N' Locked
          FROM Reranking r
          JOIN Query2    q
            ON r.New_Seq = q.New_Seq
       ),
    Query3a AS
       (SELECT Seq_No Ordinal, Seq_No New_Seq, Name, Seq_No Old_Seq, Locked
          FROM My_Table
         WHERE Locked = 'Y'
       )
    SELECT Name, Ordinal, Locked
      FROM Query7
    UNION
    SELECT Name, Ordinal, Locked
      FROM Query3a;

DELETE FROM My_Table;
INSERT INTO My_Table(Name, Seq_No, Locked) FROM ReSequenceTable;
COMMIT;
Run Code Online (Sandbox Code Playgroud)

您可以使用适当的UPDATE来完成它; 你需要做一些思考.


摘要

这并不容易,但可以做到.

关键步骤(至少对我来说)是从结果集查询6,其中制定了解锁行的新位置在更新后的结果集.这并不是很明显,但产生答案至关重要.

其余的只是围绕该关键步骤的支持代码.

如前所述,可能有很多方法可以改进某些查询.例如,1..N从表中生成序列可能很简单SELECT ROWNUM FROM My_Table,这会压缩查询(非常有用 - 它很详细).有OLAP功能; 其中一个或多个可以帮助进行排名操作(可能更简洁;也可以更好地执行).

所以,这不是一个优秀的最终答案; 但这是对正确方向的有力推动.


PoC测试

该代码已针对Informix进行了测试.我不得不使用一些不同的符号,因为Informix(尚未)支持CTE.它确实具有非常方便,非常简单的每个会话动态临时表,INTO TEMP <temp-table-name>这些表出现在ORDER BY子句可能出现的位置.因此,我模拟了查询8a:

+ BEGIN;
+ SELECT O.Ordinal
  FROM (SELECT COUNT(*) AS ordinal
          FROM My_Table AS t1
          JOIN My_Table AS t2
            ON t1.Seq_No <= t2.Seq_No
         GROUP BY t1.Seq_No
       ) AS O
 WHERE O.Ordinal NOT IN (SELECT Seq_No FROM My_Table WHERE Locked = 'Y')
 INTO TEMP HoleyList;
+ SELECT * FROM HoleyList ORDER BY Ordinal;
1
2
5
+ SELECT H1.Ordinal, COUNT(*) AS New_Seq
  FROM HoleyList AS H1
  JOIN HoleyList AS H2
    ON H1.Ordinal >= H2.Ordinal
 GROUP BY H1.Ordinal
 INTO TEMP ReRanking;
+ SELECT * FROM ReRanking ORDER BY Ordinal;
1|1
2|2
5|3
+ SELECT m1.Name, m1.Seq_No AS Old_Seq, COUNT(*) AS New_Seq
  FROM My_Table m1
  JOIN My_Table m2
    ON m1.Seq_No >= m2.Seq_No
 WHERE m1.Locked = 'N' AND m2.Locked = 'N'
 GROUP BY m1.Name, m1.Seq_No
  INTO TEMP Query2;
+ SELECT * FROM Query2 ORDER BY New_Seq;
Foo|1|1
Baz|5|2
Cde|7|3
+ SELECT r.Ordinal, r.New_Seq, q.Name, q.Old_Seq, 'N' Locked
  FROM Reranking r
  JOIN Query2    q
    ON r.New_Seq = q.New_Seq
  INTO TEMP Query7;
+ SELECT * FROM Query7 ORDER BY Ordinal;
1|1|Foo|1|N
2|2|Baz|5|N
5|3|Cde|7|N
+ SELECT Seq_NO Ordinal, Seq_No New_Seq, Name, Seq_No Old_Seq, Locked
  FROM My_Table
 WHERE Locked = 'Y'
  INTO TEMP Query3a;
+ SELECT * FROM Query3a ORDER BY Ordinal;
3|3|Bar|3|Y
4|4|Abc|4|Y
+ SELECT Ordinal, New_Seq, Name, Old_Seq, Locked
  FROM Query7
UNION
SELECT Ordinal, New_Seq, Name, Old_Seq, Locked
  FROM Query3a
  INTO TEMP Query8;
+ SELECT * FROM Query8 ORDER BY Ordinal;
1|1|Foo|1|N
2|2|Baz|5|N
3|3|Bar|3|Y
4|4|Abc|4|Y
5|3|Cde|7|N
+ ROLLBACK;
Run Code Online (Sandbox Code Playgroud)

  • 答案很长.你可以参加TLAS(StackOverflow上最长的答案):)我会在我的咖啡休息时间阅读它;) (3认同)