Наличие поля XML приводит к тому, что большая часть данных таблицы располагается на страницах LOB_DATA (на самом деле ~ 90% страниц таблицы составляют LOB_DATA).
Простое наличие столбца XML в таблице не имеет такого эффекта. Это наличие XML - данных , которые, при определенных условиях , вызывает некоторую часть данных подряд, чтобы хранить от строки, на страницах LOB_DATA. И хотя один (или, может быть, несколько ;-) может утверждать, что да, в XML
столбце подразумевается, что действительно будут данные XML, не гарантируется, что данные XML нужно будет хранить вне строки: если строка в значительной степени уже не заполнена вне каких-либо данных XML небольшие документы (до 8000 байт) могут помещаться в строку и никогда не переходить на страницу LOB_DATA.
Правильно ли я считаю, что страницы LOB_DATA могут вызывать медленное сканирование не только из-за их размера, но и потому, что SQL Server не может эффективно сканировать кластеризованный индекс, когда в таблице много страниц LOB_DATA?
Сканирование относится к просмотру всех строк. Конечно, когда читается страница данных, считываются все данные в строке , даже если вы выбрали подмножество столбцов. Разница с данными больших объектов заключается в том, что если вы не выберете этот столбец, данные вне строки не будут прочитаны. Следовательно, не совсем справедливо делать вывод о том, насколько эффективно SQL Server может сканировать этот кластеризованный индекс, поскольку вы точно не тестировали его (или тестировали половину его). Вы выбрали все столбцы, включая столбец XML, и, как вы упомянули, именно там находится большая часть данных.
Итак, мы уже знаем, что SELECT TOP 1000 *
тест не просто читал серию 8k страниц данных, все подряд, а вместо этого переходил в другие места в каждой строке . Точная структура этих больших данных может варьироваться в зависимости от их размера. Основываясь на исследованиях, показанных здесь ( каков размер указателя большого объекта для (MAX) типов, таких как Varchar, Varbinary, Etc? ), Существует два типа размещения больших объектов вне строки:
- Встроенный корень - для данных размером от 8001 до 40000 (на самом деле 42000) байтов, если позволяет пространство, в строке будет находиться от 1 до 5 указателей (24–72 байта), которые указывают непосредственно на страницы больших объектов.
- TEXT_TREE - для данных размером более 42 000 байт или если указатели от 1 до 5 не могут поместиться в строке, тогда будет только 24-байтовый указатель на начальную страницу списка указателей на страницы больших объектов (т.е. text_tree ").
Одна из этих двух ситуаций возникает каждый раз, когда вы извлекаете данные больших объектов размером более 8000 байт или просто не помещаются в строку. Я разместил тестовый скрипт на PasteBin.com (скрипт T-SQL для проверки выделения и чтения больших объектов ), который показывает 3 типа выделения больших объектов (в зависимости от размера данных), а также влияние каждого из них на логические и физическое чтение. В вашем случае, если данные XML на самом деле меньше 42 000 байт на строку, то ни один из них (или их очень мало) не должен быть в наименее эффективной структуре TEXT_TREE.
Если вы хотите проверить, насколько быстро SQL Server может сканировать этот кластеризованный индекс, сделайте, SELECT TOP 1000
но укажите один или несколько столбцов, не включая этот столбец XML. Как это влияет на ваши результаты? Это должно быть немного быстрее.
Разумно ли иметь такую структуру таблицы / шаблон данных?
Учитывая, что у нас есть неполное описание фактической структуры таблицы и шаблона данных, любой ответ может быть неоптимальным в зависимости от того, что это за недостающие детали. Имея это в виду, я бы сказал, что нет ничего явно необоснованного в вашей структуре таблицы или шаблоне данных.
Я могу (в приложении ac #) сжать XML с 20 КБ до ~ 2,5 КБ и сохранить его в столбце VARBINARY, предотвращая использование страниц данных больших объектов. Это ускоряет SELECT в 20 раз в моих тестах.
Это ускорило выбор всех столбцов или даже только данных XML (теперь они есть VARBINARY
), но на самом деле это повреждает запросы, которые не выбирают данные «XML». Предполагая, что у вас есть около 50 байтов в других столбцах и значение FILLFACTOR
100, тогда:
Сжатие XML
не требуется: для 15 КБ данных требуется 2 страницы LOB_DATA, для чего требуется 2 указателя для встроенного корня. Первый указатель имеет длину 24 байта, а второй - 12, что составляет 36 байтов, хранимых в строке для данных XML. Общий размер строки составляет 86 байт, и вы можете разместить около 93 из этих строк на странице данных размером 8060 байт. Следовательно, 1 миллион строк требует 10 753 страниц данных.
Пользовательское сжатие: 2,5 тыс. VARBINARY
Данных помещается в строку. Общий размер строки составляет 2610 (2,5 * 1024 = 2560) байт, и вы можете разместить только 3 из этих строк на странице данных размером 8060 байт. Следовательно, 1 миллион строк требует 333,334 страниц данных.
Следовательно, реализация пользовательского сжатия приводит к увеличению в 30 раз количества страниц данных для кластерного индекса. Значение, все запросы с использованием индекса кластерного сканирования теперь около 322,500 больше страниц данных для чтения. Пожалуйста, ознакомьтесь с подробным разделом ниже, чтобы узнать о дополнительных последствиях такого типа сжатия.
Я бы предостерег от проведения любого рефакторинга, основанного на производительности SELECT TOP 1000 *
. Это вряд ли будет запрос, который приложение даже выдаст, и его не следует использовать в качестве единственной основы для потенциально ненужной оптимизации.
Для получения более подробной информации и дополнительных тестов см. Раздел ниже.
На этот Вопрос нельзя дать однозначного ответа, но мы можем, по крайней мере, добиться определенного прогресса и предложить дополнительные исследования, которые помогут приблизить нас к выяснению точной проблемы (в идеале на основе фактических данных).
Что мы знаем:
- Таблица содержит около 1 миллиона строк
- Размер таблицы составляет около 15 ГБ
- Таблица содержит одну
XML
колонку и несколько других столбцов типов: INT
, BIGINT
, UNIQUEIDENTIFIER
, « и т.д.»
XML
«размер» столбца составляет в среднем около 15 тыс.
- После запуска
DBCC DROPCLEANBUFFERS
для выполнения следующего запроса требуется 20-25 секунд:SELECT TOP 1000 * FROM TABLE
- Кластерный индекс сканируется
- Фрагментация кластерного индекса близка к 0%
Что мы думаем, мы знаем:
- Никакой другой дисковой активности вне этих запросов. Вы уверены? Даже если нет других пользовательских запросов, выполняются ли фоновые операции? Существуют ли процессы, внешние по отношению к SQL Server, работающие на том же компьютере, которые могут занимать некоторые операции ввода-вывода? Может и не быть, но это не ясно, основываясь исключительно на предоставленной информации.
- Возвращается 15 МБ данных XML. На чем основано это число? Оценка, полученная из 1000 строк, умноженных на среднее значение 15 КБ данных XML на строку? Или программная агрегация того, что было получено для этого запроса? Если это просто оценка, я бы не стал полагаться на нее, поскольку распределение данных XML может быть даже не таким, как подразумевается простым средним значением.
Сжатие XML может помочь. Как именно вы будете делать сжатие в .NET? Через классы GZipStream или DeflateStream ? Это не вариант с нулевой стоимостью. Это, безусловно, будет сжимать некоторые данные на большой процент, но это также потребует больше ЦП, поскольку вам потребуется дополнительный процесс для сжатия / распаковки данных каждый раз. Этот план также полностью лишит вас возможности:
- запрос данных XML с помощью тех
.nodes
, .value
, .query
и .modify
функции XML.
индексировать данные XML.
Помните (поскольку вы упомянули, что XML «сильно избыточен»), XML
тип данных уже оптимизирован, поскольку он сохраняет имена элементов и атрибутов в словаре, присваивая целочисленный идентификатор индекса каждому элементу, а затем использует этот целочисленный идентификатор. во всем документе (следовательно, он не повторяет полное имя для каждого использования и не повторяет его снова как закрывающий тег для элементов). В реальных данных также удалены посторонние пробелы. Вот почему извлеченные XML-документы не сохраняют свою первоначальную структуру и почему извлекаются пустые элементы, <element />
даже если они были введены как<element></element>
, Таким образом, любой выигрыш от сжатия с помощью GZip (или чего-либо еще) будет найден только путем сжатия значений элемента и / или атрибута, который представляет собой гораздо меньшую площадь поверхности, которую можно улучшить, чем многие ожидают, и, скорее всего, не стоит потери Возможности, как указано выше.
Также имейте в виду, что сжатие данных XML и сохранение VARBINARY(MAX)
результата не исключит доступ к LOB, а только уменьшит его. В зависимости от размера оставшихся данных в строке сжатое значение может помещаться в строку или для него все еще могут потребоваться страницы больших объектов.
Этой информации, хотя и полезной, недостаточно. Есть много факторов, которые влияют на производительность запросов, поэтому нам нужно гораздо более детальное представление о том, что происходит.
Что мы не знаем, но нужно:
- Почему производительность имеет
SELECT *
значение? Это шаблон, который вы используете в коде. Если так, то почему?
- Какова производительность выбора только столбца XML? Какова статистика и сроки, если вы просто
SELECT TOP 1000 XmlColumn FROM TABLE;
:?
Сколько из 20-25 секунд, необходимых для возврата этих 1000 строк, связано с сетевыми факторами (получение данных по сети), а также с клиентскими факторами (что составляет примерно 15 МБ плюс остальные XML-данные в сетку в SSMS или, возможно, сохранение на диск)?
Выделяя эти два аспекта операции, иногда можно просто не возвращать данные. Теперь можно подумать, что нужно выбрать временную таблицу или переменную таблицы, но это будет просто ввести несколько новых переменных (например, дисковый ввод-вывод для tempdb
, запись журнала транзакций, возможный автоматический рост данных tempdb и / или файла журнала, необходимость пространство в буферном пуле и т. д.). Все эти новые факторы могут фактически увеличить время запроса. Вместо этого я обычно сохраняю столбцы в переменные (соответствующего типа данных; нет SQL_VARIANT
), которые перезаписываются каждой новой строкой (т.е. SELECT @Column1 = tab.Column1,...
).
ОДНАКО , как было отмечено @PaulWhite в этом DBA.StackExchange Q & A, логическое чтение отличается при доступе к одним и тем же данным большого объекта, с дополнительным исследованием моего собственного, опубликованного на PasteBin ( сценарий T-SQL для проверки различных сценариев для чтения большого объекта ) , LOBs не обращались последовательно между SELECT
, SELECT INTO
, SELECT @XmlVariable = XmlColumn
, SELECT @XmlVariable = XmlColumn.query(N'/')
, и SELECT @NVarCharVariable = CONVERT(NVARCHAR(MAX), XmlColumn)
. Таким образом, наши возможности здесь немного более ограничены, но вот что можно сделать:
- Исключите проблемы с сетью, выполнив запрос на сервере с SQL Server, либо в SSMS, либо в SQLCMD.EXE.
- Исключите проблемы клиента в SSMS, перейдя в Параметры запроса -> Результаты -> Сетка и отметив опцию «Отменить результаты после выполнения». Обратите внимание, что этот параметр предотвратит вывод ВСЕХ, включая сообщения, но все же может быть полезным, чтобы исключить время, которое требуется SSMS для выделения памяти для каждой строки, а затем нарисовать ее в сетке.
Кроме того , можно выполнить запрос через Sqlcmd.exe и направить выход идти в никуда через: -o NUL:
.
- Есть ли тип ожидания, связанный с этим запросом? Если да, что это за тип ожидания?
Каков фактический размер данных для возвращаемыхXML
столбцов ? Средний размер этого столбца по всей таблице на самом деле не имеет значения, если строки «TOP 1000» содержат непропорционально большую часть общих данных. Если вы хотите узнать о ТОП 1000 строк, посмотрите на эти строки. Пожалуйста, запустите следующее:XML
SELECT TOP 1000 tab.*,
SUM(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [TotalXmlKBytes],
AVG(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [AverageXmlKBytes]
STDEV(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [StandardDeviationForXmlKBytes]
FROM SchemaName.TableName tab;
- Точная схема таблицы. Пожалуйста, предоставьте полный
CREATE TABLE
отчет, включая все индексы.
- План запроса? Это то, что вы можете опубликовать? Эта информация, вероятно, ничего не изменит, но лучше знать, что это не так, чем догадываться, что это не так и будет неправильно ;-)
- Есть ли физическая / внешняя фрагментация в файле данных? Хотя это может и не быть существенным фактором, поскольку вы используете «потребительские SATA», а не SSD или даже супердорогие SATA, эффект неоптимально упорядоченных секторов будет более заметным, особенно с учетом количества этих секторов. это должно быть прочитано увеличивается.
Каковы точные результаты следующего запроса:
SELECT * FROM sys.dm_db_index_physical_stats(DB_ID(),
OBJECT_ID(N'dbo.SchemaName.TableName'), 1, 0, N'LIMITED');
ОБНОВИТЬ
Мне пришло в голову, что я должен попытаться воспроизвести этот сценарий, чтобы увидеть, испытываю ли я подобное поведение. Итак, я создал таблицу с несколькими столбцами (аналогично смутному описанию в вопросе), а затем заполнил ее 1 миллионом строк, и столбец XML содержит приблизительно 15 тыс. Данных на строку (см. Код ниже).
Я обнаружил, что выполнение SELECT TOP 1000 * FROM TABLE
завершается за 8 секунд в первый раз, а затем через 2–4 секунды каждый раз (да, выполняется DBCC DROPCLEANBUFFERS
перед каждым запуском SELECT *
запроса). Мой ноутбук, которому несколько лет, не быстр: SQL Server 2012 SP2 Developer Edition, 64-битная, 6 ГБ оперативной памяти, двухъядерный 2,5 ГГц Core i5 и диск SATA 5400 об / мин. Я также использую SSMS 2014, SQL Server Express 2014, Chrome и некоторые другие.
Основываясь на времени отклика моей системы, я повторю, что нам нужно больше информации (т.е. подробностей о таблице и данных, результатах предлагаемых тестов и т. Д.), Чтобы помочь сузить причину времени отклика в 20-25 секунд. что ты видишь.
SET ANSI_NULLS, NOCOUNT ON;
GO
IF (OBJECT_ID(N'dbo.XmlReadTest') IS NOT NULL)
BEGIN
PRINT N'Dropping table...';
DROP TABLE dbo.XmlReadTest;
END;
PRINT N'Creating table...';
CREATE TABLE dbo.XmlReadTest
(
ID INT NOT NULL IDENTITY(1, 1),
Col2 BIGINT,
Col3 UNIQUEIDENTIFIER,
Col4 DATETIME,
Col5 XML,
CONSTRAINT [PK_XmlReadTest] PRIMARY KEY CLUSTERED ([ID])
);
GO
DECLARE @MaxSets INT = 1000,
@CurrentSet INT = 1;
WHILE (@CurrentSet <= @MaxSets)
BEGIN
RAISERROR(N'Populating data (1000 sets of 1000 rows); Set # %d ...',
10, 1, @CurrentSet) WITH NOWAIT;
INSERT INTO dbo.XmlReadTest (Col2, Col3, Col4, Col5)
SELECT TOP 1000
CONVERT(BIGINT, CRYPT_GEN_RANDOM(8)),
NEWID(),
GETDATE(),
N'<test>'
+ REPLICATE(CONVERT(NVARCHAR(MAX), CRYPT_GEN_RANDOM(1), 2), 3750)
+ N'</test>'
FROM [master].[sys].all_columns sac1;
IF ((@CurrentSet % 100) = 0)
BEGIN
RAISERROR(N'Executing CHECKPOINT ...', 10, 1) WITH NOWAIT;
CHECKPOINT;
END;
SET @CurrentSet += 1;
END;
--
SELECT COUNT(*) FROM dbo.XmlReadTest; -- Verify that we have 1 million rows
-- O.P. states that the "clustered index fragmentation is close to 0%"
ALTER INDEX [PK_XmlReadTest] ON dbo.XmlReadTest REBUILD WITH (FILLFACTOR = 90);
CHECKPOINT;
--
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET STATISTICS IO, TIME ON;
SELECT TOP 1000 * FROM dbo.XmlReadTest;
SET STATISTICS IO, TIME OFF;
/*
Scan count 1, logical reads 21, physical reads 1, read-ahead reads 4436,
lob logical reads 5676, lob physical reads 1, lob read-ahead reads 3967.
SQL Server Execution Times:
CPU time = 171 ms, elapsed time = 8329 ms.
*/
И, поскольку мы хотим учесть время, затрачиваемое на чтение страниц без LOB, я выполнил следующий запрос, чтобы выбрать все, кроме столбца XML (один из тестов, которые я предложил выше). Это возвращается через 1,5 секунды довольно последовательно.
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET STATISTICS IO, TIME ON;
SELECT TOP 1000 ID, Col2, Col3, Col4 FROM dbo.XmlReadTest;
SET STATISTICS IO, TIME OFF;
/*
Scan count 1, logical reads 21, physical reads 1, read-ahead reads 4436,
lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 1666 ms.
*/
Заключение (на данный момент)
Исходя из моей попытки воссоздать ваш сценарий, я не думаю, что мы можем указать ни на диск SATA, ни на непоследовательный ввод / вывод в качестве основной причины 20 - 25 секунд, особенно потому, что мы все еще не знаю, как быстро возвращается запрос, не включая столбец XML. И я не смог воспроизвести большое количество логических операций чтения (не больших объектов), которые вы показываете, но у меня есть ощущение, что мне нужно добавить больше данных в каждую строку в свете этого и утверждения:
~ 90% страниц таблицы составляют LOB_DATA
Моя таблица содержит 1 миллион строк, каждая из которых содержит более 15 000 XML-данных, и sys.dm_db_index_physical_stats
показывает, что существует 2 миллиона страниц LOB_DATA. Тогда оставшиеся 10% будут 222 тыс. Страниц данных IN_ROW, но у меня их всего 11 630. Итак, еще раз, нам нужно больше информации относительно фактической схемы таблицы и фактических данных.