PostgreSQL

Cảm ơn bạn đã đăng ký khóa học!

Tài liệu về PostgreSQL vẫn đang được cập nhật.
Để tôn trọng công sức của tác giả, vui lòng không chia sẻ tài liệu.
Chúc bạn học tập hiệu quả!

Cài Đặt PostgreSQL

1. Cài Đặt PostgreSQL

Bạn có thể tải PostgreSQL từ trang chính thức:

PostgreSQL là một hệ quản trị cơ sở dữ liệu mã nguồn mở mạnh mẽ, hỗ trợ nhiều nền tảng như Windows, macOS, và Linux. Khi cài đặt, bạn sẽ được cài đặt cả psql (công cụ dòng lệnh chính) và thường kèm theo pgAdmin (giao diện đồ họa để quản lý cơ sở dữ liệu).

Các phiên bản

PostgreSQL không phân chia thành nhiều phiên bản thương mại như SQL Server, nhưng có các bản phát hành thường xuyên (ví dụ: 15, 16). Bạn nên chọn phiên bản mới nhất để tận dụng các tính năng và bản vá lỗi mới nhất.

2. Cài Đặt Công Cụ Quản Lý (pgAdmin)

pgAdmin là công cụ GUI phổ biến để quản lý PostgreSQL. Tải tại:

3. Kết Nối và Làm Việc Với PostgreSQL

  1. Sử dụng psql (dòng lệnh):

    • Mở terminal và chạy lệnh:
      psql -U postgres
      
    • Nhập mật khẩu mặc định của user postgres (được thiết lập lúc cài đặt).
    • Khi kết nối thành công, bạn sẽ thấy dấu nhắc lệnh: postgres=#.
  2. Thông tin kết nối:

    • Server name: localhost hoặc địa chỉ IP của máy chủ (mặc định port là 5432).
    • Authentication: PostgreSQL sử dụng cơ chế xác thực dựa trên pg_hba.conf (có thể là mật khẩu, trust, hoặc các phương thức khác).
  3. Sử dụng pgAdmin:

    • Mở pgAdmin, thêm một server mới, nhập thông tin kết nối (host, port, username, password) và nhấn Save.

Quản lý cơ sở dữ liệu (Database)

Tạo cơ sở dữ liệu (CREATE DATABASE)

CREATE DATABASE StudentManagementSystemTest;

Xóa cơ sở dữ liệu (DROP DATABASE)

DROP DATABASE StudentManagementSystemTest;

Lưu ý: PostgreSQL yêu cầu không có kết nối nào đến cơ sở dữ liệu trước khi xóa. Nếu cần, sử dụng:

DROP DATABASE StudentManagementSystemTest WITH (FORCE);

Sao lưu cơ sở dữ liệu

PostgreSQL không có câu lệnh BACKUP DATABASE như SQL Server. Thay vào đó, sử dụng công cụ dòng lệnh pg_dump:

pg_dump -U postgres -F c StudentManagementSystem > StudentManagementSystem.backup
  • -F c: Định dạng nén tùy chỉnh.
  • StudentManagementSystem: Tên cơ sở dữ liệu.

Khôi phục cơ sở dữ liệu

Sử dụng pg_restore:

pg_restore -U postgres -d StudentManagementSystem StudentManagementSystem.backup

Hoặc nếu dùng file SQL:

psql -U postgres -d StudentManagementSystem -f StudentManagementSystem.sql

Chọn cơ sở dữ liệu để làm việc

\connect StudentManagementSystem

Hoặc trong script:

SET search_path TO public;

Liệt kê cơ sở dữ liệu

SELECT datname FROM pg_database;

Quản lý bảng (Table)

Tạo bảng (CREATE TABLE)

CREATE TABLE Teachers (
    TeacherID SERIAL PRIMARY KEY,
    Name VARCHAR(50) NOT NULL
);
  • SERIAL: Tương đương với IDENTITY trong SQL Server, tự động tăng giá trị.

Xóa bảng (DROP TABLE)

DROP TABLE Teachers;

Thay đổi cấu trúc bảng (ALTER TABLE)

Thêm cột mới

ALTER TABLE Students ADD COLUMN Email VARCHAR(100);

Thay đổi kiểu dữ liệu của cột

ALTER TABLE Students ALTER COLUMN Email TYPE VARCHAR(150);

Đổi tên cột

ALTER TABLE Students RENAME COLUMN Email TO StudentEmail;

Xóa cột

ALTER TABLE Students DROP COLUMN StudentEmail;

Đổi tên bảng

ALTER TABLE Students RENAME TO Pupils;

Liệt kê bảng

SELECT table_name 
FROM information_schema.tables 
WHERE table_schema = 'public';

Kiểu dữ liệu (Data Types)

Kiểu số (Numeric Data Types)

Kiểu dữ liệuMô tảVí dụ
INTEGERSố nguyên 4 byte, phạm vi từ -2,147,483,648 đến 2,147,483,647.10, 100, -50
BIGINTSố nguyên lớn 8 byte, phạm vi từ -9,223,372,036,854,775,808 đến 9,223,372,036,854,775,807.9223372036854775807
SMALLINTSố nguyên nhỏ 2 byte, phạm vi từ -32,768 đến 32,767.32767
NUMERIC(p,s)Số thập phân có độ chính xác cao. p là tổng số chữ số, s là số chữ số sau dấu thập phân.NUMERIC(10,2)12345.67
REALSố thực 4 byte, độ chính xác thấp hơn DOUBLE PRECISION.3.14159
DOUBLE PRECISIONSố thực 8 byte, độ chính xác cao.3.14159265359
MONEYKiểu tiền tệ, chính xác đến 2 chữ số thập phân.$1,000.99
BOOLEANGiá trị logic: TRUE hoặc FALSE.TRUE, FALSE

Kiểu chuỗi (String Data Types)

Kiểu dữ liệuMô tảVí dụ
CHAR(n)Chuỗi ký tự cố định độ dài n.'ABCD ' (5 ký tự, gồm cả khoảng trắng)
VARCHAR(n)Chuỗi ký tự độ dài thay đổi, tối đa n.'John Doe'
TEXTChuỗi ký tự độ dài không giới hạn (tối đa 1GB).'Lorem ipsum dolor sit amet...'

Dùng CHAR(n) khi: Chuỗi có độ dài cố định (ví dụ: mã bưu chính).
Dùng VARCHAR(n) khi: Chuỗi có độ dài thay đổi (ví dụ: tên người).
Dùng TEXT khi: Chuỗi rất dài (ví dụ: bài viết, nội dung JSON).

Lưu ý: PostgreSQL không có NCHAR hay NVARCHAR như SQL Server vì tất cả chuỗi đều hỗ trợ Unicode mặc định.

Kiểu ngày và thời gian (Date & Time Data Types)

Kiểu dữ liệuMô tảVí dụ
DATEChỉ lưu ngày, phạm vi từ 4713 BC đến 5874897 AD.'2025-03-01'
TIMEChỉ lưu giờ, phút, giây.'12:30:45'
TIMESTAMPLưu ngày và giờ, không có múi giờ.'2025-03-01 12:30:45'
TIMESTAMPTZLưu ngày và giờ với múi giờ.'2025-03-01 12:30:45+07'
INTERVALKhoảng thời gian.'1 year 2 months 3 days'

Kiểu dữ liệu khác

Kiểu dữ liệuMô tảVí dụ
BYTEADữ liệu nhị phân (tương tự VARBINARY).\xDEADBEEF
UUIDĐịnh danh duy nhất toàn cục (GUID).'550e8400-e29b-41d4-a716-446655440000'
JSON / JSONBLưu trữ dữ liệu JSON (JSONB là định dạng nhị phân, hỗ trợ lập chỉ mục).'{"name": "John"}'

Ví dụ tạo bảng với kiểu dữ liệu phổ biến

CREATE TABLE Employees (
    EmployeeID UUID DEFAULT gen_random_uuid() PRIMARY KEY, -- ID duy nhất, tự động sinh
    Name VARCHAR(100) NOT NULL, -- Họ và tên
    Birthdate DATE, -- Ngày sinh
    WorkStartTime TIME, -- Giờ bắt đầu làm việc
    Salary NUMERIC(10,2), -- Lương
    ProfilePicture BYTEA, -- Ảnh đại diện
    IsActive BOOLEAN DEFAULT TRUE -- Trạng thái hoạt động
);

Biến (Variables)

Trong PostgreSQL, biến thường được sử dụng trong các khối mã PL/pgSQL (ngôn ngữ lập trình tích hợp trong PostgreSQL). Biến không được khai báo trực tiếp trong các truy vấn SQL thông thường mà phải nằm trong một khối DO, hàm, hoặc thủ tục.

Khai báo biến

Sử dụng từ khóa DECLARE trong khối PL/pgSQL:

DO $$
DECLARE
    student_name VARCHAR(50);
    birth_year INTEGER;
