Traffic Count

T-SQL: Ngôn ngữ kỳ diệu của thế giới dữ liệu

1. Tại Sao T-SQL Là Ngôn Ngữ Kỳ Diệu?

Hãy tưởng tượng bạn đang đứng trước một kho lưu trữ khổng lồ chứa hàng triệu, thậm chí hàng tỷ bản ghi dữ liệu. Câu hỏi đặt ra là: làm thế nào để bạn tìm được chính xác những gì mình cần, trong vòng vài mili-giây, mà không phải lật từng trang một như đọc sách? Câu trả lời chính là T-SQL - Transact-SQL, ngôn ngữ truy vấn mạnh mẽ được Microsoft xây dựng cho SQL Server.

T-SQL không chỉ là một ngôn ngữ lập trình đơn thuần. Nó là cầu nối giữa tư duy con người và kho dữ liệu khổng lồ của doanh nghiệp. Với T-SQL, bạn có thể:

  • Tìm kiếm và lọc hàng triệu bản ghi trong tích tắc
  •  Phân tích xu hướng kinh doanh với các hàm cửa sổ tinh vi
  •  Kết hợp dữ liệu từ nhiều bảng theo vô số cách sáng tạo
  •  Tối ưu hiệu suất để truy vấn chạy nhanh gấp hàng nghìn lần
  •  Khám phá các cấu trúc phân cấp phức tạp như cây gia phả hay mạng lưới giao thông
  •  Thao tác dữ liệu với độ chính xác và an toàn tuyệt đối

Cuốn sách T-SQL Querying của Itzik Ben-Gan và các đồng tác giả là một trong những tài liệu chuyên sâu và toàn diện nhất về T-SQL từng được xuất bản. Với hơn 800 trang kiến thức đúc kết từ nhiều năm kinh nghiệm thực chiến, cuốn sách này đã trở thành "kinh thánh" của mọi DBA và nhà phát triển SQL Server trên thế giới.

Điều thú vị về T-SQL

 T-SQL được xây dựng dựa trên nền tảng toán học của Đại Số Quan Hệ (Relational Algebra) -  một lĩnh vực được Edgar F. Codd phát minh vào năm 1970. Điều này có nghĩa là mỗi câu lệnh T-SQL bạn viết thực sự là một bài toán toán học thanh lịch đang được giải quyết!

 Triết Lý Của Ngôn Ngữ Quan Hệ

 Điểm khác biệt cốt lõi của T-SQL so với các ngôn ngữ lập trình thông thường nằm ở tư duy. Trong Python hay Java, bạn nghĩ theo kiểu "từng bước một" (procedural): làm bước này, rồi bước kia. Nhưng với T-SQL, bạn phải học cách tư duy theo "tập hợp" (set-based).

 Thay vì nói "hãy duyệt từng bản ghi và kiểm tra điều kiện", bạn nói "hãy cho tôi tập hợp tất cả các bản ghi thỏa mãn điều kiện". Sự thay đổi tư duy này tưởng nhỏ nhưng lại mang lại sự khác biệt khổng lồ về hiệu suất - đôi khi lên đến hàng nghìn lần nhanh hơn!

2. Xử Lý Truy Vấn Logic - Bí Mật Đằng Sau Mỗi Câu Lệnh

Đây là một trong những điều thú vị nhất về T-SQL mà phần lớn lập trình viên chưa từng biết: thứ tự bạn VIẾT câu lệnh SQL không phải là thứ tự SQL thực sự THỰC THI nó. Nghe có vẻ điên rồ? Nhưng đây chính là điều tạo nên sức mạnh của ngôn ngữ này!

2.1 Thứ Tự Logic - Bài Học Đầu Tiên

Khi bạn viết một câu SELECT, bạn thường viết theo thứ tự: SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY. Nhưng SQL Server thực sự xử lý chúng theo thứ tự hoàn toàn khác:

 -- Thứ tự BẠN VIẾT:

 SELECT   custid, SUM(qty) AS total_qty

 FROM     Sales.Orders

 WHERE    orderdate >= '20150101'

 GROUP BY custid

 HAVING   SUM(qty) > 100

 ORDER BY total_qty DESC;

 

 -- Thứ tự SQL SERVER THỰC THI:

 -- (1) FROM     → Xác định nguồn dữ liệu

 -- (2) WHERE    → Lọc bản ghi

 -- (3) GROUP BY → Nhóm dữ liệu

 -- (4) HAVING   → Lọc nhóm

 -- (5) SELECT   → Chọn cột

 -- (6) ORDER BY → Sắp xếp

Tại sao điều này lại quan trọng? Vì nó giải thích TẠI SAO bạn không thể dùng alias từ SELECT trong mệnh đề WHERE. Đơn giản là vì khi WHERE được thực thi, SELECT chưa chạy nên alias chưa tồn tại!

2.2 Ba Giá Trị Logic: TRUE, FALSE và UNKNOWN

