СУММА ДАННЫХ, не соответствующих размеру таблицы из sys.allocation_units

11

У меня сложилось впечатление, что если бы я суммировал DATALENGTH()все поля для всех записей в таблице, я бы получил общий размер таблицы. Я ошибаюсь?

SELECT 
SUM(DATALENGTH(Field1)) + 
SUM(DATALENGTH(Field2)) + 
SUM(DATALENGTH(Field3)) TotalSizeInBytes
FROM SomeTable
WHERE X, Y, and Z are true

Я использовал этот запрос ниже (который я получил из Интернета, чтобы получить размеры таблиц, только кластеризованные индексы, чтобы он не включал индексы NC), чтобы получить размер конкретной таблицы в моей базе данных. Для выставления счетов (мы взимаем с наших отделов количество места, которое они используют), мне нужно выяснить, сколько места каждый отдел использовал в этой таблице. У меня есть запрос, который определяет каждую группу в таблице. Мне просто нужно выяснить, сколько места занимает каждая группа.

Пространство на строку может VARCHAR(MAX)сильно колебаться из-за полей в таблице, поэтому я не могу просто взять средний размер * соотношение строк для отдела. Когда я использую DATALENGTH()описанный выше подход, я получаю только 85% от общего пространства, используемого в запросе ниже. Мысли?

SELECT 
s.Name AS SchemaName,
t.NAME AS TableName,
p.rows AS RowCounts,
(SUM(a.total_pages) * 8)/1024 AS TotalSpaceMB, 
(SUM(a.used_pages) * 8)/1024 AS UsedSpaceMB, 
((SUM(a.total_pages) - SUM(a.used_pages)) * 8)/1024 AS UnusedSpaceMB
FROM 
    sys.tables t with (nolock)
INNER JOIN 
    sys.schemas s with (nolock) ON s.schema_id = t.schema_id
INNER JOIN      
    sys.indexes i with (nolock) ON t.OBJECT_ID = i.object_id
INNER JOIN 
    sys.partitions p with (nolock) ON i.object_id = p.OBJECT_ID AND i.index_id = p.index_id
INNER JOIN 
    sys.allocation_units a with (nolock) ON p.partition_id = a.container_id
WHERE 
    t.is_ms_shipped = 0
    AND i.OBJECT_ID > 255 
    AND i.type_desc = 'Clustered'
GROUP BY 
    t.Name, s.Name, p.Rows
ORDER BY 
    TotalSpaceMB desc

Было предложено создать отфильтрованный индекс для каждого отдела или раздела таблицы, чтобы я мог напрямую запрашивать пространство, используемое для каждого индекса. Отфильтрованные индексы можно создавать программно (и снова отбрасывать во время окна обслуживания или когда мне нужно выполнять периодическое выставление счетов) вместо постоянного использования пространства (в этом отношении лучше использовать разделы).

Мне нравится это предложение, и обычно я это делаю. Но, если честно, я использую «каждый отдел» в качестве примера, чтобы объяснить, зачем мне это нужно, но, честно говоря, это не совсем так. Из-за соображений конфиденциальности я не могу объяснить точную причину, почему мне нужны эти данные, но они аналогичны различным отделам.

Относительно некластеризованных индексов в этой таблице: Если бы я мог получить размеры индексов NC, это было бы здорово. Тем не менее, индексы NC составляют <1% от размера кластеризованного индекса, поэтому мы в порядке, не включая эти. Однако как бы мы включили индексы NC в любом случае? Я даже не могу получить точный размер для кластерного индекса :)

Крис Вудс
источник
Итак, в сущности, у вас есть два вопроса: (1) почему сумма длин строк не соответствует метаданным, учитывающим размер всей таблицы? Ответ ниже касается, по крайней мере, частично (и может колебаться в зависимости от выпуска и функции, например сжатия, columnstore и т. Д.). И что еще более важно: (2) как вы можете точно определить фактическое пространство, используемое на отдел? Я не знаю, что вы можете сделать это точно - потому что по некоторым данным, указанным в ответе, невозможно определить, к какому отделу он принадлежит.
Аарон Бертран
Я не думаю, что проблема в том, что у вас нет точного размера для кластеризованного индекса - метаданные определенно точно сообщают вам, сколько места занимает ваш индекс. Что метаданные не предназначены, чтобы сказать вам - по крайней мере, учитывая ваш текущий дизайн / структуру - сколько данных связано с каждым отделом.
Аарон Бертран

Ответы:

19

                          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_tableoptionlarge 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;
Соломон Руцкий
источник
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Пол Уайт 9
Хотя обновленный запрос, который вы указали для второго запроса, еще дальше (в другом направлении сейчас :)), я согласен с этим ответом. Это очень крепкий орешек, и, несмотря на это, я рад, что даже эксперты, помогающие мне, все еще не смогли выяснить точную причину, по которой два метода не совпадают. Я собираюсь просто использовать методологию в другом ответе для экстраполяции. Я бы хотел проголосовать за оба этих ответа, но @srutzky помог со всеми причинами, по которым они оба были отключены.
Крис Вудс
6

Может быть, это грандиозный ответ, но я бы так и сделал.

Таким образом, DATALENGTH составляют только 86% от общего числа. Это все еще очень представительный раскол. Накладные расходы в отличном ответе от srutzky должны иметь довольно равномерное разделение.

Я бы использовал ваш второй запрос (страниц) для общего количества. И используйте первое (длина данных) для выделения разделения. Многие затраты распределяются с использованием нормализации.

И вы должны учитывать, что более близкий ответ увеличит стоимость, так что даже отдел, который проиграл на сплите, все еще может заплатить больше.

папараццо
источник