BEGIN
    -- Logic xử lý
END $$;

Gán giá trị cho biến

PostgreSQL sử dụng toán tử := để gán giá trị:

Gán giá trị trực tiếp

DO $$
DECLARE
    student_name VARCHAR(50);
BEGIN
    student_name := 'Hoàng Hiệp';
    RAISE NOTICE 'Student Name: %', student_name;
END $$;

Gán giá trị từ truy vấn

DO $$
DECLARE
    birth_year INTEGER;
BEGIN
    SELECT EXTRACT(YEAR FROM birthdate) INTO birth_year
    FROM Students 
    WHERE student_id = 1;
    RAISE NOTICE 'Birth Year: %', birth_year;
END $$;

Sử dụng biến

DO $$
DECLARE
    year INTEGER := 2000;
BEGIN
    FOR rec IN SELECT * FROM Students WHERE EXTRACT(YEAR FROM birthdate) = year LOOP
        RAISE NOTICE 'Student: %', rec.name;
    END LOOP;
END $$;

Biến bảng (Record/Row Type)

PostgreSQL không có "biến bảng" như SQL Server, nhưng bạn có thể sử dụng kiểu RECORD hoặc khai báo một cursor:

DO $$
DECLARE
    student_rec RECORD;
BEGIN
    FOR student_rec IN SELECT student_id, name, birthdate FROM Students LOOP
        RAISE NOTICE 'ID: %, Name: %, Birthdate: %', 
            student_rec.student_id, student_rec.name, student_rec.birthdate;
    END LOOP;
END $$;

Biến toàn cục

PostgreSQL không có biến toàn cục như @@VERSION trong SQL Server, nhưng cung cấp các hàm hệ thống:

  • version(): Lấy thông tin phiên bản PostgreSQL.
  • row_count(): Số dòng bị ảnh hưởng bởi truy vấn gần nhất (trong PL/pgSQL).
DO $$
BEGIN
    RAISE NOTICE 'PostgreSQL Version: %', version();
END $$;

Hàm (Functions)

PostgreSQL hỗ trợ tạo các hàm bằng PL/pgSQL hoặc các ngôn ngữ khác như Python, C.

Hàm xử lý chuỗi (String Functions)

HàmChức năngVí dụ
LENGTH(string)Trả về độ dài chuỗiLENGTH('Hello')5
LEFT(string, n)Lấy n ký tự bên tráiLEFT('PostgreSQL', 4)'Post'
RIGHT(string, n)Lấy n ký tự bên phảiRIGHT('PostgreSQL', 3)'SQL'
SUBSTRING(string FROM start FOR length)Cắt chuỗi conSUBSTRING('PostgreSQL' FROM 5 FOR 3)'gre'
UPPER(string)Chuyển thành chữ in hoaUPPER('sql')'SQL'
LOWER(string)Chuyển thành chữ thườngLOWER('SQL')'sql'
TRIM(string)Xóa khoảng trắng hai bênTRIM(' SQL ')'SQL'
REPLACE(string, old, new)Thay thế chuỗi conREPLACE('Hello World', 'World', 'SQL')'Hello SQL'
POSITION(substring IN string)Vị trí của chuỗi conPOSITION('SQL' IN 'Learn SQL')7
CONCAT(string1, string2, …)Nối chuỗiCONCAT('Hello', ' ', 'SQL')'Hello SQL'

Hàm xử lý số (Numeric Functions)

HàmChức năngVí dụ
ABS(number)Trị tuyệt đốiABS(-5)5
CEIL(number)Làm tròn lênCEIL(4.2)5
FLOOR(number)Làm tròn xuốngFLOOR(4.9)4
ROUND(number, decimals)Làm tròn sốROUND(4.567, 2)4.57
POWER(base, exponent)Lũy thừaPOWER(2, 3)8
SQRT(number)Căn bậc haiSQRT(9)3
RANDOM()Số ngẫu nhiên từ 0 đến 1RANDOM()0.684

Hàm xử lý ngày tháng (Date Functions)

HàmChức năngVí dụ
NOW()Ngày giờ hiện tại với múi giờNOW()'2025-03-15 10:15:30+07'
CURRENT_DATENgày hiện tạiCURRENT_DATE'2025-03-15'
EXTRACT(part FROM date)Trích xuất phần của ngàyEXTRACT(YEAR FROM '2025-03-15'::DATE)2025
AGE(date)Khoảng cách từ ngày đó đến hiện tạiAGE('2000-01-01'::DATE)'25 years 2 months'
DATE_TRUNC('part', date)Cắt ngắn đến phần cụ thểDATE_TRUNC('month', NOW())'2025-03-01 00:00:00+07'
TO_CHAR(date, format)Định dạng ngàyTO_CHAR(NOW(), 'DD/MM/YYYY')'15/03/2025'

Các phần trong EXTRACT

PhầnÝ nghĩaVí dụ
YEARNămEXTRACT(YEAR FROM '2025-03-15'::DATE)2025
MONTHThángEXTRACT(MONTH FROM '2025-03-15'::DATE)3
DAYNgàyEXTRACT(DAY FROM '2025-03-15'::DATE)15
HOURGiờEXTRACT(HOUR FROM NOW())10
WEEKTuần trong nămEXTRACT(WEEK FROM '2025-03-15'::DATE)11

Hàm chuyển đổi dữ liệu

HàmChức năngVí dụ
CAST(expression AS type)Chuyển kiểu dữ liệuCAST(123 AS TEXT)'123'
::Toán tử chuyển kiểu ngắn gọn'2025-03-15'::DATE'2025-03-15'
TO_CHAR(expression, format)Chuyển thành chuỗi có định dạngTO_CHAR(123.45, '999.99')'123.45'

Hàm xử lý giá trị NULL

HàmChức năngVí dụ
COALESCE(val1, val2, …)Trả về giá trị không NULL đầu tiênCOALESCE(NULL, 'SQL', 'Server')'SQL'
NULLIF(val1, val2)Trả về NULL nếu hai giá trị bằng nhauNULLIF(10, 10)NULL

Tạo hàm tùy chỉnh

CREATE FUNCTION get_student_count()
RETURNS INTEGER AS $$
BEGIN
    RETURN (SELECT COUNT(*) FROM Students);
END;
$$ LANGUAGE plpgsql;

-- Gọi hàm
SELECT get_student_count();

Ràng buộc dữ liệu

Tổng quan các ràng buộc

Ràng buộcChức năng
NOT NULLKhông cho phép giá trị NULL.
UNIQUEĐảm bảo giá trị trong cột là duy nhất.
PRIMARY KEYKết hợp NOT NULLUNIQUE.
FOREIGN KEYTạo mối quan hệ giữa hai bảng.
CHECKKiểm tra giá trị thỏa mãn điều kiện.
DEFAULTGán giá trị mặc định nếu không nhập.

NOT NULL

CREATE TABLE Teachers (
    TeacherID SERIAL PRIMARY KEY,
    Name VARCHAR(50) NOT NULL
);

UNIQUE

CREATE TABLE Doctors (
    DoctorID SERIAL PRIMARY KEY,
    Email VARCHAR(100) UNIQUE
);

PRIMARY KEY

CREATE TABLE Departments (
    DeptID SERIAL PRIMARY KEY,
    DeptName VARCHAR(50) NOT NULL
);

FOREIGN KEY

CREATE TABLE Enrollments (
    StudentID INTEGER,
    CourseID INTEGER,
    FOREIGN KEY (StudentID) REFERENCES Students(StudentID) 
        ON DELETE CASCADE 
        ON UPDATE CASCADE,
    FOREIGN KEY (CourseID) REFERENCES Courses(CourseID) 
        ON DELETE SET NULL
);

CHECK

CREATE TABLE Employees (
    EmployeeID SERIAL PRIMARY KEY,
    Age INTEGER CHECK (Age >= 18)
);

DEFAULT