Hầu hết các ngôn ngữ lập trình chỉ có hai giá trị logic: TRUE và FALSE. Nhưng T-SQL có ba giá trị - bao gồm cả UNKNOWN. Giá trị bí ẩn này xuất hiện khi bạn so sánh với NULL (giá trị thiếu hoặc chưa biết).

 Cẩn thận với NULL!

 NULL không bằng NULL. Thậm chí NULL cũng không khác NULL. Câu lệnh 'WHERE column = NULL' luôn trả về UNKNOWN (không phải TRUE), vì vậy không bao giờ lọc được bản ghi nào! Thay vào đó, hãy dùng 'WHERE column IS NULL'.

 -- SAI: Không bao giờ hoạt động đúng!

 SELECT * FROM Employees WHERE ManagerID = NULL;

 -- ĐÚNG: Dùng IS NULL

 SELECT * FROM Employees WHERE ManagerID IS NULL;

 -- ĐÚNG: Dùng IS NOT NULL

 SELECT * FROM Employees WHERE ManagerID IS NOT NULL;

 2.3 Sức Mạnh Của Tư Duy Tập Hợp

Đây là ví dụ minh họa sự khác biệt giữa tư duy lặp (iterative) và tư duy tập hợp (set-based). Giả sử bạn cần tăng lương 10% cho tất cả nhân viên có mức lương dưới 5 triệu:

 -- Tư duy lặp (chậm, kém hiệu quả):

 DECLARE @empid INT;

 DECLARE cur CURSOR FOR SELECT empid FROM Employees WHERE salary < 5000000;

 OPEN cur;

 FETCH NEXT FROM cur INTO @empid;

 WHILE @@FETCH_STATUS = 0

 BEGIN

  UPDATE Employees SET salary = salary * 1.1 WHERE empid = @empid;

  FETCH NEXT FROM cur INTO @empid;

 END;

 CLOSE cur; DEALLOCATE cur;

 -- Tư duy tập hợp (nhanh, thanh lịch, hiệu quả):

 UPDATE Employees SET salary = salary * 1.1

 WHERE salary < 5000000;

Câu lệnh tư duy tập hợp không chỉ ngắn hơn 10 lần mà còn có thể nhanh hơn hàng trăm lần trên dữ liệu lớn, vì SQL Server có thể tối ưu và xử lý song song toàn bộ tập hợp cùng một lúc!

3. Query Tuning - Nghệ Thuật Biến Truy Vấn Chậm Thành Nhanh Như Chớp

Query Tuning (Tối ưu hóa truy vấn) là kỹ năng phân biệt một DBA bình thường với một DBA xuất sắc. Đây là nơi khoa học gặp nghệ thuật - nơi bạn có thể biến một truy vấn chạy 30 giây thành một truy vấn chạy chưa đến 1 giây!

 3.1 Kiến Trúc Bên Trong SQL Server

 Để tối ưu truy vấn hiệu quả, trước tiên bạn cần hiểu cách SQL Server lưu trữ dữ liệu. Dữ liệu được tổ chức theo trang (pages) 8KB, và các trang được nhóm thành các extent 64KB.

Cấu trúc

Kích thước

Chức năng

Page (Trang)

8 KB

Đơn vị lưu trữ cơ bản của SQL Server

Extent (Khối)

64 KB = 8 trang

Đơn vị cấp phát bộ nhớ

Heap (Đống)

Không giới hạn

Bảng không có clustered index

B-Tree

Phụ thuộc dữ liệu

Cấu trúc dạng cây cho clustered index

Clustered Index

Toàn bộ bảng

Sắp xếp vật lý dữ liệu trên đĩa

Nonclustered Index

Một phần

Index phụ trỏ đến dữ liệu chính

 

3.2 Index - Vũ Khí Bí Mật Của Query Tuning

Nếu T-SQL là một cuộc đua tốc độ, thì Index (chỉ mục) chính là động cơ phản lực. Một index được thiết kế đúng có thể làm giảm số lần đọc đĩa từ hàng triệu xuống còn vài chục - tức là cải thiện hiệu suất hàng nghìn lần!

 -- Tạo một Clustered Index (chỉ mục chính):

 CREATE CLUSTERED INDEX idx_orders_date ON Sales.Orders(orderdate);

 -- Tạo Nonclustered Index với Include columns (covering index):

 CREATE NONCLUSTERED INDEX idx_orders_cust

  ON Sales.Orders(custid, orderdate DESC, orderid DESC)

  INCLUDE(empid, qty);

 -- Index lọc (Filtered Index) - chỉ index dữ liệu bạn cần:

 CREATE NONCLUSTERED INDEX idx_active_orders

  ON Sales.Orders(orderdate, custid)

  WHERE status = 'Active';

3.3 Columnstore Index - Cách Mạng Trong Phân Tích Dữ Liệu

Năm 2012, Microsoft giới thiệu Columnstore Index - một cuộc cách mạng thực sự trong thế giới cơ sở dữ liệu. Thay vì lưu trữ dữ liệu theo hàng (row-based), Columnstore lưu theo cột. Điều này giúp nén dữ liệu hiệu quả hơn và tăng tốc độ truy vấn phân tích lên đến 100 lần!

 Columnstore Index - Sức Mạnh Thật Sự

 Trong thử nghiệm thực tế, các truy vấn phân tích (analytics queries) chạy trên bảng có 100 triệu bản ghi đã giảm từ 45 giây xuống còn 0.3 giây sau khi thêm Columnstore Index. Đây không phải phép màu - đây là toán học và kỹ thuật tuyệt vời!

 -- Tạo Columnstore Index cho bảng phân tích lớn:

 CREATE COLUMNSTORE INDEX idx_col_sales

  ON Sales.OrderDetails(productid, qty, unitprice, orderdate);

 -- Clustered Columnstore (SQL Server 2014+):

 CREATE CLUSTERED COLUMNSTORE INDEX idx_ccol

  ON Warehouse.FactSales;

