Mengapa ALTER COLUMN untuk TIDAK NULL menyebabkan pertumbuhan file log yang besar?

56

Saya punya meja dengan baris 64m yang mengambil 4,3 GB pada disk untuk datanya.

Setiap baris sekitar 30 byte dari kolom integer, ditambah NVARCHAR(255)kolom variabel untuk teks.

Saya menambahkan kolom NULLABLE dengan tipe data Datetimeoffset(0).

Saya kemudian MEMPERBARUI kolom ini untuk setiap baris dan memastikan semua sisipan baru memberi nilai pada kolom ini.

Setelah tidak ada entri NULL saya kemudian menjalankan perintah ini untuk membuat bidang baru saya wajib:

ALTER TABLE tblCheckResult 
ALTER COLUMN [dtoDateTime] [datetimeoffset](0) NOT NULL

Hasilnya adalah pertumbuhan BESAR dalam ukuran log transaksi - dari 6GB menjadi lebih dari 36GB hingga kehabisan ruang!

Adakah yang tahu apa yang dilakukan SQL Server 2008 R2 untuk perintah sederhana ini untuk menghasilkan pertumbuhan yang sangat besar?

PapillonUK
sumber
7
SQL Server 2012 Enterprise menambahkan kemampuan untuk menambahkan NOT NULLkolom dengan default sebagai operasi metadata. Lihat juga "Menambahkan TIDAK NULL Kolom sebagai Operasi Online" di dokumentasi .
Paul White

Jawaban:

48

Ketika Anda mengubah kolom menjadi TIDAK NULL, SQL Server harus menyentuh setiap halaman, bahkan jika tidak ada nilai NULL. Bergantung pada faktor pengisian Anda, ini sebenarnya dapat menyebabkan banyak pemisahan halaman. Setiap halaman yang disentuh, tentu saja, harus dicatat, dan saya curiga karena perpecahan itu dua perubahan mungkin harus dicatat untuk banyak halaman. Karena semuanya dilakukan dalam satu pass, log harus memperhitungkan semua perubahan sehingga, jika Anda menekan cancel, ia tahu persis apa yang harus diurungkan.


Sebuah contoh. Meja sederhana:

DROP TABLE dbo.floob;
GO

CREATE TABLE dbo.floob
(
  id INT IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED, 
  bar INT NULL
);

INSERT dbo.floob(bar) SELECT NULL UNION ALL SELECT 4 UNION ALL SELECT NULL;

ALTER TABLE dbo.floob ADD CONSTRAINT df DEFAULT(0) FOR bar

Sekarang, mari kita lihat detail halaman. Pertama-tama kita perlu mencari tahu halaman apa dan DB_ID yang sedang kita hadapi. Dalam kasus saya, saya membuat database bernama foo, dan DB_ID kebetulan 5.

DBCC TRACEON(3604, -1);
DBCC IND('foo', 'dbo.floob', 1);
SELECT DB_ID();

Keluaran menunjukkan bahwa saya tertarik pada halaman 159 (satu-satunya baris dalam DBCC INDkeluaran dengan PageType = 1).

Sekarang, mari kita lihat beberapa detail halaman terpilih saat kita melangkah melalui skenario OP.

DBCC PAGE(5, 1, 159, 3);

masukkan deskripsi gambar di sini

UPDATE dbo.floob SET bar = 0 WHERE bar IS NULL;    
DBCC PAGE(5, 1, 159, 3);

masukkan deskripsi gambar di sini

ALTER TABLE dbo.floob ALTER COLUMN bar INT NOT NULL;
DBCC PAGE(5, 1, 159, 3);

masukkan deskripsi gambar di sini

Sekarang, saya tidak memiliki semua jawaban untuk ini, karena saya bukan orang internal yang dalam. Tetapi jelas bahwa - walaupun operasi pembaruan dan penambahan batasan NOT NULL tidak dapat disangkal menulis ke halaman - yang terakhir melakukannya dengan cara yang sama sekali berbeda. Tampaknya benar-benar mengubah struktur catatan, daripada hanya mengutak-atik bit, dengan menukar kolom nullable untuk kolom non-nullable. Mengapa harus melakukan itu, saya tidak yakin - pertanyaan yang bagus untuk tim mesin penyimpanan , saya kira. Saya percaya bahwa SQL Server 2012 menangani beberapa skenario ini jauh lebih baik, FWIW - tapi saya belum melakukan pengujian lengkap.

