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.
Data bukan satu-satunya hal yang menggunakan ruang pada halaman data 8k:
Ada ruang yang dipesan. Anda hanya diperbolehkan menggunakan 8060 dari 8192 byte (pertama, 132 byte yang tidak pernah menjadi milik Anda):
- Header halaman: Ini persis 96 byte.
- Slot array: ini adalah 2 byte per baris dan menunjukkan offset tempat setiap baris dimulai pada halaman. Ukuran array ini tidak terbatas pada 36 byte yang tersisa (132 - 96 = 36), jika tidak, Anda akan dibatasi secara efektif hanya dengan menempatkan 18 baris maks pada halaman data. Ini berarti bahwa setiap baris berukuran 2 byte lebih besar dari yang Anda kira. Nilai ini tidak termasuk dalam "ukuran catatan" seperti yang dilaporkan oleh
DBCC PAGE
, oleh karena itu disimpan terpisah di sini daripada dimasukkan dalam info per-baris di bawah ini.
- Meta-data Per-Baris (termasuk, tetapi tidak terbatas pada):
- Ukurannya bervariasi tergantung pada definisi tabel (yaitu jumlah kolom, panjang variabel atau panjang tetap, dll). Info diambil dari komentar @ PaulWhite dan @ Harun yang dapat ditemukan dalam diskusi terkait dengan jawaban dan pengujian ini.
- Head-row: 4 byte, 2 di antaranya menunjukkan tipe record, dan dua lainnya merupakan offset ke NULL Bitmap
- Jumlah kolom: 2 byte
- Bitmap NULL: kolom mana yang saat ini
NULL
. 1 byte per setiap set 8 kolom. Dan untuk semua kolom, bahkan kolom NOT NULL
. Oleh karena itu, minimal 1 byte.
- Array offset kolom panjang variabel: minimum 4 byte. 2 byte untuk menahan jumlah kolom panjang variabel, dan kemudian 2 byte per setiap kolom panjang variabel untuk menahan offset ke tempat dimulai.
- Info Versi: 14 byte (ini akan hadir jika database Anda diatur ke salah satu
ALLOW_SNAPSHOT_ISOLATION ON
atau READ_COMMITTED_SNAPSHOT ON
).
- Silakan lihat Pertanyaan dan Jawaban berikut untuk detail lebih lanjut tentang ini: Slot Array dan Ukuran Halaman Total
- Silakan lihat posting blog berikut dari Paul Randall yang memiliki beberapa detail menarik tentang bagaimana halaman data disusun: Menyodok dengan DBCC PAGE (Bagian 1 dari?)
Pointer LOB untuk data yang tidak disimpan dalam baris. Jadi itu akan menjelaskan DATALENGTH
+ pointer_size. Tapi ini bukan ukuran standar. Silakan lihat posting blog berikut untuk detail tentang topik kompleks ini: Berapa Ukuran Pointer LOB untuk (MAX) Jenis Seperti Varchar, Varbinary, Etc? . Di antara pos tertaut dan beberapa pengujian tambahan yang telah saya lakukan , aturan (default) adalah sebagai berikut:
- Legacy / usang jenis LOB bahwa tak seorang pun harus menggunakan lagi karena dari SQL Server 2005 (
TEXT
, NTEXT
, dan IMAGE
):
- Secara default, selalu simpan datanya di halaman LOB dan selalu gunakan pointer 16 byte ke penyimpanan LOB.
- JIKA sp_tableoption digunakan untuk mengatur
text in row
opsi, maka:
- jika ada ruang pada halaman untuk menyimpan nilai, dan nilainya tidak lebih besar dari ukuran maks dalam baris (kisaran yang dapat dikonfigurasi dari 24 - 7000 byte dengan standar 256), maka itu akan disimpan dalam baris,
- selain itu akan menjadi pointer 16-byte.
- Untuk jenis LOB baru diperkenalkan di SQL Server 2005 (
VARCHAR(MAX)
, NVARCHAR(MAX)
, dan VARBINARY(MAX)
):
- Secara default:
- Jika nilainya tidak lebih dari 8000 byte, dan ada ruang pada halaman, maka akan disimpan secara baris.
- Inline Root - untuk data antara 8001 dan 40.000 (benar-benar 42.000) byte, ruang mengizinkan, akan ada 1 hingga 5 pointer (24 - 72 byte) DALAM ROW yang mengarah langsung ke halaman LOB. 24 byte untuk halaman 8k LOB awal, dan 12 byte per setiap halaman 8k tambahan hingga empat halaman 8k lebih.
- TEXT_TREE - untuk data lebih dari 42.000 byte, atau jika 1 hingga 5 pointer tidak dapat dimasukkan secara berturut-turut, maka hanya akan ada pointer 24 byte ke halaman awal daftar pointer ke halaman LOB (yaitu "text_tree" "halaman).
- JIKA sp_tableoption digunakan untuk mengatur
large value types out of row
opsi, maka selalu gunakan pointer 16 byte ke penyimpanan LOB.
- Saya mengatakan aturan "default" karena saya tidak menguji nilai dalam baris terhadap dampak fitur tertentu seperti Kompresi Data, Enkripsi tingkat kolom, Enkripsi Data Transparan, Enkripsi Data Selalu, dll.
Halaman overflow LOB: Jika nilainya 10k, maka itu akan membutuhkan 1 halaman penuh 8k, dan kemudian bagian dari halaman ke-2. Jika tidak ada data lain yang dapat mengambil ruang yang tersisa (atau bahkan diizinkan, saya tidak yakin akan aturan itu), maka Anda memiliki kira-kira 6kb ruang "terbuang" pada datapage LOB ke-2.
Ruang yang tidak digunakan: Halaman data 8k hanya itu: 8192 byte. Ukurannya tidak bervariasi. Data dan meta-data yang ditempatkan di atasnya, bagaimanapun, tidak selalu cocok dengan semua 8192 byte. Dan baris tidak dapat dipisah menjadi beberapa halaman data. Jadi jika Anda memiliki 100 byte yang tersisa tetapi tidak ada baris (atau tidak ada baris yang sesuai dengan lokasi itu, tergantung pada beberapa faktor) dapat masuk ke sana, halaman data masih mengambil 8192 byte, dan permintaan 2 Anda hanya menghitung jumlah halaman data. Anda dapat menemukan nilai ini di dua tempat (perlu diingat bahwa sebagian dari nilai ini adalah sejumlah ruang yang dipesan):
DBCC PAGE( db_name, file_id, page_id ) WITH TABLERESULTS;
Cari ParentObject
= "PAGE HEADER:" dan Field
= "m_freeCnt". The Value
lapangan adalah jumlah byte yang tidak terpakai.
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;
Ini adalah nilai yang sama seperti yang dilaporkan oleh "m_freeCnt". Ini lebih mudah daripada DBCC karena bisa mendapatkan banyak halaman, tetapi juga mengharuskan halaman tersebut dibaca di dalam buffer pool.
Ruang dicadangkan oleh FILLFACTOR
<100. Halaman yang baru dibuat tidak menghormati FILLFACTOR
pengaturan, tetapi melakukan REBUILD akan mencadangkan ruang itu pada setiap halaman data. Gagasan di balik ruang yang dipesan adalah bahwa itu akan digunakan oleh sisipan non-sekuensial dan / atau pembaruan yang memperluas ukuran baris pada halaman, karena kolom panjang variabel diperbarui dengan sedikit lebih banyak data (tetapi tidak cukup untuk menyebabkan halaman-split). Tetapi Anda dapat dengan mudah memesan ruang pada halaman data yang secara alami tidak akan pernah mendapatkan baris baru dan tidak pernah memiliki baris yang diperbarui, atau setidaknya tidak diperbarui dengan cara yang akan meningkatkan ukuran baris.
Page-Splits (fragmentasi): Perlu menambahkan baris ke lokasi yang tidak memiliki ruang untuk baris akan menyebabkan pemisahan halaman. Dalam hal ini, sekitar 50% dari data yang ada dipindahkan ke halaman baru dan baris baru ditambahkan ke salah satu dari 2 halaman. Tetapi sekarang Anda memiliki sedikit lebih banyak ruang kosong yang tidak diperhitungkan dengan DATALENGTH
perhitungan.
Baris yang ditandai untuk dihapus. Ketika Anda menghapus baris, mereka tidak selalu segera dihapus dari halaman data. Jika mereka tidak dapat segera dihapus, mereka "ditandai untuk mati" (referensi Steven Segal) dan kemudian akan dihapus secara fisik oleh proses pembersihan hantu (saya percaya itu adalah namanya). Namun, ini mungkin tidak relevan dengan Pertanyaan khusus ini.
Halaman hantu? Tidak yakin apakah itu istilah yang tepat, tetapi kadang-kadang halaman data tidak bisa dihapus sampai REBUILD Indeks Clustered selesai. Itu juga akan menjelaskan lebih banyak halaman daripada yang DATALENGTH
akan ditambahkan. Ini umumnya tidak boleh terjadi, tetapi saya pernah mengalami satu kali, beberapa tahun yang lalu.
Kolom SPARSE: Kolom jarang menghemat ruang (kebanyakan untuk tipe data panjang tetap) dalam tabel di mana sebagian besar baris adalah NULL
untuk satu atau beberapa kolom. The SPARSE
pilihan membuat NULL
jenis nilai sampai 0 bytes (bukan normal jumlah tetap-panjang, seperti 4 byte untuk INT
), namun , non-NULL nilai masing-masing mengambil sebuah tambahan 4 byte untuk jenis fixed-panjang dan jumlah variabel untuk tipe panjang variabel. Masalahnya di sini adalah bahwa DATALENGTH
tidak termasuk tambahan 4 byte untuk nilai non-NULL dalam kolom SPARSE, sehingga 4 byte tersebut perlu ditambahkan kembali. Anda dapat memeriksa untuk melihat apakah ada SPARSE
kolom melalui:
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;
Dan kemudian untuk setiap SPARSE
kolom, perbarui kueri asli untuk menggunakan:
SUM(DATALENGTH(FieldN) + 4)
Harap perhatikan bahwa perhitungan di atas untuk menambahkan standar 4 byte agak sederhana karena hanya bekerja untuk jenis yang memiliki panjang tetap. DAN, ada meta-data tambahan per baris (dari apa yang bisa saya katakan sejauh ini) yang mengurangi ruang yang tersedia untuk data, hanya dengan memiliki setidaknya satu kolom SPARSE. Untuk detail lebih lanjut, silakan lihat halaman MSDN untuk Menggunakan Kolom Jarang .
Halaman indeks dan lainnya (mis. IAM, PFS, GAM, SGAM, dll): ini bukan halaman "data" dalam hal data pengguna. Ini akan mengembang ukuran total tabel. Jika menggunakan SQL Server 2012 atau yang lebih baru, Anda dapat menggunakan sys.dm_db_database_page_allocations
Dynamic Management Function (DMF) untuk melihat tipe halaman (versi SQL Server sebelumnya dapat digunakan 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
Baik DBCC IND
maupun sys.dm_db_database_page_allocations
(dengan klausa WHERE) itu akan melaporkan halaman Indeks mana pun, dan hanya DBCC IND
akan melaporkan setidaknya satu halaman IAM.
DATA_COMPRESSION: Jika Anda memiliki ROW
atau PAGE
Kompresi diaktifkan pada Clustered Index atau Heap, maka Anda dapat melupakan sebagian besar dari apa yang telah disebutkan sejauh ini. Header Halaman 96 byte, 2 Slot Slot byte-per-baris, dan Info Versi 14 byte-per-baris masih ada, tetapi representasi fisik data menjadi sangat kompleks (lebih dari apa yang telah disebutkan ketika Kompresi sedang tidak digunakan). Misalnya, dengan Kompresi Baris, SQL Server mencoba menggunakan wadah sekecil mungkin untuk memenuhi setiap kolom, per setiap baris. Jadi jika Anda memiliki BIGINT
kolom yang jika tidak (dengan asumsi SPARSE
juga tidak diaktifkan) selalu mengambil 8 byte, jika nilainya antara -128 dan 127 (yaitu ditandatangani bilangan bulat 8-bit) maka akan menggunakan hanya 1 byte, dan jika nilai bisa masuk ke dalamSMALLINT
, itu hanya akan memakan waktu 2 byte. Tipe integer yang salah NULL
atau tidak 0
memakan ruang dan hanya ditunjukkan sebagai sedang NULL
atau "kosong" (yaitu 0
) dalam array memetakan kolom. Dan ada banyak, banyak aturan lainnya. Punya data Unicode ( NCHAR
,, NVARCHAR(1 - 4000)
tetapi tidak NVARCHAR(MAX)
, bahkan jika disimpan dalam baris)? Unicode Compression ditambahkan dalam SQL Server 2008 R2, tetapi tidak ada cara untuk memprediksi hasil dari nilai "terkompresi" di semua situasi tanpa melakukan kompresi aktual mengingat kompleksitas aturan .
Jadi sungguh, permintaan kedua Anda, walaupun lebih akurat dalam hal total ruang fisik yang digunakan pada disk, hanya benar-benar akurat saat melakukan REBUILD
Indeks Clustered. Dan setelah itu, Anda masih perlu memperhitungkan FILLFACTOR
pengaturan apa pun di bawah 100. Dan meskipun demikian selalu ada tajuk halaman, dan seringkali cukup sejumlah ruang "terbuang" yang tidak dapat diisi karena terlalu kecil untuk ditampung dalam baris apa pun di ini tabel, atau setidaknya baris yang secara logis harus masuk dalam slot itu.
Mengenai keakuratan kueri ke-2 dalam menentukan "penggunaan data", tampaknya paling adil untuk membatalkan byte Page Header karena mereka bukan penggunaan data: itu adalah biaya overhead bisnis. Jika ada 1 baris pada halaman data dan baris itu hanya a TINYINT
, maka 1 byte itu tetap mensyaratkan bahwa halaman data ada dan karenanya 96 byte header. Haruskah 1 departemen dikenakan biaya untuk seluruh halaman data? Jika halaman data itu kemudian diisi oleh Departemen # 2, apakah mereka akan membagi secara merata biaya "overhead" atau membayar secara proporsional? Tampaknya paling mudah untuk mundur saja. Dalam hal ini, menggunakan nilai 8
untuk menggandakan melawan number of pages
terlalu tinggi. Bagaimana tentang:
-- 8192 byte data page - 96 byte header = 8096 (approx) usable bytes.
SELECT 8060.0 / 1024 -- 7.906250
Karenanya, gunakan sesuatu seperti:
(SUM(a.total_pages) * 7.91) / 1024 AS [TotalSpaceMB]
untuk semua perhitungan terhadap kolom "number_of_pages".
DAN , mengingat bahwa menggunakan DATALENGTH
per bidang masing-masing tidak dapat mengembalikan meta-data per-baris, yang harus ditambahkan ke kueri per-tabel di mana Anda mendapatkan DATALENGTH
per bidang masing-masing, memfilter pada setiap "departemen":
- Jenis Rekam dan offset ke NULL Bitmap: 4 byte
- Jumlah Kolom: 2 byte
- Slot Array: 2 byte (tidak termasuk dalam "ukuran rekaman" tetapi masih perlu memperhitungkan)
- NULL Bitmap: 1 byte per setiap 8 kolom (untuk semua kolom)
- Versi Baris: 14 byte (jika database memiliki
ALLOW_SNAPSHOT_ISOLATION
atau READ_COMMITTED_SNAPSHOT
diatur ke ON
)
- Kolom Panjang Variabel-Panjang Array: 0 byte jika semua kolom memiliki panjang tetap. Jika ada kolom yang panjang variabel, maka 2 byte, ditambah 2 byte per masing-masing hanya kolom panjang variabel.
- LOB pointer: bagian ini sangat tidak tepat karena tidak akan ada pointer jika nilainya
NULL
, dan jika nilainya cocok pada baris maka itu bisa jauh lebih kecil atau lebih besar dari pointer, dan jika nilainya disimpan off- baris, maka ukuran pointer mungkin tergantung pada seberapa banyak data yang ada. Namun, karena kami hanya menginginkan perkiraan (mis. "Barang curian"), sepertinya 24 byte adalah nilai yang baik untuk digunakan (well, sebagus ;-) lainnya. Ini adalah per MAX
bidang masing-masing .
Karenanya, gunakan sesuatu seperti:
Secara umum (tajuk baris + jumlah kolom + susunan slot + bitmap NULL):
([RowCount] * (( 4 + 2 + 2 + (1 + (({NumColumns} - 1) / 8) ))
Secara umum (deteksi otomatis jika "info versi" ada):
+ (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())
JIKA ada kolom panjang variabel, lalu tambahkan:
+ 2 + (2 * {NumVariableLengthColumns})
JIKA ada MAX
/ kolom LOB, lalu tambahkan:
+ (24 * {NumLobColumns})
Secara umum:
)) AS [MetaDataBytes]
Ini tidak tepat, dan sekali lagi tidak akan berfungsi jika Anda mengaktifkan Kompresi Baris atau Halaman pada Heap atau Clustered Index, tetapi pasti akan membuat Anda lebih dekat.
PEMBARUAN Mengenai Misteri Perbedaan 15%
Kami (termasuk saya) sangat fokus pada pemikiran tentang bagaimana halaman data disusun dan bagaimana DATALENGTH
mungkin menjelaskan hal-hal yang kami tidak menghabiskan banyak waktu untuk meninjau permintaan ke-2. Saya menjalankan kueri itu terhadap satu tabel dan kemudian membandingkan nilai-nilai itu dengan apa yang dilaporkan oleh sys.dm_db_database_page_allocations
dan mereka bukan nilai yang sama untuk jumlah halaman. Pada firasat, saya menghapus fungsi agregat dan GROUP BY
, dan mengganti SELECT
daftar dengan a.*, '---' AS [---], p.*
. Dan kemudian menjadi jelas: orang-orang harus berhati-hati di mana pada jalinan keruh ini mereka mendapatkan info dan skrip mereka dari ;-). Kueri ke-2 yang diposting di Pertanyaan tidak sepenuhnya benar, terutama untuk Pertanyaan khusus ini.
Masalah kecil: di luarnya tidak masuk akal untuk GROUP BY rows
(dan tidak memiliki kolom dalam fungsi agregat), GABUNG antara sys.allocation_units
dan sys.partitions
secara teknis tidak benar. Ada 3 jenis Unit Alokasi, dan salah satunya harus BERGABUNG ke bidang yang berbeda. Cukup sering partition_id
dan hobt_id
sama, sehingga mungkin tidak pernah ada masalah, tetapi terkadang kedua bidang tersebut memiliki nilai yang berbeda.
Masalah utama: kueri menggunakan used_pages
bidang. Bidang itu mencakup semua jenis halaman: Data, Indeks, IAM, dll, tc. Ada, bidang lain yang lebih tepat untuk digunakan saat yang bersangkutan dengan hanya data aktual: data_pages
.
Saya menyesuaikan kueri ke-2 dalam Pertanyaan dengan item di atas, dan menggunakan ukuran halaman data yang mendukung header halaman. Saya juga menghapus dua GABUNGAN yang tidak perlu: sys.schemas
(diganti dengan ajakan untuk SCHEMA_NAME()
), dan sys.indexes
(Indeks Clustered selalu index_id = 1
dan kami ada index_id
di 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;