План обслуживания «на каждый день» – Часть 1: Автоматическая дефрагментация индексов

  • Tutorial


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

Для минимизации вероятности возникновения таких ситуаций создают планы обслуживания из задач, гарантирующих стабильность и оптимальную производительность базы данных.

Среди подобных задач можно выделить следующие:

1. Дефрагментация индексов
2. Обновление статистики
3. Резервное копирование

Рассмотрим по порядку автоматизацию каждой из этих задач.

Итак, пункт первый…

Помимо фрагментации файловой системы и лог-файла, ощутимое влияние на производительность базы данных оказывает фрагментация внутри файлов данных:

1. Фрагментация внутри отдельных страниц индекса

После операций вставки, обновления и удаления записей неизбежно возникают пустые пространства на страницах. Ничего страшного в этом нет, поскольку данная ситуация вполне нормальная, если бы не одно но…

Очень важную роль играет длина строки. Например, если строка имеет размер, который занимает более половины страницы, свободная половина этой страницы не будет использоваться. В результате при увеличении числа строк будет наблюдаться рост неиспользуемого места в базе данных.

Бороться с данным видом фрагментации стоит на этапе проектировании схемы, т. е. выбирать такие типы данных, которые бы компактно умещались на страницах.

2. Фрагментация внутри структур индекса

Основная причина возникновения этого вида фрагментации — операции разбиения страницы. Например, согласно структуре первичного ключа, новую строку необходимо вставить на определенную страницу индекса, но этой на странице недостаточно места, чтобы разместить вставляемые данные.

В таком случае, создается новая страница, на которую переместиться примерно половина записей со старой страницы. Новая страница, зачастую не является физически смежной со старой и, следовательно, помечается системой как фрагментированная.

В любом случае, фрагментация ведет к росту числа страниц для хранения того же объема информации. Это автоматически приводит к увеличению размера базы данных и росту неиспользуемого места.

При выполнении запросов, в которых идет обращение к фрагментированым индексам, требуется больше IO операций. Кроме того, фрагментация накладывает дополнительные расходы на память самого сервера, которому приходится хранить в кэше лишние страницы.

Для борьбы с фрагментацией индексов в арсенале SQL Server предусмотрены команды: ALTER INDEX REBUILD / REORGANIZE.

Перестройка индекса подразумевает удаление старого и создание нового экземпляра индекса. Эта операция устраняет фрагментацию, восстанавливает дисковое пространство путем уплотнения страницы, резервируя при этом свободное место на странице, которое можно задать опцией FILLFACTOR. Важно отметить, что операция по перестройке индекса весьма затратна.

Поэтому, в случае, когда фрагментация незначительна, предпочтительно реорганизовывать существующий индекс. Данная операция требует меньших системных ресурсов, чем пересоздание индекса и заключается в реорганизации leaf-level страниц. Кроме того реорганизация при возможности сжимает страницы индексов.

Степень фрагментации того или иного индекса можно узнать из динамического системного представления sys.dm_db_index_physical_stats:

SELECT *
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, NULL)
WHERE avg_fragmentation_in_percent > 0

В данном запросе, последний параметр задает режим, от значения которого возможно быстрое, но не совсем точное определения уровня фрагментации индекса (режимы LIMITED/NULL). Поэтому рекомендуется задавать режимы SAMPLED/DETAILED.

Мы знаем откуда получить список фрагментированных индексов. Теперь необходимо для каждого из них сгенерировать соответствующую ALTER INDEX команду. Традиционно для этого используют курсор:

DECLARE @SQL NVARCHAR(MAX)

