Please note that the following info is not intended to be a comprehensive
description of how data pages are laid out, such that one can calculate
the number of bytes used per any set of rows, as that is very complicated.
Dữ liệu không phải là thứ duy nhất chiếm dung lượng trên trang dữ liệu 8k:
Có không gian dành riêng. Bạn chỉ được phép sử dụng 8060 trong số 8192 byte (đó là 132 byte không bao giờ là của bạn ở vị trí đầu tiên):
- Tiêu đề trang: Đây chính xác là 96 byte.
- Mảng vị trí: đây là 2 byte mỗi hàng và cho biết phần bù của mỗi hàng bắt đầu trên trang. Kích thước của mảng này không giới hạn ở 36 byte còn lại (132 - 96 = 36), nếu không, bạn sẽ bị giới hạn một cách hiệu quả khi chỉ đặt tối đa 18 hàng trên một trang dữ liệu. Điều này có nghĩa là mỗi hàng lớn hơn 2 byte so với bạn nghĩ. Giá trị này không được bao gồm trong "kích thước bản ghi" như được báo cáo bởi
DBCC PAGE
, đó là lý do tại sao nó được giữ riêng ở đây thay vì được bao gồm trong thông tin mỗi hàng bên dưới.
- Dữ liệu meta Per-Row (bao gồm, nhưng không giới hạn):
- Kích thước thay đổi tùy theo định nghĩa bảng (nghĩa là số cột, chiều dài thay đổi hoặc chiều dài cố định, v.v.). Thông tin được lấy từ ý kiến của @ PaulWhite và @ Aaron có thể được tìm thấy trong cuộc thảo luận liên quan đến câu trả lời và thử nghiệm này.
- Tiêu đề hàng: 4 byte, 2 trong số chúng biểu thị loại bản ghi và hai loại còn lại là phần bù cho Bitmap NULL
- Số cột: 2 byte
- NULL Bitmap: cột nào hiện tại
NULL
. 1 byte cho mỗi bộ 8 cột. Và cho tất cả các cột, ngay cả NOT NULL
những người. Do đó, tối thiểu 1 byte.
- Mảng bù có độ dài cột thay đổi: tối thiểu 4 byte. 2 byte để giữ số lượng cột có độ dài thay đổi và sau đó là 2 byte cho mỗi cột có độ dài thay đổi để giữ phần bù cho vị trí bắt đầu.
- Thông tin phiên bản: 14 byte (điều này sẽ có mặt nếu cơ sở dữ liệu của bạn được đặt thành
ALLOW_SNAPSHOT_ISOLATION ON
hoặc READ_COMMITTED_SNAPSHOT ON
).
- Vui lòng xem Câu hỏi và Trả lời sau để biết thêm chi tiết về điều này: Slot Array và Tổng kích thước trang
- Vui lòng xem bài đăng trên blog sau của Paul Randall, trong đó có một số chi tiết thú vị về cách các trang dữ liệu được trình bày: Trêu chọc với TRANG DBCC (Phần 1 của?)
Con trỏ LOB cho dữ liệu không được lưu trữ trong hàng. Vì vậy, nó sẽ chiếm DATALENGTH
+ con trỏ. Nhưng đây không phải là một kích thước tiêu chuẩn. Vui lòng xem bài đăng trên blog sau để biết chi tiết về chủ đề phức tạp này: Kích thước của Con trỏ LOB cho các loại (MAX) như Varchar, Varbinary, Etc là gì? . Giữa bài đăng được liên kết đó và một số thử nghiệm bổ sung mà tôi đã thực hiện , các quy tắc (mặc định) phải như sau:
- Legacy / phản đối loại LOB mà không ai nên sử dụng nữa như của SQL Server 2005 (
TEXT
, NTEXT
và IMAGE
):
- Theo mặc định, luôn lưu trữ dữ liệu của họ trên các trang LOB và luôn sử dụng con trỏ 16 byte để lưu trữ LOB.
- NẾU sp_tableoption đã được sử dụng để đặt
text in row
tùy chọn, sau đó:
- nếu có không gian trên trang để lưu trữ giá trị và giá trị không lớn hơn kích thước liên tiếp tối đa (phạm vi có thể định cấu hình là 24 - 7000 byte với mặc định là 256), thì nó sẽ được lưu liên tiếp,
- nếu không nó sẽ là một con trỏ 16 byte.
- Đối với các loại LOB mới giới thiệu trong SQL Server 2005 (
VARCHAR(MAX)
, NVARCHAR(MAX)
và VARBINARY(MAX)
):
- Theo mặc định:
- Nếu giá trị không lớn hơn 8000 byte và có chỗ trên trang, thì nó sẽ được lưu liên tiếp.
- Root nội tuyến - đối với dữ liệu trong khoảng từ 8001 đến 40.000 (thực sự là 42.000) byte, cho phép không gian, sẽ có 1 đến 5 con trỏ (24 - 72 byte) IN ROW trỏ trực tiếp vào (các) trang LOB. 24 byte cho trang LOB 8k ban đầu và 12 byte cho mỗi trang 8k bổ sung cho tối đa bốn trang 8k nữa.
- TEXT_TREE - đối với dữ liệu trên 42.000 byte hoặc nếu 1 đến 5 con trỏ không khớp với nhau, thì sẽ chỉ có một con trỏ 24 byte đến trang bắt đầu của danh sách các con trỏ tới các trang LOB (tức là "text_tree " trang).
- NẾU sp_tableoption đã được sử dụng để đặt
large value types out of row
tùy chọn, sau đó luôn sử dụng con trỏ 16 byte để lưu trữ LOB.
- Tôi đã nói quy tắc "mặc định" vì tôi không kiểm tra các giá trị liên tiếp chống lại tác động của một số tính năng nhất định như Nén dữ liệu, Mã hóa cấp cột, Mã hóa dữ liệu trong suốt, Luôn được mã hóa, v.v.
Các trang tràn LOB: Nếu một giá trị là 10k, thì điều đó sẽ yêu cầu 1 trang tràn 8k đầy đủ, và sau đó là một phần của trang thứ 2. Nếu không có dữ liệu nào khác có thể chiếm dung lượng còn lại (hoặc thậm chí được phép, tôi không chắc chắn về quy tắc đó), thì bạn có khoảng 6kb không gian "lãng phí" trên kho dữ liệu tràn LOB thứ 2 đó.
Dung lượng không sử dụng: Một trang dữ liệu 8k chỉ có thế: 8192 byte. Nó không thay đổi kích thước. Tuy nhiên, dữ liệu và siêu dữ liệu được đặt trên nó, không phải lúc nào cũng vừa vặn với tất cả 8192 byte. Và các hàng không thể được chia thành nhiều trang dữ liệu. Vì vậy, nếu bạn còn 100 byte nhưng không có hàng nào (hoặc không có hàng nào phù hợp với vị trí đó, tùy thuộc vào một số yếu tố), trang dữ liệu vẫn chiếm 8192 byte và truy vấn thứ 2 của bạn chỉ đếm số lượng trang dữ liệu. Bạn có thể tìm thấy giá trị này ở hai nơi (chỉ cần lưu ý rằng một phần của giá trị này là một phần của không gian dành riêng đó):
DBCC PAGE( db_name, file_id, page_id ) WITH TABLERESULTS;
Tìm kiếm ParentObject
= "TRANG ĐẦU:" và Field
= "m_freeCnt". Các Value
lĩnh vực là số byte không sử dụng.
SELECT buff.free_space_in_bytes FROM sys.dm_os_buffer_descriptors buff WHERE buff.[database_id] = DB_ID(N'db_name') AND buff.[page_id] = page_id;
Đây là cùng một giá trị như được báo cáo bởi "m_freeCnt". Điều này dễ hơn DBCC vì nó có thể nhận được nhiều trang, nhưng cũng yêu cầu các trang đã được đọc vào nhóm bộ đệm ở vị trí đầu tiên.
Dung lượng được dành riêng bởi FILLFACTOR
<100. Các trang mới được tạo không tôn trọng FILLFACTOR
cài đặt, nhưng thực hiện REBUILD sẽ dành chỗ đó trên mỗi trang dữ liệu. Ý tưởng đằng sau không gian dành riêng là nó sẽ được sử dụng bởi các chèn và / hoặc cập nhật không tuần tự mở rộng kích thước của các hàng trên trang, do các cột có chiều dài thay đổi được cập nhật với dữ liệu nhiều hơn một chút (nhưng không đủ để gây ra tách trang). Nhưng bạn có thể dễ dàng dự trữ không gian trên các trang dữ liệu tự nhiên sẽ không bao giờ nhận được các hàng mới và không bao giờ có các hàng hiện có được cập nhật hoặc ít nhất là không được cập nhật theo cách làm tăng kích thước của hàng.
Chia trang (phân mảnh): Cần thêm một hàng vào một vị trí không có chỗ cho hàng sẽ gây ra sự phân chia trang. Trong trường hợp này, khoảng 50% dữ liệu hiện có được chuyển sang một trang mới và hàng mới được thêm vào một trong 2 trang. Nhưng bây giờ bạn có thêm một chút không gian trống mà không được DATALENGTH
tính toán.
Hàng được đánh dấu để xóa. Khi bạn xóa các hàng, chúng không phải lúc nào cũng bị xóa ngay lập tức khỏi trang dữ liệu. Nếu chúng không thể bị xóa ngay lập tức, chúng sẽ được "đánh dấu cho cái chết" (tham chiếu của Steven Segal) và sẽ bị xóa về mặt vật lý sau quá trình dọn dẹp ma (tôi tin đó là tên). Tuy nhiên, những điều này có thể không liên quan đến Câu hỏi cụ thể này.
Trang ma? Không chắc đó có phải là thuật ngữ phù hợp hay không, nhưng đôi khi các trang dữ liệu không bị xóa cho đến khi hoàn thành REBUILD của Chỉ mục cụm. Điều đó cũng sẽ chiếm nhiều trang hơn so DATALENGTH
với. Điều này thường không nên xảy ra, nhưng tôi đã gặp phải nó một lần, vài năm trước.
Các cột SPARSE: Các cột thưa thớt tiết kiệm không gian (chủ yếu cho các kiểu dữ liệu có độ dài cố định) trong các bảng trong đó một% lớn các hàng NULL
dành cho một hoặc nhiều cột. Các SPARSE
tùy chọn làm cho các NULL
kiểu giá trị lên 0 byte (thay vì số tiền cố định chiều dài bình thường, chẳng hạn như 4 byte cho một INT
), nhưng , không NULL giá trị từng cất lên thêm 4 byte với nhiều loại chiều dài cố định và một số lượng biến cho các loại chiều dài thay đổi. Vấn đề ở đây là DATALENGTH
không bao gồm 4 byte bổ sung cho các giá trị không phải NULL trong cột SPARSE, vì vậy 4 byte đó cần được thêm lại. Bạn có thể kiểm tra xem có bất kỳ SPARSE
cột nào thông qua:
SELECT OBJECT_SCHEMA_NAME(sc.[object_id]) AS [SchemaName],
OBJECT_NAME(sc.[object_id]) AS [TableName],
sc.name AS [ColumnName]
FROM sys.columns sc
WHERE sc.is_sparse = 1;
Và sau đó cho mỗi SPARSE
cột, cập nhật truy vấn ban đầu để sử dụng:
SUM(DATALENGTH(FieldN) + 4)
Xin lưu ý rằng phép tính ở trên để thêm vào 4 byte tiêu chuẩn là một chút đơn giản vì nó chỉ hoạt động đối với các loại có độ dài cố định. VÀ, có thêm dữ liệu meta trên mỗi hàng (từ những gì tôi có thể nói cho đến nay) làm giảm không gian có sẵn cho dữ liệu, chỉ bằng cách có ít nhất một cột SPARSE. Để biết thêm chi tiết, vui lòng xem trang MSDN để sử dụng Cột thưa .
Các trang Index và các trang khác (ví dụ: IAM, PFS, GAM, SGAM, v.v.): đây không phải là các trang "dữ liệu" về mặt dữ liệu người dùng. Chúng sẽ làm tăng tổng kích thước của bảng. Nếu sử dụng SQL Server 2012 hoặc mới hơn, bạn có thể sử dụng sys.dm_db_database_page_allocations
Chức năng quản lý động (DMF) để xem các loại trang (phiên bản trước của SQL Server có thể sử dụng DBCC IND(0, N'dbo.table_name', 0);
):
SELECT *
FROM sys.dm_db_database_page_allocations(
DB_ID(),
OBJECT_ID(N'dbo.table_name'),
1,
NULL,
N'DETAILED'
)
WHERE page_type = 1; -- DATA_PAGE
Cả mệnh đề WHERE DBCC IND
cũng không sys.dm_db_database_page_allocations
(sẽ có báo cáo bất kỳ trang Index nào và chỉ DBCC IND
báo cáo sẽ có ít nhất một trang IAM.
DATA_COMPRESSION: Nếu bạn đã bật ROW
hoặc PAGE
Nén trên Chỉ mục cụm hoặc Heap, thì bạn có thể quên hầu hết những gì đã được đề cập cho đến nay. Tiêu đề trang 96 byte, Mảng slot 2 byte mỗi hàng và Thông tin phiên bản 14 byte mỗi hàng vẫn còn đó, nhưng biểu diễn vật lý của dữ liệu trở nên rất phức tạp (nhiều hơn so với những gì đã được đề cập khi Nén không được sử dụng). Ví dụ: với Nén hàng, SQL Server cố gắng sử dụng bộ chứa nhỏ nhất có thể để phù hợp với từng cột, trên mỗi hàng. Vì vậy, nếu bạn có một BIGINT
cột khác (giả sử SPARSE
cũng không được bật) luôn chiếm 8 byte, nếu giá trị nằm trong khoảng từ -128 đến 127 (tức là số nguyên 8 bit đã ký) thì nó sẽ chỉ sử dụng 1 byte và nếu giá trị có thể phù hợp với mộtSMALLINT
, nó sẽ chỉ mất 2 byte. Các kiểu số nguyên NULL
hoặc 0
chiếm không gian và được chỉ định đơn giản là NULL
hoặc "trống" (nghĩa là 0
) trong một mảng ánh xạ ra các cột. Và còn nhiều, rất nhiều quy tắc khác. Có dữ liệu Unicode ( NCHAR
, NVARCHAR(1 - 4000)
nhưng không NVARCHAR(MAX)
, ngay cả khi được lưu liên tiếp)? Nén nén Unicode đã được thêm vào trong SQL Server 2008 R2, nhưng không có cách nào để dự đoán kết quả của giá trị "nén" trong mọi tình huống mà không thực hiện nén thực tế do sự phức tạp của các quy tắc .
Vì vậy, thực sự, truy vấn thứ hai của bạn, trong khi chính xác hơn về tổng dung lượng vật lý chiếm trên đĩa, chỉ thực sự chính xác khi thực hiện một REBUILD
Chỉ số cụm. Và sau đó, bạn vẫn cần tính đến bất kỳ FILLFACTOR
cài đặt nào dưới 100. Và thậm chí sau đó luôn có các tiêu đề trang và thường đủ một lượng không gian "lãng phí" mà không thể lấp đầy do quá nhỏ để phù hợp với bất kỳ hàng nào trong này bảng, hoặc ít nhất là hàng hợp lý nên đi trong khe đó.
Về tính chính xác của truy vấn thứ 2 trong việc xác định "mức sử dụng dữ liệu", có vẻ công bằng nhất khi sao lưu các byte Tiêu đề trang vì chúng không sử dụng dữ liệu: chúng là chi phí kinh doanh. Nếu có 1 hàng trên một trang dữ liệu và hàng đó chỉ là một TINYINT
, thì 1 byte đó vẫn yêu cầu trang dữ liệu tồn tại và do đó 96 byte của tiêu đề. 1 bộ phận đó có nên bị tính phí cho toàn bộ trang dữ liệu không? Nếu trang dữ liệu đó sau đó được Bộ số 2 điền vào, họ sẽ chia đều chi phí "phí" đó hay trả theo tỷ lệ? Có vẻ dễ nhất để chỉ cần sao lưu nó ra. Trong trường hợp đó, sử dụng giá trị 8
để nhân lên number of pages
là quá cao. Làm thế nào về:
-- 8192 byte data page - 96 byte header = 8096 (approx) usable bytes.
SELECT 8060.0 / 1024 -- 7.906250
Do đó, sử dụng một cái gì đó như:
(SUM(a.total_pages) * 7.91) / 1024 AS [TotalSpaceMB]
cho tất cả các tính toán đối với các cột "number_of_pages".
VÀ , xem xét rằng việc sử dụng DATALENGTH
mỗi trường không thể trả về siêu dữ liệu trên mỗi hàng, nên được thêm vào truy vấn trên mỗi bảng của bạn, nơi bạn nhận được DATALENGTH
mỗi trường, lọc trên mỗi "bộ phận":
- Loại bản ghi và bù vào Bitmap NULL: 4 byte
- Đếm cột: 2 byte
- Slot Array: 2 byte (không bao gồm trong "kích thước bản ghi" nhưng vẫn cần tính đến)
- Bitmap NULL: 1 byte cho mỗi 8 cột (cho tất cả các cột)
- Phiên bản hàng: 14 byte (nếu cơ sở dữ liệu có
ALLOW_SNAPSHOT_ISOLATION
hoặc READ_COMMITTED_SNAPSHOT
được đặt thành ON
)
- Cột có độ dài thay đổi Mảng bù: 0 byte nếu tất cả các cột có độ dài cố định. Nếu bất kỳ cột nào có độ dài thay đổi, thì 2 byte, cộng với 2 byte cho mỗi cột chỉ có chiều dài thay đổi.
- Con trỏ LOB: phần này rất thiếu chính xác vì sẽ không có con trỏ nếu giá trị là
NULL
và nếu giá trị khớp với hàng thì nó có thể nhỏ hơn hoặc lớn hơn nhiều so với con trỏ và nếu giá trị được lưu trữ ngoài- hàng, sau đó kích thước của con trỏ có thể phụ thuộc vào lượng dữ liệu có. Tuy nhiên, vì chúng tôi chỉ muốn một ước tính (tức là "swag"), có vẻ như 24 byte là một giá trị tốt để sử dụng (tốt, tốt như bất kỳ loại nào khác ;-). Đây là mỗi MAX
lĩnh vực.
Do đó, sử dụng một cái gì đó như:
Nói chung (tiêu đề hàng + số cột + mảng khe + bitmap NULL):
([RowCount] * (( 4 + 2 + 2 + (1 + (({NumColumns} - 1) / 8) ))
Nói chung (tự động phát hiện nếu có "thông tin phiên bản"):
+ (SELECT CASE WHEN snapshot_isolation_state = 1 OR is_read_committed_snapshot_on = 1
THEN 14 ELSE 0 END FROM sys.databases WHERE [database_id] = DB_ID())
NẾU có bất kỳ cột có chiều dài thay đổi, sau đó thêm:
+ 2 + (2 * {NumVariableLengthColumns})
NẾU có bất kỳ MAX
cột / LOB nào, sau đó thêm:
+ (24 * {NumLobColumns})
Nói chung:
)) AS [MetaDataBytes]
Điều này không chính xác và một lần nữa sẽ không hoạt động nếu bạn bật Row hoặc Page nén trên Heap hoặc Clustered Index, nhưng chắc chắn sẽ giúp bạn tiến gần hơn.
CẬP NHẬT về Bí ẩn khác biệt 15%
Chúng tôi (bao gồm cả tôi) đã rất tập trung vào việc suy nghĩ về cách các trang dữ liệu được trình bày và làm thế nào DATALENGTH
có thể giải thích cho những thứ mà chúng tôi đã không dành nhiều thời gian để xem xét truy vấn thứ hai. Tôi đã chạy truy vấn đó với một bảng duy nhất và sau đó so sánh các giá trị đó với những gì được báo cáo sys.dm_db_database_page_allocations
và chúng không phải là cùng một giá trị cho số lượng trang. Trong một linh cảm, tôi đã loại bỏ các hàm tổng hợp GROUP BY
và thay thế SELECT
danh sách bằng a.*, '---' AS [---], p.*
. Và sau đó, mọi thứ trở nên rõ ràng: mọi người phải cẩn thận khi những người đan xen âm u này nhận được thông tin và tập lệnh của họ từ ;-). Truy vấn thứ 2 được đăng trong Câu hỏi không chính xác, đặc biệt đối với Câu hỏi cụ thể này.
Vấn đề nhỏ: bên ngoài nó không có ý nghĩa nhiều GROUP BY rows
(và không có cột đó trong hàm tổng hợp), THAM GIA giữa sys.allocation_units
và sys.partitions
không đúng về mặt kỹ thuật. Có 3 loại Đơn vị phân bổ và một trong số chúng sẽ THAM GIA vào một lĩnh vực khác. Khá thường xuyên partition_id
và hobt_id
giống nhau, vì vậy có thể không bao giờ có vấn đề, nhưng đôi khi hai trường đó có giá trị khác nhau.
Vấn đề chính: truy vấn sử dụng used_pages
trường. Trường đó bao gồm tất cả các loại trang: Dữ liệu, Chỉ mục, IAM, v.v., tc. Có một trường khác, phù hợp hơn để sử dụng khi chỉ liên quan đến dữ liệu thực tế : data_pages
.
Tôi đã điều chỉnh truy vấn thứ 2 trong Câu hỏi với các mục ở trên và sử dụng kích thước trang dữ liệu sao lưu tiêu đề trang. Tôi cũng lấy ra hai câu lệnh JOIN đó là không cần thiết: sys.schemas
(thay thế bằng cuộc gọi đến SCHEMA_NAME()
), và sys.indexes
(các Clustered Index luôn index_id = 1
và chúng tôi có index_id
trong sys.partitions
).
SELECT SCHEMA_NAME(st.[schema_id]) AS [SchemaName],
st.[name] AS [TableName],
SUM(sp.[rows]) AS [RowCount],
(SUM(sau.[total_pages]) * 8.0) / 1024 AS [TotalSpaceMB],
(SUM(CASE sau.[type]
WHEN 1 THEN sau.[data_pages]
ELSE (sau.[used_pages] - 1) -- back out the IAM page
END) * 7.91) / 1024 AS [TotalActualDataMB]
FROM sys.tables st
INNER JOIN sys.partitions sp
ON sp.[object_id] = st.[object_id]
INNER JOIN sys.allocation_units sau
ON ( sau.[type] = 1
AND sau.[container_id] = sp.[partition_id]) -- IN_ROW_DATA
OR ( sau.[type] = 2
AND sau.[container_id] = sp.[hobt_id]) -- LOB_DATA
OR ( sau.[type] = 3
AND sau.[container_id] = sp.[partition_id]) -- ROW_OVERFLOW_DATA
WHERE st.is_ms_shipped = 0
--AND sp.[object_id] = OBJECT_ID(N'dbo.table_name')
AND sp.[index_id] < 2 -- 1 = Clustered Index; 0 = Heap
GROUP BY SCHEMA_NAME(st.[schema_id]), st.[name]
ORDER BY [TotalSpaceMB] DESC;