3.4 Execution Plan - X-quang Cho Truy Vấn

Execution Plan (kế hoạch thực thi) là bản đồ chi tiết cho thấy SQL Server sẽ làm gì để thực thi truy vấn của bạn. Đây là công cụ không thể thiếu khi bạn cần tối ưu hiệu suất.

 -- Bật thống kê I/O và thời gian:

 SET STATISTICS IO ON;

 SET STATISTICS TIME ON;

 -- Chạy truy vấn - kết quả sẽ hiển thị số lần đọc trang

 SELECT custid, COUNT(*) AS num_orders

 FROM Sales.Orders

 WHERE orderdate >= '20150101'

 GROUP BY custid;

 -- Tắt thống kê

 SET STATISTICS IO OFF;

 SET STATISTICS TIME OFF;

4. Multi-table Queries - Vũ Điệu Của Nhiều Bảng

Một trong những sức mạnh lớn nhất của T-SQL là khả năng kết hợp dữ liệu từ nhiều bảng khác nhau. Đây là nơi T-SQL thực sự tỏa sáng - và cũng là nơi nhiều lập trình viên cảm thấy bối rối nhất lúc đầu.

4.1 JOIN - Nghệ Thuật Kết Hợp Bảng

JOIN là công cụ để kết hợp dữ liệu từ hai hay nhiều bảng dựa trên một điều kiện chung. Có nhiều loại JOIN, mỗi loại có ý nghĩa toán học riêng biệt:

 -- INNER JOIN: Chỉ lấy bản ghi khớp ở CẢ HAI bảng

 SELECT c.companyname, o.orderid, o.orderdate

 FROM Sales.Customers AS c INNER JOIN Sales.Orders AS o

  ON c.custid = o.custid;

 -- LEFT OUTER JOIN: Lấy TẤT CẢ từ bảng trái + khớp từ bảng phải

 SELECT c.companyname, o.orderid

 FROM Sales.Customers AS c LEFT JOIN Sales.Orders AS o

  ON c.custid = o.custid;

 -- (Khách hàng chưa có đơn hàng sẽ có o.orderid = NULL)

 

 -- CROSS JOIN: Tích Descartes - mọi cặp có thể

 SELECT e1.firstname, e2.firstname AS colleague

 FROM HR.Employees AS e1 CROSS JOIN HR.Employees AS e2

 WHERE e1.empid <> e2.empid;

4.2 Subquery - Truy Vấn Lồng Nhau

Subquery là truy vấn bên trong truy vấn - như các hộp bí ẩn lồng vào nhau. Chúng cho phép bạn giải quyết những bài toán phức tạp một cách thanh lịch:

 -- Tìm đơn hàng của những khách hàng ở cùng quốc gia với khách hàng 1:

 SELECT orderid, custid, orderdate

 FROM Sales.Orders

 WHERE custid IN (

 SELECT custid FROM Sales.Customers

 WHERE country = (

  SELECT country FROM Sales.Customers

  WHERE custid = 1

  )

 );

 -- Tìm đơn hàng có giá trị lớn hơn trung bình tất cả đơn hàng:

 SELECT orderid, custid, freight

 FROM Sales.Orders

 WHERE freight > (SELECT AVG(freight) FROM Sales.Orders);

4.3 CTE - Common Table Expression: Người Bạn Tốt Nhất

CTE (Common Table Expression) là một trong những tính năng yêu thích của mọi T-SQL developer. Nó cho phép bạn đặt tên cho một kết quả tạm thời và sử dụng nó trong truy vấn chính, làm code dễ đọc và dễ bảo trì hơn rất nhiều.

 -- CTE cơ bản - đặt tên cho tập dữ liệu phức tạp:

 WITH OrderStats AS (

  SELECT

  custid,

  COUNT(*) AS num_orders,

  SUM(freight) AS total_freight,

  AVG(freight) AS avg_freight

  FROM Sales.Orders

 GROUP BY custid

 )

 SELECT c.companyname, os.num_orders, os.total_freight

 FROM Sales.Customers AS c

  JOIN OrderStats AS os ON c.custid = os.custid

 WHERE os.num_orders > 10

 ORDER BY os.total_freight DESC;

4.4 APPLY - Vũ Khí Bí Mật

APPLY là một trong những tính năng đặc biệt của T-SQL mà không có trong SQL chuẩn. Nó cho phép bạn gọi một hàm bảng (table-valued function) cho mỗi hàng trong bảng bên trái - một khả năng cực kỳ mạnh mẽ!

 -- CROSS APPLY: Tìm 3 đơn hàng gần nhất cho mỗi khách hàng

 SELECT c.custid, c.companyname, a.orderid, a.orderdate

 FROM Sales.Customers AS c

  CROSS APPLY (

  SELECT TOP (3) orderid, orderdate

  FROM Sales.Orders AS o

  WHERE o.custid = c.custid

  ORDER BY orderdate DESC, orderid DESC

  ) AS a;

 

 -- OUTER APPLY: Bao gồm cả khách hàng chưa có đơn hàng

 SELECT c.custid, c.companyname, a.orderid

 FROM Sales.Customers AS c

  OUTER APPLY (

  SELECT TOP (1) orderid, orderdate

  FROM Sales.Orders AS o

  WHERE o.custid = c.custid

  ORDER BY orderdate DESC

  ) AS a;

