3

維持しなければならないレガシー製品があります。表の 1 つは、次の例に多少似ています。

DECLARE @t TABLE
(
 id INT,
 DATA NVARCHAR(30)
);

INSERT  INTO @t
        SELECT  1,
                'name: Jim Ey'
        UNION ALL
        SELECT  2,
                'age: 43'
        UNION ALL
        SELECT  3,
                '----------------'
        UNION ALL
        SELECT  4,
                'name: Johnson Dom'
        UNION ALL
        SELECT  5,
                'age: 34'
        UNION ALL
        SELECT  6,
                '----------------'
        UNION ALL
        SELECT  7,
                'name: Jason Thwe'
        UNION ALL
        SELECT  8,
                'age: 22'

SELECT  *
FROM    @t;
/*
You will get the following result
id          DATA
----------- ------------------------------
1           name: Jim Ey
2           age: 43
3           ----------------
4           name: Johnson Dom
5           age: 34
6           ----------------
7           name: Jason Thwe
8           age: 22
*/

今、次の形式で情報を取得したいと思います。

name           age
-------------- --------
Jim Ey         43
Johnson Dom    34
Jason Thwe     22

これを行う最も簡単な方法は何ですか? ありがとう。

4

4 に答える 4

4

(少し病的な)好奇心から、私はあなたが提供した正確な入力データを変換する手段を考え出そうとしました。

もちろん、はるかに優れているのは、元のデータを適切に構造化することです。レガシーシステムでは、これは不可能な場合がありますが、ETLプロセスを作成してこの情報を中間の場所に移動し、このような醜いクエリをリアルタイムで実行する必要がないようにすることができます。

例1

この例では、すべてのIDが一貫していて連続していることを前提としています(そうでない場合は、ROW_NUMBER()IDに対する正しい剰余演算を保証するために、追加の列または新しいID列を使用する必要があります)。

SELECT
    Name = REPLACE( Name, 'name: ', '' ),
    Age = REPLACE( Age, 'age: ', '' )
FROM
(
    SELECT
        Name = T2.Data,
        Age = T1.Data,
        RowNumber = ROW_NUMBER() OVER( ORDER BY T1.Id ASC )

    FROM @t T1 
        INNER JOIN @t T2 ON T1.id = T2.id +1 -- offset by one to combine two rows
    WHERE T1.id % 3 != 0 -- skip delimiter records
) Q1
 -- skip every other record (minus delimiters, which have already been stripped)
WHERE RowNumber % 2 != 0

例2:シーケンシャルIDに依存しない

実際のID値は重要ではなく、行シーケンスのみが重要であるため、これはより実用的な例です。

DECLARE @NumberedData TABLE( RowNumber INT, Data VARCHAR( 100 ) );

INSERT @NumberedData( RowNumber, Data )
    SELECT 
        RowNumber = ROW_NUMBER() OVER( ORDER BY id ASC ),
        Data
    FROM @t;

SELECT 
    Name = REPLACE( N2.Data, 'name: ', '' ),
    Age = REPLACE( N1.Data, 'age: ', '' ) 
FROM @NumberedData N1 
    INNER JOIN @NumberedData N2 ON N1.RowNumber = N2.RowNumber + 1
WHERE ( N1.RowNumber % 3 ) = 2;

DELETE @NumberedData;

例3:カーソル

繰り返しになりますが、このようなクエリをリアルタイムで実行することは避け、スケジュールされたトランザクションETLプロセスを使用するのが最善です。私の経験では、このような半構造化データは異常になりがちです。

例1と2(および他の人が提供するソリューション)はデータを操作する巧妙な方法を示していますが、このデータを変換するためのより実用的な方法はカーソルです。なんで?実際にはパフォーマンスが向上する可能性があり(ネストされたクエリ、再帰、ピボット、または行の番号付けはありません)、低速であってもエラー処理の機会がはるかに多くなります。

-- this could be a table variable, temp table, or staging table
DECLARE @Results TABLE ( Name VARCHAR( 100 ), Age INT );