DECLARE cur CURSOR LOCAL READ_ONLY FORWARD_ONLY FOR
	SELECT '
	ALTER INDEX [' + i.name + N'] ON [' + SCHEMA_NAME(o.[schema_id]) + '].[' + o.name + '] ' +
		CASE WHEN s.avg_fragmentation_in_percent > 30
			THEN 'REBUILD WITH (SORT_IN_TEMPDB = ON)'
			ELSE 'REORGANIZE'
		END + ';'
	FROM (
		SELECT 
			  s.[object_id]
			, s.index_id
			, avg_fragmentation_in_percent = MAX(s.avg_fragmentation_in_percent)
		FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, NULL) s
		WHERE s.page_count > 128 -- > 1 MB
			AND s.index_id > 0 -- <> HEAP
			AND s.avg_fragmentation_in_percent > 5
		GROUP BY s.[object_id], s.index_id
	) s
	JOIN sys.indexes i WITH(NOLOCK) ON s.[object_id] = i.[object_id] AND s.index_id = i.index_id
	JOIN sys.objects o WITH(NOLOCK) ON o.[object_id] = s.[object_id]

OPEN cur

FETCH NEXT FROM cur INTO @SQL

WHILE @@FETCH_STATUS = 0 BEGIN

	EXEC sys.sp_executesql @SQL

	FETCH NEXT FROM cur INTO @SQL
	
END 

CLOSE cur 
DEALLOCATE cur 

Чтобы ускорить процесс пересоздания индекса рекомендуется дополнительно указывать опцию SORT_IN_TEMPDB. Еще нужно отдельно упомянуть про опцию ONLINE — она замедляет пересоздание индекса. Но иногда бывает полезной. Например, чтение из кластерного индекса очень дорогое. Мы создали покрывающий индекс и решили проблему с производительностью. Далее мы делаем REBUILD некластерного индекса. В этот момент нам придется снова обращаться к кластерному индексу — что снижает перфоманс.

SORT_IN_TEMPDB позволяет перестраивать индексы в базе tempdb, что бывает особенно полезно для больших индексов в случае нехватки памяти и ином случае — опция игнорируется. Кроме того, если база tempdb расположена на другом диске — это существенно сократит время создания индекса. ONLINE позволяет пересоздать индекс не блокируя при этом запросы к объекту для которого этот индекс создается.

Как показала практика, дефрагментирование индексов с низкой степенью фрагментации либо с небольшим количеством страниц не приносит каких-либо заметных улучшений, способствующих повышению производительности при работе с ними.

В дополнении, приведенный выше запрос можно переписать без применения курсора:

DECLARE
      @IsDetailedScan BIT = 0
    , @IsOnline BIT = 0

DECLARE @SQL NVARCHAR(MAX)
SELECT @SQL = (
	SELECT '
	ALTER INDEX [' + i.name + N'] ON [' + SCHEMA_NAME(o.[schema_id]) + '].[' + o.name + '] ' +
		CASE WHEN s.avg_fragmentation_in_percent > 30
			THEN 'REBUILD WITH (SORT_IN_TEMPDB = ON'
				-- Enterprise, Developer
				+ CASE WHEN SERVERPROPERTY('EditionID') IN (1804890536, -2117995310) AND @IsOnline = 1
						THEN ', ONLINE = ON'
						ELSE ''
				  END + ')'
			ELSE 'REORGANIZE'
		END + ';
	'
	FROM (
		SELECT 
			  s.[object_id]
			, s.index_id
			, avg_fragmentation_in_percent = MAX(s.avg_fragmentation_in_percent)
		FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 
								CASE WHEN @IsDetailedScan = 1 
									THEN 'DETAILED'
									ELSE 'LIMITED'
								END) s
		WHERE s.page_count > 128 -- > 1 MB
			AND s.index_id > 0 -- <> HEAP
			AND s.avg_fragmentation_in_percent > 5
		GROUP BY s.[object_id], s.index_id
	) s
	JOIN sys.indexes i ON s.[object_id] = i.[object_id] AND s.index_id = i.index_id
	JOIN sys.objects o ON o.[object_id] = s.[object_id]
	FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)')

PRINT @SQL
EXEC sys.sp_executesql @SQL

В результате оба запроса при выполнении будут генерировать запросы по дефрагментации проблемных индексов:

ALTER INDEX [IX_TransactionHistory_ProductID] 
  ON [Production].[TransactionHistory] REORGANIZE;
	
ALTER INDEX [IX_TransactionHistory_ReferenceOrderID_ReferenceOrderLineID] 
  ON [Production].[TransactionHistory] REBUILD WITH (SORT_IN_TEMPDB = ON, ONLINE = ON);
	