5. Window Functions - Ma Thuật Phân Tích Dữ Liệu

Nếu bạn chưa biết Window Functions, bạn đang bỏ lỡ một trong những tính năng tuyệt vời nhất của T-SQL hiện đại. Được giới thiệu trong SQL Server 2005 và mở rộng mạnh mẽ trong SQL Server 2012, Window Functions cho phép bạn thực hiện các phép tính phức tạp trên "cửa sổ" dữ liệu mà không cần GROUP BY!

 Window Function là gì?

 Window Function hoạt động trên một "cửa sổ" (window) các hàng liên quan đến hàng hiện tại. Khác với GROUP BY (làm thu gọn dữ liệu), Window Function giữ nguyên tất cả các hàng và thêm kết quả tính toán vào mỗi hàng. Đây là sự kết hợp hoàn hảo giữa linh hoạt và sức mạnh!

 5.1 Aggregate Window Functions

 -- Tính tổng tích lũy (running total) - điều mà không Window Function rất khó làm:

 SELECT

  orderid, custid, orderdate, freight,

  SUM(freight) OVER (

  PARTITION BY custid

  ORDER BY orderdate, orderid

  ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

  ) AS running_total,

  AVG(freight) OVER (PARTITION BY custid) AS avg_per_customer,

  MAX(freight) OVER () AS overall_max

 FROM Sales.Orders;

5.2 Ranking Functions - Xếp Hạng Thần Tốc

 -- So sánh các hàm xếp hạng:

 SELECT

  productname, unitprice,

  ROW_NUMBER() OVER (ORDER BY unitprice DESC) AS row_num,

  RANK()       OVER (ORDER BY unitprice DESC) AS rank_num,

  DENSE_RANK() OVER (ORDER BY unitprice DESC) AS dense_rank_num,

  NTILE(4)     OVER (ORDER BY unitprice DESC) AS quartile

 FROM Production.Products;

 

 -- Kết quả minh họa (với các sản phẩm có cùng giá):

 -- productname  unitprice  row_num  rank  dense_rank  quartile

 -- Product A    100.00     1        1         1           1

 -- Product B    90.00       2        2         2           1

 -- Product C    90.00       3        2         2           1  (ties!)

 -- Product D    80.00       4        4        3            2

5.3 Offset Functions - Nhìn Về Trước và Sau

 -- LAG và LEAD: Lấy giá trị từ hàng trước/sau

 SELECT

  orderid, custid, orderdate, freight,

  LAG(freight)  OVER (PARTITION BY custid ORDER BY orderdate) AS prev_freight,

  LEAD(freight) OVER (PARTITION BY custid ORDER BY orderdate) AS next_freight,

  freight - LAG(freight) OVER (PARTITION BY custid ORDER BY orderdate)

  AS freight_change

 FROM Sales.Orders;

 

 -- FIRST_VALUE và LAST_VALUE:

 SELECT

  custid, orderdate, freight,

  FIRST_VALUE(freight) OVER (

  PARTITION BY custid ORDER BY orderdate

        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING

) AS first_freight

FROM Sales.Orders;

5.4 Bài Toán Gaps và Islands - Ứng Dụng Thực Tiễn

Một ứng dụng cực kỳ thú vị của Window Functions là bài toán 'Gaps and Islands' - tìm các khoảng trống và các nhóm liên tiếp trong dữ liệu. Ví dụ: tìm các khoảng thời gian nhân viên làm việc liên tục, hoặc tìm các chuỗi số liên tiếp.

 -- Tìm các 'island' (chuỗi ngày liên tiếp) trong bảng lịch làm việc:

 WITH Gaps AS (

  SELECT empid, workdate,

  workdate - CAST(ROW_NUMBER() OVER (PARTITION BY empid

  ORDER BY workdate) AS INT) AS grp

  FROM HR.WorkSchedule

 )

 SELECT

  empid,

  MIN(workdate) AS start_date,

  MAX(workdate) AS end_date,

  COUNT(*) AS num_days

 FROM Gaps

 GROUP BY empid, grp

 ORDER BY empid, start_date;

6. TOP và OFFSET-FETCH - Lọc Dữ Liệu Tinh Tế

Đôi khi bạn không cần toàn bộ dữ liệu - bạn chỉ cần N bản ghi đầu tiên, hoặc muốn phân trang dữ liệu để hiển thị từng trang cho người dùng. TOP và OFFSET-FETCH chính là công cụ hoàn hảo cho những nhu cầu này.

6.1 TOP Filter - Đơn Giản Mà Hiệu Quả

 -- 3 đơn hàng mới nhất:

 SELECT TOP (3) orderid, orderdate, custid

 FROM Sales.Orders

 ORDER BY orderdate DESC;

 

 -- Top 1% đơn hàng có giá trị cao nhất:

 SELECT TOP (1) PERCENT orderid, freight

 FROM Sales.Orders

 ORDER BY freight DESC;

 

 -- WITH TIES: Bao gồm tất cả bản ghi có cùng giá trị với hàng cuối:

 SELECT TOP (3) WITH TIES orderid, orderdate

 FROM Sales.Orders

 ORDER BY orderdate DESC;

6.2 OFFSET-FETCH - Phân Trang Chuyên Nghiệp