DECLARE @Index INT = 0, @Data VARCHAR( 100 ), @Name VARCHAR( 100 ), @Age INT;

DECLARE Person_Cursor CURSOR FOR SELECT Data FROM @t;
OPEN Person_Cursor;
FETCH NEXT FROM Person_Cursor INTO @Data;

WHILE( 1 = 1 )BEGIN -- busy loop so we can handle the iteration following completion
    IF( @Index = 2 ) BEGIN
        INSERT @Results( Name, Age ) VALUES( @Name, @Age );
        SET @Index = 0;
    END
    ELSE BEGIN
            -- optional: examine @Data for integrity

        IF( @Index = 0 ) SET @Name = REPLACE( @Data, 'name: ', '' );
        IF( @Index = 1 ) SET @Age = CAST( REPLACE( @Data, 'age: ', '' ) AS INT );
        SET @Index = @Index + 1;
    END

    -- optional: examine @Index to see that there are no superfluous trailing 
    -- rows or rows omitted at the end.

    IF( @@FETCH_STATUS != 0 ) BREAK;
    FETCH NEXT FROM Person_Cursor INTO @Data;
END

CLOSE Person_Cursor;
DEALLOCATE Person_Cursor;

パフォーマンス

10万行のサンプルソースデータを作成しました。前述の3つの例は、データの変換とほぼ同等のようです。

100万行のソースデータを作成しました。次のようなクエリを使用すると、行のサブセット(Webページやレポートのグリッドで使用されるものなど)を選択するための優れたパフォーマンスが得られます。

-- INT IDENTITY( 1, 1 ) numbers the rows for us
DECLARE @NumberedData TABLE( RowNumber INT IDENTITY( 1, 1 ), Data VARCHAR( 100 ) );

-- subset selection; ordering/filtering can be done here but it will need to preserve
-- the original 3 rows-per-result structure and it will impact performance
INSERT @NumberedData( Data )
    SELECT TOP 1000 Data FROM @t;

SELECT
    N1.RowNumber,
    Name = REPLACE( N2.Data, 'name: ', '' ),
    Age = REPLACE( N1.Data, 'age: ', '' ) 
FROM @NumberedData N1 
    INNER JOIN @NumberedData N2 ON N1.RowNumber = N2.RowNumber + 1
WHERE ( N1.RowNumber % 3 ) = 2;

DELETE @NumberedData;

百万のレコードのセットに対して4-10ms(i7-3960x)の実行時間が見られます。

于 2012-04-22T09:00:47.707 に答える
1

そのテーブルが与えられると、これを行うことができます:

;WITH DATA
AS
(
    SELECT
        SUBSTRING(t.DATA,CHARINDEX(':',t.DATA)+2,LEN(t.DATA)) AS value,
        SUBSTRING(t.DATA,0,CHARINDEX(':',t.DATA)) AS ValueType,
        ID,
        ROW_NUMBER() OVER(ORDER BY ID) AS RowNbr
    FROM
        @t AS t
    WHERE
        NOT t.DATA='----------------'
)
, RecursiveCTE
AS
(
    SELECT
        Data.RowNbr,
        Data.value,
        Data.ValueType,
        NEWID() AS ID
    FROM
        Data
    WHERE
        Data.RowNbr=1
    UNION ALL
    SELECT
        Data.RowNbr,
        Data.value,
        Data.ValueType,
        CASE 
            WHEN Data.ValueType='age'
            THEN RecursiveCTE.ID
            ELSE NEWID()
        END AS ID
    FROM
        Data
        JOIN RecursiveCTE
            ON RecursiveCTE.RowNbr+1=Data.RowNbr
)
SELECT
    pvt.name,
    pvt.age
FROM
    (
        SELECT
            ID,
            value,
            ValueType
        FROM
            RecursiveCTE
    ) AS SourceTable
    PIVOT
    (
        MAX(Value)
        FOR ValueType IN ([name],[age])
    ) AS pvt

