Atlas now manages SQL Server filegroups and their files as filegroup blocks, and places partition schemes, indexes, and constraints on them.
atlas schema inspect prints filegroups as filegroup blocks, and atlas migrate diff and atlas schema apply plan their changes. A filegroup block takes:
- file blocks, each with a filename, size, maxsize, and growth.
- default, to make it the default filegroup of the database.
- read_only, to make it read-only.
- memory_optimized, to make it the memory-optimized filegroup of the database.
Declaring Filegroups
Declare the filegroups and reference them from the objects stored on them. Here, FG_A is the default filegroup, and the second partition of the partition_scheme is stored on FG_R:
filegroup "FG_A" {default = truefile "filegroup_a1" {filename = "/var/opt/mssql/data/filegroup_a1.ndf"size = "8MB"maxsize = "UNLIMITED"growth = "64MB"}}filegroup "FG_R" {file "filegroup_r1" {filename = "/var/opt/mssql/data/filegroup_r1.ndf"size = "16MB"maxsize = "1GB"growth = "10%"}}partition_function "PF" {input = intrange = RIGHTvalues = ["100"]}partition_scheme "PS" {partition = partition_function.PFfilegroups = ["PRIMARY", filegroup.FG_R]}table "Orders" {schema = schema.dbocolumn "ID" {null = falsetype = int}primary_key {columns = [column.ID]}partition {scheme = partition_scheme.PScolumn = column.ID}}
atlas migrate diff creates each filegroup with its files before it becomes the default, and before the objects stored on it. SQL Server doesn't run ALTER DATABASE in a transaction, so the file is atlas:txmode none:
-- atlas:txmode none-- Create filegroup "FG_A"ALTER DATABASE CURRENT ADD FILEGROUP [FG_A];-- Add file "filegroup_a1" to filegroup "FG_A"ALTER DATABASE CURRENT ADD FILE (NAME = 'filegroup_a1', FILENAME = '/var/opt/mssql/data/filegroup_a1.ndf', SIZE = 8MB, MAXSIZE = UNLIMITED, FILEGROWTH = 64MB) TO FILEGROUP [FG_A];-- Modify filegroup "FG_A"ALTER DATABASE CURRENT MODIFY FILEGROUP [FG_A] DEFAULT;-- Create filegroup "FG_R"ALTER DATABASE CURRENT ADD FILEGROUP [FG_R];-- Add file "filegroup_r1" to filegroup "FG_R"ALTER DATABASE CURRENT ADD FILE (NAME = 'filegroup_r1', FILENAME = '/var/opt/mssql/data/filegroup_r1.ndf', SIZE = 16MB, MAXSIZE = 1GB, FILEGROWTH = 10%) TO FILEGROUP [FG_R];-- Create partition function "PF"CREATE PARTITION FUNCTION [PF] (int) AS RANGE RIGHT FOR VALUES (100);-- Create partition scheme "PS"CREATE PARTITION SCHEME [PS] AS PARTITION [PF] TO ([PRIMARY], [FG_R]);-- Create "Orders" tableCREATE TABLE [dbo].[Orders] ([ID] int NOT NULL,CONSTRAINT [PK_Orders] PRIMARY KEY CLUSTERED ([ID] ASC)) ON [PS] ([ID]);
The dev database creates the declared files under paths of its own, so it can run on the same SQL Server instance as the target.
Changing Filegroups
Files are added and resized in place. In the next version, FG_A gets a second file, the file of FG_R gets a new max size and growth, and a new FG_B becomes the default filegroup. Until FG_B has a file, PRIMARY is the default:
-- atlas:txmode none-- Set default filegroup to PRIMARYALTER DATABASE CURRENT MODIFY FILEGROUP [PRIMARY] DEFAULT;-- Add file "filegroup_a2" to filegroup "FG_A"ALTER DATABASE CURRENT ADD FILE (NAME = 'filegroup_a2', FILENAME = '/var/opt/mssql/data/filegroup_a2.ndf', SIZE = 8MB, MAXSIZE = UNLIMITED, FILEGROWTH = 64MB) TO FILEGROUP [FG_A];-- Modify file "filegroup_r1" in filegroup "FG_R"ALTER DATABASE CURRENT MODIFY FILE (NAME = 'filegroup_r1', MAXSIZE = 2GB);-- Modify file "filegroup_r1" in filegroup "FG_R"ALTER DATABASE CURRENT MODIFY FILE (NAME = 'filegroup_r1', FILEGROWTH = 32MB);-- Create filegroup "FG_B"ALTER DATABASE CURRENT ADD FILEGROUP [FG_B];-- Add file "filegroup_b1" to filegroup "FG_B"ALTER DATABASE CURRENT ADD FILE (NAME = 'filegroup_b1', FILENAME = '/var/opt/mssql/data/filegroup_b1.ndf', SIZE = 8MB, MAXSIZE = UNLIMITED, FILEGROWTH = 64MB) TO FILEGROUP [FG_B];-- Modify filegroup "FG_B"ALTER DATABASE CURRENT MODIFY FILEGROUP [FG_B] DEFAULT;
Removing a filegroup from the schema removes its files, then the filegroup. Atlas refuses to drop a filegroup that a remaining table or index is stored on:
-- atlas:txmode none-- Remove file "filegroup_a1" from filegroup "FG_A"ALTER DATABASE CURRENT REMOVE FILE [filegroup_a1];-- Remove file "filegroup_a2" from filegroup "FG_A"ALTER DATABASE CURRENT REMOVE FILE [filegroup_a2];-- Drop filegroup "FG_A"ALTER DATABASE CURRENT REMOVE FILEGROUP [FG_A];
Indexes on Filegroups
An index, a nonclustered primary key, or a unique constraint stored apart from its table names its filegroup, as a reference or as the name of a filegroup such as PRIMARY. One without it is stored with its table:
table "contacts" {schema = schema.dbocolumn "ID" {null = falsetype = bigint}column "AccountID" {null = falsetype = int}column "ContactID" {null = falsetype = bigint}column "Code" {null = falsetype = int}column "Name" {null = falsetype = nvarchar(100)}primary_key {columns = [column.ID]nonclustered = truefilegroup = filegroup.FG_IX1}index "uidx_contacts_contactid" {unique = truecolumns = [column.ContactID]nonclustered = truefilegroup = filegroup.FG_IX1}index "ix_contacts_name" {columns = [column.Name]nonclustered = true}index "ix_contacts_code_name" {columns = [column.Code, column.Name]nonclustered = truefilegroup = filegroup.FG_IX2}unique "uq_contacts_code" {nonclustered = truefilegroup = "PRIMARY"columns = [column.Code]}partition {scheme = partition_scheme.ps_accountcolumn = column.AccountID}}
-- Create "contacts" tableCREATE TABLE [dbo].[contacts] ([ID] bigint NOT NULL,[AccountID] int NOT NULL,[ContactID] bigint NOT NULL,[Code] int NOT NULL,[Name] nvarchar(100) NOT NULL,CONSTRAINT [PK_contacts] PRIMARY KEY NONCLUSTERED ([ID] ASC) ON [FG_IX1],CONSTRAINT [uq_contacts_code] UNIQUE NONCLUSTERED ([Code] ASC) ON [PRIMARY]) ON [ps_account] ([AccountID]);-- Create index "uidx_contacts_contactid" to table: "contacts"CREATE UNIQUE NONCLUSTERED INDEX [uidx_contacts_contactid] ON [dbo].[contacts] ([ContactID] ASC) ON [FG_IX1];-- Create index "ix_contacts_name" to table: "contacts"CREATE NONCLUSTERED INDEX [ix_contacts_name] ON [dbo].[contacts] ([Name] ASC);-- Create index "ix_contacts_code_name" to table: "contacts"CREATE NONCLUSTERED INDEX [ix_contacts_code_name] ON [dbo].[contacts] ([Code] ASC, [Name] ASC) ON [FG_IX2];
Setting filegroup = filegroup.FG_IX2 on ix_contacts_name moves it by re-creating it:
-- Drop index "ix_contacts_name" from table: "contacts"DROP INDEX IF EXISTS [ix_contacts_name] ON [dbo].[contacts];-- Create index "ix_contacts_name" to table: "contacts"CREATE NONCLUSTERED INDEX [ix_contacts_name] ON [dbo].[contacts] ([Name] ASC) ON [FG_IX2];
Limitations
- READ_ONLY and READ_WRITE need exclusive access to the database (Msg 5070), and the advisory lock Atlas holds on it blocks them. Apply a migration that sets a filegroup read-only or read-write, or changes objects stored on a read-only filegroup, with atlas migrate apply --skip-lock, while no other session uses the database and no other migration runs against it. If a session is still connected, the statement fails after about 20 seconds: run the command again once the database is free, and Atlas continues from that statement. atlas migrate diff takes the same lock on the dev database, so once a migration file sets a filegroup read-only, the dev database can no longer replay the migration directory.
- A plan that adds, drops, or changes a filegroup runs with atlas:txmode none, so if a statement fails, the statements before it are not rolled back.
- Filegroups belong to the database, so inspect and plan them with a database-scoped URL (mode=database). A plan scoped to one schema rejects them.
- Tables have no filegroup attribute. A new table is stored on the default filegroup or on its partition scheme, and Atlas doesn't move an existing table to another filegroup. TEXTIMAGE_ON and FILESTREAM_ON are not managed.
- Atlas refuses to drop a filegroup while a table, an index, or an indexed view stays on it (cannot drop filegroup "FG_A" while table "t" remains). Move or drop them first. The same plan can move an index whose filegroup changes, a partition scheme that stops using the filegroup, and the full-text index of a table.
- Renaming a filegroup is planned as a drop and a create, so the same rule applies. A file that keeps its filename under a new name is renamed in place.
- Changing the filename of a file (moving it) is not supported. Files are never shrunk: a size below the current size is ignored, and a maxsize below it is rejected.
- The PRIMARY filegroup can't be declared, and its files and the log file are not managed. Refer to it as "PRIMARY".
- FILESTREAM filegroups are not supported, and inspection skips them.
- memory_optimized can't be changed on an existing filegroup. Before SQL Server 2025, a memory-optimized filegroup that has held data can't be removed (Msg 41802), so a plan that drops one fails. On SQL Server 2025, its removal waits until the transaction log is truncated past it.
- Azure SQL Managed Instance places files itself, so leave out filename there: inspection doesn't report it. Fabric Data Warehouse has no user filegroups.
- Requires Atlas Pro. Run atlas login to start a free trial.
Index Placement
- filegroup is supported only on nonclustered and spatial indexes. A clustered index is stored where its table is.
- Moving an index to another filegroup drops and re-creates it, so the index is missing while the migration runs, which can be costly on large tables.
- An index can't be placed on a partition scheme other than its table's.