列NVARCHAR(4000)からNVARCHAR(260)への高速変更


12

いくつかのNVARCHAR(4000)列でこのテーブルを処理する非常に大きなメモリ許可でパフォーマンスの問題があります。これらの列はより大きいことはありませんNVARCHAR(260)

を使用して

ALTER TABLE [table] ALTER COLUMN [col] NVARCHAR(260) NULL

SQL Serverはテーブル全体を書き換えます(ログスペースで2倍のテーブルサイズを使用します)。これは何十億行で、何も変更しないだけで、オプションではありません。列の幅を大きくしてもこの問題はありませんが、小さくすると問題が発生します。

制約を作成しようとしたCHECK (DATALENGTH([col]) <= 520)CHECK (LEN([col]) <= 260)、SQL Serverがテーブル全体を書き直すことにしました。

列のデータ型をメタデータのみの操作として変更する方法はありますか?テーブル全体を書き換える費用なしで?SQL Server 2017(14.0.2027.2および14.0.3192.2)を使用しています。

以下は、再現に使用するサンプルDDLテーブルです。

CREATE TABLE [table](
    id INT IDENTITY(1,1) NOT NULL,
    [col] NVARCHAR(4000) NULL,
    CONSTRAINT [PK_test] PRIMARY KEY CLUSTERED (id ASC)
);

そして、を実行しALTERます。

回答:


15

列のデータ型をメタデータのみの操作として変更する方法はありますか?

私はそうは思わない、これが今の製品の仕組みだ。Joeの回答で提案されたこの制限に対するいくつかの本当に素晴らしい回避策があります

...結果として、SQL Serverはテーブル全体を書き換えます(ログ領域で2倍のテーブルサイズを使用します)

その声明の2つの部分に個別に対応します。

テーブルの書き換え

前述したように、これを回避する方法はありません。顧客としての私たちの観点から完全に意味をなさない場合でも、それは状況の現実のようです。

DBCC PAGE列を4000から260に変更する前後を見ると、すべてのデータがデータページに複製されていることがわかります(私のテストテーブル'A'の行数は260回でした)。

前後のdbccページのデータ部分のスクリーンショット

この時点で、ページにはまったく同じデータの2つのコピーがあります。「古い」列は基本的に削除され(idはid = 2からid = 67108865に変更されます)、ページのデータの新しいオフセットを指すように「新しい」バージョンの列が更新されます。

前後のdbccページの列メタデータ部分のスクリーンショット

ログスペースで2倍のテーブルサイズを使用する

ステートメントWITH (ONLINE = ON)の最後に追加すると、ロギングアクティビティが約半分削減されるため、これは必要なディスク/ディスク領域への書き込み量を削減するための改善点の1つです。ALTER

このテストハーネスを使用して試してみました。

USE [master];
GO
DROP DATABASE IF EXISTS [248749];
GO
CREATE DATABASE [248749] 
ON PRIMARY 
(
    NAME = N'248749', 
    FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL14.SQL2017\MSSQL\DATA\248749.mdf', 
    SIZE = 2048000KB, 
    FILEGROWTH = 65536KB
)
LOG ON 
(
    NAME = N'248749_log', 
    FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL14.SQL2017\MSSQL\DATA\248749_log.ldf', 
    SIZE = 2048000KB, 
    FILEGROWTH = 65536KB
);
GO
USE [248749];
GO

CREATE TABLE dbo.[table]
(
    id int IDENTITY(1,1) NOT NULL,
    [col] nvarchar (4000) NULL,

    CONSTRAINT [PK_test] PRIMARY KEY CLUSTERED (id ASC)
);

INSERT INTO dbo.[table]
SELECT TOP (1000000)
    REPLICATE(N'A', 260)
FROM master.dbo.spt_values v1
    CROSS JOIN master.dbo.spt_values v2
    CROSS JOIN master.dbo.spt_values v3;
GO

ステートメントsys.dm_io_virtual_file_stats(DB_ID(N'248749'), DEFAULT)を実行する前後にチェックALTERしましたが、違いは次のとおりです。

デフォルト(オフライン) ALTER

  • データファイルの書き込み/書き込まれたバイト数:34,809 / 2,193,801,216
  • ログファイルの書き込み/書き込まれたバイト数:40,953 / 1,484,910,080