CREATE TABLE Orders (
    OrderID SERIAL PRIMARY KEY,
    OrderDate TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Ví dụ bảng với đầy đủ ràng buộc

CREATE TABLE Departments (
    DeptID SERIAL PRIMARY KEY,
    DeptName VARCHAR(100) NOT NULL UNIQUE
);

CREATE TABLE Employees (
    EmployeeID SERIAL PRIMARY KEY,
    Name VARCHAR(100) NOT NULL,
    Email VARCHAR(100) UNIQUE,
    Birthdate DATE CHECK (Birthdate >= '1990-01-01'),
    Salary NUMERIC(10,2) DEFAULT 1000.00,
    DepartmentID INTEGER REFERENCES Departments(DeptID)
);

Thao tác dữ liệu

Thêm dữ liệu (INSERT INTO)

Thêm một sinh viên mới vào bảng Students:

INSERT INTO Students (name, birthdate) 
VALUES ('Hoàng Hiệp', '2001-05-20');

Cập nhật dữ liệu (UPDATE)

Cập nhật ngày sinh cho sinh viên có student_id = 4:

UPDATE Students 
SET birthdate = '2000-06-15' 
WHERE student_id = 4;

Xóa dữ liệu (DELETE)

Xóa sinh viên có student_id = 2:

DELETE FROM Students 
WHERE student_id = 2;

Lưu ý:

  • Nếu không có WHERE, toàn bộ dữ liệu trong bảng sẽ bị xóa.
  • DELETE có thể kích hoạt trigger nếu được định nghĩa.

Xóa toàn bộ dữ liệu (TRUNCATE)

Xóa toàn bộ dữ liệu trong bảng, nhanh hơn DELETE:

TRUNCATE TABLE Students;

Lưu ý:

  • TRUNCATE không ghi log từng dòng, không kích hoạt trigger.
  • Để reset giá trị SERIAL, dùng TRUNCATE TABLE Students RESTART IDENTITY.

Sao chép dữ liệu vào bảng mới (CREATE TABLE AS)

Tạo bảng StudentsBackup và sao chép dữ liệu từ Students:

CREATE TABLE StudentsBackup AS 
SELECT * FROM Students;

Chèn dữ liệu từ bảng khác (INSERT INTO SELECT)

Chèn dữ liệu từ Students vào StudentsBackup:

INSERT INTO StudentsBackup (name, birthdate)
SELECT name, birthdate 
FROM Students;

Hợp nhất dữ liệu (MERGE)

PostgreSQL không có lệnh MERGE như SQL Server, nhưng có thể dùng INSERT ... ON CONFLICT:

INSERT INTO StudentsBackup (student_id, name, birthdate)
SELECT student_id, name, birthdate 
FROM Students
ON CONFLICT (student_id) 
DO UPDATE SET 
    name = EXCLUDED.name, 
    birthdate = EXCLUDED.birthdate;

Kiểm tra tồn tại (EXISTS)

Kiểm tra xem sinh viên “Hoàng Hiệp” đã tồn tại chưa:

DO $$
BEGIN
    IF EXISTS (SELECT 1 FROM Students WHERE name = 'Hoàng Hiệp') THEN
        RAISE NOTICE 'Sinh viên đã tồn tại';
    ELSE
        INSERT INTO Students (name, birthdate) 
        VALUES ('Hoàng Hiệp', '2001-05-20');
    END IF;
END $$;

Lấy dữ liệu bị ảnh hưởng (RETURNING)

Lấy thông tin các sinh viên bị xóa:

DELETE FROM Students 
WHERE birthdate < '2000-01-01'
RETURNING student_id, name;

Toán tử và điều kiện lọc

Toán tử so sánh

Toán tửChức năng
=So sánh bằng
!= hoặc <>So sánh khác
>Lớn hơn
<Nhỏ hơn
>=Lớn hơn hoặc bằng
<=Nhỏ hơn hoặc bằng
BETWEENTrong khoảng
INTrong danh sách
LIKEKhớp mẫu ký tự
AND
ORHoặc
NOTPhủ định

AND

Lấy sinh viên có tên bắt đầu bằng 'P' và sinh sau 2000:

SELECT * FROM Students
WHERE name LIKE 'P%' AND birthdate > '2000-01-01';

OR

Lấy sinh viên có tên là “Trần Nhật Anh” hoặc “Vũ Khánh Linh”:

SELECT * FROM Students
WHERE name = 'Trần Nhật Anh' OR name = 'Vũ Khánh Linh';

NOT

Lấy tất cả sinh viên trừ “Hoàng Hiệp”:

SELECT * FROM Students
WHERE name != 'Hoàng Hiệp';

LIKE

Tìm sinh viên có tên bắt đầu bằng “T”:

SELECT * FROM Students
WHERE name LIKE 'T%';

Wildcards

  • %: Đại diện cho bất kỳ chuỗi ký tự nào.
  • _: Đại diện cho một ký tự duy nhất.
SELECT * FROM Students
WHERE name LIKE '%ng%'; -- Tên chứa “ng”

IN

Lấy sinh viên có tên trong danh sách:

SELECT * FROM Students
WHERE name IN ('Vũ Khánh Linh', 'Trần Nhật Anh', 'Lê Thu Hà');

BETWEEN

Lấy sinh viên sinh từ 2000 đến 2003:

SELECT * FROM Students
WHERE birthdate BETWEEN '2000-01-01' AND '2003-12-31';

IS NULL và IS NOT NULL

Lấy sinh viên chưa có ngày sinh:

SELECT * FROM Students 
WHERE birthdate IS NULL;

ANY và ALL

Lấy sinh viên sinh muộn hơn ít nhất một sinh viên trong danh sách:

SELECT * FROM Students
WHERE birthdate > ANY (
    SELECT birthdate 
    FROM Students 
    WHERE name IN ('Vũ Khánh Linh', 'Trần Nhật Anh')
);

EXISTS

Lấy sinh viên đã đăng ký khóa học:

SELECT name 
FROM Students s
WHERE EXISTS (
    SELECT 1 
    FROM Enrollments e 
    WHERE s.student_id = e.student_id
);

IF ELSE (trong PL/pgSQL)

DO $$
DECLARE
    total_students INTEGER;
BEGIN
    SELECT COUNT(*) INTO total_students FROM Students;
    IF total_students > 100 THEN
        RAISE NOTICE 'Có hơn 100 sinh viên.';
    ELSE
        RAISE NOTICE 'Số lượng sinh viên ít hơn hoặc bằng 100.';
    END IF;
END $$;

CASE WHEN

Xác định nhóm tuổi của sinh viên:

SELECT 
    name,
    CASE 
        WHEN EXTRACT(YEAR FROM AGE(birthdate)) < 18 THEN 'Teen'
        WHEN EXTRACT(YEAR FROM AGE(birthdate)) BETWEEN 18 AND 24 THEN 'Young Adult'
        ELSE 'Adult'
    END AS age_group
FROM Students;

Truy vấn dữ liệu

SELECT

Lấy toàn bộ dữ liệu từ Students:

SELECT * FROM Students;

SELECT DISTINCT

Loại bỏ trùng lặp:

SELECT DISTINCT name 
FROM Students;

WHERE

Lọc sinh viên sinh sau 2001:

SELECT * FROM Students 
WHERE birthdate > '2001-01-01';

ORDER BY

Sắp xếp theo tên tăng dần:

SELECT * FROM Students 
ORDER BY name ASC;

LIMIT và OFFSET

Lấy 3 sinh viên trẻ nhất:

SELECT * FROM Students 
ORDER BY birthdate DESC 
LIMIT 3;

Kết hợp bảng

INNER JOIN

Lấy sinh viên đã đăng ký khóa học:

SELECT s.student_id, s.name AS student_name, c.course_id, c.name AS course_name
FROM Students s
INNER JOIN Enrollments e ON s.student_id = e.student_id
INNER JOIN Courses c ON e.course_id = c.course_id;

LEFT JOIN

Lấy tất cả sinh viên, kể cả chưa đăng ký:

SELECT s.student_id, s.name AS student_name, c.course_id, c.name AS course_name
FROM Students s
LEFT JOIN Enrollments e ON s.student_id = e.student_id
LEFT JOIN Courses c ON e.course_id = c.course_id;

RIGHT JOIN

Lấy tất cả khóa học, kể cả chưa có sinh viên:

SELECT s.student_id, s.name AS student_name, c.course_id, c.name AS course_name
FROM Students s
RIGHT JOIN Enrollments e ON s.student_id = e.student_id
RIGHT JOIN Courses c ON e.course_id = c.course_id;

FULL JOIN

Lấy tất cả dữ liệu từ cả hai bảng:

SELECT s.student_id, s.name AS student_name, c.course_id, c.name AS course_name
FROM Students s
FULL JOIN Enrollments e ON s.student_id = e.student_id
FULL JOIN Courses c ON e.course_id = c.course_id;

CROSS JOIN

Kết hợp tất cả hàng:

SELECT s.name AS student_name, c.name AS course_name
FROM Students s
CROSS JOIN Courses c;

SELF JOIN

Nối bảng với chính nó:

ALTER TABLE Students 
ADD COLUMN mentor_id INTEGER REFERENCES Students(student_id);

SELECT s1.student_id, s1.name AS student_name, 
       s1.mentor_id, s2.name AS mentor_name
FROM Students s1
LEFT JOIN Students s2 ON s1.mentor_id = s2.student_id;

UNION

Kết hợp dữ liệu, loại bỏ trùng lặp:

SELECT name FROM Students
UNION
SELECT name FROM Courses;

UNION ALL

Kết hợp dữ liệu, giữ trùng lặp:

SELECT name FROM Students
UNION ALL
SELECT name FROM Courses;

EXCEPT

Lấy bản ghi có trong tập đầu nhưng không trong tập sau:

SELECT name FROM Students
EXCEPT
SELECT name FROM Courses;

INTERSECT

Lấy bản ghi xuất hiện trong cả hai tập:

SELECT name FROM Students
INTERSECT
SELECT name FROM Courses;

Hàm tổng hợp

COUNT

Đếm số lượng bản ghi:

SELECT COUNT(*) AS total_students 
FROM Students;

SUM

Tính tổng giá trị:

SELECT SUM(fee) AS total_fees 
FROM Enrollments;

AVG

Tính giá trị trung bình:

SELECT AVG(fee) AS average_fee 
FROM Enrollments;

MIN và MAX

Tìm giá trị nhỏ nhất và lớn nhất:

SELECT MIN(birthdate) AS oldest, MAX(birthdate) AS youngest 
FROM Students;

GROUP BY

Nhóm dữ liệu:

SELECT c.course_id, c.name, SUM(e.fee) AS total_fee
FROM Courses c
LEFT JOIN Enrollments e ON c.course_id = e.course_id
GROUP BY c.course_id, c.name;

HAVING

Lọc nhóm dữ liệu:

SELECT c.course_id, c.name, SUM(e.fee) AS total_fee
FROM Courses c
LEFT JOIN Enrollments e ON c.course_id = e.course_id
GROUP BY c.course_id, c.name
HAVING SUM(e.fee) >= 20;

COUNT DISTINCT

Đếm số lượng giá trị không trùng lặp:

SELECT c.course_id, c.name, COUNT(DISTINCT e.student_id) AS enrolled_students
FROM Courses c
LEFT JOIN Enrollments e ON c.course_id = e.course_id
GROUP BY c.course_id, c.name;

STRING_AGG

Ghép các giá trị thành chuỗi:

SELECT class_id, STRING_AGG(name, ', ') AS student_list
FROM Students
GROUP BY class_id;

STDDEV và VARIANCE

Tính độ lệch chuẩn và phương sai:

SELECT STDDEV(score) AS standard_deviation, VARIANCE(score) AS variance 
FROM Scores;

Index

Index trong PostgreSQL giúp tăng tốc truy vấn bằng cách tạo chỉ mục trên một hoặc nhiều cột.

Chỉ mục trong PostgreSQL

  • B-tree: Chỉ mục mặc định, phù hợp với hầu hết các truy vấn.
  • GIN: Dùng cho dữ liệu phức tạp như mảng, JSON.
  • GiST: Dùng cho dữ liệu không gian (spatial data).

Tạo chỉ mục

CREATE TABLE Employees (
    employee_id SERIAL PRIMARY KEY, -- Tự động tạo B-tree index
    name VARCHAR(100) NOT NULL,
    email VARCHAR(100)```sql
CREATE INDEX idx_employee_name ON Employees (name);

Chỉ mục duy nhất

CREATE UNIQUE INDEX idx_employee_email ON Employees (email);

Xóa chỉ mục

DROP INDEX idx_employee_name;

So sánh

Tiêu chíB-tree Index
Sắp xếp dữ liệu?❌ Không (chỉ là chỉ mục)
Số lượng?✅ Nhiều
Tốc độ truy vấn?⚡ Nhanh
Khi dữ liệu thay đổi?❌ Có thể chậm do cập nhật index

Lưu ý:

  • Index tăng tốc WHERE, ORDER BY, JOIN.
  • Làm chậm INSERT, UPDATE, DELETE do phải cập nhật index.

SQL nâng cao

Subquery (Truy vấn con)

Single-row Subquery

Lấy sinh viên có ngày sinh sớm nhất:

SELECT * FROM Students
WHERE birthdate = (SELECT MIN(birthdate) FROM Students);

Multiple-row Subquery

Lấy sinh viên cùng năm sinh với “Trần Nhật Anh” hoặc “Phạm Thanh Hương”:

SELECT * FROM Students
WHERE EXTRACT(YEAR FROM birthdate) IN (
    SELECT EXTRACT(YEAR FROM birthdate) 
    FROM Students 
    WHERE name IN ('Trần Nhật Anh', 'Phạm Thanh Hương')
);

Correlated Subquery

Lấy sinh viên có ngày sinh sớm nhất trong mỗi khóa học:

SELECT c.course_id, c.name AS course_name, s.student_id, s.birthdate
FROM Courses c
LEFT JOIN Enrollments e ON c.course_id = e.course_id
LEFT JOIN Students s ON e.student_id = s.student_id
WHERE s.birthdate = (
    SELECT MIN(s2.birthdate)
    FROM Students s2
    JOIN Enrollments e2 ON s2.student_id = e2.student_id
    WHERE e2.course_id = c.course_id
) OR s.student_id IS NULL;

Derived Table

Sinh viên dưới 23 tuổi:

SELECT *
FROM (
    SELECT *, EXTRACT(YEAR FROM AGE(birthdate)) AS age
    FROM Students
) AS young_students
WHERE age < 23;

Cursor

Cursor trong PostgreSQL cho phép xử lý từng dòng dữ liệu trong một tập kết quả lớn, thay vì tải toàn bộ dữ liệu vào bộ nhớ. Cursor thường được dùng trong khối PL/pgSQL.

Tạo và sử dụng Cursor

DO $$
DECLARE
    student_cursor CURSOR FOR 
        SELECT student_id, name, birthdate 
        FROM Students 
        WHERE birthdate > '2000-01-01';
    student_rec RECORD;
BEGIN
    OPEN student_cursor;
    LOOP
        FETCH student_cursor INTO student_rec;
        EXIT WHEN NOT FOUND;
        RAISE NOTICE 'ID: %, Name: %, Birthdate: %', 
            student_rec.student_id, student_rec.name, student_rec.birthdate;
    END LOOP;
    CLOSE student_cursor;
END $$;

Cursor với tham số

DO $$
DECLARE
    student_cursor CURSOR (min_year INTEGER) FOR 
        SELECT student_id, name, birthdate 
        FROM Students 
        WHERE EXTRACT(YEAR FROM birthdate) >= min_year;
    student_rec RECORD;
BEGIN
    OPEN student_cursor(2000);
    LOOP
        FETCH student_cursor INTO student_rec;
        EXIT WHEN NOT FOUND;
        RAISE NOTICE 'ID: %, Name: %, Birthdate: %', 
            student_rec.student_id, student_rec.name, student_rec.birthdate;
    END LOOP;
    CLOSE student_cursor;
END $$;

REF CURSOR

PostgreSQL không có REF CURSOR như SQL Server, nhưng bạn có thể trả về một cursor từ hàm:

CREATE FUNCTION get_students_after_year(min_year INTEGER)
RETURNS refcursor AS $$
DECLARE
    cur refcursor := 'student_cursor';
BEGIN
    OPEN cur FOR 
        SELECT student_id, name, birthdate 
        FROM Students 
        WHERE EXTRACT(YEAR FROM birthdate) >= min_year;
    RETURN cur;
END;
$$ LANGUAGE plpgsql;

-- Sử dụng hàm
DO $$
DECLARE
    student_rec RECORD;
    cur refcursor;
BEGIN
    cur := get_students_after_year(2000);
    LOOP
        FETCH cur INTO student_rec;
        EXIT WHEN NOT FOUND;
        RAISE NOTICE 'ID: %, Name: %, Birthdate: %', 
            student_rec.student_id, student_rec.name, student_rec.birthdate;
    END LOOP;
    CLOSE cur;
END $$;

Tip

Tối ưu hóa truy vấn

  1. Sử dụng Index: Tạo chỉ mục cho các cột thường xuyên xuất hiện trong WHERE, JOIN, hoặc ORDER BY.
  2. EXPLAIN: Phân tích kế hoạch truy vấn:
    EXPLAIN SELECT * FROM Students WHERE birthdate > '2000-01-01';
    
  3. **Tránh SELECT *** : Chỉ lấy các cột cần thiết:
    SELECT student_id, name FROM Students;
    
  4. Sử dụng CTE hoặc Subquery hợp lý: Giảm lặp lại logic phức tạp.

Quản lý dữ liệu

  • Sao lưu thường xuyên: Dùng pg_dump để sao lưu định kỳ.
  • Kiểm tra ràng buộc: Đảm bảo các khóa ngoại (FOREIGN KEY) được thiết lập để duy trì tính toàn vẹn dữ liệu.
  • Sử dụng TRANSACTION: Đảm bảo an toàn khi thao tác dữ liệu:
    BEGIN;
    UPDATE Students SET birthdate = '2001-01-01' WHERE student_id = 1;
    COMMIT;
    

Viết code sạch

  • Đặt tên bảng, cột rõ ràng (ví dụ: Students, student_id).
  • Sử dụng comment trong hàm PL/pgSQL:
    -- Tính số sinh viên theo năm sinh
    CREATE FUNCTION count_students_by_year(birth_year INTEGER)
    RETURNS INTEGER AS $$
    BEGIN
        RETURN (SELECT COUNT(*) FROM Students WHERE EXTRACT(YEAR FROM birthdate) = birth_year);
    END;
    $$ LANGUAGE plpgsql;
    

Bài tập thực hành

Hệ thống quản lý sinh viên tại trường đại học EZSE

Cơ sở dữ liệu dưới đây được thiết kế để quản lý thông tin về sinh viên, giảng viên, ngành học, môn học, và các lớp học tại trường đại học EZSE. Hệ thống hỗ trợ các chức năng như đăng ký môn học, theo dõi điểm số, và quản lý các lớp học.

Câu lệnh SQL tạo Database và dữ liệu (PostgreSQL)

-- Không có lệnh USE trong PostgreSQL, thay vào đó sử dụng \connect hoặc tạo database trước
DROP DATABASE IF EXISTS EZSEStudentManagementSystem;

CREATE DATABASE EZSEStudentManagementSystem;

\connect EZSEStudentManagementSystem

-- Xóa các bảng nếu đã tồn tại, kèm theo CASCADE để xóa các ràng buộc khóa ngoại
DO $$
DECLARE
    r RECORD;
BEGIN
    FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP
        EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
    END LOOP;
END $$;

---------------------------- Create table ----------------------------------

-- Bảng Ngành học (Majors)
CREATE TABLE Majors (
    MajorID SERIAL PRIMARY KEY,
    MajorName VARCHAR(100) NOT NULL
);

-- Bảng Sinh viên (Students)
CREATE TABLE Students (
    StudentID SERIAL PRIMARY KEY,
    FirstName VARCHAR(50) NOT NULL,
    LastName VARCHAR(50) NOT NULL,
    DateOfBirth DATE NOT NULL,
    Gender VARCHAR(15) CHECK (Gender IN ('Male', 'Female', 'Other')),
    Email VARCHAR(100) UNIQUE NOT NULL,
    Phone VARCHAR(15) UNIQUE,
    Address VARCHAR(255),
    EnrollmentDate DATE NOT NULL,
    MajorID INTEGER REFERENCES Majors(MajorID)
);

-- Bảng Giảng viên (Teachers)
CREATE TABLE Teachers (
    TeacherID SERIAL PRIMARY KEY,
    FirstName VARCHAR(50) NOT NULL,
    LastName VARCHAR(50) NOT NULL,
    Email VARCHAR(100) UNIQUE NOT NULL,
    Phone VARCHAR(15) UNIQUE,
    Address VARCHAR(255)
);

-- Bảng Môn học (Courses)
CREATE TABLE Courses (
    CourseID SERIAL PRIMARY KEY,
    CourseName VARCHAR(100) NOT NULL,
    Credits INTEGER CHECK (Credits > 0)
);

-- Bảng Môn học theo ngành (MajorCourses)
CREATE TABLE MajorCourses (
    MajorID INTEGER REFERENCES Majors(MajorID),
    CourseID INTEGER REFERENCES Courses(CourseID),
    PRIMARY KEY (MajorID, CourseID)
);

-- Bảng Lớp học (Classes)
CREATE TABLE Classes (
    ClassID SERIAL PRIMARY KEY,
    CourseID INTEGER REFERENCES Courses(CourseID),
    TeacherID INTEGER REFERENCES Teachers(TeacherID),
    Semester VARCHAR(20) NOT NULL,
    Year INTEGER NOT NULL
);

-- Bảng Đăng ký môn học (Enrollments)
CREATE TABLE Enrollments (
    StudentID INTEGER REFERENCES Students(StudentID),
    ClassID INTEGER REFERENCES Classes(ClassID),
    Attempt INTEGER NOT NULL DEFAULT 1,
    Grade DECIMAL(4,2) CHECK (Grade >= 0 AND Grade <= 10),
    PRIMARY KEY (StudentID, ClassID, Attempt)
);

-- Chèn dữ liệu vào bảng Majors
INSERT INTO Majors (MajorName) VALUES
('Computer Science'),
('Software Engineering'),
('Information Security');

-- Chèn dữ liệu vào bảng Courses
INSERT INTO Courses (CourseName, Credits) VALUES
('Data Structures & Algorithms', 3),
('Object-Oriented Programming', 3),
('Database Systems', 3),
('Computer Networks', 3),
('Web Development', 4),
('Agile Software Development', 3),
('Cyber Security', 3),
('Mobile Application Development', 3);

-- Chèn dữ liệu vào bảng Students
INSERT INTO Students (FirstName, LastName, DateOfBirth, Gender, Email, Phone, Address, EnrollmentDate, MajorID) VALUES
('Phạm', 'Gia Bảo', '2002-03-10', 'Male', '[email protected]', '0912345678', '12 Trần Phú, Hà Nội', '2021-09-01', 1),
('Nguyễn', 'Thảo Linh', '2003-07-22', 'Female', '[email protected]', '0987654321', '34 Lê Lợi, TP.HCM', '2021-09-01', 2),
('Trần', 'Quang Vinh', '2001-05-15', 'Male', '[email protected]', '0933123456', '56 Nguyễn Trãi, Đà Nẵng', '2020-09-01', 3),
('Lê', 'Minh Nhật', '2002-09-25', 'Male', '[email protected]', '0977890123', '78 Hai Bà Trưng, Hải Phòng', '2021-09-01', 1),
('Vũ', 'Hồng Ngọc', '2003-01-30', 'Female', '[email protected]', '0966543210', '90 Quang Trung, Cần Thơ', '2022-09-01', 2),
('Đỗ', 'Thanh Huyền', '2004-02-14', 'Female', '[email protected]', '0913456789', '15 Hùng Vương, Huế', '2022-09-01', 3),
('Hoàng', 'Đức Anh', '2003-11-05', 'Male', '[email protected]', '0934567890', '27 Nguyễn Huệ, Quy Nhơn', '2021-09-01', 1),
('Ngô', 'Khánh Duy', '2002-08-19', 'Male', '[email protected]', '0945678901', '39 Lê Đại Hành, Vinh', '2020-09-01', 2),
('Bùi', 'Thục Đoan', '2004-06-25', 'Female', '[email protected]', '0956789012', '51 Phạm Ngũ Lão, Nha Trang', '2023-09-01', 3),
('Phan', 'Tiến Đạt', '2003-04-12', 'Male', '[email protected]', '0967890123', '63 Nguyễn Văn Cừ, Đà Lạt', '2022-09-01', 1),
('Đặng', 'Mỹ Linh', '2004-09-08', 'Female', '[email protected]', '0978901234', '75 Trần Hưng Đạo, Hà Nội', '2023-09-01', 2),
('Võ', 'Hoàng Phúc', '2002-12-30', 'Male', '[email protected]', '0989012345', '87 Bạch Đằng, TP.HCM', '2020-09-01', 3),
('Trương', 'Ngọc Ánh', '2003-03-17', 'Female', '[email protected]', '0990123456', '99 Nguyễn Thị Minh Khai, Đà Nẵng', '2021-09-01', 1),
('Lý', 'Quốc Hưng', '2004-07-21', 'Male', '[email protected]', '0901234567', '111 Lê Lai, Hải Phòng', '2023-09-01', 2),
('Nguyễn', 'Văn Hùng', '2003-05-10', 'Male', '[email protected]', '0912345689', '123 Nguyễn Huệ, Hà Nội', '2022-09-01', 1),
('Trần', 'Thị Mai', '2004-08-15', 'Female', '[email protected]', '0987654332', '45 Lê Lợi, TP.HCM', '2023-09-01', 1),
('Lê', 'Quang Minh', '2002-11-20', 'Male', '[email protected]', '0933123467', '67 Trần Phú, Đà Nẵng', '2021-09-01', 1),
('Phạm', 'Hồng Phúc', '2003-02-25', 'Male', '[email protected]', '0977890134', '89 Hai Bà Trưng, Hải Phòng', '2022-09-01', 1),
('Vũ', 'Thị Lan', '2004-04-12', 'Female', '[email protected]', '0966543221', '101 Quang Trung, Cần Thơ', '2023-09-01', 2),
('Đỗ', 'Minh Tuấn', '2003-09-30', 'Male', '[email protected]', '0913456790', '23 Hùng Vương, Huế', '2022-09-01', 2),
('Hoàng', 'Ngọc Ánh', '2002-07-18', 'Female', '[email protected]', '0934567901', '34 Nguyễn Huệ, Quy Nhơn', '2021-09-01', 2),
('Ngô', 'Văn Long', '2003-12-05', 'Male', '[email protected]', '0945678912', '56 Lê Đại Hành, Vinh', '2022-09-01', 3),
('Bùi', 'Thị Hương', '2004-03-22', 'Female', '[email protected]', '0956789023', '78 Phạm Ngũ Lão, Nha Trang', '2023-09-01', 3),
('Phan', 'Quốc Anh', '2002-06-15', 'Male', '[email protected]', '0967890134', '90 Nguyễn Văn Cừ, Đà Lạt', '2021-09-01', 3);

-- Chèn dữ liệu vào bảng Teachers
INSERT INTO Teachers (FirstName, LastName, Email, Phone, Address) VALUES
('Hoàng', 'Văn Hiệp', '[email protected]', '0944123456', '101 Lý Thường Kiệt, Hà Nội'),
('Lê', 'Thu Hà', '[email protected]', '0933221122', '202 Phạm Văn Đồng, TP.HCM'),
('Hoàng', 'Việt Thắng', '[email protected]', '0922334455', '303 Trường Chinh, Đà Nẵng'),
('Nguyễn', 'Minh Tuấn', '[email protected]', '0911445566', '404 Nguyễn Văn Linh, Hải Phòng'),
('Phạm', 'Quốc Khánh', '[email protected]', '0902345678', '505 Điện Biên Phủ, Huế'),
('Vũ', 'Thị Duyên', '[email protected]', '0913567890', '606 Nguyễn Đình Chiểu, TP.HCM'),
('Trần', 'Đình Phong', '[email protected]', '0924678901', '707 Trần Phú, Hà Nội'),
('Đỗ', 'Ngọc Mai', '[email protected]', '0935789012', '808 Lê Hồng Phong, Đà Nẵng'),
('Bùi', 'Gia Bảo', '[email protected]', '0946890123', '909 Nguyễn Trãi, Hải Phòng'),
('Ngô', 'Thị Lan Anh', '[email protected]', '0957901234', '1010 Phạm Hùng, Cần Thơ');

-- Chèn dữ liệu vào bảng MajorCourses
INSERT INTO MajorCourses (MajorID, CourseID) VALUES
(1, 1), (1, 3), (1, 4), (1, 6),
(2, 2), (2, 5), (2, 6),
(3, 4), (3, 7), (3, 8);

-- Chèn dữ liệu vào bảng Classes
INSERT INTO Classes (CourseID, TeacherID, Semester, Year) VALUES
(1, 1, 'Spring', 2023),
(2, 2, 'Spring', 2023),
(3, 1, 'Summer', 2023),
(4, 3, 'Fall', 2023),
(5, 2, 'Spring', 2023),
(6, 4, 'Summer', 2023),
(7, 3, 'Fall', 2023),
(8, 5, 'Spring', 2023),
(1, 1, 'Spring', 2024),
(2, 2, 'Fall', 2024),
(3, 1, 'Spring', 2024),
(4, 3, 'Fall', 2024),
(5, 2, 'Summer', 2024),
(6, 4, 'Spring', 2024),
(7, 3, 'Fall', 2024),
(8, 5, 'Summer', 2024),
(1, 1, 'Fall', 2024),
(3, 6, 'Fall', 2024),
(1, 1, 'Spring', 2025),
(2, 2, 'Spring', 2025),
(3, 1, 'Summer', 2025),
(4, 3, 'Fall', 2025),
(5, 2, 'Spring', 2025),
(6, 4, 'Summer', 2025),
(7, 3, 'Fall', 2025),
(8, 5, 'Spring', 2025),
(2, 2, 'Fall', 2025),
(4, 3, 'Spring', 2025);

-- Chèn dữ liệu vào bảng Enrollments
INSERT INTO Enrollments (StudentID, ClassID, Attempt, Grade) VALUES
(1, 1, 1, 8.5),
(1, 9, 1, 7.2),
(1, 3, 1, 6.8),
(1, 4, 1, 5.5),
(1, 12, 2, 7.5),
(2, 2, 1, 8.0),
(2, 5, 1, 9.0),
(2, 6, 1, 6.0),
(2, 14, 1, 4.8),
(3, 4, 1, 7.5),
(3, 7, 1, 8.8),
(3, 8, 1, 9.2),
(4, 1, 1, 5.0),
(4, 9, 2, 7.2),
(4, 5, 1, 8.5),
(4, 6, 1, 9.1),
(5, 2, 1, 6.8),
(5, 3, 1, 5.5),
(5, 11, 2, 7.5),
(5, 8, 1, 8.3),
(6, 12, 1, 8.0),
(6, 15, 1, 7.8),
(6, 16, 1, 9.0),
(7, 9, 1, 7.5),
(7, 11, 1, 8.2),
(7, 12, 1, 6.5),
(8, 2, 1, 8.8),
(8, 5, 1, 7.0),
(8, 6, 1, 6.2),
(9, 20, 1, 8.5),
(9, 23, 1, 9.0),
(9, 16, 1, 7.8),
(10, 9, 1, 6.5),
(10, 11, 1, 7.0),
(10, 14, 1, 8.5),
(15, 10, 1, 7.8),
(19, 13, 1, 8.2),
(16, 17, 1, 6.5),
(20, 19, 1, 7.5),
(22, 24, 1, 8.5),
(21, 25, 1, 6.8),
(23, 26, 1, 7.2),
(24, 27, 1, 8.8);

Các bảng trong Database

1. Bảng Majors (Ngành học)

Bảng này lưu thông tin về các ngành học tại trường.

CREATE TABLE Majors (
    MajorID SERIAL PRIMARY KEY,
    MajorName VARCHAR(100) NOT NULL
);
  • MajorID: Mã ngành học, khóa chính, tự động tăng.
  • MajorName: Tên ngành học.
2. Bảng Students (Sinh viên)

Bảng này lưu trữ thông tin về sinh viên trong hệ thống.

CREATE TABLE Students (
    StudentID SERIAL PRIMARY KEY,
    FirstName VARCHAR(50) NOT NULL,
    LastName VARCHAR(50) NOT NULL,
    DateOfBirth DATE NOT NULL,
    Gender VARCHAR(15) CHECK (Gender IN ('Male', 'Female', 'Other')),
    Email VARCHAR(100) UNIQUE NOT NULL,
    Phone VARCHAR(15) UNIQUE,
    Address VARCHAR(255),
    EnrollmentDate DATE NOT NULL,
    MajorID INTEGER REFERENCES Majors(MajorID)
);
  • StudentID: Mã sinh viên, khóa chính, tự động tăng.
  • FirstName, LastName: Tên và họ của sinh viên.
  • DateOfBirth: Ngày sinh.
  • Gender: Giới tính (Male, Female, Other).
  • Email: Email của sinh viên.
  • Phone: Số điện thoại.
  • Address: Địa chỉ của sinh viên.
  • EnrollmentDate: Ngày nhập học.
  • MajorID: Mã ngành học của sinh viên, tham chiếu đến bảng Majors.
3. Bảng Teachers (Giảng viên)

Bảng này lưu trữ thông tin về các giảng viên.

CREATE TABLE Teachers (
    TeacherID SERIAL PRIMARY KEY,
    FirstName VARCHAR(50) NOT NULL,
    LastName VARCHAR(50) NOT NULL,
    Email VARCHAR(100) UNIQUE NOT NULL,
    Phone VARCHAR(15) UNIQUE,
    Address VARCHAR(255)
);
  • TeacherID: Mã giảng viên, khóa chính, tự động tăng.
  • FirstName, LastName: Tên và họ của giảng viên.
  • Email: Email của giảng viên.
  • Phone: Số điện thoại.
  • Address: Địa chỉ của giảng viên.
4. Bảng Courses (Môn học)

Bảng này lưu thông tin về các môn học.

CREATE TABLE Courses (
    CourseID SERIAL PRIMARY KEY,
    CourseName VARCHAR(100) NOT NULL,
    Credits INTEGER CHECK (Credits > 0)
);
  • CourseID: Mã môn học, khóa chính, tự động tăng.
  • CourseName: Tên môn học.
  • Credits: Số tín chỉ của môn học.
5. Bảng MajorCourses (Môn học theo ngành)

Bảng này liên kết các ngành học với các môn học cố định mà sinh viên phải học.

CREATE TABLE MajorCourses (
    MajorID INTEGER REFERENCES Majors(MajorID),
    CourseID INTEGER REFERENCES Courses(CourseID),
    PRIMARY KEY (MajorID, CourseID)
);
  • MajorID: Mã ngành học.
  • CourseID: Mã môn học.
  • Mối quan hệ giữa ngành học và môn học là một-nhiều.
6. Bảng Classes (Lớp học)

Bảng này lưu thông tin về các lớp học, bao gồm thông tin môn học, giảng viên, và học kỳ.

CREATE TABLE Classes (
    ClassID SERIAL PRIMARY KEY,
    CourseID INTEGER REFERENCES Courses(CourseID),
    TeacherID INTEGER REFERENCES Teachers(TeacherID),
    Semester VARCHAR(20) NOT NULL,
    Year INTEGER NOT NULL
);
  • ClassID: Mã lớp học, khóa chính, tự động tăng.
  • CourseID: Mã môn học, tham chiếu đến bảng Courses.
  • TeacherID: Mã giảng viên, tham chiếu đến bảng Teachers.
  • Semester: Học kỳ.
  • Year: Năm học.
7. Bảng Enrollments (Đăng ký môn học)

Bảng này lưu trữ thông tin về việc sinh viên đăng ký học các lớp, bao gồm thông tin về lần đăng ký (Attempt) và điểm số của sinh viên.

CREATE TABLE Enrollments (
    StudentID INTEGER REFERENCES Students(StudentID),
    ClassID INTEGER REFERENCES Classes(ClassID),
    Attempt INTEGER NOT NULL DEFAULT 1,
    Grade DECIMAL(4,2) CHECK (Grade >= 0 AND Grade <= 10),
    PRIMARY KEY (StudentID, ClassID, Attempt)
);
  • StudentID: Mã sinh viên, tham chiếu đến bảng Students.
  • ClassID: Mã lớp học, tham chiếu đến bảng Classes.
  • Attempt: Số lần sinh viên đăng ký học môn này (ví dụ, lần đầu hoặc học lại, cải thiện điểm).
  • Grade: Điểm số của sinh viên trong lớp học.

Đề thực hành (Practical Exam)

Câu hỏi 1

Viết truy vấn để hiển thị tất cả các giảng viên.

Question 1


Câu hỏi 2

Viết truy vấn để hiển thị tất cả các sinh viên thuộc các ngành có MajorID = 2 hoặc MajorID = 3.

Question 2


Câu hỏi 3

Viết truy vấn để lấy thông tin các lớp học mà giảng viên có họ là "Hoàng" và tên là "Văn Hiệp" phụ trách.

Hiển thị các cột như phía dưới.

Question 3


Câu hỏi 4

Viết truy vấn để lấy danh sách sinh viên đã đăng ký ít nhất một trong các lớp thuộc các môn học "Data Structures & Algorithms", "Database Systems", hoặc "Web Development".

Hiển thị các cột như phía dưới, không hiển thị kết quả trùng nhau.
Kết quả sắp xếp theo CourseID tăng dần, sau đó theo StudentID tăng dần.

Question 4


Câu hỏi 5

Viết truy vấn để hiển thị số lượng sinh viên đã đăng ký vào các khóa học trong năm 2025.

Hiển thị các cột như phía dưới.
Kết quả sắp xếp theo NumberOfEnrollments giảm dần, sau đó theo CourseID tăng dần.

Question 5


Câu hỏi 6

Viết truy vấn để tìm các giảng viên có số lượng lớp dạy nhiều nhất trong từng năm.

Hiển thị các cột như phía dưới.
Kết quả sắp xếp theo Year tăng dần, sau đó theo TeacherID tăng dần.

Question 6


Câu hỏi 7

Viết truy vấn để đếm số sinh viên duy nhất đã đăng ký môn học theo từng năm của ngành "Computer Science""Software Engineering".

Hiển thị các cột như phía dưới.

Question 7


Câu hỏi 8

Tạo function GetNumberOfStudentsByCourseID để tính số lượng sinh viên duy nhất đã đăng ký một môn học cụ thể theo CourseID.

Sau khi viết xong, hãy kiểm tra lại function bằng câu lệnh SQL dưới.

SELECT CourseID, CourseName, GetNumberOfStudentsByCourseID(CourseID) AS NumberOfStudents
FROM Courses
WHERE CourseID IN (1, 2, 3, 4); -- Kiểm tra với một số CourseID cụ thể

Question 8


Câu hỏi 9

Tạo trigger để xử lý dữ liệu khi thêm sinh viên mới vào bảng Students có tên trg_InsteadOfInsertStudent đảm bảo hai điều kiện:

  1. Nếu Email không chứa ký tự "@", tự động thêm @ezse.net vào cuối.
  2. Nếu MajorID bị NULL, tự động đặt MajorID thành ID của ngành "Software Engineering".

Sau khi viết xong, hãy kiểm tra lại trigger bằng câu SQL lệnh dưới.

-- Trường hợp thiếu @ -> Tự động thêm @ezse.net, MajorID có giá trị nên giữ nguyên
INSERT INTO Students (FirstName, LastName, DateOfBirth, Gender, Email, Phone, Address, EnrollmentDate, MajorID)
VALUES ('Nguyễn', 'Văn A', '2002-05-10', 'Male', 'nguyenvana', '0734567890', 'Hà Nội', '2024-04-02', 3);

-- Trường hợp thiếu MajorID -> Tự động gán MajorID của ngành Software Engineering
INSERT INTO Students (FirstName, LastName, DateOfBirth, Gender, Email, Phone, Address, EnrollmentDate, MajorID)
VALUES ('Trần', 'Thị B', '2001-08-20', 'Female', '[email protected]', '0678123456', 'HCM', '2024-04-02', NULL);

-- Kiểm tra kết quả
SELECT StudentID, FirstName, LastName, Email, MajorID FROM Students;

-- Chú ý: Vì Phone là Unique, khi test nhiều lần, bạn hãy thay đổi số điện thoại.

Question 9


Câu hỏi 10

Xóa sinh viên có StudentID = 3StudentID = 5 khỏi bảng Students, đồng thời xóa các hàng liên quan trong bảng Enrollments.


Đáp án đề thực hành (PostgreSQL)

Câu hỏi 1

Viết truy vấn để hiển thị tất cả các giảng viên.

Question 1

Đáp án:

SELECT *
FROM Teachers;

Câu hỏi 2

Viết truy vấn để hiển thị tất cả các sinh viên thuộc các ngành có MajorID = 2 hoặc MajorID = 3.

Question 2

Đáp án:

SELECT *
FROM Students
WHERE MajorID IN (2, 3);

Câu hỏi 3

Viết truy vấn để lấy thông tin các lớp học mà giảng viên có họ là "Hoàng" và tên là "Văn Hiệp" phụ trách.

Hiển thị các cột như phía dưới.

Question 3

Đáp án:

SELECT 
    c.ClassID, 
    c.CourseID, 
    co.CourseName, 
    c.Semester, 
    c.Year, 
    t.TeacherID, 
    t.FirstName, 
    t.LastName
FROM Classes c
JOIN Teachers t ON c.TeacherID = t.TeacherID
JOIN Courses co ON c.CourseID = co.CourseID
WHERE t.FirstName = 'Hoàng' AND t.LastName = 'Văn Hiệp';

Câu hỏi 4

Viết truy vấn để lấy danh sách sinh viên đã đăng ký ít nhất một trong các lớp thuộc các môn học "Data Structures & Algorithms", "Database Systems", hoặc "Web Development".

Hiển thị các cột như phía dưới, không hiển thị kết quả trùng nhau.
Kết quả sắp xếp theo CourseID tăng dần, sau đó theo StudentID tăng dần.

Question 4

Đáp án:

SELECT DISTINCT 
    co.CourseID, 
    co.CourseName, 
    s.StudentID, 
    CONCAT(s.FirstName, ' ', s.LastName) AS StudentFullName, 
    s.DateOfBirth
FROM Students s
JOIN Enrollments e ON s.StudentID = e.StudentID
JOIN Classes c ON e.ClassID = c.ClassID
JOIN Courses co ON c.CourseID = co.CourseID
WHERE co.CourseName IN ('Data Structures & Algorithms', 'Database Systems', 'Web Development')
ORDER BY co.CourseID, s.StudentID;

Câu hỏi 5

Viết truy vấn để hiển thị số lượng sinh viên đã đăng ký vào các khóa học trong năm 2025.

Hiển thị các cột như phía dưới.
Kết quả sắp xếp theo NumberOfEnrollments giảm dần, sau đó theo CourseID tăng dần.

Question 5

Đáp án:

SELECT 
    c.CourseID, 
    c.CourseName, 
    COUNT(e.StudentID) AS NumberOfEnrollments
FROM Courses c
LEFT JOIN Classes cl ON c.CourseID = cl.CourseID AND cl.Year = 2025
LEFT JOIN Enrollments e ON cl.ClassID = e.ClassID
GROUP BY c.CourseID, c.CourseName
ORDER BY NumberOfEnrollments DESC, c.CourseID ASC;

Câu hỏi 6

Viết truy vấn để tìm các giảng viên có số lượng lớp dạy nhiều nhất trong từng năm.

Hiển thị các cột như phía dưới.
Kết quả sắp xếp theo Year tăng dần, sau đó theo TeacherID tăng dần.

Question 6

Đáp án:

WITH TeacherClasses AS (
    SELECT 
        c.Year,
        t.TeacherID,
        CONCAT(t.FirstName, ' ', t.LastName) AS TeacherFullName,
        COUNT(c.ClassID) AS NumberOfClasses
    FROM Teachers t
    LEFT JOIN Classes c ON t.TeacherID = c.TeacherID
    GROUP BY c.Year, t.TeacherID, t.FirstName, t.LastName
),
MaxClasses AS (
    SELECT 
        Year,
        MAX(NumberOfClasses) AS MaxClasses
    FROM TeacherClasses
    GROUP BY Year
)
SELECT 
    tc.Year,
    tc.TeacherID,
    tc.TeacherFullName,
    tc.NumberOfClasses
FROM TeacherClasses tc
JOIN MaxClasses mc ON tc.Year = mc.Year AND tc.NumberOfClasses = mc.MaxClasses
ORDER BY tc.Year, tc.TeacherID;

Câu hỏi 7

Viết truy vấn để đếm số sinh viên duy nhất đã đăng ký môn học theo từng năm của ngành "Computer Science""Software Engineering".

Hiển thị các cột như phía dưới.

Question 7

Đáp án:

WITH StudentEnrollments AS (
    SELECT 
        m.MajorID,
        m.MajorName,
        c.Year,
        COUNT(DISTINCT e.StudentID) AS TotalStudents
    FROM Enrollments e
    JOIN Classes c ON e.ClassID = c.ClassID
    JOIN Students s ON e.StudentID = s.StudentID
    JOIN Majors m ON s.MajorID = m.MajorID
    WHERE m.MajorName IN ('Computer Science', 'Software Engineering')
    GROUP BY m.MajorID, m.MajorName, c.Year
),
MajorsCTE AS (
    SELECT MajorID, MajorName 
    FROM Majors 
    WHERE MajorName IN ('Computer Science', 'Software Engineering')
),
Years AS (
    SELECT DISTINCT Year FROM Classes
),
AllCombinations AS (
    SELECT y.Year, m.MajorID, m.MajorName
    FROM MajorsCTE m CROSS JOIN Years y
)
SELECT 
    ac.Year,
    ac.MajorID,
    ac.MajorName,
    COALESCE(se.TotalStudents, 0) AS TotalStudents
FROM AllCombinations ac
LEFT JOIN StudentEnrollments se 
    ON ac.MajorID = se.MajorID 
    AND ac.Year = se.Year
ORDER BY ac.Year, ac.MajorID;

Câu hỏi 8

Tạo function GetNumberOfStudentsByCourseID để tính số lượng sinh viên duy nhất đã đăng ký một môn học cụ thể theo CourseID.

Sau khi viết xong, hãy kiểm tra lại function bằng câu lệnh SQL dưới.

SELECT CourseID, CourseName, GetNumberOfStudentsByCourseID(CourseID) AS NumberOfStudents
FROM Courses
WHERE CourseID IN (1, 2, 3, 4); -- Kiểm tra với một số CourseID cụ thể

Question 8

Đáp án:

CREATE FUNCTION GetNumberOfStudentsByCourseID(p_course_id INTEGER)
RETURNS INTEGER AS $$
BEGIN
    RETURN (
        SELECT COUNT(DISTINCT e.StudentID)
        FROM Enrollments e
        JOIN Classes c ON e.ClassID = c.ClassID
        WHERE c.CourseID = p_course_id
    );
END;
$$ LANGUAGE plpgsql;

-- Kiểm tra function
SELECT CourseID, CourseName, GetNumberOfStudentsByCourseID(CourseID) AS NumberOfStudents
FROM Courses
WHERE CourseID IN (1, 2, 3, 4);

Chú thích:

  • PostgreSQL không dùng @ cho tham số như SQL Server, thay vào đó là tên tham số trực tiếp (p_course_id).

Câu hỏi 9

Tạo trigger để xử lý dữ liệu khi thêm sinh viên mới vào bảng Students có tên trg_InsteadOfInsertStudent đảm bảo hai điều kiện:

  1. Nếu Email không chứa ký tự "@", tự động thêm @ezse.net vào cuối.
  2. Nếu MajorID bị NULL, tự động đặt MajorID thành ID của ngành "Software Engineering".

Sau khi viết xong, hãy kiểm tra lại trigger bằng câu SQL lệnh dưới.

-- Trường hợp thiếu @ -> Tự động thêm @ezse.net, MajorID có giá trị nên giữ nguyên
INSERT INTO Students (FirstName, LastName, DateOfBirth, Gender, Email, Phone, Address, EnrollmentDate, MajorID)
VALUES ('Nguyễn', 'Văn A', '2002-05-10', 'Male', 'nguyenvana', '0734567890', 'Hà Nội', '2024-04-02', 3);

-- Trường hợp thiếu MajorID -> Tự động gán MajorID của ngành Software Engineering
INSERT INTO Students (FirstName, LastName, DateOfBirth, Gender, Email, Phone, Address, EnrollmentDate, MajorID)
VALUES ('Trần', 'Thị B', '2001-08-20', 'Female', '[email protected]', '0678123456', 'HCM', '2024-04-02', NULL);

-- Kiểm tra kết quả
SELECT StudentID, FirstName, LastName, Email, MajorID FROM Students;

-- Chú ý: Vì Phone là Unique, khi test nhiều lần, bạn hãy thay đổi số điện thoại.

Question 9

Đáp án:

CREATE OR REPLACE FUNCTION trg_InsteadOfInsertStudent()
RETURNS TRIGGER AS $$
DECLARE
    v_software_engineering_id INTEGER;
BEGIN
    -- Lấy MajorID của ngành "Software Engineering"
    SELECT MajorID INTO v_software_engineering_id 
    FROM Majors 
    WHERE MajorName = 'Software Engineering';

    -- Xử lý Email và MajorID trước khi chèn
    INSERT INTO Students (FirstName, LastName, DateOfBirth, Gender, Email, Phone, Address, EnrollmentDate, MajorID)
    VALUES (
        NEW.FirstName,
        NEW.LastName,
        NEW.DateOfBirth,
        NEW.Gender,
        CASE 
            WHEN NEW.Email NOT LIKE '%@%' THEN NEW.Email || '@ezse.net'
            ELSE NEW.Email 
        END,
        NEW.Phone,
        NEW.Address,
        NEW.EnrollmentDate,
        COALESCE(NEW.MajorID, v_software_engineering_id)
    );

    RETURN NULL; -- Với trigger INSTEAD OF, không cần tiếp tục chèn bản gốc
END;
$$ LANGUAGE plpgsql;

-- Tạo trigger
DROP TRIGGER IF EXISTS trg_InsteadOfInsertStudent ON Students;
CREATE TRIGGER trg_InsteadOfInsertStudent
BEFORE INSERT ON Students
FOR EACH ROW
EXECUTE FUNCTION trg_InsteadOfInsertStudent();

-- Kiểm tra trigger
INSERT INTO Students (FirstName, LastName, DateOfBirth, Gender, Email, Phone, Address, EnrollmentDate, MajorID)
VALUES ('Nguyễn', 'Văn A', '2002-05-10', 'Male', 'nguyenvana', '0734567890', 'Hà Nội', '2024-04-02', 3);

INSERT INTO Students (FirstName, LastName, DateOfBirth, Gender, Email, Phone, Address, EnrollmentDate, MajorID)
VALUES ('Trần', 'Thị B', '2001-08-20', 'Female', '[email protected]', '0678123456', 'HCM', '2024-04-02', NULL);

SELECT StudentID, FirstName, LastName, Email, MajorID FROM Students;

Chú thích:

  • PostgreSQL không có trigger INSTEAD OF trực tiếp trên bảng cơ sở dữ liệu, chỉ hỗ trợ trên view. Ở đây, tôi sử dụng trigger BEFORE INSERT để xử lý trước khi chèn.
  • || là toán tử nối chuỗi trong PostgreSQL thay vì CONCAT như SQL Server trong trường hợp này.
  • COALESCE thay cho ISNULL.

Câu hỏi 10

Xóa sinh viên có StudentID = 3StudentID = 5 khỏi bảng Students, đồng thời xóa các hàng liên quan trong bảng Enrollments.

Đáp án:

-- Xóa dữ liệu liên quan trong bảng Enrollments trước
DELETE FROM Enrollments
WHERE StudentID IN (3, 5);

-- Xóa sinh viên trong bảng Students
DELETE FROM Students
WHERE StudentID IN (3, 5);

Giải thích:

  1. Xóa dữ liệu liên quan trong Enrollments trước

    • Bảng Enrollments có khóa ngoại StudentID tham chiếu đến Students.
    • Nếu không có ON DELETE CASCADE, ta phải xóa các bản ghi liên quan trong Enrollments trước.
  2. Xóa dữ liệu chính trong Students

    • Sau khi xóa các bản ghi trong Enrollments, ta có thể xóa sinh viên có StudentID = 3StudentID = 5 mà không vi phạm ràng buộc.

Trường hợp có ON DELETE CASCADE trên Enrollments.StudentID
Nếu khi tạo bảng Enrollments, ta đã thiết lập:

StudentID INTEGER REFERENCES Students(StudentID) ON DELETE CASCADE

thì chỉ cần chạy:

DELETE FROM Students WHERE StudentID IN (3, 5);

Các bản ghi trong Enrollments sẽ tự động bị xóa.