Префиксно-словарное и var-сжатие
Ранее этот пост находился на форуме сообщества Russian SQL Server User Group по адресу https://sqlclub.ru/forum/viewtopic.php?f=6&t=1064, что позволяло во время демонстрации ссылаться на скрипт, чтобы слушатели могли не только посмотреть, но и впоследствии самостоятельно воспроизвести демку для закрепления материала. К сожалению, безответственный администратор по имени Сергей Заворуев положил сайт sqlclub.ru и ударился в бега, став недоступным ни по e-mail, ни по телефону, чем немало подставил сообщество. Хотелось бы предостеречь будущих возможных деловых партнеров Сергея Заворуева (https://1stat.ru/?show=whois\&person=Sergey V Zavoruev), разработка, продвижение и сопровождение сайтов, сетевые работы любой сложности, о риске, который они на себя берут, затевая с ним совместные проекты. Может внезапно кидануть и смыться.
----------------------------------------------------------------------------------------------------------------------
/* Компрессия страниц и записей в SQL Server 2008. Эта дема будет показываться на лонче 18.03.2008 */
--Компрессия может использоваться для таблиц и индексов. Что такое компрессия
--для страниц и записей, рассказывается на слайдах.
--При партиционировании компрессию можно применять к каждой партиции в отдельности.
--Создаем БД о трех файл-группах.
use tempdb
if exists (select 1 from sys.databases where name = 'TestCompression') begin
alter database TestCompression set single_user with rollback immediate
drop database TestCompression
end
create database TestCompression
on primary
(name = TestCompression,
filename = 'C:\Temp\TestCompression.mdf'),
filegroup TestCompression_FG1
(name = TestCompression1,
filename = 'C:\Temp\TestCompression_FG1_File1.mdf'),
filegroup TestCompression_FG2
(name = TestCompression2,
filename = 'C:\Temp\TestCompression_FG2_File1.mdf')
go
use TestCompression
--Создаем функцию и схему для автоматического разложения по партициям
create partition function myRangePF (int)
as range left for values (0, 1, 2);
--left означает, что междиапазонная граница относится к левому диапазону:
--(-бесконечность, 0], (0, 1], (1, 2], (2, +бесконечность)
create partition scheme myRangePS
as partition myRangePF
to ([primary], TestCompression_FG1, TestCompression_FG2, [primary]);
--сделали привязку интервалов к файл-группам
--Создаем таблицу для экспериментов
create table Customers(
CustomerKey int,
Title nvarchar(10),
FirstName nvarchar(50),
LastName nvarchar(50),
Gender nvarchar(1),
YearlyIncome money,
TotalChildren tinyint,
Education nvarchar(40),
Occupation nvarchar(100),
AddressLine nvarchar(255),
Phone nvarchar(20),
PartitionID int
)
on MyRangePS(PartitionID)
--Наполняем ее так, что по 1000 одинаковых записей попадает в primary, FG1, FG2
insert Customers
(CustomerKey, Title, FirstName, LastName, Gender, YearlyIncome, TotalChildren,
Education, Occupation, AddressLine, Phone, PartitionID)
select top 1000 CustomerKey, Title, FirstName, LastName, Gender, YearlyIncome, TotalChildren,
EnglishEducation, EnglishOccupation, AddressLine1 + AddressLine2, Phone, 0
from AdventureWorksDW.dbo.DimCustomer
insert Customers
(CustomerKey, Title, FirstName, LastName, Gender, YearlyIncome, TotalChildren,
Education, Occupation, AddressLine, Phone, PartitionID)
select top 1000 CustomerKey, Title, FirstName, LastName, Gender, YearlyIncome, TotalChildren,
EnglishEducation, EnglishOccupation, AddressLine1 + AddressLine2, Phone, 1
from AdventureWorksDW.dbo.DimCustomer
insert Customers
(CustomerKey, Title, FirstName, LastName, Gender, YearlyIncome, TotalChildren,
Education, Occupation, AddressLine, Phone, PartitionID)
select top 1000 CustomerKey, Title, FirstName, LastName, Gender, YearlyIncome, TotalChildren,
EnglishEducation, EnglishOccupation, AddressLine1 + AddressLine2, Phone, 2
from AdventureWorksDW.dbo.DimCustomer
--Вот информация по партициям, сколько в каждой записей с какими диапазонами
select $partition. myRangePF(PartitionID) as PartNo, min(PartitionID), max(PartitionID), count(*)
from Customers group by $partition. myRangePF(PartitionID) order by PartNo
--Вот сколько места они занимают
select au.data_space_id, p.partition_number, au.total_pages, au.used_pages, au.data_pages, p.rows, p.data_compression_desc, ds.name
from sys.allocation_units au join sys.partitions p on au.container_id = p.partition_id
join sys.data_spaces ds on au.data_space_id = ds.data_space_id
where p.object_id = object_id('Customers')
--Крайние левая и правая партиции отображены на файл-группу primary.
--В левой лежит 1000 строк с PartitionID = 0, в правой, понятно, ничего нет
--В двух средних лежат по 1000 строк с признаком PartitionID = 1 и 2 соответственно.
--Оценка потенциального выигрыша в месте от компрессии
--Для таблицы в целом
exec sp_estimate_data_compression_savings @schema_name = 'dbo', @object_name = 'Customers',
@index_id = null, @partition_number = null, @data_compression = 'page'
--По партициям и методам сжатия
exec sp_estimate_data_compression_savings @schema_name = 'dbo', @object_name = 'Customers',
@index_id = null, @partition_number = 2, @data_compression = 'page'
exec sp_estimate_data_compression_savings @schema_name = 'dbo', @object_name = 'Customers',
@index_id = null, @partition_number = 3, @data_compression = 'row'
--Сжимаем
alter table Customers rebuild partition = 2 with (data_compression = page)
alter table Customers rebuild partition = 3 with (data_compression = row)
--Вот сколько места они занимают теперь
select au.data_space_id, p.partition_number, au.total_pages, au.used_pages, au.data_pages, p.rows, p.data_compression_desc, ds.name
from sys.allocation_units au join sys.partitions p on au.container_id = p.partition_id
join sys.data_spaces ds on au.data_space_id = ds.data_space_id
where p.object_id = object_id('Customers')
--Расжатие
alter table Customers rebuild partition = 3 with (data_compression = none)
----------------------------------------------------------------------------------------------------------------------
Йа нопесал оч.хор.хр.пр., кот. сжимает/разжимает все пользовательские таблицы внутри БД вместе с их индексами. Возьмем какую-нибудь базу данных и посмотрим, как там чего разлеглось.
select o.name, au.total_pages, au.used_pages, au.data_pages, p.rows, p.data_compression_desc, p.partition_number, au.data_space_id, ds.name
from sys.allocation_units au join sys.partitions p on au.container_id = p.partition_id
join sys.data_spaces ds on au.data_space_id = ds.data_space_id
join sys.objects o on p.object_id = o.object_id
where o.is_ms_shipped = 0
Скрипт 1
Можно видеть, что она состоит из двух несжатых таблиц по столько-то записей каждая, занимающих столько-то страниц. Можно умножить на 8К и получить размер на диске.
name |
total_pages |
used_pages |
data_pages |
rows |
data_compression_desc |
partition_number |
data_space_id |
name |
WebStat_Jan09 |
10418 |
10411 |
10410 |
262077 |
NONE |
1 |
1 |
PRIMARY |
WebStat_Mar09 |
22146 |
22141 |
22140 |
1002422 |
NONE |
1 |
1 |
PRIMARY |
Табл.1
Данная процедура их сжимает:
if exists(select 1 from sys.procedures where name = ' СжатьРазжатьВсюБазу' and schema_id = schema_id()) drop proc СжатьРазжатьВсюБазу
go
create proc СжатьРазжатьВсюБазу @dbname sysname = null, @compress bit = 1 as
begin
declare @precmd nvarchar(1000), @postcmd nvarchar(1000)
if @dbname is not null select @precmd = 'use ' + @dbname, @postcmd = 'use ' + db_name()
declare @compression varchar(4) = case @compress when 1 then 'page' else 'none' end
declare @cmd1 nvarchar(1000) = 'alter table ? rebuild partition = all with (data_compression = ' + @compression + ')'
declare @cmd2 nvarchar(1000) = 'alter index all on ? rebuild partition = all with (data_compression = ' + @compression + ')'
exec sp_MSforeachtable @command1 = @cmd1, @command2 = @cmd2, @replacechar = '?', @precommand = @precmd, @postcommand = @postcmd
end
Скрипт 2
Замечание: процедура sp_MSforeachtable перебирает только пользовательские таблицы, не обращая внимание на служебные, что в данном случае совпадает с желаниями рабочего подростка, поск. избавляет от необходимости фильтровать наподобие where o.is_ms_shipped = 0 в Скрипте 1.
Пример вызова.
use pubs
exec WebStat.. СжатьРазжатьВсюБазу @dbname = 'Webstat', @compress = 1
use WebStat
Смотрим результат. Повторяем запрос Скрипт 1:
name |
total_pages |
used_pages |
data_pages |
rows |
data_compression_desc |
partition_number |
data_space_id |
name |
WebStat_Jan09 |
4434 |
4432 |
4431 |
262077 |
PAGE |
1 |
1 |
PRIMARY |
WebStat_Mar09 |
4970 |
4964 |
4963 |
1002422 |
PAGE |
1 |
1 |
PRIMARY |
Табл.2
Видим, что все сжалось. Прикидки по длительности процесса делайте сами, т.к. это сильно зависит от конкретного железа и распределения данных. Данный детский пример на вистовой виртуалке на ноутбуке длился 42 сек. Т.е. грубо 40 сек. на 30 тыс.страниц.
В процедуре СжатьРазжатьВсюБазу захардкодена page-компрессия, т.к. в большинстве задач, с которыми я сталкивался, она оказывалась предпочтительнее. Если есть желание использовать также row-компрессию, перепишите параметр @compress. Можете сделать его не булевым, а строковым и передавать в него тип компрессии (page, row, none) напрямую.