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.
Дані - не єдине, що займає місце на сторінці даних 8K:
Є зарезервований простір. Ви можете використовувати лише 8060 з 8192 байт (це 132 байти, які ніколи не були вашими):
- Заголовок сторінки: це рівно 96 байт.
- Масив слотів: це 2 байти на рядок і вказує на зміщення місця початку кожного рядка на сторінці. Розмір цього масиву не обмежується іншими 36 байтами (132 - 96 = 36), інакше ви будете фактично обмежені лише розміщенням максимум 18 рядків на сторінці даних. Це означає, що кожен рядок на 2 байти більший, ніж ви думаєте. Це значення не включається до "розміру запису", як повідомляється
DBCC PAGE
, тому воно зберігається окремо тут, замість того, щоб його включати в інформацію про рядки нижче.
- Мета-дані на рядок (включаючи, але не обмежуючись ними):
- Розмір змінюється залежно від визначення таблиці (тобто кількості стовпців, змінної довжини або фіксованої довжини тощо). Інформація, взята з коментарів @ PaulWhite та @ Aaron, які можна знайти в дискусії, що стосується цієї відповіді та тестування.
- Рядок-заголовок: 4 байти, 2 з яких позначають тип запису, а два інших - зміщення до растрової карти NULL
- Кількість стовпців: 2 байти
- NULL Bitmap: які стовпці зараз є
NULL
. 1 байт на кожен набір із 8 стовпців. І для всіх стовпців, навіть NOT NULL
тих. Значить, мінімум 1 байт.
- Масив зміщення стовпців змінної довжини: мінімум 4 байти. 2 байти, щоб вмістити кількість стовпців змінної довжини, а потім 2 байти на кожен стовпчик змінної довжини, щоб утримувати зміщення до місця, де воно починається.
- Інформація про версію: 14 байт (це буде наявно, якщо для вашої бази даних встановлено
ALLOW_SNAPSHOT_ISOLATION ON
або READ_COMMITTED_SNAPSHOT ON
).
- Будь ласка, перегляньте наступне запитання та відповіді, щоб отримати детальну інформацію про це: Масив слотів та загальний розмір сторінки
- Перегляньте наступну публікацію в блозі від Пола Рендала, в якій є кілька цікавих подробиць про те, як розміщені сторінки даних: Познайомлення з DBCC PAGE (Частина 1?)
Покажчики LOB для даних, які не зберігаються у рядку. Отже, це складе DATALENGTH
+ pointer_size. Але вони не стандартного розміру. Детальну інформацію про цю складну тему див. У наступному блозі: Який розмір покажчика LOB для (MAX) типів, таких як Varchar, Varbinary, Etc? . Між цією пов’язаною публікацією та деякими додатковими тестуваннями, які я зробив , правила (за замовчуванням) повинні бути такими:
- Спадщина / засуджується типи LOB , що ніхто не повинен використовувати більше від SQL Server 2005 (
TEXT
, NTEXT
і IMAGE
):
- За замовчуванням завжди зберігайте свої дані на сторінках LOB і завжди використовуйте 16-байтовий покажчик на сховище LOB.
- Якщо для встановлення параметра використовується параметр sp_tableoption
text in row
, тоді:
- якщо на сторінці є простір для зберігання значення, а значення не більше максимального розміру рядка (діапазон, який можна налаштувати 24 - 7000 байт за замовчуванням 256), він буде зберігатися в рядку,
- інакше це буде 16-байтовий покажчик.
- Для нових типів великих об'єктів , введених в SQL Server 2005 (
VARCHAR(MAX)
, NVARCHAR(MAX)
і VARBINARY(MAX)
):
- За замовчуванням:
- Якщо значення не перевищує 8000 байт, а на сторінці є місце, то воно буде зберігатися в рядку.
- Вбудований корінь - для даних між 8001 і 40 000 (дійсно 42 000) байтів, дозволяючи простір, буде 1 - 5 покажчиків (24 - 72 байти) В РАДКІ, що вказують безпосередньо на сторінку LOB. 24 байти для початкової сторінки з 8 кб LOB і 12 байт на кожну додаткову 8 к сторінок до ще чотирьох 8 к сторінок.
- TEXT_TREE - для даних, що перевищують 42 000 байт, або якщо від 1 до 5 покажчиків не може вписатись в рядок, тоді буде лише 24-байтовий покажчик на початкову сторінку списку покажчиків на сторінки LOB (тобто "text_tree" "сторінка).
- Якщо для встановлення параметра використовувалася sp_tableoption
large value types out of row
, то завжди використовуйте 16-байтовий вказівник на сховище LOB.
- Я сказав "правила за замовчуванням", оскільки не перевіряв значення рядків на вплив певних функцій, таких як стиснення даних, шифрування на рівні стовпців, прозоре шифрування даних, завжди зашифрований тощо.
Сторінки переповнення LOB: Якщо значення дорівнює 10k, то для цього знадобиться 1 повна 8k сторінка переповнення, а потім частина 2-ї сторінки. Якщо жодна інша інформація не може займати залишковий простір (або це навіть дозволено, я не впевнений у цьому правилі), то у вас є приблизно 6 кбіт "витраченого" простору на цій другій сторінці переповнення LOB.
Невикористаний простір. Сторінка даних 8k - це лише те: 8192 байт. Він не відрізняється за розміром. Дані та метадані, розміщені на них, однак, не завжди добре вписуються у всі 8192 байти. І рядки не можна розділити на кілька сторінок даних. Отже, якщо у вас залишилося 100 байтів, але жоден рядок (або жодна рядок, який би розміщувався в цьому місці, залежно від кількох факторів), не може вміститись там, сторінка даних все ще займає 8192 байти, а ваш другий запит підраховує лише кількість сторінки даних. Ви можете знайти це значення у двох місцях (лише майте на увазі, що деяка частина цього значення - це деяка кількість зарезервованого простору):
DBCC PAGE( db_name, file_id, page_id ) WITH TABLERESULTS;
Шукайте ParentObject
= "PAGE HEADER:" і Field
= "m_freeCnt". Value
Поле число невикористаних байтів.
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;
Це те саме значення, що повідомляється "m_freeCnt". Це простіше, ніж DBCC, оскільки він може отримати багато сторінок, але також вимагає, щоб сторінки були прочитані в буферному пулі в першу чергу.
Простір, зарезервований FILLFACTOR
<100. Новостворені сторінки не враховують FILLFACTOR
налаштування, але виконати REBUILD зарезервує це місце на кожній сторінці даних. Ідея зарезервованого простору полягає в тому, що він буде використовуватися непослідовними вставками та / або оновленнями, які вже збільшують розмір рядків на сторінці, через те, що стовпці змінної довжини оновлюються трохи більше даних (але недостатньо, щоб викликати розділення сторінки). Але ви можете легко зарезервувати простір на сторінках даних, які, природно, ніколи не отримуватимуть нові рядки і ніколи не оновлюватимуть існуючі рядки або принаймні не оновлюватимуться таким чином, що збільшуватиме розмір рядка.
Розділення сторінок (фрагментація): Необхідність додавання рядка до місця, де немає місця для рядка, спричинить розкол сторінки. У цьому випадку приблизно 50% існуючих даних переміщуються на нову сторінку, а новий рядок додається до однієї з 2 сторінок. Але тепер у вас є трохи більше вільного місця, яке не враховується DATALENGTH
підрахунками.
Рядки, позначені для видалення. Видаляючи рядки, вони не завжди одразу видаляються зі сторінки даних. Якщо їх не вдасться зняти негайно, вони будуть "позначені смертю" (посилання Стівена Сегала) і пізніше будуть фізично видалені шляхом прибирання привидів (я вважаю, що це назва). Однак це може не стосуватися цього конкретного питання.
Сторінки привидів? Не впевнений, чи це правильний термін, але іноді сторінки даних не видаляються, поки не буде виконано ПОВТОРЕННЯ кластерного індексу. Це також враховує більше сторінок, ніж DATALENGTH
до них. Це взагалі не повинно статися, але я натрапив на це один раз, кілька років тому.
РІЗНАЧНІ стовпці: Рідкі стовпці економлять простір (переважно для типів даних фіксованої довжини) у таблицях, де великий% рядків припадає NULL
на один або кілька стовпців. SPARSE
Варіант робить NULL
тип значення до 0 байт (замість нормального кількості фіксованою довжиною, наприклад , як 4 байта для INT
), але , що не нульових значень кожного займає ще 4 байта для типів фіксованої довжини і кількості змінного для типи змінної довжини. Проблема тут полягає в тому, DATALENGTH
що не включаються зайві 4 байти для значень, що не мають NULL, у стовпці SPARSE, тому ці 4 байти потрібно додати ще раз. Ви можете перевірити, чи є SPARSE
колонки через:
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;
А потім для кожного SPARSE
стовпця оновіть початковий запит, щоб використовувати:
SUM(DATALENGTH(FieldN) + 4)
Зауважте, що вищевказаний розрахунок для додавання в стандартні 4 байти трохи спрощений, оскільки працює лише для типів фіксованої довжини. І, є додаткові метадані на рядок (з того, що я можу сказати досі), що зменшує доступний для даних простір, просто маючи принаймні один стовпець SPARSE. Для отримання більш детальної інформації див. Сторінку MSDN для використання Розріджених стовпців .
Індексні та інші (наприклад, IAM, PFS, GAM, SGAM тощо): це не "сторінки даних" з точки зору даних користувачів. Вони збільшать загальний розмір столу. Якщо ви використовуєте SQL Server 2012 або новішу sys.dm_db_database_page_allocations
версію , ви можете використовувати функцію динамічного управління (DMF), щоб переглянути типи сторінок (можна використовувати більш ранні версії SQL Server 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
Ні DBCC IND
ані норми sys.dm_db_database_page_allocations
(з цим пунктом WHERE) не повідомлятимуть про будь-які сторінки індексу, і лише DBCC IND
повідомлення повідомлятиме про щонайменше одну сторінку IAM.
DATA_COMPRESSION: Якщо у вас ввімкнено ROW
або PAGE
стиснення в кластерному індексі або купі, ви можете забути про більшість згаданих досі. 96-байтний заголовок сторінок, 2 байти на рядок слотового масиву та 14 байт на рядок інформація про версію все ще є, але фізичне представлення даних стає дуже складним (набагато більше, ніж те, що вже згадувалося при стисненні не використовується). Наприклад, за допомогою стиснення рядків SQL Server намагається використати найменший контейнер для розміщення кожного стовпця в кожному рядку. Отже, якщо у вас є BIGINT
стовпець, який інакше (якщо SPARSE
також не включено) завжди займає 8 байт, якщо значення становить від -128 до 127 (тобто підписане 8-бітове ціле число), то він буде використовувати лише 1 байт, і якщо значення може вписатися вSMALLINT
, він займе лише 2 байти. Цілі типи, які є NULL
або 0
не займають місця і просто вказуються як NULL
"порожні" (тобто 0
) у масиві, що відображає стовпці. І є багато-багато інших правил. Чи є дані Unicode ( NCHAR
, NVARCHAR(1 - 4000)
але ні NVARCHAR(MAX)
, навіть якщо вони зберігаються в рядку)? Стиснення Unicode було додано в SQL Server 2008 R2, але неможливо передбачити результат "стисненого" значення у всіх ситуаціях, не роблячи фактичного стиснення, враховуючи складність правил .
Тож справді, ваш другий запит, хоча більш точний з точки зору загального фізичного простору, зайнятого на диску, є дійсно точним лише при виконанні REBUILD
кластерного індексу. А після цього вам все одно потрібно враховувати будь-які FILLFACTOR
налаштування нижче 100. І навіть тоді завжди є заголовки сторінок, і часто досить деякої кількості "витраченого" простору, який просто не можна заповнити через те, що він занадто малий, щоб вмістити будь-який рядок у цьому таблиці або, принаймні, рядок, який логічно повинен містити цей слот.
Що стосується точності 2-го запиту при визначенні "використання даних", то найбільш справедливим є резервне копіювання байтів заголовка сторінки, оскільки вони не є використанням даних: вони є витратами на витрату бізнесу. Якщо на сторінці даних є 1 рядок, а цей рядок є лише a TINYINT
, тоді для байта даних потрібен 1 байт і, отже, 96 байт заголовка. Чи повинен за цей 1 відділ стягуватися плата за всю сторінку даних? Якщо цю сторінку даних заповнити Департамент №2, чи розподілили б вони рівномірно цю "накладну" вартість або сплатили пропорційно? Здається, найпростіше просто відмовитись. У такому випадку використання значення 8
множення на проти number of pages
занадто високе. Як на рахунок:
-- 8192 byte data page - 96 byte header = 8096 (approx) usable bytes.
SELECT 8060.0 / 1024 -- 7.906250
Отже, використовуйте щось на кшталт:
(SUM(a.total_pages) * 7.91) / 1024 AS [TotalSpaceMB]
для всіх обчислень у стовпцях "number_of_pages".
І , враховуючи, що використання DATALENGTH
кожного поля не може повернути метадані на рядки, їх слід додати до запиту DATALENGTH
на кожну таблицю, де ви отримуєте кожне поле, фільтруючи по кожному "відділу":
- Тип запису та зміщення до NULL растрової карти: 4 байти
- Кількість стовпців: 2 байти
- Масив слотів: 2 байти (не входять до "розміру запису", але все ще потрібно враховувати)
- Растровий файл NULL: 1 байт на кожні 8 стовпців (для всіх стовпців)
- Версія версії: 14 байт (якщо база даних має
ALLOW_SNAPSHOT_ISOLATION
або READ_COMMITTED_SNAPSHOT
встановлено ON
)
- Стовпець зміщеної довжини Масив зміщення: 0 байт, якщо всі стовпці фіксованої довжини. Якщо будь-які стовпці мають змінну довжину, то 2 байти плюс 2 байти на кожен із стовпців змінної довжини.
- Покажчики LOB: ця частина дуже неточна, оскільки не буде вказівника, якщо значення є
NULL
, і якщо значення підходить до рядка, то воно може бути набагато меншим або значно більшим, ніж покажчик, і якщо значення зберігається не- рядок, то розмір вказівника може залежати від кількості даних. Однак, оскільки ми просто хочемо оцінювати (тобто "swag"), схоже, 24 байти - це корисне значення для використання (ну, так само добре, як і будь-який інший ;-). Це для кожного MAX
поля.
Отже, використовуйте щось на кшталт:
Загалом (заголовок рядка + кількість стовпців + масив слотів + NULL растрова карта):
([RowCount] * (( 4 + 2 + 2 + (1 + (({NumColumns} - 1) / 8) ))
Загалом (автоматично виявляє, якщо є "інформація про версію"):
+ (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())
Якщо є стовпчики змінної довжини, то додайте:
+ 2 + (2 * {NumVariableLengthColumns})
Якщо є MAX
колони / LOB, додайте:
+ (24 * {NumLobColumns})
В загальному:
)) AS [MetaDataBytes]
Це не точно, і знову не вийде, якщо у вас ввімкнено стиснення рядків або сторінок у Heap або Clustered Index, але, безумовно, слід наблизити вас.
ОНОВЛЕННЯ щодо таємниці різниці 15%
Ми (включаючи мене) були настільки зосереджені на думці про те, як розкладені сторінки даних і як DATALENGTH
можна пояснити речі, на які ми не витратили багато часу на перегляд 2-го запиту. Я провів цей запит по одній таблиці, а потім порівняв ці значення з тим, про що повідомлялося, sys.dm_db_database_page_allocations
і вони не були однаковими для кількості сторінок. Під час перегляду я видалив сукупні функції GROUP BY
і замінив SELECT
список на a.*, '---' AS [---], p.*
. І тоді стало зрозуміло: люди повинні бути обережними, звідки на цих мутних переплетеннях вони отримують інформацію та сценарії ;-). 2-й запит, розміщений у Питання, не зовсім коректний, особливо для цього конкретного питання.
Незначна проблема: поза нею не має великого сенсу GROUP BY rows
(і немає цього стовпця в сукупній функції), ПРИЄДНАЙТЕСЬ між sys.allocation_units
і sys.partitions
технічно не є правильним. Існує 3 типи одиниць розподілу, і один з них повинен приєднатися до іншого поля. Досить часто partition_id
і hobt_id
той же, так що не може бути проблемою, але іноді ці два поля мають різні значення.
Основна проблема: запит використовує used_pages
поле. Це поле охоплює всі типи сторінок: Дані, Індекс, IAM тощо, тс. Існує ще один, більш відповідне поле для використання при стосується тільки фактичні дані: data_pages
.
Я адаптував 2-й запит у запитанні з урахуванням вищезазначених пунктів і, використовуючи розмір сторінки даних, що резервує заголовок сторінки. Я також видалив два JOIN і що НЕ було необхідності: sys.schemas
(замінена на виклик SCHEMA_NAME()
), і sys.indexes
(індекс кластерного завжди , index_id = 1
і ми маємо index_id
в 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;