🔒 Dữ liệu bản miễn phí có thể được dùng để cải thiện AI. Nâng cấp Pro để bảo mật tuyệt đối

Partition Table SQL Server: Chia bảng theo ngày tăng tốc truy vấn

Partition Table SQL Server: Chia bảng theo ngày tăng tốc truy vấn

2025-11-19 09:01 | 19 phút đọc | 230 lượt xem | Tác giả: Nguyễn Thái (Kỹ sư phần mềm)

1️⃣ Partition Table là gì?

  1. Là cách chia dữ liệu 1 bảng lớn thành nhiều phần (partition) dựa theo giá trị cột
  2. Từ người dùng → vẫn là 1 bảng
  3. Từ DB engine → chia nhỏ ở storage

Hiệu năng tăng vì truy vấn chỉ đọc đúng partition cần thiết


2️⃣ Vì sao cần Partition?

Dùng bullet dễ đọc:

  1. Bảng log to dần → đọc chậm => Đọc đúng partition theo ngày/tháng
  2. Xóa dữ liệu cũ rất chậm => Chỉ cần switch/drop partition
  3. Index quá nặng => Index theo partition → rebuild từng phần
  4. Backup/Restore lâu => Chỉ backup partition mới


3️⃣ Khi nào nên dùng Partition?

✅ Dữ liệu > 10–20 triệu dòng

✅ Có cột phân loại thời gian (CreatedDate)

✅ Truy vấn theo range: ngày, tháng

⛔ Không nên dùng nếu:

  1. Bảng nhỏ
  2. Truy vấn không có điều kiện lọc theo cột partition


4️⃣ Cách hoạt động của Partition trong SQL Server

Giải thích 3 khái niệm:

  1. Partition Function => Xác định điểm phân chia (range)
  2. Partition Scheme => Tạo “maps” partition → filegroup
  3. Partitioned Table => Bảng thực tế lưu dữ liệu


5️⃣ Ví dụ thực tế — Partition theo ngày

Bước 1: xem bảng có partition chưa:

SELECT
t.name AS TableName, i.name AS IndexName, i.index_id,
ps.name AS PartitionScheme, pf.name AS PartitionFunction,
p.partition_number, p.rows
FROM sys.tables t
JOIN sys.indexes i ON t.object_id = i.object_id AND i.index_id <= 1 -- CI/heap
LEFT JOIN sys.partition_schemes ps ON i.data_space_id = ps.data_space_id
LEFT JOIN sys.partition_functions pf ON ps.function_id = pf.function_id
JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id = i.index_id
WHERE t.name = 'FlightRequestLog'
ORDER BY p.partition_number;


Các bạn để ý thấy PartitionScheme null là chưa có schema liên quan để bảng này.


Bước 2: (Re)Create Partition Function (RANGE RIGHT) nếu chưa có:

=> tạo biên tới hôm nay là đủ, ngày mai sẽ do proc split thêm.

IF NOT EXISTS (SELECT 1 FROM sys.partition_functions WHERE name = 'PF_FlightRequestByDay')
BEGIN
DECLARE @d0 date = DATEADD(day, -8, CAST(GETDATE() AS date));
-- seed 30 boundary mỗi ngày từ d0+1 .. today
DECLARE @sql nvarchar(max) = N'CREATE PARTITION FUNCTION PF_FlightRequestByDay (date) AS RANGE RIGHT FOR VALUES (';
;WITH D AS (
SELECT DATEADD(day, 1, @d0) AS d
UNION ALL
SELECT DATEADD(day, 1, d) FROM D WHERE d < CAST(GETDATE() AS date)
)
SELECT @sql += STRING_AGG('''' + CONVERT(nvarchar(10), d, 23) + '''', ',') WITHIN GROUP (ORDER BY d)
FROM D OPTION (MAXRECURSION 0);
SET @sql += N');';
EXEC sp_executesql @sql;
END;
GO


Bước 3: (Re)Create Partition Scheme nếu chưa có :

=> sau này có thể map theo nhiều filegroup để trải I/O.

IF NOT EXISTS (SELECT 1 FROM sys.partition_schemes WHERE name = 'PS_FlightRequestByDay')
BEGIN
CREATE PARTITION SCHEME PS_FlightRequestByDay
AS PARTITION PF_FlightRequestByDay
ALL TO ([PRIMARY]); -- tạm thời đặt hết vào PRIMARY
END;
GO


Bước 4: mình sẽ thêm cột PartitionDate để đánh partition theo thằng này:

=> Nên sửa/siết ràng buộc NULL cho CreatedDate để tránh rơi vào partition “lạ”

IF COL_LENGTH('dbo.FlightRequestLog','PartitionDate') IS NULL
BEGIN
ALTER TABLE dbo.FlightRequestLog
ADD PartitionDate AS CAST(CreatedDate AS date) PERSISTED;
END
GO


Bước 5: mình chuyển cluster index về để nhường do partition(Nếu đang là heap hoặc CI khác, drop/rebuild CI):

IF EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID('dbo.FlightRequestLog') AND name = 'CX_FlightRequestLog')
BEGIN
DROP INDEX CX_FlightRequestLog ON dbo.FlightRequestLog WITH (ONLINE = OFF);
END


