有时,您的1..COUNT(*)
编号目标和“不要重新编号锁定的行”会导致无法解决的冲突。例如:
NAME SEQ_NO LOCKED
Foo 1 N
Bar 13 Y
Abc 14 Y
Baz 5 N
Cde 7 N
我将假设这种情况所需的输出是:
NAME SEQ_NO LOCKED
Foo 1 N
Baz 2 N
Cde 3 N
Bar 13 Y
Abc 14 Y
您的示例显示未锁定的数据按其原始序列号顺序保留,而锁定的数据显然没有得到新的编号。
我假设原始数据中没有重复的序列号。
快速总结
这是一个有趣而棘手的问题。重新排序数据的关键是知道在哪里放置未锁定的行。在示例数据中:
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
我们可以给解锁的行一个从 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 }。锁定的行产生一组新:旧值,其中新 = 旧在每种情况下,所以D { 3:3, 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。
可重新编号的行
-- 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
在适当的时候,可以使用 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
这是一个非等值连接,这就是使它不是特别快的操作的原因。
不可重新编号的行
-- Query 3
SELECT Name, Seq_No
FROM My_Table
WHERE Locked = 'Y'
ORDER BY Seq_No;
NAME Seq_No
Bar 3
Abc 4
新的序列号
假设我们设法得到一个数字列表,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
然后我们可以从此列表中删除锁定的序列号:
-- 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
现在我们需要对它们进行排名,这意味着另一个自连接,但这次是在那个表达式上。是时候使用“通用表表达式”或 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
整理起来
所以,现在我们需要将该结果与查询 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
这需要结合查询 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
结果集
结合这些产量:
-- 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;
这给出了结果:
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
因此,可以(尽管远非简单)编写一个正确排序数据的 SELECT 语句。
转换为 UPDATE 操作
现在我们必须找到一种方法将这种怪物放入 UPDATE 语句中。留给我自己的设备,我会考虑将查询 8 的结果选择到临时表中的事务,然后从源表 ( My_Table
) 中删除所有记录并将查询 8 的结果的适当项目插入到原始表,然后提交。
Oracle 似乎不支持动态创建的“每个会话”临时表;只有全局临时表。并且有充分的理由不使用它们,因为它们都是 SQL 标准。尽管如此,它会在这里解决问题,我不确定还有什么可以工作:
与这项工作分开:
CREATE GLOBAL TEMPORARY TABLE ReSequenceTable
(
Name CHAR(3) NOT NULL,
Seq_No INTEGER NOT NULL,
Locked CHAR(1) NOT NULL
)
ON COMMIT DELETE ROWS;
然后:
-- 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;
您可能可以通过适当的更新来做到这一点;你需要做一些思考。
概括
这并不容易,但可以做到。
关键步骤(至少对我而言)是来自Query 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;