У SQL Server Express есть три потолка, под которые можно спроектировать систему, и один отсутствующий компонент, который придётся заменить. Потолки такие: 10 ГБ данных на базу, около 1,4 ГБ буферного пула на экземпляр и меньшее из значений - один сокет или четыре ядра. Недостающий компонент - SQL Server Agent, планировщик, который подразумевается в большинстве учебников. Ответ есть на все четыре пункта: держите каждую базу компактной с помощью сжатия, архивации и раздельных баз, подбирайте сервер под тот кэш, который вы реально получите, а плановые задачи выносите из движка в cron, Task Scheduler, само приложение или собственные расписания хостинга. Этот текст - про обходные пути. О том, что именно за лимиты и когда пора уходить с Express, читайте сначала в статье SQL Server Express hosting: limits and when it is enough; здесь предполагается, что вы решили остаться.
Какой лимит вы встретите первым#
На большинстве реальных нагрузок порядок такой: диск, затем память, затем отсутствие Agent, затем процессор, а потолок в 10 ГБ приходит последним или не приходит вовсе. Лимит на данные пугает, потому что он стоит в заголовках, но типичная прикладная база спустя годы работы занимает от сотен мегабайт до пары гигабайт. Раньше кусаются другие вещи: журнал транзакций, который никто не бэкапит, таблица аудита без срока хранения и ночное задание, которое ни разу не запустилось, потому что запускать его было некому.
Сначала измерьте. Два запроса показывают, где вы находитесь: первый - файлы данных базы, к которой вы подключены, второй - сколько кэша занимает каждая база.
SELECT name, type_desc, size * 8 / 1024 AS size_mb, FILEPROPERTY(name, 'SpaceUsed') * 8 / 1024 AS used_mbFROM sys.database_files;SELECT DB_NAME(database_id) AS db, COUNT(*) * 8 / 1024 AS cached_mbFROM sys.dm_os_buffer_descriptorsGROUP BY database_idORDER BY cached_mb DESC;Лимит применяется к выделенному размеру файлов данных, поэтому с 10 240 нужно сравнивать size_mb, а не used_mb. Файлы журнала в лимит не входят. Если size_mb меньше половины потолка и растёт медленно, у вас в запасе годы; тратьте силы на резервные копии и расписание.
Как уложиться в 10 ГБ#
Когда файлу данных нужно вырасти за потолок, рост отклоняется, а вставки, которым нужны новые страницы, падают с ошибкой; чтение при этом продолжает работать. Промежуточного предупреждения нет, поэтому все обходные пути сводятся к тому, чтобы не подходить к границе близко. По возрастанию трудозатрат:
Найдите, что именно большое
Прежде чем что-то решать, выполните запрос с размерами по таблицам. Три верхние таблицы почти всегда объясняют всё.
SELECT TOP (10) s.name + '.' + t.name AS table_name, SUM(p.reserved_page_count) * 8 / 1024 AS reserved_mb, SUM(CASE WHEN p.index_id IN (0, 1) THEN p.row_count END) AS row_countFROM sys.dm_db_partition_stats AS pJOIN sys.tables AS t ON t.object_id = p.object_idJOIN sys.schemas AS s ON s.schema_id = t.schema_idGROUP BY s.name, t.nameORDER BY reserved_mb DESC;Сравните зарезервированный размер с числом строк. Таблица на десять миллионов узких строк объёмом 2 ГБ - это нормально. Таблица на 40 000 строк объёмом 3 ГБ хранит бинарные данные, а их в базе с ограничением по размеру держать не стоит.
Сожмите таблицы и индексы
Сжатие строк и страниц работает в Express (оно доступно во всех редакциях начиная с SQL Server 2016 SP1). Сжатие страниц нередко уменьшает большую повторяющуюся таблицу вроде журнала аудита вдвое, ценой небольшой дополнительной нагрузки на процессор при записи и чтении. Перед перестроением попросите движок оценить экономию:
EXEC sp_estimate_data_compression_savings @schema_name = 'dbo', @object_name = 'AuditLog', @index_id = NULL, @partition_number = NULL, @data_compression = 'PAGE';ALTER TABLE dbo.AuditLog REBUILD WITH (DATA_COMPRESSION = PAGE);Для перестроения нужно рабочее место примерно размером с таблицу, к тому же оно увеличивает журнал, поэтому делайте его, когда есть запас и там и там, и предварительно сделайте резервную копию. Для широких таблиц, которые в основном читают, кластеризованный columnstore-индекс сжимает намного сильнее, чем сжатие страниц, но в Express кэш columnstore ограничен 352 МБ, а такие индексы подходят для аналитических сканирований, а не для точечных выборок, так что проверяйте на своих запросах.
Для отдельных больших текстовых или JSON-колонок функции COMPRESS() и DECOMPRESS() применяют GZIP прямо в T-SQL и сохраняют результат в колонке varbinary(max). Это экономит место на крупных полезных нагрузках; цена в том, что такую колонку больше нельзя напрямую индексировать и искать по ней.
Вынесите бинарные данные
Изображения, PDF и загруженные документы в varbinary(max) заполняют базу на 10 ГБ быстрее всего остального, и каждый такой файл ещё и проходит через ваш буферный пул в 1,4 ГБ, вытесняя нужные страницы. Храните файл в объектном хранилище или на диске, а в таблице оставьте ключ, размер и тип содержимого. Если хотите держать хранилище рядом с базой, у RE:NODE есть отдельная линейка S3-совместимого объектного хранилища; работает любой S3-эндпоинт, принцип один и тот же.
Архивируйте пакетами
Старые строки, которые нужно хранить, но читают редко, можно перенести во вторую базу того же экземпляра: у неё свои 10 ГБ. T-SQL не умеет переключать партиции между базами, поэтому переносите небольшими пакетами, удаляя из источника и вставляя в архив одним оператором:
WHILE 1 = 1BEGIN DELETE TOP (5000) FROM app.dbo.AuditLog OUTPUT deleted.* INTO archive.dbo.AuditLog WHERE CreatedAt < DATEADD(MONTH, -18, SYSUTCDATETIME()); IF @@ROWCOUNT = 0 BREAK;ENDДля OUTPUT ... INTO у целевой таблицы должны быть те же колонки, а включённых триггеров и внешних ключей быть не должно. Небольшие пакеты делают каждую транзакцию короткой, поэтому журнал не раздувается, а блокировки быстро снимаются. Запускайте это из планового задания (см. ниже), а не вручную.
Удаляйте то, что не нужно
Просроченные сессии, токены сброса пароля, помеченные удалёнными строки старше срока хранения и подробные журналы запросов незачем держать в базе вечно. Определите срок хранения для каждой таблицы, напишите по одному оператору очистки на таблицу и поставьте их в расписание. Это самый дешёвый обходной путь, и большинство баз его так и не применяет.
Уберите неиспользуемые индексы
Индексы входят в размер данных. Представление sys.dm_db_index_usage_stats показывает чтения и записи по каждому индексу с последнего перезапуска. Индекс с множеством записей и без чтений - чистые расходы. Доверяйте ему только после того, как сервер отработал полный бизнес-цикл (месячный отчёт может быть единственным потребителем индекса), потому что счётчики сбрасываются при перезапуске.
Разделяйте по назначению
Одна база на приложение и отдельная база для отчётов или архива под неограниченно растущие данные - хороший дизайн в любой редакции, а в Express он ещё и умножает запас. Лимит действует на базу, а не на экземпляр. Запросы между базами одного экземпляра работают с трёхчастными именами (archive.dbo.AuditLog), так что разделение редко требует переписывать запросы.
Сжатие файла - один раз
После крупной очистки файл остаётся большим. DBCC SHRINKFILE возвращает место, но сильно фрагментирует индексы, поэтому запускайте его один раз после большой чистки, перестройте важные индексы и никогда не ставьте в расписание. Поскольку лимит считается по выделенному размеру, наполовину пустой файл в 9,8 ГБ остаётся рядом с потолком, пока вы его не сожмёте.
Диск тоже лимит, и на маленьком тарифе он наступает первым#
Потолок в 10 ГБ - это правило лицензии, а диск - физическое ограничение, и на хостинге оно обычно меньше. Базе на 10 ГБ нужно место под журнал, под tempdb, под резервные копии, которые лежат на том же томе, пока вы их не вывезли, и под собственные файлы операционной системы. У RE:NODE линейка SQL Server идёт от Express Starter (2 ГБ памяти, диск 10 ГБ, 1 vCPU) до Express Max (6 ГБ памяти, диск 80 ГБ, 4 vCPU), от $6 в месяц. Арифметика простая: на диске 10 ГБ база в 10 ГБ у вас не появится, а на 80 ГБ можно держать несколько баз у самого потолка.
Поэтому выбирайте тариф сначала по диску. Грубый бюджет для одной базы, которая в пике будет занимать N ГБ: N на данные, от N/4 до N/2 на журнал, один полный файл резервной копии (около N, ведь Express не сжимает резервные копии), пока он лежит на сервере, и несколько гигабайт на tempdb и запас. Вторая копия лежит вне машины, в этом и смысл; см. раздел про расписание.
Как жить с 1,4 ГБ буферного пула#
Лимит памяти относится к буферному пулу - кэшу страниц данных. Процесс в целом использует больше, потому что планы, соединения и сортировки в лимит не входят. На практике из этого следуют две вещи.
Во-первых, сервер с памятью больше примерно 4 ГБ движку не помогает, потому что кэш останавливается на 1410 МБ. Покупайте память только до того предела, когда буферный пул может заполниться, а у операционной системы и движка есть запас; дальше покупайте диск. Тарифы RE:NODE это отражают: переход от Express Pro к Express Max (с 4 до 6 ГБ памяти) нужен в основном ради диска и всех четырёх ядер.
Во-вторых, рабочий набор больше 1,4 ГБ чаще читается с диска. NVMe, который стоит на всех тарифах RE:NODE, делает это куда менее болезненным, чем на вращающихся дисках, но обращений к диску можно и сократить:
- Более узкие строки и индексы. Покрывающий индекс только с теми колонками, которые нужны горячему запросу, помещает в кэшированную страницу больше полезных строк. Сжатие упаковывает больше строк в страницу, растягивая и кэш, и диск.
- Меньше индексов, но лучше. Каждый поддерживаемый индекс конкурирует за те же 1,4 ГБ. Indexes and execution plans объясняет, как читать план и убирать то, что не помогает.
- **Никаких
SELECT *.** Выборка широких строк, которые вам не нужны, тянет через кэш бинарные данные и редко нужные колонки. - Найдите дорогие запросы. Обычно несколько запросов дают основную часть чтений страниц. Query Store and slow queries показывает, как их найти в Express, где Query Store доступен.
- Кэшируйте в приложении. Справочные данные, которые не меняются, лучше держать в памяти приложения или в Valkey, а не в запросе к SQL Server на каждое обращение.
Ограничьте экземпляр, чтобы остальным хватило памяти
SQL Server при установке по умолчанию старается занять всю выданную память, вплоть до лимита редакции, а в Express буферный пул останавливается на 1410 МБ. Если у контейнера или машины памяти сверх этого немного, задайте явный потолок, чтобы операционная система сохранила свою часть:
EXEC sp_configure 'show advanced options', 1;RECONFIGURE;EXEC sp_configure 'max server memory (MB)', 1536;RECONFIGURE;Можно ли это менять, зависит от того, как хостинг запускает экземпляр и какой у вас логин, так что проверьте, прежде чем рассчитывать на это. У RE:NODE лимит памяти тарифа обеспечивает контейнер: на пределе памяти ядро останавливает контейнер, и он чисто перезапускается, а не уходит в своп. Такое поведение хорошо для стабильности и служит причиной оставлять запас. Берите тариф, памяти в котором заметно больше, чем нужно процессу SQL Server, и смотрите на график памяти на вкладке Console, а не гадайте.
Лимит в четыре ядра#
Express использует меньшее из значений: один сокет или четыре ядра, так что хост с восемью ядрами быстрее его не сделает. Обходной путь - использовать ядра хорошо, а не желать их больше:
- Сначала чините худшие запросы. Отсутствующий индекс, превращающий seek в scan на каждый запрос, обходится в процессоре дороже любой настройки в другом месте.
- Держите параллелизм в разумных пределах. Параллельные планы на четырёх ядрах могут вытеснять остальную работу. Серверная настройка
max degree of parallelism(EXEC sp_configure 'max degree of parallelism', 2;после включения расширенных параметров) - частый выбор для небольших серверов, но проверьте её на своей нагрузке, а не копируйте число. - Уносите отчёты из часов пик. Тяжёлые агрегации лучше запускать в тихую часть суток, и это ещё одна причина иметь планировщик.
- Разгружайте то, что не является запросами. Полнотекстовый поиск, кэширование и очереди лучше отдать другому инструменту, чем заставлять SQL Server делать всё.
На RE:NODE процессор тарифа - это жёсткое ограничение до купленной доли, так что на Express Starter у вас в сумме один vCPU, а не четыре. Сервер на 100% процессора работает медленно, но не сломан, и за это его никогда не приостанавливают. Если график процессора упирается в потолок в часы пик, это знак настраивать запросы или переходить на тариф выше.
Нет SQL Server Agent: чем заменить#
Шаг задания Agent - это команда, расписание и статус завершения. Любой планировщик, умеющий запускать sqlcmd, даёт то же самое. Вы теряете встроенную историю, оповещения и почту, и заменяете их собственным журналированием. Варианты от простого к трудоёмкому:
Расписание самого хостинга - для резервных копий
На RE:NODE у каждого сервера есть вкладка Schedules: cron-выражение, затем упорядоченные задачи с задержками, где задача - это команда консоли, резервная копия или действие с питанием. Задача резервного копирования делает ночную копию сервера без скрипта, который вам пришлось бы поддерживать. В тарифах SQL Server от 1 до 4 слотов резервных копий; копии хранятся вне защищаемой ими машины, их можно блокировать от ротации, скачивать и восстанавливать кнопкой. Это покрывает самую важную работу, которую обычно делает Agent. Не понимайте это как запуск произвольного T-SQL из панели; для этого используйте следующие варианты. Синтаксис описан в Cron expressions explained, а что стоит поставить в расписание, подсказывает scheduled tasks worth having.
cron плюс sqlcmd
На любой подконтрольной вам Linux-машине (небольшой VDS, CI-раннер, сервер приложения) строка в crontab запускает файл скрипта против базы. Чтобы пароль не попадал в командную строку, положите его в переменную окружения SQLCMDPASSWORD:
$ export SQLCMDPASSWORD='the-generated-password'$ sqlcmd -S db.example.net,14330 -U sa -d app -C -b -i /opt/jobs/purge.sql-S принимает host,port через запятую, -C доверяет сертификату сервера (нужно для самоподписанного и допустимо, только если вы знаете хост), -b заставляет sqlcmd вернуть ненулевой код при ошибке скрипта, а -i задаёт файл скрипта. Именно код возврата позволяет cron или обёртке понять, что что-то пошло не так. Флаги и две разные программы с одним именем описаны в sqlcmd and bcp.
Запись в crontab для ночной очистки в 03:15 с дописыванием вывода в файл журнала:
15 3 * * * /opt/jobs/run-sql.sh purge.sql >> /var/log/sqljobs.log 2>&1Оберните вызов в небольшой shell-скрипт, который завершается с ненулевым кодом при сбое и присылает вам сообщение (письмо, webhook в чат), чтобы тихий сбой стал заметным. Это замена оповещениям Agent и Database Mail, которых в Express нет.
Планировщик заданий Windows
Если ваше приложение работает на Windows, запланированная задача, запускающая sqlcmd -S ... -i script.sql, делает то же самое. Настройте запуск независимо от того, вошёл ли пользователь, храните учётные данные в задаче, а не в скрипте, и задайте разумный предел «остановить задачу, если она выполняется дольше», чтобы зависшее соединение не блокировало завтрашний запуск.
Внутри приложения
Hosted service в .NET, задание Hangfire или Quartz.NET, cron-библиотека для Node или фоновый воркер на любом языке могут по расписанию выполнять служебный T-SQL, и у них есть преимущество: строка подключения и логгер уже есть. Сторону подключения разбирает .NET with PostgreSQL, MySQL or SQL Server. Слабое место в том, что если приложение остановлено, то и задание тоже, а задание, которое работает в каждом экземпляре масштабированного приложения, выполняется по нескольку раз. Используйте его для очистки и статистики, но не делайте единственным, что делает резервные копии.
Чего делать не стоит
Не запускайте соединение, висящее в цикле WAITFOR DELAY в окне запроса или в долгой процедуре, чтобы имитировать планировщик. Оно вечно держит соединение и сессию, молча умирает при перезапуске сервера и ничего не сообщает о сбое. То же относится к триггеру, который выполняет тяжёлую работу на каждую вставку, чтобы обойтись без задания: он переносит стоимость в запросы ваших пользователей.
Чем заменить задания, которые обычно вёл Agent#
В большинстве установок Agent выполняет четыре вида заданий. У каждого есть прямой эквивалент.
| Задание Agent | Замена в Express |
|---|---|
| Ночная полная резервная копия | Резервная копия по расписанию хостинга или BACKUP DATABASE из cron |
| Копии журнала каждые 15-60 минут | Скрипт BACKUP LOG из cron или простая модель восстановления |
| Обслуживание индексов и статистики | Еженедельный скрипт или готовый набор скриптов |
| Очистка, архив, отчётные таблицы | Пакетный T-SQL из cron или приложения |
Для нативных резервных копий хватает файла скрипта такого размера, а следующий шаг - скопировать результат в другое место:
BACKUP DATABASE [app] TO DISK = N'/var/opt/mssql/backup/app.bak' WITH INIT, CHECKSUM, STATS = 10;RESTORE VERIFYONLY FROM DISK = N'/var/opt/mssql/backup/app.bak' WITH CHECKSUM;Путь - это каталог резервных копий по умолчанию при установке на Linux; на хостинге он может отличаться. Файл .bak на том же диске, что и база, защищает от опечатки, но не от потери диска. О восстановлении, WITH MOVE и осиротевших пользователях рассказывает SQL Server backup and restore. Поскольку Express сам не сжимает резервные копии, сожмите файл после записи (gzip или zstd в том же скрипте), прежде чем вывозить его с машины.
Копии журнала нужны только в том случае, если база работает в полной модели восстановления. Если восстановление на момент времени вам не нужно, ALTER DATABASE [app] SET RECOVERY SIMPLE; убирает это задание целиком и предотвращает разрастание журнала, описанное в recovery models and log growth. Если нужно, поставьте в расписание BACKUP LOG и проверьте восстановление на момент времени, потому что цепочка журналов, которую вы ни разу не восстанавливали, - гипотеза.
С обслуживанием индексов и статистики делайте меньше, чем советуют старые рекомендации. Автообновление статистики включено по умолчанию и хватает для небольших баз. Фрагментация на NVMe почти не важна; перестраивайте или реорганизуйте индекс, когда можете показать, что он замедляет конкретный запрос. Если нужна готовая процедура, бесплатное и широко используемое решение для обслуживания от Ola Hallengren - это обычный T-SQL, который работает в Express, пока его вызывает что-то внешнее, в чём и смысл этого раздела.
Расписание на практике: ночное задание от начала до конца#
Вместе разумный ночной порядок для базы Express выглядит так:
- 02:00 - расписание хостинга запускает резервную копию сервера (или ваш скрипт выполняет
BACKUP DATABASEи копирует файл прочь). - 03:15 - cron на подконтрольной вам машине запускает скрипт очистки и архивации через
sqlcmd -b. - 03:45 - та же машина запускает
sp_updatestats, если вы видели устаревшую статистику, и запрос размера, который пишет размер базы в журнал или метрику. - Раз в неделю - восстановление последней резервной копии в тестовую базу, затем
DBCC CHECKDBпо восстановленной копии, а не по рабочей базе. - Раз в месяц - прочитайте тренд размера и решите, что делать: сжимать, архивировать или переходить на тариф выше.
Разнесите задания по времени, чтобы долгая очистка не пересекалась с резервной копией. На четырёхъядерном экземпляре с кэшем 1,4 ГБ два тяжёлых задания одновременно будут тормозить приложение всё время пересечения.
Переезд на больший тариф без потери данных#
Когда обходных путей перестаёт хватать, переход на старшую редакцию или другой хостинг - это резервная копия и восстановление: .bak из Express восстанавливается в Standard или Enterprise той же или более новой версии без преобразования. Планируйте переезд до того, как его вынудит потолок. Migrating a database to SQL Server hosting описывает правила версий в обратном направлении, а which database should I use сравнивает движки, если настоящий ответ - другая СУБД. Смена тарифа на RE:NODE не пересоздаёт сервер, так что переход с меньшего тарифа Express на больший ради диска - это не миграция.
FAQ#
Как быстрее всего уложиться в лимит 10 ГБ?
Найдите самые большие таблицы, удалите или заархивируйте ненужные строки пакетами по несколько тысяч, перенесите хранимые файлы в объектное хранилище, а оставшееся сожмите через DATA_COMPRESSION = PAGE. Большинство баз, дошедших до потолка, забиты журналами и бинарными данными.
Можно ли выполнять задания по расписанию в SQL Server Express?
Да, снаружи движка. Используйте sqlcmd из cron или Task Scheduler, исполнитель заданий в приложении или собственные расписания хостинга для резервных копий. Отсутствует только сам SQL Server Agent.
Входит ли журнал транзакций в 10 ГБ?
Нет, считаются только файлы данных. Журнал ограничен местом на диске, поэтому необслуживаемый журнал в полной модели восстановления - реальный риск, а диск важен при выборе тарифа.
Увеличит ли больше оперативной памяти лимит буферного пула в 1,4 ГБ?
Нет. Буферный пул останавливается на 1410 МБ на экземпляр, сколько бы памяти ни было у сервера. Лишняя память помогает только остальной части процесса и операционной системе, поэтому тратьте деньги на диск или на более удачные индексы.
Как получать оповещения без Database Mail?
Пусть скрипт задания возвращает ненулевой код при сбое через sqlcmd -b, а обёртка отправляет сообщение на почту или в webhook чата, когда это происходит. Тогда оповещение живёт в вашем планировщике, а не в движке.