OFFSET-FETCH là tính năng chuẩn SQL được giới thiệu trong SQL Server 2012, cho phép phân trang dữ liệu một cách tự nhiên và thanh lịch:

 -- Stored procedure phân trang chuyên nghiệp:

 CREATE PROC dbo.GetOrdersPage

  @pagenum  BIGINT = 1,

  @pagesize BIGINT = 25

 AS

 -- Tối ưu: lấy chỉ keys trước, rồi join để lấy dữ liệu đầy đủ

 WITH Keys AS (

  SELECT orderid

  FROM Sales.Orders

  ORDER BY orderid

  OFFSET (@pagenum - 1) * @pagesize ROWS

  FETCH NEXT @pagesize ROWS ONLY

 )

 SELECT o.orderid, o.orderdate, o.custid, o.freight

 FROM Sales.Orders AS o

  JOIN Keys AS k ON o.orderid = k.orderid

 ORDER BY o.orderid;

 GO

 -- Gọi procedure:

 EXEC dbo.GetOrdersPage @pagenum = 3, @pagesize = 25;

6.3 Top N Per Group - Bài Toán Kinh Điển

Một trong những bài toán phổ biến nhất trong thực tế là 'Top N per group' - ví dụ: 3 đơn hàng gần nhất của mỗi khách hàng. T-SQL có nhiều cách giải thanh lịch:

 -- Tạo POC index để tối ưu hiệu suất:

 CREATE UNIQUE INDEX idx_poc

  ON Sales.Orders(custid, orderdate DESC, orderid DESC)

 INCLUDE(empid);

 

 -- Giải pháp 1: Dùng ROW_NUMBER (tốt cho mật độ thấp):

 WITH RankedOrders AS (

  SELECT

  custid, orderid, orderdate, empid,

  ROW_NUMBER() OVER (

  PARTITION BY custid

  ORDER BY orderdate DESC, orderid DESC

  ) AS rn

  FROM Sales.Orders

 )

 SELECT custid, orderid, orderdate, empid

 FROM RankedOrders

 WHERE rn <= 3;

 

 -- Giải pháp 2: CROSS APPLY + TOP (tốt cho mật độ cao):

 SELECT c.custid, a.orderid, a.orderdate, a.empid

 FROM Sales.Customers AS c

  CROSS APPLY (

  SELECT TOP (3) orderid, orderdate, empid

  FROM Sales.Orders AS o

  WHERE o.custid = c.custid

  ORDER BY orderdate DESC, orderid DESC

  ) AS a;

7. Data Modification - Biến Đổi Thế Giới Dữ Liệu

T-SQL không chỉ giỏi đọc dữ liệu - nó cũng có những công cụ mạnh mẽ để thêm, sửa, xóa và gộp dữ liệu. Đặc biệt, mệnh đề OUTPUT và câu lệnh MERGE là những tính năng độc đáo không thể tìm thấy ở nhiều DBMS khác.

7.1 MERGE - Câu Lệnh Thần Kỳ

MERGE là câu lệnh cho phép bạn thực hiện INSERT, UPDATE và DELETE cùng một lúc dựa trên điều kiện so sánh với nguồn dữ liệu khác. Đây là công cụ hoàn hảo cho các tình huống 'upsert' (update if exists, insert if not):

 -- MERGE: Đồng bộ dữ liệu khách hàng từ nguồn mới

 MERGE INTO Sales.Customers AS tgt

 USING Sales.CustomerStaging AS src

  ON tgt.custid = src.custid

 WHEN MATCHED AND tgt.companyname <> src.companyname THEN

  UPDATE SET tgt.companyname = src.companyname,

  tgt.phone = src.phone

 WHEN NOT MATCHED BY TARGET THEN

  INSERT (custid, companyname, phone)

  VALUES (src.custid, src.companyname, src.phone)

 WHEN NOT MATCHED BY SOURCE THEN

  DELETE

 OUTPUT

  $action AS action_type,

  deleted.custid AS old_custid,

  inserted.custid AS new_custid;

7.2 OUTPUT - Xem Kết Quả Ngay Lập Tức

 -- OUTPUT với DELETE để lưu trữ dữ liệu đã xóa:

 DELETE FROM Sales.Orders

 OUTPUT

  deleted.orderid,

  deleted.custid,

  deleted.orderdate,

  GETDATE() AS deleted_at

 INTO Sales.OrdersArchive (orderid, custid, orderdate, deleted_at)

 WHERE orderdate < '20100101';

 

 -- OUTPUT với INSERT để lấy ID vừa được tạo:

 DECLARE @NewCustomers TABLE (custid INT, companyname NVARCHAR(40));

 

 INSERT INTO Sales.Customers (companyname, phone)

 OUTPUT inserted.custid, inserted.companyname

  INTO @NewCustomers

 VALUES ('Công ty ABC', '0901234567'),

  ('Công ty XYZ', '0987654321');

 

 SELECT * FROM @NewCustomers;

7.3 Xóa Dữ Liệu Lớn - Không Làm Tắc Hệ Thống

Khi cần xóa hàng triệu bản ghi, đừng chạy một câu DELETE lớn - nó có thể khóa bảng và làm tắc nghẽn toàn bộ hệ thống. Hãy xóa theo từng lô nhỏ:

 -- Xóa theo lô nhỏ - an toàn và không làm tắc nghẽn hệ thống:

 DECLARE @batch_size INT = 3000;

 DECLARE @rows_deleted INT;

 