Bước 6: tạo cluster index cho cặp partition và id

CREATE UNIQUE CLUSTERED INDEX CX_FlightRequestLog
ON dbo.FlightRequestLog (PartitionDate, Id)
ON PS_FlightRequestByDay (PartitionDate);
GO


Bước 7: Nếu có các index khác thì phải Tạo NCI dạng partitioned (aligned với PF/PS hiện tại):

Trước đó mình có index này:

CREATE NONCLUSTERED INDEX [IX_FlightRequestLog_Date_Code] ON [dbo].[FlightRequestLog]
(
[CreatedDate] DESC,
[SystemCode] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
GO


Giờ mình sẽ xóa và tạo lại: => PartitionDate phải nằm trong KEY của NCI (không chỉ “ON PS(PartitionDate)”) để aligned 100%

DROP INDEX IF EXISTS IX_FlightRequestLog_Date_Code ON dbo.FlightRequestLog;

CREATE NONCLUSTERED INDEX IX_FlightRequestLog_Date_Code
ON dbo.FlightRequestLog
(
PartitionDate ASC, -- bắt buộc có trong KEY
CreatedDate DESC,
SystemCode ASC
)
ON PS_FlightRequestByDay (PartitionDate);


Bước 8: mình sẽ tạo 1 bảng CI giống bảng chính (không partition ở stage, nhưng key & column order phải identical

  1. Schema identical tuyệt đối: thứ tự cột, kiểu, nullability, computed (PERSISTED), collation, compression, set options.
  2. Tạo CI giống hệt
CREATE TABLE [dbo].[FlightRequestLog_Stage](
...
...
[PartitionDate] AS CAST([CreatedDate] AS DATE) PERSISTED
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY];
GO

Tạo lại cluster index giống bảng chính:

CREATE UNIQUE CLUSTERED INDEX CX_FlightRequestLog_Stage
ON dbo.FlightRequestLog_Stage (PartitionDate, Id);
GO

Giờ bạn kiểm tra object này đáng lẻ phải khác null:

SELECT OBJECT_ID('dbo.FlightRequestLog_Stage') AS StageObjectId;

Bước 10: kiểm tra

  1. Boundary sau sweep. Ở bước trước đó chúng ta đã tạo partition cho 7 ngày trước và ngày hiện tại tổng cộng là 8 ngày.
SELECT prv.boundary_id, CONVERT(date, prv.value) AS BoundaryDate
FROM sys.partition_functions pf
JOIN sys.partition_range_values prv ON pf.function_id = prv.function_id
WHERE pf.name = 'PF_FlightRequestByDay'
ORDER BY prv.boundary_id;


  1. Số rows theo partition:(test nha nên hơi ít data hehe)
SELECT p.partition_number, SUM(p.rows) AS rows
FROM sys.partitions p
JOIN sys.tables t ON t.object_id = p.object_id
JOIN sys.indexes i ON i.object_id = p.object_id AND i.index_id = p.index_id
WHERE t.name = 'FlightRequestLog' AND i.index_id = 1
GROUP BY p.partition_number
ORDER BY p.partition_number;

  1. À còn nếu bạn muốn xem tổng bao nhiêu partition và tổng row thì xem cái query này nha:
SELECT i.index_id,
COUNT(DISTINCT p.partition_number) AS partitions,
SUM(p.rows) AS rows
FROM sys.indexes i
JOIN sys.objects o ON o.object_id=i.object_id AND o.name='FlightRequestLog'
JOIN sys.partitions p ON p.object_id=i.object_id AND p.index_id=i.index_id
WHERE i.index_id IN (0,1)
GROUP BY i.index_id;


Bước 11: bước này quan trọng mình sẽ viết 1 proc để làm nhiệm vụ: truncate partition n ngày trước đó, tạo partition cho ngày mới, merge stage, switch.

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
Create OR ALTER PROC [dbo].[usp_FlightRequestLog_Sweep]
@KeepDays int = 5
AS
BEGIN
SET NOCOUNT ON;

DECLARE @today date = CAST(GETDATE() AS date);
DECLARE @tomorrow date = DATEADD(day, 1, @today);
DECLARE @dropDay date = DATEADD(day, -(@KeepDays + 1), @today);
DECLARE @dayAfterDrop date = DATEADD(day, 1, @dropDay);

/* 1) Đảm bảo có biên cho NGÀY MAI (để nhận dữ liệu mới) */
IF NOT EXISTS (
SELECT 1
FROM sys.partition_functions pf
JOIN sys.partition_range_values prv ON pf.function_id = prv.function_id
WHERE pf.name = 'PF_FlightRequestByDay'
AND CONVERT(date, prv.value) = @tomorrow
)
ALTER PARTITION FUNCTION PF_FlightRequestByDay()
SPLIT RANGE (@tomorrow);

/* 1b) Đảm bảo có biên cho @dropDay+1 (rất quan trọng cho SWITCH của @dropDay) */
IF NOT EXISTS (
SELECT 1
FROM sys.partition_functions pf
JOIN sys.partition_range_values prv ON pf.function_id = prv.function_id
WHERE pf.name = 'PF_FlightRequestByDay'
AND CONVERT(date, prv.value) = @dayAfterDrop
)
ALTER PARTITION FUNCTION PF_FlightRequestByDay()
SPLIT RANGE (@dayAfterDrop);

/* 2) SWITCH OUT partition @dropDay -> _Stage */
DECLARE @part int = $PARTITION.PF_FlightRequestByDay(@dropDay);

-- drop CHECK cũ, tạo CHECK mới theo khoảng [@dropDay, @dropDay+1)
IF EXISTS (SELECT 1 FROM sys.check_constraints WHERE name = 'CK_FlightRequestLog_Stage_Day')
ALTER TABLE dbo.FlightRequestLog_Stage DROP CONSTRAINT CK_FlightRequestLog_Stage_Day;

DECLARE @d0 nvarchar(10) = CONVERT(nvarchar(10), @dropDay, 23); -- 'YYYY-MM-DD'
DECLARE @d1 nvarchar(10) = CONVERT(nvarchar(10), @dayAfterDrop, 23); -- 'YYYY-MM-DD'
DECLARE @sql nvarchar(max) =
N'ALTER TABLE dbo.FlightRequestLog_Stage WITH CHECK '+
N'ADD CONSTRAINT CK_FlightRequestLog_Stage_Day '+
N'CHECK (PartitionDate >= '''+@d0+N''' AND PartitionDate < '''+@d1+N''');';
EXEC sp_executesql @sql;

-- SWITCH (nguồn/đích phải có index & schema align)
ALTER TABLE dbo.FlightRequestLog
SWITCH PARTITION @part
TO dbo.FlightRequestLog_Stage;

/* 3) Xoá nhanh + gộp biên cũ */
TRUNCATE TABLE dbo.FlightRequestLog_Stage;
ALTER PARTITION FUNCTION PF_FlightRequestByDay()
MERGE RANGE (@dropDay);
END


Bạn có thể bỏ câu lệnh này lên rồi chạy thôi:

EXEC dbo.usp_FlightRequestLog_Sweep @KeepDays = 5;


Bước 12: Ủa bạn nghĩ xong rồi à? Chưa đâu, mình cũng k rảnh để ngày nào cũng vào chạy => ok tạo job hàng ngày thôi let's go.(vào sql agent tạo nha)


Bước 13: Kiểm chứng hiệu năng và giám sát định kỳ

Sau khi bật partition, phải chứng minh rằng SQL Server đã dùng Partition Elimination → truy vấn chỉ quét đúng partition.

Bạn kiểm tra bằng:

SET STATISTICS IO ON;
SELECT COUNT(*)
FROM dbo.FlightRequestLog
WHERE PartitionDate = CAST(GETDATE() AS date);
SET STATISTICS IO OFF;

Kết quả chuẩn:

  1. Logical Reads giảm rất mạnh
  2. Không quét toàn bảng

Bước 14: Checklist vận hành lâu dài:

  1. Partition luôn có partition trống bên phải? => Có job Split ngày mai ✅
  2. Stage luôn empty trước SWITCH? => TRUNCATE mỗi lần chạy ✅
  3. Statistics tối ưu? => UPDATE STATISTICS hàng tuần
  4. I/O filegroup tối ưu? => Cân nhắc phân FG theo tháng/quý
  5. Log growth an toàn? => Giám sát khi SWITCH/MERGE tạo log nhiều
  6. Backup/Restore partition cũ? => Có thể archive FG cũ để tiết kiệm storage

Vậy là ok rồi đó. Chúc bạn thành công nha


✅ Phần Kết: Tóm lược “Chuẩn Partition thực chiến”

Partition đúng =
🔹 đúng cột (date)
🔹 đúng cấu trúc CI/NCI (aligned)
🔹 đúng thứ tự SPLIT → SWITCH → TRUNCATE → MERGE
🔹 đúng automation (Agent Job)
🔹 có giám sát & kiểm chứng

Nếu làm đủ →

Hệ thống luôn gọn nhẹ & nhanh

Quản lý log cũ như “xoá file” (TRUNCATE partition)

Tăng trưởng data không còn là nỗi lo 🚀

Frequently Asked Questions

Q: "Partition Table trong SQL Server là gì?",

A: "Partition Table là kỹ thuật chia một bảng lớn thành nhiều phần vật lý (partition) dựa trên cột phân vùng như ngày. Người dùng vẫn thấy một bảng thống nhất, nhưng SQL Server chỉ đọc đúng partition cần thiết nên tăng hiệu năng."

Q: "Khi nào nên dùng partition?",

A: "Khi bảng có >10–20 triệu dòng và truy vấn thường lọc theo thời gian (range). Không nên dùng nếu bảng nhỏ hoặc không có filter theo cột partition."

Q: "Partitioning giúp cải thiện hiệu năng như thế nào?",

A: "Vì SQL Server sử dụng Partition Elimination, chỉ đọc các phân vùng liên quan thay vì quét toàn bảng. Ngoài ra, có thể TRUNCATE từng partition rất nhanh và bảo trì theo range."

Q: "Aligned index là gì và tại sao quan trọng?",

A: "Aligned index là index sử dụng cùng partition scheme và có cột partition trong KEY. Điều này giúp SWITCH PARTITION hoạt động thành công và tối ưu hiệu năng truy vấn."

Q: "Sliding window trong partition hoạt động ra sao?",

A: "Một partition trống luôn được tạo sẵn cho ngày mới (SPLIT), và partition cũ sẽ được SWITCH OUT sang bảng Stage và TRUNCATE rồi MERGE lại boundary để giải phóng dữ liệu nhanh."

Q: "Có cần tạo bảng Stage để SWITCH không?",

A: "Có. Bảng Stage phải identical 100% về schema, index, computed column và collation với bảng chính. Stage phải luôn rỗng trước khi SWITCH."

Q: "Partition có ảnh hưởng đến backup/restore không?",

A: "Có thể backup và restore theo filegroup, giúp tối ưu chi phí lưu trữ và bảo trì dữ liệu theo từng giai đoạn thời gian."

Was this article helpful?

Latest from Our Blog

Không có bài viết nào