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 Bitmap
- Количество столбцов: 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. Но они не стандартного размера. Пожалуйста, обратитесь к следующему сообщению в блоге для получения подробной информации по этой сложной теме: Каков размер указателя большого объекта для (MAX) типов, таких как Varchar, Varbinary, Etc? , Между этой ссылкой и некоторым дополнительным тестированием, которое я провел , правила (по умолчанию) должны быть следующими:
- Наследие / осуждается типы LOB , что никто не должен использовать больше от SQL Server 2005 (
TEXT
, NTEXT
и IMAGE
):
- По умолчанию всегда храните свои данные на страницах больших объектов и всегда используйте 16-байтовый указатель на хранилище больших объектов.
- Если sp_tableoption был использован для установки
text in row
параметра, то:
- если на странице есть место для хранения значения, а значение не превышает максимальный размер строки (настраиваемый диапазон 24–7000 байт при значении по умолчанию 256), то оно будет храниться в строке,
- иначе это будет 16-байтовый указатель.
- Для новых типов больших объектов , введенных в SQL Server 2005 (
VARCHAR(MAX)
, NVARCHAR(MAX)
и VARBINARY(MAX)
):
- По умолчанию:
- Если значение не превышает 8000 байт, и на странице есть место, оно будет сохранено в строке.
- Встроенный корень - для данных размером от 8001 до 40000 (на самом деле 42000) байтов, если позволяет пространство, в строке будет от 1 до 5 указателей (24–72 байта), которые указывают непосредственно на страницу (ы) больших объектов. 24 байта для начальной страницы 8 КБ и 12 байт на каждую дополнительную страницу 8 КБ для еще четырех дополнительных страниц 8 КБ.
- TEXT_TREE - для данных размером более 42 000 байтов или если указатели от 1 до 5 не могут поместиться в строке, тогда будет только 24-байтовый указатель на начальную страницу списка указателей на страницы больших объектов (т. Е. "Text_tree" "страница).
- Если для задания параметра был использован sp_tableoption
large value types out of row
, то всегда используется 16-байтовый указатель на хранилище больших объектов.
- Я сказал «стандартные» правила, потому что я не проверял значения в строке на предмет воздействия определенных функций, таких как сжатие данных, шифрование на уровне столбцов, прозрачное шифрование данных, всегда зашифрованное и т. Д.
Страницы переполнения больших объектов: если значение равно 10 КБ, для этого потребуется 1 полная страница 8 КБ переполнения, а затем часть 2-й страницы. Если никакие другие данные не могут занять оставшееся пространство (или даже разрешено, я не уверен в этом правиле), то у вас есть приблизительно 6 КБ «потерянного» пространства в этой 2-й странице данных переполнения большого объекта.
Неиспользуемое пространство: страница данных 8 КБ - это всего лишь 8192 байта. Он не отличается по размеру. Однако размещенные на нем данные и метаданные не всегда хорошо вписываются во все 8192 байта. И строки не могут быть разделены на несколько страниц данных. Таким образом, если у вас осталось 100 байт, но ни одна строка (или ни одна строка, которая бы не подходила в этом месте, в зависимости от нескольких факторов) не могла бы туда поместиться, страница данных по-прежнему занимает 8192 байта, а ваш второй запрос только подсчитывает количество страницы данных. Вы можете найти это значение в двух местах (просто имейте в виду, что некоторая часть этого значения составляет некоторое количество этого зарезервированного пространства):
DBCC PAGE( db_name, file_id, page_id ) WITH TABLERESULTS;
Ищите ParentObject
= "СТРАНИЦА ЗАГОЛОВОК:" и 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% существующих данных перемещается на новую страницу, а новая строка добавляется на одну из двух страниц. Но теперь у вас есть немного больше свободного места, которое не учитывается в DATALENGTH
расчетах.
Строки помечены для удаления. При удалении строк они не всегда сразу удаляются со страницы данных. Если они не могут быть удалены немедленно, они «помечены для смерти» (ссылка Стивена Сегала) и будут физически удалены позже процессом очистки призрака (я полагаю, что это имя). Однако они могут не относиться к данному конкретному Вопросу.
Призрачные страницы? Не уверен, что это правильный термин, но иногда страницы данных не удаляются, пока не будет выполнено REBUILD Кластерного индекса. Это также будет учитывать больше страниц, чем DATALENGTH
в сумме. Обычно этого не должно происходить, но я столкнулся с этим один раз, несколько лет назад.
SPARSE столбцы: разреженные столбцы экономят пространство (в основном для типов данных фиксированной длины) в таблицах, где большой процент строк предназначен NULL
для одного или нескольких столбцов. SPARSE
Вариант делает NULL
тип значения до 0 байт (вместо нормального количества фиксированной длиной, например , как 4 байта для INT
), но , не нулевых значений каждого занимает еще 4 байта для типов фиксированной длины и количества переменного для типы переменной длины. Проблема здесь заключается в том, что DATALENGTH
в столбец SPARSE не входят дополнительные 4 байта для значений, отличных от NULL, поэтому эти 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 байта. Типы Integer , которые являются либо 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 Bitmap: 4 байта
- Количество столбцов: 2 байта
- Массив слотов: 2 байта (не входит в «размер записи», но все же необходимо учитывать)
- NULL Bitmap: 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]
Это не точно, и опять-таки не будет работать, если у вас включено сжатие строк или страниц в кучном или кластерном индексе, но вы должны определенно приблизиться.
ОБНОВЛЕНИЕ Относительно Тайны Разницы 15%
Мы (включая меня) были настолько сосредоточены на размышлениях о том, как устроены страницы данных и как DATALENGTH
можно объяснить вещи, которые мы не тратили много времени на рассмотрение второго запроса. Я запустил этот запрос для одной таблицы, а затем сравнил эти значения с тем, о чем сообщалось, sys.dm_db_database_page_allocations
и они не были одинаковыми для количества страниц. По догадкам я удалил агрегатные функции и GROUP BY
и заменил SELECT
список на a.*, '---' AS [---], p.*
. И тогда стало ясно: люди должны быть осторожны, откуда в этих мутных сетях они получают информацию и сценарии ;-). Второй запрос, размещенный в Вопросе, не совсем корректен, особенно для этого конкретного Вопроса.
Незначительная проблема: вне этого не имеет особого смысла GROUP BY rows
(и не иметь этого столбца в статистической функции), соединение sys.allocation_units
и sys.partitions
не является технически правильным. Существует 3 типа единиц распределения, и один из них должен присоединиться к другому полю. Довольно часто partition_id
и hobt_id
тот же, так что не может быть проблемой, но иногда эти два поля имеют разные значения.
Основная проблема: в запросе используется used_pages
поле. Это поле охватывает все типы страниц: данные, индекс, IAM и т. Д. И т. Д. Существует еще один, более соответствующее поле для использования при касается только фактические данные: data_pages
.
Я адаптировал 2-й запрос в Вопросе с учетом вышеупомянутых элементов и использовал размер страницы данных, который поддерживает заголовок страницы. Я также удалил два JOIN, которые были ненужными: sys.schemas
(заменено на call to SCHEMA_NAME()
) и sys.indexes
(Clustered Index всегда есть, 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;