SET NOCOUNT ON;

 WHILE 1 = 1

 BEGIN

  DELETE TOP (@batch_size)

  FROM Sales.Orders

  WHERE orderdate < '20130101';

  

  SET @rows_deleted = @@ROWCOUNT;

  IF @rows_deleted < @batch_size BREAK;

 

  WAITFOR DELAY '00:00:01'; -- Nghỉ 1 giây giữa các lô

 END;

8. Ngày Tháng và Thời Gian - Chủ Nhân Của Thời Gian

Dữ liệu thời gian là một trong những loại dữ liệu phức tạp nhất trong cơ sở dữ liệu. Múi giờ, giờ mùa hè, năm nhuận, định dạng ngày quốc tế... T-SQL cung cấp một hệ thống đầy đủ để xử lý tất cả những thách thức này.

8.1 Các Kiểu Dữ Liệu Thời Gian

Kiểu dữ liệu

Phạm vi

Độ chính xác

Lưu ý

DATE

0001-01-01 đến 9999-12-31

1 ngày

Chỉ lưu ngày

TIME

00:00:00 đến 23:59:59

100ns

Chỉ lưu giờ

DATETIME2

0001 đến 9999

100ns

Khuyên dùng thay DATETIME

DATETIME

1753 đến 9999

3.33ms

Cũ, hạn chế

DATETIMEOFFSET

0001 đến 9999

100ns

Bao gồm múi giờ UTC

SMALLDATETIME

1900 đến 2079

1 phút

Ít chính xác

 

8.2 Các Hàm Thời Gian Hay Dùng

 -- Lấy ngày/giờ hiện tại:

 SELECT SYSDATETIME()          AS current_datetime,

  SYSDATETIMEOFFSET()    AS with_timezone,

  CAST(SYSDATETIME() AS DATE) AS today_only;

 

-- Tính khoảng cách thời gian:

 SELECT DATEDIFF(day,  '20150101', SYSDATETIME()) AS days_since,

  DATEDIFF(month,'20150101', SYSDATETIME()) AS months_since,

  DATEDIFF(year, '20150101', SYSDATETIME()) AS years_since;

 

 -- Thêm/bớt thời gian:

 SELECT DATEADD(day,   7,  SYSDATETIME()) AS next_week,

  DATEADD(month, -1, SYSDATETIME()) AS last_month,

  DATEADD(year,  1,  SYSDATETIME()) AS next_year;

 

 -- Trích xuất thành phần:

 SELECT YEAR(orderdate)  AS order_year,

  MONTH(orderdate) AS order_month,

  DAY(orderdate)   AS order_day,

  DATEPART(weekday, orderdate) AS day_of_week

 FROM Sales.Orders;

8.3 Lọc Dữ Liệu Thời Gian - Tránh Sai Lầm Phổ Biến

Cẩn Thận Với SARG (Search ARGument)

 Một lỗi phổ biến khi lọc ngày tháng là áp dụng hàm lên cột - điều này khiến SQL Server không thể dùng index hiệu quả! Thay vào đó, hãy chuyển điều kiện về phía hằng số.

 9. Stored Procedures & Programmable Objects

T-SQL không chỉ là ngôn ngữ truy vấn - nó còn là ngôn ngữ lập trình đầy đủ cho cơ sở dữ liệu. Stored Procedures, User-Defined Functions, Triggers... tất cả tạo nên một hệ sinh thái lập trình mạnh mẽ ngay trong database.

9.1 Stored Procedures - Chương Trình Trong Database

 -- Stored Procedure với xử lý lỗi đầy đủ:

 CREATE PROC dbo.AddOrder

  @custid   INT,

  @empid    INT,

  @freight  MONEY,

  @orderid  INT OUTPUT

 AS

 BEGIN

  SET NOCOUNT ON;

 

  BEGIN TRY

  BEGIN TRANSACTION;

 

  INSERT INTO Sales.Orders (custid, empid, orderdate, freight)

  VALUES (@custid, @empid, SYSDATETIME(), @freight);

 

  SET @orderid = SCOPE_IDENTITY();

 

COMMIT TRANSACTION;

  END TRY

  BEGIN CATCH

  IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;

 

  THROW; -- Ném lỗi cho caller xử lý

  END CATCH

 END;

 GO


-- Gọi procedure:

DECLARE @new_orderid INT;

EXEC dbo.AddOrder

@custid   = 1,

@empid    = 3,

@freight  = 25.50,

@orderid  = @new_orderid OUTPUT;

PRINT 'Đơn hàng mới: ' + CAST(@new_orderid AS VARCHAR);

9.2 Dynamic SQL - Truy Vấn Động

Đôi khi bạn cần xây dựng câu truy vấn động dựa trên điều kiện người dùng nhập. Dynamic SQL là công cụ cho phép điều này, nhưng cần được dùng cẩn thận để tránh SQL Injection:

-- Dynamic SQL AN TOÀN với sp_executesql:

 CREATE PROC dbo.SearchOrders

  @custid    INT    = NULL,

  @startdate DATE   = NULL,

 @enddate   DATE   = NULL

 AS

 BEGIN\

  DECLARE @sql NVARCHAR(MAX) = N'

  SELECT orderid, custid, orderdate, freight

  FROM Sales.Orders

  WHERE 1=1';

  IF @custid IS NOT NULL

  SET @sql += N' AND custid = @custid';

 

  IF @startdate IS NOT NULL

  SET @sql += N' AND orderdate >= @startdate';

 

  IF @enddate IS NOT NULL

  SET @sql += N' AND orderdate <= @enddate';

 