Aaron Bertrand
sumber
4
Perilaku ini sangat berubah di versi SQL Server yang lebih baru. Saya telah memeriksa RC2 2016 dan menemukan bahwa untuk skenario yang tepat ini dan 1 juta baris dalam tabel, hanya 29 catatan log yang dihasilkan selama perubahan dari NULL ke NOT NULL jika semua nilai sudah ditentukan untuk kolom.
Endrju
32

Saat menjalankan perintah

ALTER COLUMN ... NOT NULL

Ini tampaknya diimplementasikan sebagai operasi Tambahkan Kolom, Perbarui, Jatuhkan Kolom.

  • Baris baru dimasukkan ke dalam sys.sysrscolsuntuk mewakili kolom baru. The statusbit untuk 128diatur menunjukkan kolom tidak memungkinkan NULLs
  • Pembaruan dilakukan pada setiap baris tabel yang mengatur nilai kolom baru dengan nilai kolom lama. Jika versi "sebelum" dan "setelah" baris persis sama, ini tidak menyebabkan apa pun ditulis ke log transaksi jika tidak, pembaruan akan dicatat.
  • Kolom asli ditandai sebagai dijatuhkan (ini adalah metadata yang hanya berubah masuk sys.sysrscols. rscolidDiperbarui ke bilangan bulat besar dan statusbit 2 ditetapkan untuk diindikasikan dijatuhkan)
  • Entri sys.sysrscolsuntuk kolom baru diubah untuk memberikannya rscolidpada kolom lama.

Operasi yang berpotensi menyebabkan banyak logging adalah UPDATEdari semua baris dalam tabel namun itu tidak berarti bahwa ini akan selalu terjadi. Jika gambar "sebelum" dan "setelah" dari baris identik maka ini akan dianggap sebagai pembaruan yang tidak memperbarui dan tidak dicatat dari pengujian saya sejauh ini.

Jadi penjelasan mengapa Anda mendapatkan banyak logging akan bergantung pada mengapa versi baris "sebelum" dan "setelah" tidak sama.

Untuk kolom panjang variabel yang disimpan dalam FixedVarformat saya menemukan bahwa pengaturan NOT NULLselalu menyebabkan perubahan pada baris yang perlu dicatat. Jumlah kolom dan jumlah kolom panjang variabel keduanya bertambah dan kolom baru ditambahkan ke akhir bagian panjang variabel menduplikasi data.

datetimeoffset(0)Namun panjang tetap dan untuk kolom panjang tetap disimpan dalam FixedVarformat kolom lama dan baru keduanya tampaknya diberi slot yang sama di bagian data panjang tetap baris dan karena keduanya memiliki panjang yang sama dan nilai "sebelum" dan "setelah" versi barisnya sama . Ini bisa dilihat pada jawaban @ Harun. Kedua versi baris sebelum dan sesudah ALTER TABLE dbo.floob ALTER COLUMN bar INT NOT NULL;adalah

0x10000c00 01000000 00000000 020000

Ini belum dicatat.

Secara logis dari uraian saya tentang peristiwa, baris sebenarnya harus berbeda di sini karena jumlah kolom 02harus dinaikkan menjadi 03tetapi tidak ada perubahan seperti itu yang terjadi dalam praktiknya.

Beberapa kemungkinan alasan mengapa hal ini dapat terjadi pada kolom dengan panjang tetap adalah

  • Jika kolom awalnya dinyatakan sebagai SPARSEmaka kolom baru akan disimpan di bagian baris yang berbeda dari aslinya sehingga gambar baris sebelum dan sesudah berbeda.
  • Jika Anda menggunakan salah satu opsi kompresi maka versi sebelum dan sesudah baris akan berbeda karena bagian jumlah kolom dalam array CD bertambah.
  • Pada database dengan salah satu opsi isolasi snapshot diaktifkan maka informasi versi di setiap baris diperbarui (@SQL Kiwi menunjukkan bahwa ini juga dapat terjadi dalam database tanpa SI diaktifkan seperti yang dijelaskan di sini ).
  • Mungkin ada beberapa ALTER TABLEoperasi sebelumnya yang dilaksanakan hanya sebagai perubahan metadata dan belum diterapkan ke baris. Sebagai contoh jika kolom panjang variabel nullable baru ditambahkan maka ini awalnya diterapkan sebagai perubahan metadata saja dan itu hanya benar-benar ditulis ke baris ketika mereka selanjutnya diperbarui (tulisan yang benar-benar terjadi dalam contoh terakhir ini hanya pembaruan untuk bagian jumlah kolom dan NULL_BITMAPsebagai NULL varcharkolom pada akhir baris tidak memakan tempat)
Martin Smith
sumber
5

Saya menghadapi masalah yang sama mengenai meja yang memiliki 200.000.000 baris. Awalnya saya menambahkan kolom nullable, kemudian memperbarui semua baris, dan akhirnya mengubah kolom NOT NULLmelalui ALTER TABLE ALTER COLUMNpernyataan. Ini menghasilkan dua transaksi besar yang meledakkan logfile dengan luar biasa (pertumbuhan 170 GB).

Cara tercepat yang saya temukan adalah sebagai berikut:

  1. Tambahkan kolom menggunakan nilai default

    ALTER TABLE table1 ADD column1 INT NOT NULL DEFAULT (1)
  2. Jatuhkan batasan default dengan menggunakan SQL dinamis karena batasan belum diberi nama sebelumnya:

    DECLARE 
        @constraint_name SYSNAME,
        @stmt NVARCHAR(510);
    
    SELECT @CONSTRAINT_NAME = DC.NAME
    FROM SYS.DEFAULT_CONSTRAINTS DC
    INNER JOIN SYS.COLUMNS C
        ON DC.PARENT_OBJECT_ID = C.OBJECT_ID
        AND DC.PARENT_COLUMN_ID = C.COLUMN_ID
    WHERE
        PARENT_OBJECT_ID = OBJECT_ID('table1')
        AND C.NAME = 'column1';

Waktu pelaksanaan turun dari> 30 menit menjadi 10 menit, termasuk mereplikasi perubahan melalui Replikasi Transaksional. Saya menjalankan instalasi SQL Server 2008 (SP2).

Fritz
sumber
2

Saya menjalankan tes berikut:

create table tblCheckResult(
        ColID   int identity
    ,   dtoDateTime Datetimeoffset(0) null
    )

 go

insert into tblCheckResult (dtoDateTime)
select getdate()
go 10000

checkpoint 

ALTER TABLE tblCheckResult 
ALTER COLUMN [dtoDateTime] [datetimeoffset](0) NOT NULL

select * from fn_dblog(null,null)

Saya percaya bahwa ini ada hubungannya dengan ruang yang disediakan log untuk berjaga-jaga jika Anda mengembalikan transaksi. Lihat di fungsi fn_dblog di kolom 'Log Reserve' untuk baris LOP_BEGIN_XACT dan lihat berapa banyak ruang yang dicadangkan untuk dicadangkan.

Keith Tate
sumber
Jika Anda mencoba, select * FROM fn_dblog(null, null) where AllocUnitName='dbo.tblCheckResult' AND Operation = 'LOP_MODIFY_ROW'Anda dapat melihat pembaruan 10.000 baris.
Martin Smith
-2

Perilaku untuk ini berbeda di SQL Server 2012. Lihat http://rusanu.com/2011/07/13/online-non-null-with-values-column-add-in-sql-server-11/

Jumlah catatan log yang dihasilkan untuk SQL Server 2008 R2 dan rilis di bawah ini akan secara signifikan lebih tinggi daripada jumlah catatan log untuk SQL Server 2012.

Memecahkan masalah SQL
sumber
2
Pertanyaannya adalah mengapa mengubah kolom yang ada NOT NULLmenyebabkan logging. Perubahan pada tahun 2012 adalah tentang menambahkan NOT NULLkolom baru dengan default.
Martin Smith