DDL - Data Definition Language
DDL (Data Definition Language) là tập hợp các câu lệnh SQL dùng để tạo, thay đổi, và xóa cấu trúc database objects (database, table, index, schema, v.v.). Các câu lệnh DDL chính: CREATE, ALTER, DROP, TRUNCATE.
1. Database Objects
CREATE DATABASE
-- Tạo database đơn giản
CREATE DATABASE SalesDB;
-- Tạo database với cấu hình chi tiết
CREATE DATABASE SalesDB
ON PRIMARY (
NAME = 'SalesDB', -- Logical name
FILENAME = 'D:\Data\SalesDB.mdf', -- Physical file path
SIZE = 1024 MB, -- Initial size
MAXSIZE = 10240 MB, -- Maximum size
FILEGROWTH = 512 MB -- Auto-growth increment
)
LOG ON (
NAME = 'SalesDB_log',
FILENAME = 'D:\Logs\SalesDB_log.ldf',
SIZE = 256 MB,
MAXSIZE = 4096 MB,
FILEGROWTH = 128 MB
);
-- Sử dụng database
USE SalesDB;
GO
-- Xem danh sách databases
SELECT name, state_desc, recovery_model_desc, compatibility_level
FROM sys.databases
ORDER BY name;
ALTER DATABASE
-- Thay đổi recovery model
ALTER DATABASE SalesDB SET RECOVERY SIMPLE;
ALTER DATABASE SalesDB SET RECOVERY FULL;
ALTER DATABASE SalesDB SET RECOVERY BULK_LOGGED;
-- Thay đổi compatibility level
ALTER DATABASE SalesDB SET COMPATIBILITY_LEVEL = 160; -- SQL Server 2022
-- Thêm data file vào filegroup
ALTER DATABASE SalesDB
ADD FILE (
NAME = 'SalesDB_data2',
FILENAME = 'D:\Data\SalesDB_data2.ndf',
SIZE = 512 MB,
FILEGROWTH = 256 MB
) TO FILEGROUP [PRIMARY];
-- Đặt database thành READ-ONLY
ALTER DATABASE SalesDB SET READ_ONLY;
ALTER DATABASE SalesDB SET READ_WRITE;
-- Đổi tên database
ALTER DATABASE SalesDB MODIFY NAME = SalesDatabase;
DROP DATABASE
-- Xóa database (KHÔNG THỂ HOÀN TÁC!)
DROP DATABASE SalesDB;
-- An toàn hơn: kiểm tra trước khi xóa
IF EXISTS (SELECT 1 FROM sys.databases WHERE name = 'SalesDB')
DROP DATABASE SalesDB;
-- SQL Server 2016+
DROP DATABASE IF EXISTS SalesDB;
2. Tables (Bảng dữ liệu)
CREATE TABLE - Cơ bản
-- Cú pháp đầy đủ
CREATE TABLE Employees (
-- Integer types
EmployeeId INT NOT NULL,
DepartmentId INT NULL,
-- String types
FirstName NVARCHAR(100) NOT NULL,
LastName NVARCHAR(100) NOT NULL,
Email VARCHAR(320) NOT NULL,
Phone VARCHAR(20) NULL,
-- Numeric
Salary DECIMAL(15, 2) NOT NULL DEFAULT 0,
-- Date types
BirthDate DATE NULL,
HireDate DATE NOT NULL DEFAULT CAST(GETDATE() AS DATE),
CreatedAt DATETIME2(7) NOT NULL DEFAULT SYSDATETIME(),
UpdatedAt DATETIME2(7) NULL,
-- Boolean
IsActive BIT NOT NULL DEFAULT 1,
-- Constraints (inline)
CONSTRAINT PK_Employees PRIMARY KEY (EmployeeId),
CONSTRAINT FK_Employees_Departments FOREIGN KEY (DepartmentId)
REFERENCES Departments(DepartmentId),
CONSTRAINT UQ_Employees_Email UNIQUE (Email),
CONSTRAINT CHK_Employees_Salary CHECK (Salary >= 0),
CONSTRAINT CHK_Employees_BirthDate CHECK (BirthDate IS NULL OR BirthDate < CAST(GETDATE() AS DATE))
);
Các loại Constraints
PRIMARY KEY
-- Column-level (single column)
CREATE TABLE Orders (
OrderId INT NOT NULL CONSTRAINT PK_Orders PRIMARY KEY,
...
);
-- Hoặc viết ngắn gọn
CREATE TABLE Orders (
OrderId INT PRIMARY KEY,
...
);
-- Table-level (composite key)
CREATE TABLE OrderDetails (
OrderId INT NOT NULL,
ProductId INT NOT NULL,
Quantity INT NOT NULL,
UnitPrice DECIMAL(10,2) NOT NULL,
CONSTRAINT PK_OrderDetails PRIMARY KEY (OrderId, ProductId)
-- Composite PK phải dùng table-level constraint
);
FOREIGN KEY
CREATE TABLE Orders (
OrderId INT PRIMARY KEY,
CustomerId INT NOT NULL,
EmployeeId INT NULL,
-- Column-level FK
CustomerId INT REFERENCES Customers(CustomerId),
-- Table-level FK với đặt tên rõ ràng (khuyến nghị)
CONSTRAINT FK_Orders_Customers FOREIGN KEY (CustomerId)
REFERENCES Customers(CustomerId)
ON DELETE NO ACTION -- Không xóa cascading
ON UPDATE CASCADE, -- Tự cập nhật khi Customer PK thay đổi
CONSTRAINT FK_Orders_Employees FOREIGN KEY (EmployeeId)
REFERENCES Employees(EmployeeId)
ON DELETE SET NULL -- Đặt NULL khi Employee bị xóa
ON UPDATE CASCADE
);
UNIQUE Constraint
CREATE TABLE Users (
UserId INT PRIMARY KEY,
Username NVARCHAR(50) NOT NULL,
Email VARCHAR(320) NOT NULL,
SSN CHAR(9) NULL,
-- Single column unique
CONSTRAINT UQ_Users_Username UNIQUE (Username),
CONSTRAINT UQ_Users_Email UNIQUE (Email),
-- Composite unique (NULL values được phép - nhiều NULLs không vi phạm UNIQUE)
CONSTRAINT UQ_Users_SSN UNIQUE (SSN)
);
-- Filtered unique index (SQL Server) - unique chỉ với non-NULL values
CREATE UNIQUE INDEX UX_Users_SSN_NotNull
ON Users (SSN)
WHERE SSN IS NOT NULL;
CHECK Constraint
CREATE TABLE Products (
ProductId INT PRIMARY KEY,
ProductName NVARCHAR(200) NOT NULL,
Price DECIMAL(10,2) NOT NULL,
Stock INT NOT NULL,
Category NVARCHAR(50) NOT NULL,
Status CHAR(1) NOT NULL DEFAULT 'A',
CONSTRAINT CHK_Products_Price CHECK (Price > 0),
CONSTRAINT CHK_Products_Stock CHECK (Stock >= 0),
CONSTRAINT CHK_Products_Status CHECK (Status IN ('A', 'I', 'D')), -- Active, Inactive, Discontinued
CONSTRAINT CHK_Products_Name CHECK (LEN(ProductName) >= 2)
);
DEFAULT Constraint
CREATE TABLE AuditLog (
LogId INT PRIMARY KEY,
TableName NVARCHAR(128) NOT NULL,
Action CHAR(1) NOT NULL,
-- Default values
CreatedAt DATETIME2(7) NOT NULL CONSTRAINT DF_AuditLog_CreatedAt DEFAULT SYSDATETIME(),
CreatedBy NVARCHAR(128) NOT NULL CONSTRAINT DF_AuditLog_CreatedBy DEFAULT SUSER_SNAME(),
IsProcessed BIT NOT NULL CONSTRAINT DF_AuditLog_IsProcessed DEFAULT 0,
CONSTRAINT CHK_AuditLog_Action CHECK (Action IN ('I', 'U', 'D'))
);
3. ALTER TABLE
-- Thêm column mới
ALTER TABLE Employees ADD MiddleName NVARCHAR(100) NULL;
-- Thêm column với DEFAULT (cho rows hiện có)
ALTER TABLE Employees ADD
CreatedBy NVARCHAR(128) NOT NULL DEFAULT SUSER_SNAME(),
IsDeleted BIT NOT NULL DEFAULT 0;
-- Thay đổi kiểu dữ liệu của column
ALTER TABLE Employees ALTER COLUMN Phone VARCHAR(30) NULL;
-- Chú ý: không thể ALTER column nếu có INDEX hoặc CONSTRAINT trên column đó
-- Xóa column
ALTER TABLE Employees DROP COLUMN MiddleName;
-- Thêm constraint
ALTER TABLE Employees
ADD CONSTRAINT CHK_Employees_Email CHECK (Email LIKE '%@%.%');
ALTER TABLE Employees
ADD CONSTRAINT FK_Employees_Departments
FOREIGN KEY (DepartmentId) REFERENCES Departments(DepartmentId);
-- Xóa constraint
ALTER TABLE Employees DROP CONSTRAINT CHK_Employees_Email;
ALTER TABLE Employees DROP CONSTRAINT FK_Employees_Departments;
-- Xóa default constraint
ALTER TABLE Employees DROP CONSTRAINT DF_Employees_CreatedAt;
-- Disable / Enable constraint tạm thời
ALTER TABLE Employees NOCHECK CONSTRAINT FK_Employees_Departments;
ALTER TABLE Employees CHECK CONSTRAINT FK_Employees_Departments;
-- Xóa bảng (KHÔNG THỂ HOÀN TÁC)
DROP TABLE IF EXISTS Employees;
DROP TABLE Employees; -- Lỗi nếu table không tồn tại
4. Schemas (Lược đồ)
Schema là namespace logic để nhóm các database objects:
-- Tạo schema
CREATE SCHEMA Hr AUTHORIZATION dbo;
CREATE SCHEMA Sales;
CREATE SCHEMA Finance;
-- Tạo table trong schema
CREATE TABLE Hr.Employees (...);
CREATE TABLE Sales.Orders (...);
CREATE TABLE Finance.Invoices (...);
-- Move table sang schema khác
ALTER SCHEMA Sales TRANSFER dbo.OldOrdersTable;
-- Xem các schemas
SELECT schema_id, name, principal_id
FROM sys.schemas
ORDER BY name;
-- Xem objects trong schema
SELECT
s.name AS schema_name,
o.name AS object_name,
o.type_desc
FROM sys.objects o
JOIN sys.schemas s ON o.schema_id = s.schema_id
WHERE o.type IN ('U', 'V', 'P') -- Tables, Views, Stored Procedures
ORDER BY s.name, o.name;
-- Drop schema (phải empty trước)
DROP SCHEMA Finance;
Best Practice: Dùng schemas để:
- Phân tách theo domain:
Hr,Sales,Finance- Phân quyền theo nhóm: GRANT trên schema thay vì từng table
- Tránh naming conflicts
5. Identity Columns và Sequences
IDENTITY
-- IDENTITY(seed, increment)
CREATE TABLE Products (
ProductId INT IDENTITY(1, 1) PRIMARY KEY, -- Bắt đầu từ 1, tăng 1
ProductName NVARCHAR(200) NOT NULL
);
-- INSERT không cần specify ProductId
INSERT INTO Products (ProductName) VALUES ('Widget A');
INSERT INTO Products (ProductName) VALUES ('Widget B');
-- Lấy giá trị IDENTITY vừa insert
SELECT SCOPE_IDENTITY(); -- Khuyến nghị: chỉ trong scope hiện tại
SELECT @@IDENTITY; -- Trả về identity của lần INSERT cuối trong session (kể cả trigger)
SELECT IDENT_CURRENT('Products'); -- Identity hiện tại của table
-- INSERT với IDENTITY_INSERT ON (khi cần insert giá trị cụ thể)
SET IDENTITY_INSERT Products ON;
INSERT INTO Products (ProductId, ProductName) VALUES (100, 'Special Item');
SET IDENTITY_INSERT Products OFF;
-- Reset IDENTITY
DBCC CHECKIDENT ('Products', RESEED, 0); -- Reset về 0, next insert = 1
-- Xem thông tin IDENTITY
SELECT
name AS column_name,
seed_value,
increment_value,
last_value
FROM sys.identity_columns
WHERE object_id = OBJECT_ID('Products');
NEWID() và NEWSEQUENTIALID()
-- NEWID(): Random GUID - BAD cho clustered PK vì gây fragmentation
CREATE TABLE Sessions_Bad (
SessionId UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(), -- Random, gây page splits
UserId INT
);
-- NEWSEQUENTIALID(): Sequential GUID - tốt hơn cho clustered PK
CREATE TABLE Sessions_Good (
SessionId UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWSEQUENTIALID(), -- Sequential
UserId INT
);
-- Lưu ý: NEWSEQUENTIALID() chỉ dùng được trong DEFAULT constraint
-- Không gọi trực tiếp trong query được
-- Tại sao NEWID() gây vấn đề?
-- Mỗi INSERT có thể xen vào giữa các pages hiện có
-- -> Page splits -> Fragmentation -> Hiệu năng giảm
CREATE SEQUENCE
-- SEQUENCE là đối tượng độc lập, linh hoạt hơn IDENTITY
CREATE SEQUENCE dbo.OrderIdSeq
START WITH 1000
INCREMENT BY 1
MINVALUE 1000
MAXVALUE 9999999
NO CYCLE -- Không quay lại từ đầu khi đến max
CACHE 50; -- Cache 50 giá trị để tăng hiệu năng
-- Sử dụng SEQUENCE
INSERT INTO Orders (OrderId, CustomerId)
VALUES (NEXT VALUE FOR dbo.OrderIdSeq, 42);
-- Dùng trong DEFAULT
CREATE TABLE Orders (
OrderId INT DEFAULT (NEXT VALUE FOR dbo.OrderIdSeq),
CustomerId INT,
OrderDate DATE DEFAULT GETDATE()
);
-- Xem giá trị hiện tại
SELECT current_value FROM sys.sequences WHERE name = 'OrderIdSeq';
-- Đặt lại SEQUENCE
ALTER SEQUENCE dbo.OrderIdSeq RESTART WITH 1000;
-- Ưu điểm SEQUENCE vs IDENTITY:
-- - Có thể dùng cho nhiều tables
-- - Có thể lấy giá trị trước khi INSERT (cho audit, logging)
-- - Hỗ trợ cycle
-- - Linh hoạt về min/max/increment
DECLARE @nextId INT = NEXT VALUE FOR dbo.OrderIdSeq;
-- Bây giờ có thể dùng @nextId trước khi INSERT
6. Computed Columns
-- Non-persisted computed column (tính lại mỗi khi query)
CREATE TABLE Rectangles (
RectId INT PRIMARY KEY,
Width DECIMAL(10,2) NOT NULL,
Height DECIMAL(10,2) NOT NULL,
-- Tính toán mỗi lần query
Area AS (Width * Height),
-- Persisted: lưu kết quả vào disk, tính lại khi INSERT/UPDATE
Perimeter AS (2 * (Width + Height)) PERSISTED
);
-- Ví dụ thực tế
CREATE TABLE OrderDetails (
OrderDetailId INT PRIMARY KEY,
Quantity INT NOT NULL,
UnitPrice DECIMAL(10,2) NOT NULL,
Discount DECIMAL(5,4) NOT NULL DEFAULT 0,
-- Non-persisted
SubTotal AS (Quantity * UnitPrice),
-- Persisted - có thể tạo index trên persisted computed column
NetAmount AS (Quantity * UnitPrice * (1 - Discount)) PERSISTED
);
-- Index trên persisted computed column
CREATE INDEX IX_OrderDetails_NetAmount ON OrderDetails (NetAmount);
-- Xem computed columns
SELECT
name,
definition,
is_persisted
FROM sys.computed_columns
WHERE object_id = OBJECT_ID('OrderDetails');
7. Temporal Tables (System-Versioned)
Temporal tables tự động lưu lịch sử thay đổi dữ liệu:
-- Tạo temporal table
CREATE TABLE Employees (
EmployeeId INT NOT NULL CONSTRAINT PK_Employees PRIMARY KEY,
FullName NVARCHAR(200) NOT NULL,
Salary DECIMAL(15,2) NOT NULL,
DepartmentId INT NULL,
-- Bắt buộc: 2 datetime2 columns cho period
ValidFrom DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL,
ValidTo DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL,
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH (SYSTEM_VERSIONING = ON (
HISTORY_TABLE = dbo.EmployeesHistory -- SQL Server tự tạo history table
));
-- INSERT/UPDATE/DELETE hoạt động bình thường
UPDATE Employees SET Salary = 75000 WHERE EmployeeId = 1;
-- Row cũ tự động chuyển vào EmployeesHistory
-- Query dữ liệu lịch sử
-- Lấy dữ liệu tại một thời điểm cụ thể
SELECT * FROM Employees
FOR SYSTEM_TIME AS OF '2025-01-01 00:00:00';
-- Lấy tất cả versions trong khoảng thời gian
SELECT * FROM Employees
FOR SYSTEM_TIME BETWEEN '2025-01-01' AND '2026-01-01';
-- Lấy tất cả versions từng tồn tại trong khoảng
SELECT * FROM Employees
FOR SYSTEM_TIME FROM '2025-01-01' TO '2026-01-01';
-- Tất cả versions kể cả đã bị xóa
SELECT * FROM Employees
FOR SYSTEM_TIME ALL;
-- Xem history table
SELECT * FROM dbo.EmployeesHistory WHERE EmployeeId = 1 ORDER BY ValidFrom;
-- Tắt system versioning (để có thể DROP table)
ALTER TABLE Employees SET (SYSTEM_VERSIONING = OFF);
DROP TABLE dbo.EmployeesHistory;
DROP TABLE dbo.Employees;
8. Table Constraints vs Column Constraints
-- Column-level constraint (đặt ngay sau column definition)
CREATE TABLE Example (
Col1 INT NOT NULL PRIMARY KEY, -- Column constraint
Col2 INT REFERENCES OtherTable(Id), -- Inline FK
Col3 INT CHECK (Col3 > 0), -- Inline CHECK
Col4 INT DEFAULT 42 -- Inline DEFAULT
);
-- Table-level constraint (đặt sau tất cả columns)
CREATE TABLE Example (
Col1 INT NOT NULL,
Col2 INT,
Col3 INT,
Col4 INT,
CONSTRAINT PK_Example PRIMARY KEY (Col1), -- Named PK
CONSTRAINT FK_Example_OtherTable FOREIGN KEY (Col2) -- Named FK
REFERENCES OtherTable(Id),
CONSTRAINT CHK_Example_Col3 CHECK (Col3 > 0), -- Named CHECK
CONSTRAINT UQ_Example_Col3_Col4 UNIQUE (Col3, Col4) -- Composite UNIQUE
);
-- PHẢI dùng table-level constraint khi:
-- 1. Composite PRIMARY KEY
-- 2. Composite UNIQUE
-- 3. Composite FOREIGN KEY (hiếm)
-- 4. Muốn đặt tên cho constraint (khuyến nghị)
-- Xem tất cả constraints của table
SELECT
cc.name AS constraint_name,
cc.type_desc,
cc.definition
FROM sys.check_constraints cc
WHERE OBJECT_NAME(cc.parent_object_id) = 'Example'
UNION ALL
SELECT
dc.name,
'DEFAULT_CONSTRAINT',
dc.definition
FROM sys.default_constraints dc
WHERE OBJECT_NAME(dc.parent_object_id) = 'Example';
9. Cascading Actions
-- ON DELETE / ON UPDATE options:
-- NO ACTION - Mặc định. Báo lỗi nếu có FK references
-- CASCADE - Tự xóa/cập nhật rows con
-- SET NULL - Đặt FK = NULL khi parent bị xóa/cập nhật
-- SET DEFAULT - Đặt FK = DEFAULT value khi parent bị xóa/cập nhật
-- RESTRICT - Tương tự NO ACTION (ANSI standard, không dùng trong SQL Server)
CREATE TABLE Departments (
DepartmentId INT PRIMARY KEY,
Name NVARCHAR(100) NOT NULL
);
CREATE TABLE Employees (
EmployeeId INT PRIMARY KEY,
DepartmentId INT NOT NULL,
ManagerId INT NULL,
-- CASCADE: xóa Dept -> xóa tất cả employees của dept đó
CONSTRAINT FK_Employees_Departments FOREIGN KEY (DepartmentId)
REFERENCES Departments(DepartmentId)
ON DELETE CASCADE
ON UPDATE CASCADE,
-- SET NULL: xóa Manager -> ManagerId = NULL
CONSTRAINT FK_Employees_Manager FOREIGN KEY (ManagerId)
REFERENCES Employees(EmployeeId)
ON DELETE SET NULL
ON UPDATE NO ACTION
);
-- Lưu ý: SQL Server KHÔNG cho phép circular cascade
-- Và không cho phép multiple cascade paths đến cùng 1 table
-- Ví dụ: Orders -> Customers (ON DELETE CASCADE)
-- Orders -> Employees (ON DELETE CASCADE)
-- Nếu thêm Customers -> Employees (ON DELETE CASCADE) -> ERROR: multiple cascade paths
-- Ví dụ ON DELETE SET DEFAULT
CREATE TABLE Tickets (
TicketId INT PRIMARY KEY,
AssignedTo INT NOT NULL DEFAULT 0, -- 0 = Unassigned
CONSTRAINT FK_Tickets_Users FOREIGN KEY (AssignedTo)
REFERENCES Users(UserId)
ON DELETE SET DEFAULT -- Khi User bị xóa, ticket về trạng thái unassigned
ON UPDATE CASCADE
);
10. Indexes trong DDL
-- CLUSTERED INDEX: Sắp xếp vật lý dữ liệu (mỗi table chỉ có 1)
-- PRIMARY KEY mặc định là clustered
CREATE TABLE Orders (
OrderId INT NOT NULL,
OrderDate DATE NOT NULL,
CustomerId INT,
CONSTRAINT PK_Orders PRIMARY KEY CLUSTERED (OrderId)
);
-- Nếu muốn PK non-clustered và tạo clustered index trên column khác
CREATE TABLE Orders (
OrderId INT NOT NULL,
OrderDate DATE NOT NULL,
CustomerId INT,
CONSTRAINT PK_Orders PRIMARY KEY NONCLUSTERED (OrderId)
);
CREATE CLUSTERED INDEX CIX_Orders_OrderDate ON Orders (OrderDate);
-- Clustered trên OrderDate -> data pages sắp xếp theo OrderDate
-- NONCLUSTERED INDEX (mặc định khi tạo CREATE INDEX)
CREATE NONCLUSTERED INDEX IX_Orders_CustomerId
ON Orders (CustomerId)
INCLUDE (OrderDate, TotalAmount); -- INCLUDE: đưa thêm columns vào leaf node
-- UNIQUE INDEX
CREATE UNIQUE INDEX UX_Employees_Email ON Employees (Email);
-- Khác với UNIQUE CONSTRAINT: index có thể hỗ trợ INCLUDE columns
-- FILTERED INDEX - index chỉ trên subset rows
CREATE NONCLUSTERED INDEX IX_Employees_ActiveSalary
ON Employees (Salary)
WHERE IsActive = 1; -- Chỉ index active employees
-- COMPOSITE INDEX
CREATE INDEX IX_Orders_CustomerDate
ON Orders (CustomerId, OrderDate DESC)
INCLUDE (TotalAmount, Status);
-- Xem indexes của table
SELECT
i.name AS index_name,
i.type_desc,
i.is_unique,
STRING_AGG(c.name, ', ') WITHIN GROUP (ORDER BY ic.key_ordinal) AS key_columns
FROM sys.indexes i
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
WHERE i.object_id = OBJECT_ID('Orders')
AND ic.is_included_column = 0
GROUP BY i.name, i.type_desc, i.is_unique;
-- DROP INDEX
DROP INDEX IX_Orders_CustomerId ON Orders;