Tại sao ALTER COLUMN để KHÔNG NULL gây ra sự tăng trưởng tệp nhật ký lớn?


56

Tôi có một bảng với 64m hàng lấy 4,3 GB trên đĩa cho dữ liệu của nó.

Mỗi hàng có khoảng 30 byte cột số nguyên, cộng với một NVARCHAR(255)cột biến cho văn bản.

Tôi đã thêm một cột NULLABLE với kiểu dữ liệu Datetimeoffset(0).

Sau đó tôi đã CẬP NHẬT cột này cho mỗi hàng và đảm bảo tất cả các phần chèn mới đặt một giá trị trong cột này.

Khi không có mục NULL, sau đó tôi chạy lệnh này để bắt buộc trường mới của tôi:

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

Kết quả là sự tăng trưởng lớn về kích thước nhật ký giao dịch - từ 6GB đến hơn 36GB cho đến khi hết dung lượng!

Có ai có ý tưởng gì về việc SQL Server 2008 R2 đang làm gì với lệnh đơn giản này để dẫn đến sự tăng trưởng lớn như vậy không?


7
SQL Server 2012 Enterprise thêm khả năng thêm một NOT NULLcột với mặc định là hoạt động siêu dữ liệu. Đồng thời xem "Thêm các cột KHÔNG NULL dưới dạng một hoạt động trực tuyến" trong tài liệu .
Paul White

Câu trả lời:


48

Khi bạn thay đổi một cột thành KHÔNG NULL, SQL Server phải chạm vào từng trang, ngay cả khi không có giá trị NULL. Tùy thuộc vào yếu tố điền của bạn, điều này thực sự có thể dẫn đến rất nhiều phân chia trang. Tất cả các trang được chạm vào, tất nhiên, phải được ghi lại và tôi nghi ngờ do sự chia tách mà hai thay đổi có thể phải được ghi lại cho nhiều trang. Vì tất cả đã được thực hiện trong một lần duy nhất, tuy nhiên, nhật ký phải tính đến tất cả các thay đổi để nếu bạn nhấn hủy, nó sẽ biết chính xác những gì cần hoàn tác.


Một ví dụ. Bảng đơn giản:

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

Bây giờ, hãy nhìn vào chi tiết trang. Đầu tiên chúng ta cần tìm hiểu trang nào và DB_ID chúng ta đang làm việc. Trong trường hợp của tôi, tôi đã tạo một cơ sở dữ liệu được gọi foovà DB_ID là 5.

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

Đầu ra chỉ ra rằng tôi quan tâm đến trang 159 (hàng duy nhất trong DBCC INDđầu ra với PageType = 1).

Bây giờ, hãy xem một số chi tiết trang được chọn khi chúng ta bước qua kịch bản của OP.

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

nhập mô tả hình ảnh ở đây

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

nhập mô tả hình ảnh ở đây

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

nhập mô tả hình ảnh ở đây

Bây giờ, tôi không có tất cả các câu trả lời cho điều này, vì tôi không phải là một người nội tâm sâu sắc. Nhưng rõ ràng rằng - trong khi cả hoạt động cập nhật và bổ sung ràng buộc KHÔNG NULL đều không thể phủ nhận ghi vào trang - thì điều này lại làm theo cách hoàn toàn khác. Nó dường như thực sự thay đổi cấu trúc của bản ghi, thay vì chỉ sử dụng bit, bằng cách hoán đổi cột nullable cho cột không null. Tại sao nó phải làm điều đó, tôi không chắc lắm - một câu hỏi hay cho nhóm công cụ lưu trữ , tôi đoán vậy. Tôi tin rằng SQL Server 2012 xử lý một số tình huống này tốt hơn rất nhiều, FWIW - nhưng tôi vẫn chưa thực hiện bất kỳ thử nghiệm toàn diện nào.


4
Hành vi này đã thay đổi đáng kể trong các phiên bản sau của SQL Server. Tôi đã kiểm tra RC2 2016 và phát hiện ra rằng đối với kịch bản chính xác này và 1 triệu hàng trong bảng chỉ có 29 bản ghi nhật ký được tạo trong khi thay đổi từ NULL thành KHÔNG NULL nếu tất cả các giá trị đã được chỉ định cho cột.
Endrju

32

Khi thực hiện lệnh

ALTER COLUMN ... NOT NULL

Điều này dường như được thực hiện như một hoạt động Thêm Cột, Cập nhật, Thả Cột.

  • Một hàng mới được chèn vào sys.sysrscolsđể thể hiện một cột mới. Các statusbit cho 128được thiết lập cho thấy cột không cho phép NULLs
  • Một bản cập nhật được thực hiện trên mỗi hàng của bảng, đặt giá trị cột mới thành giá trị cột cũ. Nếu các phiên bản "trước" và "sau" của hàng giống hệt nhau thì điều này không gây ra bất kỳ điều gì được ghi vào nhật ký giao dịch nếu không bản cập nhật được ghi lại.
  • Cột ban đầu được đánh dấu là đã bỏ (đây là siêu dữ liệu chỉ thay đổi sys.sysrscols. Được rscolidcập nhật thành một số nguyên lớn và statusbit 2 được đặt thành bị bỏ chỉ định)
  • Mục nhập sys.sysrscolscho cột mới được thay đổi để cung cấp cho rscolidcột cũ.

Hoạt động có khả năng gây ra nhiều bản ghi là UPDATEtất cả các hàng trong bảng tuy nhiên điều đó không có nghĩa là điều này sẽ luôn xảy ra. Nếu hình ảnh "trước" và "sau" của hàng giống hệt nhau thì điều này sẽ được coi là một bản cập nhật không cập nhật và không được ghi lại từ thử nghiệm của tôi cho đến nay.