-- Dùng sp_executesql để tránh SQL Injection:

  EXEC sp_executesql @sql,

  N'@custid INT, @startdate DATE, @enddate DATE',

  @custid, @startdate, @enddate;

 END;

9.3 Transactions - Đảm Bảo Toàn Vẹn Dữ Liệu

Transaction là khái niệm nền tảng của bất kỳ cơ sở dữ liệu quan hệ nào. Nó đảm bảo tính ACID: Atomicity (nguyên tử), Consistency (nhất quán), Isolation (cô lập) và Durability (bền vững).

-- Ví dụ transaction chuyển tiền ngân hàng:

 BEGIN TRANSACTION;

 

 BEGIN TRY

  -- Trừ tiền từ tài khoản nguồn:

  UPDATE Accounts SET balance = balance - 1000000

  WHERE account_id = 101;

 

  -- Kiểm tra không được âm:

  IF (SELECT balance FROM Accounts WHERE account_id = 101) < 0

  THROW 50001, 'Số dư không đủ!', 1;

 

  -- Cộng tiền vào tài khoản đích:

  UPDATE Accounts SET balance = balance + 1000000

  WHERE account_id = 202;

 

  COMMIT TRANSACTION;

  PRINT 'Chuyển tiền thành công!';

 END TRY

 BEGIN CATCH

  ROLLBACK TRANSACTION;

  PRINT 'Lỗi: ' + ERROR_MESSAGE();

 END CATCH;

10. In-Memory OLTP - Tốc Độ Ánh Sáng

In-Memory OLTP (còn gọi là 'Hekaton') là một trong những đột phá công nghệ lớn nhất của SQL Server 2014. Bằng cách lưu trữ toàn bộ dữ liệu trong RAM và sử dụng kiến trúc không khóa (lock-free), In-Memory OLTP có thể đạt hiệu suất nhanh hơn 30x đến 100x so với bảng thông thường!

In-Memory OLTP - Khi Tốc Độ Là Tất Cả

 Imagine một hệ thống xử lý đặt vé máy bay, nơi hàng nghìn người đặt vé cùng một lúc. Với In-Memory OLTP, SQL Server có thể xử lý hàng trăm nghìn transactions mỗi giây mà không bị nghẽn cổ chai - điều gần như không thể với kiến trúc truyền thống!

10.1 Tạo Memory-Optimized Table

 -- Bước 1: Thêm filegroup cho In-Memory:

 ALTER DATABASE AdventureWorks

 ADD FILEGROUP fg_memory CONTAINS MEMORY_OPTIMIZED_DATA;

 

ALTER DATABASE AdventureWorks

 ADD FILE (NAME='memory_data', FILENAME='C:\Data\memory_data')

 TO FILEGROUP fg_memory;

 

 -- Bước 2: Tạo bảng tối ưu bộ nhớ:

 CREATE TABLE dbo.ShoppingCart (

  cartid     INT          NOT NULL,

  productid  INT          NOT NULL,

  qty        INT          NOT NULL DEFAULT 1,

  added_at   DATETIME2    NOT NULL DEFAULT SYSDATETIME(),

  CONSTRAINT PK_Cart PRIMARY KEY NONCLUSTERED (cartid, productid)

 )

 WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);

 

-- Bước 3: Tạo Natively Compiled Stored Procedure:

CREATE PROC dbo.AddToCart

@cartid   INT,

@productid INT,

 @qty      INT

WITH NATIVE_COMPILATION, SCHEMABINDING

AS BEGIN ATOMIC WITH

(

TRANSACTION ISOLATION LEVEL = SNAPSHOT,

LANGUAGE = N'English'

)

INSERT INTO dbo.ShoppingCart (cartid, productid, qty)

VALUES (@cartid, @productid, @qty);

END;

10.2 Kiến Trúc Không Khóa

Điểm đột phá của In-Memory OLTP là kiến trúc Optimistic Concurrency: thay vì khóa bản ghi khi ghi, hệ thống cho phép nhiều transaction đọc/ghi đồng thời và chỉ kiểm tra xung đột khi commit. Điều này loại bỏ bottleneck lớn nhất của cơ sở dữ liệu truyền thống!

Đặc điểm

Bảng thông thường

In-Memory Table

Lưu trữ

Đĩa (cache RAM)

Hoàn toàn trong RAM

Khóa

Pessimistic (lock)

Optimistic (lock-free)

Biên dịch

Interpreted SQL

Native compilation (C)

Throughput

Hàng nghìn TPS

Hàng trăm nghìn TPS

Index

B-Tree, Columnstore

Hash + Bw-Tree

Durability

Write-ahead log

Tùy chọn (SCHEMA_ONLY)

 

11. Đồ Thị & Truy Vấn Đệ Quy - Khám Phá Mê Cung Dữ Liệu

Một trong những ứng dụng hấp dẫn nhất của T-SQL là xử lý dữ liệu có cấu trúc phân cấp và đồ thị. Sơ đồ tổ chức công ty, cấu trúc nguyên vật liệu (BOM), mạng lưới giao thông... tất cả đều có thể được biểu diễn và truy vấn hiệu quả bằng T-SQL!

11.1 Recursive CTE - Đệ Quy Trong SQL

