10

my_tableNAME、SEQ_NO、LOCKED のように簡略化できるテーブルがあります (と呼びます)。

アイテムが削除および追加され、シーケンスが常に 1 から COUNT(*) になり、ロックされているアイテムが SEQ_NO を保持し、ロックされていないアイテムがその番号を取得しないように、それらを並べ替えます (SEQ_NO を変更します)。ロックされていないアイテムのみが新しい SEQ_NO で更新されます。

例:

これ

名前 SEQ_NO LOCKED
ふー1N
バー 3 Y
Abc 4 Y
バズ5N
Cde 7 N

次のようになります。

名前 SEQ_NO LOCKED
ふー1N
バズ 2 N
バー 3 Y
Abc 4 Y
Cde 5 N

どうすればそれができますか?

4

4 に答える 4

8

番号付けと「ロックされた行の番号を変更しない」という目標が、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 }を残します。次に、最初のフィールドでこれら 2 つのリスト A と B を結合し、シーケンスを射影して、新しい: 古いスロット番号のペアC { 1:1, 2:5, 5:7 }を残すことができます。ロックされた行は、new = oldの一連のnew:old値を生成します。いずれの場合も、D { 3:3, 4:4 }です。最終結果は C と D の結合であるため、結果セットには次のものが含まれます。

  • 新しいシーケンス番号 1 の古いシーケンス番号 1。
  • 古い 5 を新しい 2 に。
  • (新しい 3 の古い 3);
  • (新しい 4 の古い 4); と
  • 古い 7 を新しい 5 に。

これは、ロックされた行のシーケンス番号が 13 と 14 の場合にも機能します。ロックされていない行には新しいシーケンス番号 1、2、3 が割り当てられ、ロックされている行は変更されません。質問へのコメントの 1 つは、「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 のそれぞれを生成します。これらの恐ろしい小さな非等結合トリックの 1 つを実行できますが、もっと良い方法があるはずです。

-- 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

次に、それらをランク付けする必要があります。これは、別の自己結合を意味しますが、今回はその式についてです。「共通テーブル式」または「WITH 句」とも呼ばれる CTE を使用する時が来ました。

-- 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;

おそらく適切な UPDATE でそれを行うことができます。あなたはいくつかの考えをする必要があります。


概要

簡単ではありませんが、それは可能です。

重要なステップ(少なくとも私にとって) は、更新された結果セット内のロックされていない行の新しい位置を計算するクエリ 6の結果セットでした。これはすぐにはわかりませんが、答えを出すためには重要です。

残りは、その重要なステップにラップされたサポート コードです。

前述のように、一部のクエリを改善するには多くの方法がある可能性があります。たとえば1..N、テーブルからシーケンスを生成することは、クエリを圧縮するのと同じくらい簡単かもしれませんSELECT ROWNUM FROM My_Table(非常に有益です — 冗長です)。OLAP 関数があります。それらの 1 つまたは複数は、ランキング操作に役立つ可能性があります (おそらく、より簡潔に言えば、パフォーマンスも向上します)。

したがって、これは洗練された最終的な回答ではありません。しかし、それは正しい一般的な方向への強力なプッシュです。


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;
于 2012-05-14T04:15:19.227 に答える
4
merge into my_table
using (
   select rowid as rid,
          row_number() over (order by seq_no) as rn
   from my_table
   where locked = 'N'
) t on (t.rid = my_table.rowid) 
when matched then update
   set seq_no = t.rn;
于 2012-05-03T14:13:08.583 に答える
2

これですべてのデータ ケースが得られるわけではありませんが、サンプル データでは機能します。

update my_table mt
set seq_no = 
(with renumber as (select /*+ MATERIALIZE */ rownum rn, name, seq_no, locked
from
(
select * from my_table
where locked = 'N'
order by seq_no
)
)
select rn from renumber rn where rn.seq_no = mt.seq_no
)
where locked = 'N'
;

以下の完全に実行された例:

create table my_table as
select 'Foo' name, 1 seq_no, 'N' locked from dual union
select 'Bar' name, 3 seq_no, 'Y' locked from dual union
select 'Baz' name, 5 seq_no, 'N' locked from dual 
order by seq_no
;

select * from my_table
order by seq_no
;


update my_table mt
set seq_no = 
(with renumber as (select /*+ MATERIALIZE */ rownum rn, name, seq_no, locked
from
(
select * from my_table
where locked = 'N'
order by seq_no
)
)
select rn from renumber rn where rn.seq_no = mt.seq_no
)
where locked = 'N'
;

select * from my_table
order by seq_no
;
于 2012-05-03T14:08:22.827 に答える