オンライン ALTER

  • データファイルの書き込み/書き込まれたバイト数:36,874 / 1,693,745,152(22.8%の低下)
  • ログファイルの書き込み/書き込まれたバイト数:24,680 / 866,166,272(41%の低下)

ご覧のとおり、データファイルの書き込みがわずかに減少し、ログファイルの書き込みが大幅に減少しました。


15

ここで探していることを直接達成する方法がわかりません。現時点では、クエリオプティマイザーはメモリ許可計算の制約を考慮するほどスマートではないので、とにかく制約は役に立たなかったでしょう。テーブルのデータの書き換えを回避するいくつかの方法:

  1. 列を使用するすべてのコードでNVARCHAR(260)として列をキャストします。クエリオプティマイザーは、生のデータ型ではなく、キャストされたデータ型を使用してメモリ許可を計算します。
  2. テーブルの名前を変更し、代わりにキャストを行うビューを作成します。これにより、オプション1と同じことが実現されますが、更新する必要があるコードの量が制限される場合があります。
  3. 適切なデータ型の非永続計算列を作成し、すべてのクエリで元の列ではなくその列から選択するようにします。
  4. 既存の列の名前を変更し、元の名前で計算列を追加します。次に、元の列を更新または挿入するすべてのクエリを調整して、代わりに新しい列名を使用します。

2

私は何度も同じような状況にありました。

手順:

希望する幅の新しい列を追加します

コミットごとに数千回(おそらく1万または2万回)反復するカーソルを使用して、古い列から新しい列にデータをコピーします。

古い列を削除

新しい列の名前を古い列の名前に変更します

多田!


3
すでにコピーしたレコードの一部が更新または削除された場合はどうなりますか?
ジョージ。パラシオ

1
update table set new_col = old_col where new_col <> old_col;ドロップする前にファイナルを1つ行うのは非常に簡単old_colです。
コリン 'ハート

1
アプローチは、数百万行では動作しません...取引は巨大な取得し、そのブロックをそのColin'tHartは@ ....
Jonesome復活モニカ

@samsmith最初に、上記で説明したことを行います。次に、元の列を削除する前に、その間に元のデータが更新されている場合は、その更新ステートメントを実行します。変更されたいくつかの行にのみ影響します。それとも何か不足していますか?
コリン 'ハート

プロセス中に更新された行をカバーするために、where new_col <> old_col他のフィルタリング句を使用しないフルスキャンを回避するために、トリガーを追加して、これらの変更が発生したときにそれを引き継いで、プロセスの最後に削除します。潜在的なパフォーマンスヒットはありますが、最後に1回の大きなヒットではなく、プロセスの長さにわたって多くの小さな量が発生します。 。
デビッドスピレット

1

まあ、データベース内の利用可能なスペースに応じて代替があります。

  1. new_tableからNVARCHAR(4000)に短縮する列を除き、テーブルの正確なコピーを作成します(例:)NVARCHAR(260)

    CREATE TABLE [new_table](
        id INT IDENTITY(1,1) NOT NULL,
        [col] NVARCHAR(260) NULL,
        CONSTRAINT [PK_test_new] PRIMARY KEY CLUSTERED (id ASC)
    );
  2. メンテナンスウィンドウで、「壊れた」テーブル(table)から「固定」テーブル(new_table)にデータをコピーしますINSERT ... INTO ... SELECT ....

    SET IDENTITY_INSERT [new_table] ON
    GO
    INSERT id, col INTO [new_table] SELECT id, col from [table]
    GO
    SET IDENTITY_INSERT [new_table] OFF
    GO
  3. 「壊れた」テーブルの名前をtable別のものに変更します。

    EXEC sp_rename 'table', 'old_table';  
  4. 「固定」テーブルの名前new_tabletable次のように変更します。

    EXEC sp_rename 'new_table', 'table';  
  5. すべてが正常な場合、名前が変更された「壊れた」テーブルをドロップします。

     DROP TABLE [old_table]
     GO

行くぞ

質問に答える

列のデータ型をメタデータのみの操作として変更する方法はありますか?

いいえ。現在は不可能です

テーブル全体を書き換える費用なしで?

いいえ。
私のソリューションなどを参照してください。


「from into select from」は、ENORMOUSトランザクションの大きなテーブル(数百または数十億行)で発生し、DBが数十または数百分間停止する可能性があります。(使用中の場合は、ldfを巨大にし、場合によってはログ配布を破壊するだけでなく)
ジョーンズームモニカーを復活させる
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.