Recursive CTE là công cụ cho phép bạn 'leo' qua các cấp bậc trong cấu trúc phân cấp mà không cần code phức tạp:

 -- Duyệt toàn bộ cây tổ chức từ CEO xuống:

 WITH EmployeeTree AS (

  -- Anchor: bắt đầu từ CEO (không có manager)

  SELECT empid, firstname, lastname, managerid, 0 AS level,

  CAST(firstname + ' ' + lastname AS VARCHAR(500)) AS path

  FROM HR.Employees

  WHERE managerid IS NULL

 

  UNION ALL

 

  -- Recursive: leo từng cấp xuống

  SELECT e.empid, e.firstname, e.lastname, e.managerid,

  t.level + 1,

  CAST(t.path + ' > ' + e.firstname + ' ' + e.lastname

  AS VARCHAR(500))

  FROM HR.Employees AS e

  JOIN EmployeeTree AS t ON e.managerid = t.empid

 )

 SELECT

  REPLICATE('  ', level) + firstname + ' ' + lastname AS org_chart,

  level,

  path

 FROM EmployeeTree

 ORDER BY path;

11.2 HIERARCHYID - Siêu Năng Lực Cho Dữ Liệu Phân Cấp

HIERARCHYID là một kiểu dữ liệu đặc biệt của SQL Server được thiết kế riêng cho dữ liệu phân cấp. Nó giúp việc truy vấn cây phân cấp trở nên nhanh hơn và đơn giản hơn nhiều:

 -- Tạo bảng với HIERARCHYID:

 CREATE TABLE HR.OrgChart (

  empid   INT  NOT NULL PRIMARY KEY,

  name    NVARCHAR(100) NOT NULL,

  hid     HIERARCHYID NOT NULL

 );

 

CREATE UNIQUE INDEX idx_hid ON HR.OrgChart(hid);

CREATE INDEX idx_bfs ON HR.OrgChart(hid.GetLevel(), hid);

 

-- Tìm tất cả nhân viên dưới quyền của một người:

DECLARE @mgr_hid HIERARCHYID;

SELECT @mgr_hid = hid FROM HR.OrgChart WHERE empid = 5;=

 

SELECT empid, name, hid.ToString() AS path, hid.GetLevel() AS level

FROM HR.OrgChart

WHERE hid.IsDescendantOf(@mgr_hid) = 1

ORDER BY hid;

11.3 Bài Toán Shortest Path - Tìm Đường Ngắn Nhất

T-SQL cũng có thể giải các bài toán đồ thị phức tạp như tìm đường ngắn nhất (shortest path) trong mạng lưới giao thông. Đây là một ứng dụng tuyệt vời của Recursive CTE:

 -- Tìm đường ngắn nhất giữa hai thành phố:

 WITH Routes AS (

  -- Điểm xuất phát

  SELECT from_city, to_city, distance,

  CAST(from_city + '->' + to_city AS VARCHAR(MAX)) AS path,

  1 AS hops

  FROM Roads

  WHERE from_city = N'Hà Nội'

 

  UNION ALL

 

  -- Mở rộng đường đi

  SELECT r.from_city, rd.to_city,

  r.distance + rd.distance,

  r.path + '->' + rd.to_city,

  r.hops + 1

  FROM Routes AS r

  JOIN Roads AS rd ON r.to_city = rd.from_city

  WHERE r.hops < 10  -- Giới hạn độ sâu

  AND r.path NOT LIKE '%' + rd.to_city + '%'  -- Tránh vòng lặp

 )

 SELECT TOP (1) path, distance, hops

 FROM Routes

 WHERE to_city = N'TP. Hồ Chí Minh'

 ORDER BY distance;

12. Kết Luận - Hành Trình Tiếp Theo

Bài viết này chỉ là cánh cửa mở ra thế giới rộng lớn và kỳ diệu của T-SQL. Từ những câu SELECT đơn giản đến các Window Functions phức tạp, từ Index Tuning đến In-Memory OLTP, T-SQL là một ngôn ngữ có chiều sâu không có đáy.

Điều làm T-SQL trở nên thực sự đặc biệt không chỉ là sức mạnh kỹ thuật - mà là cách nó cho phép bạn giải quyết những bài toán thực tế của doanh nghiệp: phân tích doanh thu, tối ưu chuỗi cung ứng, phát hiện gian lận, cá nhân hóa trải nghiệm người dùng...

Lộ Trình Học T-SQL

Cấp độ

Kỹ năng cần học

Thời gian

Beginner

SELECT, WHERE, JOIN, GROUP BY, ORDER BY, cơ bản về NULL

1-2 tháng

Intermediate

Subqueries, CTEs, Window Functions, Indexes, Execution Plans

2-4 tháng

Advanced

Query Tuning, Stored Procedures, Transactions, Dynamic SQL

4-8 tháng

Expert

In-Memory OLTP, Graphs, SQLCLR, Advanced Internals

1-2 năm+

 

"Sức mạnh thực sự của T-SQL nằm ở tư duy - không phải cú pháp. Khi bạn thực sự hiểu cách SQL Server nghĩ, bạn sẽ viết được những truy vấn làm người khác ngạc nhiên."

Tài liệu tham khảo:

Itzik Ben-Gan, Dejan Sarka, Adam Machanic, Kevin Farleem (2015),  T-SQL Querying, Microsoft Press (https://www.amazon.com/T-SQL-Querying-Developer-Reference-Ben-Gan/dp/0735685045)

ThS. Trương Châu Long - Trưởng bộ môn ngành HTTT - CNTT