ALTER INDEX [IX_TransactionHistoryArchive_ProductID] 
  ON [Production].[TransactionHistoryArchive] REORGANIZE;

Собственно, на этом первая часть по созданию плана обслуживания для базы данных выполнена. В следующей части мы займемся написанием запроса для автоматического обновления статистики.

Если хотите поделиться этой статьей с англоязычной аудиторией:
SQL Server Typical Maintenance Plans: Automated Index Defragmentation

UPDATE 2016-04-22: добавил возможность дефрагментации отдельных секций и исправил некоторые баги

USE ...

DECLARE
      @PageCount INT = 128
    , @RebuildPercent INT = 30
    , @ReorganizePercent INT = 10
    , @IsOnlineRebuild BIT = 0
    , @IsVersion2012Plus BIT =
        CASE WHEN CAST(SERVERPROPERTY('productversion') AS CHAR(2)) NOT IN ('8.', '9.', '10')
            THEN 1
            ELSE 0
        END
    , @IsEntEdition BIT =
        CASE WHEN SERVERPROPERTY('EditionID') IN (1804890536, -2117995310)
            THEN 1
            ELSE 0
        END
    , @SQL NVARCHAR(MAX)

SELECT @SQL = (
    SELECT
'
ALTER INDEX ' + QUOTENAME(i.name) + ' ON ' + QUOTENAME(s2.name) + '.' + QUOTENAME(o.name) + ' ' +
        CASE WHEN s.avg_fragmentation_in_percent >= @RebuildPercent
            THEN 'REBUILD'
            ELSE 'REORGANIZE'
        END + ' PARTITION = ' +
        CASE WHEN ds.[type] != 'PS'
            THEN 'ALL'
            ELSE CAST(s.partition_number AS NVARCHAR(10))
        END + ' WITH (' + 
        CASE WHEN s.avg_fragmentation_in_percent >= @RebuildPercent
            THEN 'SORT_IN_TEMPDB = ON' + 
                CASE WHEN @IsEntEdition = 1
                        AND @IsOnlineRebuild = 1 
                        AND ISNULL(lob.is_lob_legacy, 0) = 0
                        AND (
                                ISNULL(lob.is_lob, 0) = 0
                            OR
                                (lob.is_lob = 1 AND @IsVersion2012Plus = 1)
                        )
                    THEN ', ONLINE = ON'
                    ELSE ''
                END
            ELSE 'LOB_COMPACTION = ON'
        END + ')'
    FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, NULL) s
    JOIN sys.indexes i ON i.[object_id] = s.[object_id] AND i.index_id = s.index_id
    LEFT JOIN (
        SELECT
              c.[object_id]
            , index_id = ISNULL(i.index_id, 1)
            , is_lob_legacy = MAX(CASE WHEN c.system_type_id IN (34, 35, 99) THEN 1 END)
            , is_lob = MAX(CASE WHEN c.max_length = -1 THEN 1 END)
        FROM sys.columns c
        LEFT JOIN sys.index_columns i ON c.[object_id] = i.[object_id]
            AND c.column_id = i.column_id AND i.index_id > 0
        WHERE c.system_type_id IN (34, 35, 99)
            OR c.max_length = -1
        GROUP BY c.[object_id], i.index_id
    ) lob ON lob.[object_id] = i.[object_id] AND lob.index_id = i.index_id
    JOIN sys.objects o ON o.[object_id] = i.[object_id]
    JOIN sys.schemas s2 ON o.[schema_id] = s2.[schema_id]
    JOIN sys.data_spaces ds ON i.data_space_id = ds.data_space_id
    WHERE i.[type] IN (1, 2)
        AND i.is_disabled = 0
        AND i.is_hypothetical = 0
        AND s.index_level = 0
        AND s.page_count > @PageCount
        AND s.alloc_unit_type_desc = 'IN_ROW_DATA'
        AND o.[type] IN ('U', 'V')
        AND s.avg_fragmentation_in_percent > @ReorganizePercent
    FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')

PRINT @SQL
--EXEC sys.sp_executesql @SQL
Поделиться публикацией