出力

Name          Age
------------------
Jim Ey        43
Jason Thwe    22
Johnson Dom   34
于 2012-04-22T09:13:21.007 に答える
1

自己結合、再帰を使用せず、 からの行を 1 回パスする 1 つのソリューション@t:

SELECT  *
FROM
(
        SELECT  
                CASE 
                    WHEN a.DATA LIKE 'name:%' THEN 'Name'
                    ELSE 'Age'
                END AS Attribute,
                CASE 
                    WHEN a.DATA LIKE 'name:%' THEN SUBSTRING(a.DATA, 7, 4000) --or LTRIM(SUBSTRING(...,6,...))
                    ELSE SUBSTRING(a.DATA, 6, 4000) --or LTRIM(SUBSTRING(...,5,...))
                END AS Value,
                (ROW_NUMBER() OVER(ORDER BY id) + 1) / 2 AS PseudoDenseRank
        FROM    @t a
        WHERE   a.DATA LIKE 'name:%' OR a.DATA LIKE 'age:%'
) b
PIVOT( MAX(b.Value) FOR b.Attribute IN ([Name], [Age]) ) pvt

結果:

PseudoDenseRank Name        Age
--------------- ----------- ---
1               Jim Ey      43
2               Johnson Dom 34
3               Jason Thwe  22

注 1: 派生テーブルbは、 を使用してグループ化さname:%れ、age:%行が作成され(ROW_NUMBER() OVER(ORDER BY id) + 1) / 2ます。派生テーブル の結果b:

Attribute Value       ROW_NUMBER() OVER(ORDER BY id) PseudoDenseRank
--------- ----------- ------------------------------ ---------------
Name      Jim Ey      1                              1
Age       43          2                              1
Name      Johnson Dom 3                              2
Age       34          4                              2
Name      Jason Thwe  5                              3
Age       22          6                              3

注 2:列の値idにギャップがない(a.id + 1) / 2 AS PseudoDenseRank場合 (例: (id 1, name:Jim Ey), (id 3 age: 43) )の代わりに使用できます(ROW_NUMBER() OVER(ORDER BY id) + 1) / 2 AS PseudoDenseRank

注 3:ソリューションを(名前と年齢の行をグループ化するために)使用する場合(a.id + 1) / 2 AS PseudoDenseRank、最初の id 値は奇数にする必要があります。最初の id 値が偶数の場合は、この式を使用する必要がありますa.id / 2 AS PseudoDenseRank

于 2012-04-22T09:43:46.630 に答える
0

集計関数の OVER 句を実装する SQL Server 2012 にアップグレードする場合の別のオプションを次に示します。このアプローチにより、必要なことがわかっているタグのみを選択して、名前の間にいくつの行があるかに関係なくそれらを見つけることができます。

これは、1 人の人物を表す行のグループ内で名前と年齢が常に同じ順序であるとは限らない場合にも機能します。

with Ready2Pivot(tag,val,part) as (
  select
    CASE WHEN DATA like '_%:%' THEN SUBSTRING(DATA,1,CHARINDEX(':',DATA)-1) END as tag,
    CASE WHEN DATA like '_%:%' THEN SUBSTRING(DATA,CHARINDEX(':',DATA)+1,8000) END as val,
    max(id * CASE WHEN DATA LIKE 'name:%' THEN 1 ELSE 0 END)
    over (
      order by id
    )
  from @t
  where DATA like '_%:%'
)
  select [name], [age]
  from Ready2Pivot
  pivot (
    max(val)
    for tag in ([name], [age])
  ) as p

レガシ データに余分な項目 ("altName: Jimmy" など) を含むエントリがある場合、このクエリはそれを無視します。古いデータに誰かの年齢の行 (および ID 番号) がない場合、その場所に NULL が返されます。すべての情報を「name: ...」を DATA として最も近い前の行に関連付けるため、行のすべてのグループに「name: ...」行があることが重要です。

于 2012-04-23T01:32:56.237 に答える