Vì vậy, lời giải thích về lý do tại sao bạn nhận được nhiều đăng nhập sẽ phụ thuộc vào lý do tại sao chính xác các phiên bản "trước" và "sau" của hàng không giống nhau.

Đối với các cột có chiều dài thay đổi được lưu trữ ở FixedVarđịnh dạng tôi thấy rằng cài đặt NOT NULLluôn gây ra thay đổi trong hàng cần được ghi lại. Cả số lượng cột và số lượng cột có chiều dài thay đổi đều được tăng lên và cột mới được thêm vào cuối phần có độ dài thay đổi nhân đôi dữ liệu.

datetimeoffset(0)tuy nhiên là chiều dài cố định và đối với các cột có chiều dài cố định được lưu trữ theo FixedVarđịnh dạng, cả hai cột cũ và mới dường như được cung cấp cùng một vị trí trong phần dữ liệu có độ dài cố định của hàng và vì cả hai đều có cùng độ dài và giá trị "trước" và Các phiên bản "sau" của hàng giống nhau . Điều này có thể được nhìn thấy trong câu trả lời của @ Aaron. Cả hai phiên bản của hàng trước và sau khi ALTER TABLE dbo.floob ALTER COLUMN bar INT NOT NULL;Are

0x10000c00 01000000 00000000 020000

Đây không phải là đăng nhập.

Theo logic từ mô tả của tôi về các sự kiện, hàng thực tế phải khác ở đây vì số lượng cột 02nên được tăng lên 03nhưng thực tế không có thay đổi nào như vậy xảy ra trong thực tế.

Một số lý do có thể là tại sao điều này có thể xảy ra trong một cột có độ dài cố định là

  • Nếu cột ban đầu được khai báo là SPARSEthì cột mới sẽ được lưu trữ ở một phần khác của hàng so với ban đầu làm cho hình ảnh hàng trước và sau hàng khác nhau.
  • Nếu bạn đang sử dụng bất kỳ tùy chọn nén nào thì các phiên bản trước và sau của hàng sẽ khác nhau khi phần đếm cột trong mảng CD được tăng lên.
  • Trên cơ sở dữ liệu với một trong các tùy chọn cách ly ảnh chụp được bật, thông tin phiên bản trong mỗi hàng được cập nhật (@Query Kiwi chỉ ra rằng điều này cũng có thể xảy ra trong cơ sở dữ liệu mà không bật SI như được mô tả ở đây ).
  • Có thể có một số ALTER TABLEthao tác trước đó được triển khai dưới dạng siêu dữ liệu chỉ thay đổi và chưa được áp dụng cho hàng. Ví dụ: nếu một cột có độ dài biến nullable mới được thêm vào thì điều này ban đầu được áp dụng vì siêu dữ liệu chỉ thay đổi và nó chỉ thực sự được ghi ra các hàng khi chúng được cập nhật tiếp theo (cách viết thực sự xảy ra trong trường hợp cuối cùng này chỉ là cập nhật cho phần đếm cột và NULL_BITMAPlà một NULL varcharcột ở phần cuối của hàng không mất bất kỳ không gian)

5

Tôi đã đối mặt với cùng một vấn đề liên quan đến một bảng có 200.000.000 hàng. Ban đầu tôi đã thêm cột nullable, sau đó cập nhật tất cả các hàng và cuối cùng đã thay đổi cột thành NOT NULLthông qua một ALTER TABLE ALTER COLUMNcâu lệnh. Điều này dẫn đến hai giao dịch khổng lồ làm nổ tung logfile một cách đáng kinh ngạc (tăng trưởng 170 GB).

Cách nhanh nhất tôi tìm thấy là như sau:

  1. Thêm cột bằng giá trị mặc định

    ALTER TABLE table1 ADD column1 INT NOT NULL DEFAULT (1)
  2. Bỏ ràng buộc mặc định bằng cách sử dụng SQL động vì ràng buộc chưa được đặt tên trước:

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

Thời gian thực hiện giảm từ> 30 phút xuống còn 10 phút, bao gồm cả sao chép các thay đổi thông qua Sao chép giao dịch. Tôi đang chạy cài đặt SQL Server 2008 (SP2).


2

Tôi đã chạy thử nghiệm sau:

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)

Tôi tin rằng điều này có liên quan đến không gian dành riêng mà nhật ký giữ chỉ trong trường hợp bạn hoàn trả giao dịch. Tìm trong hàm fn_dblog tại Cột 'Nhật ký dự trữ' cho hàng LOP_BEGIN_XACT và xem nó đang cố gắng dự trữ bao nhiêu dung lượng.


Nếu bạn thử, select * FROM fn_dblog(null, null) where AllocUnitName='dbo.tblCheckResult' AND Operation = 'LOP_MODIFY_ROW'bạn có thể thấy các bản cập nhật 10000 hàng.
Martin Smith

-2

Hành vi cho điều này là khác nhau trong SQL Server 2012. Xem http://rusanu.com/2011/07/13/online-non-null-with-values-column-add-in-sql-server-11/

Số lượng bản ghi nhật ký được tạo cho SQL Server 2008 R2 và các bản phát hành bên dưới sẽ cao hơn đáng kể so với số lượng bản ghi nhật ký cho SQL Server 2012.


2
Câu hỏi là tại sao thay đổi một cột hiện có để NOT NULLgây ra đăng nhập. Thay đổi trong năm 2012 là về việc thêm một NOT NULLcột mới với mặc định.
Martin Smith
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.