Комментарии 26

    +1
    Стоит сказать, что перед перестроением индекса настоятельно рекомендуется отключать все триггеры, которые есть в базе.
      0
      Никогда о таком не слышал. С чем это связано?
        0
        Это связано с тем, что могут быть(и должны быть) триггеры, которые срабатывают при изменении определенной таблички. Например есть таблица со справочником — spr, у справочника много полей. Как можно отследить, что конкретный пользователь что конкретно изменил? Сделать триггер который бы в таблицу spr_his писал все изменения.
          0
          1) Ну и причем здесь перестройка индексов? Кто вам мешает перестраивать индексы при наличии триггера, вы же не думаете, что он будет при этом срабатывать? ;)
          2) Триггеры никому ничего не должны. Ну только разве что у вас 2-звенка. А у меня все изменения данных в базе проходят через слой сервера приложений, оттуда и логгирую (отдельными запросами), если что мне нужно.
            0
            1) Потому что у таблицы spr_his, как правило, тоже есть индекс. Перестроение индекса блокирует все изменения. Получается замыкание, как вам уже ответил автор AlanDenton.
            2) Просто вы еще не почувствовали кайф переноса части логики приложения с сервера приложения на сервер базы данных.
              0
              1)
              a) В статье упоминалась возможность WITH ONLINE=ON
              б) Если использование WITH ONLINE=ON невозможно, подразумевается, что перестроение индекса выполняется в момент времени, когда с базой никто не работает. А по вашему сценарию вы просто отрубаете нафиг логирование изменений при перестройке индекса на spr_his, а возможность внесения этих изменений оставляете. Ну замечательно, кто-то поменяет справочник именно в этот момент, а вы об этом уже никогда не узнаете.
              2) Я обычно чувствую кайф при обратном переносе (из базы в приложение), т.к. TSQL все-таки не полноценный язык программирования и приятнее реализовывать логику на нормальном алгоритмическом языке, а не на языке управления данными. Хотя часть логики в виде хранимых процедур в нашей системе тоже присутствует, но скорее в целях оптимизации (для ускорения работы).
                0
                1)
                WITH ONLINE=ON. Вторая опция позволяет пересоздать индекс не блокируя при этом запросы к объекту для которого этот индекс создается.
                Здесь имеются ввиду только SELECT запросы.
                2)Тут дело вкуса. Для меня централизованное хранение логики намного удобнее/практичнее/быстрее в обслуживании и т.д. и т.п. Просто те же «хранимки» нужно применять не повсеместно, а только там где это будет нужнее/необходимее/проще.
                  0
                  Здесь имеются ввиду только SELECT запросы.

                  А вот уж только выдумывать отсебятину не надо. Читайте BOL, проверяйте, потом и пишите.
                  Вот тут ясно сказано:
                  online index operations permit concurrent user update activity.
                  То, что там есть 100500 ограничений, когда это не сработает — это другой вопрос.
                  Если вас смутила фраза
                  the underlying table cannot be modified, truncated, or dropped while an online index operation is in process,
                  то она относилась, видимо, к тому, что нельзя в таблице поля добавить/удалить, пока индекс перестраивается (т.е. что метаданные самой таблицы нельзя менять).
                    0
                    Никакой отсебятины. Вы вырвали эту фразу из контекста в параграфе про производительность. Там как раз сказано, что ресурсов на UPDATE/INSERT/DELETE тратится намного больше. Знаете почему? Потому что все эти запросы ставятся в очередь в буфер и только после перестроения индекса эти изменения вносятся в базу. Поэтому использование этой опции может сильно нагрузить сервер.
                      0
                      Хотелось бы увидеть некий пруф к вашим словам. Если бы это было так, как вы пишете, то ONLINE INDEX OPERATIONS не позиционировалось бы как супер мега крутая фича, которая есть в отнюдь не дешевой ENTERPRISE редакции SQL сервера (может и еще в каких редакциях есть, точно не помню).
                      Несмотря на выдергивание фразы из контекста, по ссылке выше сказано лишь о том, что «Because both the source and target structures are maintained during the online index operation, the resource usage for insert, update, and delete transactions is increased, potentially up to double.» Т.е. ресурсов тратится больше просто потому, что серверу приходится поддерживать целостность сразу двух структур (исходных данных и перестраиваемого, но еще не до конца перестроенного индекса).
                      Возможно (но далеко не факт, т.к. никаких подтверждений не видел) ONLINE INDEX OPERATIONS ведут себя по-разному в зависимости от того, включена ли опция READ_COMMITTED_SNAPSHOT. Т.к. когда она включена, параллельные транзакции с уровнем изоляции READ_COMMITTED ведут себя очень по-разному (в одном случае незакоммиченный UPDATE блокирует READ другой транзакции, а в другом — не блокирует). Не знаю, имеет ли данная опция какое-либо отношение к перестройке индекса (с архитектурной точки зрения должна иметь).
                        0
                        Вот здесь можно почитать. Там есть раздел про обслуживание индексов.
                        Ну и про саму операцию тоже полезно почитать.
                          0
                          Вот как раз по ссылке, где про операцию, и написано:
                          ONLINE = { ON | OFF }
                          Определяет, будут ли базовые таблицы и связанные индексы доступны для запросов и изменения данных во время операций с индексами. Значение по умолчанию — OFF.

                          А статья довольно старая (2010-го года), с тех пор M$ многое могли поменять. К тому в статье отсылка на BOL вообще от 2005-го MS SQL.
        +1
        Присоединяюсь к alan008. Возможно Вы имели ввиду триггера уровня базы/сервера, которые отслеживают события ALTER_INDEX?
          0
          Насколько я знаю, перестройка индексов со срабатыванием триггеров before / after / instead of вообще никак не связана, так что мысль товарища servekon пока не понятна.
            0
            Возможна ситуация когда есть триггер, который логирует изменения на базе либо делает еще что-то:

            CREATE TRIGGER ddl_Server
                ON ALL SERVER 
                FOR ALTER_INDEX
            AS
            BEGIN
                RAISERROR('Error', 16, 1)
            END
            
            GO
            
            ALTER INDEX ... ON .... REORGANIZE
            

            В такой ситуации мы вообще не сможем изменить никаких индексов:

            Msg 50000, Level 16, State 1, Procedure ddl_Server, Line 7 Error
              0
              Ну я то про обычные индексы говорил, на таблицах :)
              Признаться, не знал/не использовал DDL триггеры. Вообще, прикольно. Но думаю для их создания нужна как минимум роль serveradmin, если не sa )))
                0
                Да именно такие триггеры я имел ввиду. В хорошем приложении, на критические и важные изменения всегда нужно вешать такие триггеры. Очень помогает при отладке.
          +1
          Мы используем такую процудуру, которую кто-то любезно написал на sqlservercentral.com
          -- =============================================
          -- Description:	Интеллектуальная перестройка индексов 
          -- =============================================
          CREATE PROCEDURE [dbo].[OptimizeDBIndexes]
          AS
          BEGIN
          	-- http://www.sql-server-performance.com/2012/performance-tuning-re-indexing-update-statistics/
          	-- Для индексов с фрагментацией > 10% выполняется REORGANIZE
          	-- Для индексов с фрагментацией > 30% выполняется REBUILD 
          	SET NOCOUNT ON;
          	DECLARE @objectid int;
          	DECLARE @indexid int;
          	DECLARE @partitioncount bigint;
          	DECLARE @schemaname nvarchar(130);
          	DECLARE @objectname nvarchar(130);
          	DECLARE @indexname nvarchar(130);
          	DECLARE @partitionnum bigint;
          	DECLARE @partitions bigint;
          	DECLARE @frag float;
          	DECLARE @command nvarchar(4000);
          	-- Conditionally select tables and indexes from the sys.dm_db_index_physical_stats function
          	-- and convert object and index IDs to names.
          	SELECT
          		object_id AS objectid,
          		index_id AS indexid,
          		partition_number AS partitionnum,
          		avg_fragmentation_in_percent AS frag
          	INTO #work_to_do
          	FROM sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL , NULL, 'LIMITED')
          	WHERE avg_fragmentation_in_percent > 10.0 AND index_id > 0;
          	SELECT * FROM #work_to_do;
          
          	-- Declare the cursor for the list of partitions to be processed.
          	DECLARE cr_partitions CURSOR FOR SELECT * FROM #work_to_do;
          
          	OPEN cr_partitions;
          	WHILE (1=1)
              BEGIN
                  FETCH NEXT
                     FROM cr_partitions
                     INTO @objectid, @indexid, @partitionnum, @frag;
                  IF @@FETCH_STATUS < 0 BREAK;
                  SELECT @objectname = QUOTENAME(o.name), @schemaname = QUOTENAME(s.name)
                  FROM sys.objects AS o
                  JOIN sys.schemas as s ON s.schema_id = o.schema_id
                  WHERE o.object_id = @objectid;
                  SELECT @indexname = QUOTENAME(name)
                  FROM sys.indexes
                  WHERE  object_id = @objectid AND index_id = @indexid;
                  SELECT @partitioncount = count (*)
                  FROM sys.partitions
                  WHERE object_id = @objectid AND index_id = @indexid;
                  
            	    -- 30 is an arbitrary decision point at which to switch between reorganizing and rebuilding.
          		IF @frag < 30.0
          			SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REORGANIZE';
          		IF @frag >= 30.0
          			SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REBUILD';
          		IF @partitioncount > 1
          			SET @command = @command + N' PARTITION=' + CAST(@partitionnum AS nvarchar(10));
          		EXEC (@command);
          		PRINT N'Executed: ' + @command;
              END;
          	CLOSE cr_partitions;
          	DEALLOCATE cr_partitions;
          	DROP TABLE #work_to_do;
          END
          GO
          



          Оригинал тут. Там же и про обновление статистики написано.
            0
            Спасибо за ссылку. Интересно было почитать.
              0
              Советую там зарегистрироваться, тогда на почту будет приходить почти каждый день рассылка с интересными статьями. Я уже больше года там подписан, время от времени натыкаюсь просто на «бриллианты» :).
          +1
          Запрос без курсора не позволит выйти из перестроения индексов в случае, если перестроение может не убраться в технологическое окно (с большими базами такое бывает). Лучше поместить результаты во временную таблицу, и перебирать ее по одной строке, добавив условие на проверку по времени — если время технологического окна закончилось, то выполнение скрипта необходимо прекратить.
            0
            Спасибо за замечание. Такое поведение действительно весьма уместно в определенных сценариях.
            0
            Кроме операций обслуживания структур данных непосредственно самого MS SQL (индексов, статистик) мы при резервном копировании выполняем еще часть задач, относящихся скорее к уровню бизнес логики, т.е. к нашим структурам данных, но таких, о которых пользователям знать не обязательно. Например, мы чистим неиспользуемые значения в справочниках (если справочник автопополняемый), удаляем обработанные сообщения из очередей (имею в виду, у нас в базе есть своя таблица а-ля очередь сообщений, в которой обработанные сообщения помечаются как обработанные, но сразу не удаляются, чтобы проще было отладить ситуацию, когда при обработке сообщения возникает какая-то ошибка), удаляем какие-то устаревшие данные (например, логи за неинтересующие нас периоды и т.п.). Можем перепаковать какие-то бинарные данные (например, можно завести столбец-признак, сжаты ли данные, при обычной работе запихивать туда данные в несжатом виде, а при бэкапе паковать — тут конечно есть возможность паковать страницы с помощью самой СУБД, но не всегда этот вариант подходит).
              0
              позвольте поинтересоваться: под какой проект потребовалось так обрабатывать базу и каков её вес на диске?
              PS: у себя в конторе максимальный эффект дала установки нормальной дисковой полки(много-много шпинделей, пара контролеров, кеш с батарейкой, рейд...). Всякие дефрагментации индексов и очистки статистики давали увеличение производительности на величину, умещающуюся в рамки погрешности. Сервер MSSQL для 1С
                0
                Тут скорее важен не размер базы, а размер отдельных таблиц. которые имеют большой размер и неприятную особенность — в них часто изменяются данные. Именно для таких таблиц — целесообразно делать дефрагментацию индексов.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое