Người bạn tốt của tôi Aaron Bertrand đã truyền cảm hứng cho tôi để viết bài báo này. Anh ấy nhắc nhở tôi về việc đôi khi chúng ta coi mọi thứ là điều hiển nhiên khi chúng có vẻ hiển nhiên với chúng ta và không phải lúc nào cũng bận tâm đến việc kiểm tra toàn bộ câu chuyện đằng sau chúng. Sự liên quan đến T-SQL là đôi khi chúng ta giả định rằng chúng ta biết mọi thứ cần biết về một số tính năng T-SQL nhất định và không phải lúc nào cũng bận tâm đến việc kiểm tra tài liệu để xem có nhiều hơn chúng hay không. Trong bài viết này, tôi đề cập đến một số tính năng của T-SQL thường bị bỏ qua hoàn toàn hoặc các tham số hoặc khả năng hỗ trợ thường bị bỏ qua. Nếu bạn có các ví dụ về đá quý T-SQL của riêng bạn mà thường bị bỏ qua, vui lòng chia sẻ chúng trong phần nhận xét của bài viết này.
Trước khi bắt đầu đọc bài viết này, hãy tự hỏi bạn biết gì về các tính năng T-SQL sau:EOMONTH, TRANSLATE, TRIM, CONCAT và CONCAT_WS, LOG, biến con trỏ và MERGE với OUTPUT.
Trong các ví dụ của mình, tôi sẽ sử dụng cơ sở dữ liệu mẫu có tên là TSQLV5. Bạn có thể tìm thấy tập lệnh tạo và điền cơ sở dữ liệu này tại đây và sơ đồ ER của nó tại đây.
EOMONTH có tham số thứ hai
Hàm EOMONTH đã được giới thiệu trong SQL Server 2012. Nhiều người nghĩ rằng nó chỉ hỗ trợ một tham số giữ ngày nhập và nó chỉ trả về ngày cuối tháng tương ứng với ngày nhập.
Cân nhắc nhu cầu tính toán phức tạp hơn một chút vào cuối tháng trước. Ví dụ:giả sử bạn cần truy vấn bảng Sales.Orders và trả lại các đơn hàng đã được đặt vào cuối tháng trước.
Một cách để đạt được điều này là áp dụng hàm EOMONTH cho SYSDATETIME để lấy ngày cuối tháng của tháng hiện tại, sau đó áp dụng hàm DATEADD để trừ một tháng khỏi kết quả, như sau:
USE TSQLV5; SELECT orderid, orderdate FROM Sales.Orders WHERE orderdate = EOMONTH(DATEADD(month, -1, SYSDATETIME()));
Lưu ý rằng nếu bạn thực sự chạy truy vấn này trong cơ sở dữ liệu mẫu TSQLV5, bạn sẽ nhận được kết quả trống vì ngày đặt hàng cuối cùng được ghi trong bảng là ngày 6 tháng 5 năm 2019. Tuy nhiên, nếu bảng có đơn đặt hàng với ngày đặt hàng rơi vào ngày cuối cùng ngày của tháng trước, truy vấn sẽ trả về những thứ đó.
Điều mà nhiều người không nhận ra là EOMONTH hỗ trợ thông số thứ hai trong đó bạn cho biết số tháng để cộng hoặc trừ. Đây là cú pháp [được tài liệu đầy đủ] của hàm:
EOMONTH ( start_date [, month_to_add ] )
Nhiệm vụ của chúng ta có thể đạt được dễ dàng và tự nhiên hơn bằng cách chỉ định -1 làm tham số thứ hai cho hàm, như sau:
SELECT orderid, orderdate FROM Sales.Orders WHERE orderdate = EOMONTH(SYSDATETIME(), -1);
DỊCH đôi khi đơn giản hơn THAY THẾ
Nhiều người đã quen thuộc với hàm REPLACE và cách nó hoạt động. Bạn sử dụng nó khi bạn muốn thay thế tất cả các lần xuất hiện của một chuỗi con bằng một chuỗi khác trong một chuỗi đầu vào. Tuy nhiên, đôi khi, khi bạn có nhiều thay thế mà bạn cần áp dụng, việc sử dụng REPLACE hơi phức tạp và dẫn đến các biểu thức phức tạp.
Ví dụ:giả sử rằng bạn được cung cấp một chuỗi nhập @s chứa một số có định dạng tiếng Tây Ban Nha. Ở Tây Ban Nha, họ sử dụng dấu chấm làm dấu phân cách cho các nhóm hàng nghìn và dấu phẩy làm dấu phân cách thập phân. Bạn cần chuyển đổi đầu vào sang định dạng Hoa Kỳ, trong đó dấu phẩy được sử dụng làm dấu phân cách cho các nhóm hàng nghìn và dấu chấm làm dấu phân cách thập phân.
Sử dụng một lệnh gọi hàm REPLACE, bạn chỉ có thể thay thế tất cả các lần xuất hiện của một ký tự hoặc chuỗi con bằng một ký tự hoặc chuỗi con khác. Để áp dụng hai thay thế (dấu chấm cho dấu phẩy và dấu phẩy cho dấu chấm), bạn cần lồng các lệnh gọi hàm. Phần khó khăn là nếu bạn sử dụng REPLACE một lần để thay đổi dấu chấm thành dấu phẩy và sau đó lần thứ hai đối với kết quả để thay đổi dấu phẩy thành dấu chấm, bạn sẽ chỉ có dấu chấm. Hãy thử nó:
DECLARE @s AS VARCHAR(20) = '123.456.789,00'; SELECT REPLACE(REPLACE(@s, '.', ','), ',', '.');
Bạn nhận được kết quả sau:
123.456.789.00
Nếu bạn muốn sử dụng hàm REPLACE, bạn cần có ba lệnh gọi hàm. Một để thay thế các dấu chấm bằng một ký tự trung tính mà bạn biết rằng thường không thể xuất hiện trong dữ liệu (giả sử, ~). Một cái khác chống lại kết quả để thay thế tất cả các dấu phẩy bằng dấu chấm. Một kết quả khác chống lại kết quả để thay thế tất cả các lần xuất hiện của ký tự tạm thời (~ trong ví dụ của chúng tôi) bằng dấu phẩy. Đây là biểu thức đầy đủ:
DECLARE @s AS VARCHAR(20) = '123.456.789,00'; SELECT REPLACE(REPLACE(REPLACE(@s, '.', '~'), ',', '.'), '~', ',');
Lần này, bạn nhận được đầu ra phù hợp:
123.456.789.00
Đó là điều có thể làm được, nhưng nó dẫn đến một biểu thức dài và phức tạp. Điều gì sẽ xảy ra nếu bạn có nhiều thay thế hơn để áp dụng?
Nhiều người không biết rằng SQL Server 2017 đã giới thiệu một chức năng mới có tên là TRANSLATE giúp đơn giản hóa rất nhiều việc thay thế như vậy. Đây là cú pháp của hàm:
TRANSLATE ( inputString, characters, translations )
Đầu vào thứ hai (các ký tự) là một chuỗi có danh sách các ký tự riêng lẻ mà bạn muốn thay thế và đầu vào thứ ba (bản dịch) là một chuỗi có danh sách các ký tự tương ứng mà bạn muốn thay thế các ký tự nguồn. Điều này đương nhiên có nghĩa là tham số thứ hai và thứ ba phải có cùng số ký tự. Điều quan trọng về chức năng là nó không thực hiện các bước chuyển riêng biệt cho từng chức năng thay thế. Nếu nó xảy ra, nó sẽ có khả năng dẫn đến lỗi tương tự như trong ví dụ đầu tiên tôi đã trình bày bằng cách sử dụng hai lệnh gọi đến hàm REPLACE. Do đó, việc xử lý công việc của chúng ta trở nên không cần bàn cãi:
DECLARE @s AS VARCHAR(20) = '123.456.789,00'; SELECT TRANSLATE(@s, '.,', ',.');
Mã này tạo ra kết quả mong muốn:
123.456.789.00
Điều đó khá gọn gàng!
TRIM hơn LTRIM (RTRIM ())
SQL Server 2017 đã giới thiệu hỗ trợ cho chức năng TRIM. Nhiều người, bao gồm cả tôi, ban đầu chỉ cho rằng đó không chỉ là một phím tắt đơn giản cho LTRIM (RTRIM (đầu vào)). Tuy nhiên, nếu bạn kiểm tra tài liệu, bạn sẽ nhận ra rằng nó thực sự mạnh hơn thế.
Trước khi tôi đi vào chi tiết, hãy xem xét nhiệm vụ sau:đưa ra một chuỗi đầu vào @s, loại bỏ các dấu gạch chéo ở đầu và ở cuối (lùi và tiến). Ví dụ:giả sử rằng @s chứa chuỗi sau:
//\\ remove leading and trailing backward (\) and forward (/) slashes \\//
Đầu ra mong muốn là:
remove leading and trailing backward (\) and forward (/) slashes
Lưu ý rằng đầu ra phải giữ lại khoảng trắng ở đầu và cuối.
Nếu bạn không biết về toàn bộ khả năng của TRIM, thì đây là một cách bạn có thể đã giải quyết được công việc:
DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//'; SELECT TRANSLATE(TRIM(TRANSLATE(TRIM(TRANSLATE(@s, ' /', '~ ')), ' \', '^ ')), ' ^~', '\/ ') AS outputstring;
Giải pháp bắt đầu bằng cách sử dụng TRANSLATE để thay thế tất cả các khoảng trắng bằng một ký tự trung tính (~) và các dấu gạch chéo về phía trước bằng các khoảng trắng, sau đó sử dụng TRIM để cắt các khoảng trắng đầu và cuối khỏi kết quả. Bước này về cơ bản cắt bớt các dấu gạch chéo ở đầu và sau cùng, tạm thời sử dụng ~ thay vì dấu cách ban đầu. Đây là kết quả của bước này:
\\~remove~leading~and~trailing~backward~(\)~and~forward~( )~slashes~\\
Bước thứ hai sau đó sử dụng TRANSLATE để thay thế tất cả các khoảng trắng bằng một ký tự trung tính khác (^) và dấu gạch chéo ngược bằng dấu cách, sau đó sử dụng TRIM để cắt bỏ các khoảng trắng ở đầu và cuối khỏi kết quả. Bước này về cơ bản cắt bớt các dấu gạch chéo ở đầu và sau cùng, tạm thời sử dụng ^ thay vì dấu cách trung gian. Đây là kết quả của bước này:
~remove~leading~and~trailing~backward~( )~and~forward~(^)~slashes~
Bước cuối cùng sử dụng TRANSLATE để thay thế khoảng trắng bằng dấu gạch chéo ngược, ^ bằng dấu gạch chéo về phía trước và ~ bằng dấu cách, tạo ra kết quả mong muốn:
remove leading and trailing backward (\) and forward (/) slashes
Như một bài tập, hãy thử giải quyết tác vụ này bằng giải pháp tương thích trước SQL Server 2017 nơi bạn không thể sử dụng TRIM và TRANSLATE.
Quay lại SQL Server 2017 trở lên, nếu bạn đã bận tâm kiểm tra tài liệu, bạn sẽ phát hiện ra rằng TRIM phức tạp hơn những gì bạn nghĩ ban đầu. Đây là Cú pháp của hàm:
TRIM ( [ characters FROM ] string )
Các ký tự FROM tùy chọn phần cho phép bạn chỉ định một hoặc nhiều ký tự mà bạn muốn cắt bớt từ đầu và cuối chuỗi đầu vào. Trong trường hợp của chúng tôi, tất cả những gì bạn cần làm là chỉ định '/ \' làm phần này, như sau:
DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//'; SELECT TRIM( '/\' FROM @s) AS outputstring;
Đó là một cải tiến khá đáng kể so với giải pháp trước đó!
CONCAT và CONCAT_WS
Nếu bạn đã làm việc với T-SQL một thời gian, bạn sẽ biết việc xử lý NULLs khó xử như thế nào khi bạn cần nối các chuỗi. Ví dụ:hãy xem xét dữ liệu vị trí được ghi lại cho nhân viên trong bảng HR.E Employees:
SELECT empid, country, region, city FROM HR.Employees;
Truy vấn này tạo ra kết quả sau:
empid country region city ----------- --------------- --------------- --------------- 1 USA WA Seattle 2 USA WA Tacoma 3 USA WA Kirkland 4 USA WA Redmond 5 UK NULL London 6 UK NULL London 7 UK NULL London 8 USA WA Seattle 9 UK NULL London
Lưu ý rằng đối với một số nhân viên, phần vùng là không liên quan và vùng không liên quan được biểu thị bằng NULL. Giả sử rằng bạn cần nối các phần vị trí (quốc gia, vùng và thành phố), sử dụng dấu phẩy làm dấu phân tách, nhưng bỏ qua các vùng NULL. Khi vùng có liên quan, bạn muốn kết quả có dạng <coutry>,<region>,<city>
và khi vùng không liên quan, bạn muốn kết quả có dạng <country>,<city>
. Thông thường, nối một cái gì đó với một NULL sẽ tạo ra một kết quả NULL. Bạn có thể thay đổi hành vi này bằng cách tắt tùy chọn phiên CONCAT_NULL_YIELDS_NULL, nhưng tôi không khuyên bạn nên bật hành vi không chuẩn.
Nếu bạn không biết về sự tồn tại của các hàm CONCAT và CONCAT_WS, có thể bạn đã sử dụng ISNULL hoặc COALESCE để thay thế NULL bằng một chuỗi trống, như sau:
SELECT empid, country + ISNULL(',' + region, '') + ',' + city AS location FROM HR.Employees;
Đây là kết quả của truy vấn này:
empid location ----------- ----------------------------------------------- 1 USA,WA,Seattle 2 USA,WA,Tacoma 3 USA,WA,Kirkland 4 USA,WA,Redmond 5 UK,London 6 UK,London 7 UK,London 8 USA,WA,Seattle 9 UK,London
SQL Server 2012 đã giới thiệu chức năng CONCAT. Hàm này chấp nhận một danh sách các đầu vào chuỗi ký tự và nối chúng, và trong khi làm như vậy, nó bỏ qua các NULL. Vì vậy, sử dụng CONCAT, bạn có thể đơn giản hóa giải pháp như sau:
SELECT empid, CONCAT(country, ',' + region, ',', city) AS location FROM HR.Employees;
Tuy nhiên, bạn phải chỉ định rõ ràng các dấu phân tách như một phần của đầu vào của hàm. Để làm cho cuộc sống của chúng ta trở nên dễ dàng hơn, SQL Server 2017 đã giới thiệu một hàm tương tự có tên CONCAT_WS, nơi bạn bắt đầu bằng cách chỉ ra dấu phân tách, sau đó là các mục mà bạn muốn nối. Với chức năng này, giải pháp được đơn giản hóa hơn như vậy:
SELECT empid, CONCAT_WS(',', country, region, city) AS location FROM HR.Employees;
Bước tiếp theo tất nhiên là đọc suy nghĩ. Vào ngày 1 tháng 4 năm 2020, Microsoft dự định phát hành CONCAT_MR. Hàm sẽ chấp nhận một đầu vào trống và tự động tìm ra phần tử nào bạn muốn nó nối bằng cách đọc suy nghĩ của bạn. Sau đó, truy vấn sẽ giống như sau:
SELECT empid, CONCAT_MR() AS location FROM HR.Employees;
LOG có tham số thứ hai
Tương tự như hàm EOMONTH, nhiều người không nhận ra rằng bắt đầu với SQL Server 2012, hàm LOG hỗ trợ tham số thứ hai cho phép bạn chỉ ra cơ số của logarit. Trước đó, T-SQL đã hỗ trợ hàm LOG (đầu vào) trả về lôgarit tự nhiên của đầu vào (sử dụng hằng số e làm cơ số) và LOG10 (đầu vào) sử dụng 10 làm cơ số.
Không nhận thức được sự tồn tại của tham số thứ hai đối với hàm LOG, khi mọi người muốn tính Nhật ký b (x), trong đó b là một cơ số khác với e và 10, họ thường đã làm điều đó một cách lâu dài. Bạn có thể dựa vào phương trình sau:
Nhật ký b (x) =Nhật ký a (x) / Nhật ký a (b)Ví dụ:để tính Nhật ký 2 (8), bạn dựa vào phương trình sau:
Nhật ký 2 (8) =Nhật ký e (8) / Nhật ký e (2)Được dịch sang T-SQL, bạn áp dụng phép tính sau:
DECLARE @x AS FLOAT = 8, @b AS INT = 2; SELECT LOG(@x) / LOG(@b);
Khi bạn nhận ra rằng LOG hỗ trợ tham số thứ hai nơi bạn chỉ ra cơ sở, thì phép tính chỉ đơn giản trở thành:
DECLARE @x AS FLOAT = 8, @b AS INT = 2; SELECT LOG(@x, @b);
Biến con trỏ
Nếu bạn đã làm việc với T-SQL một thời gian, bạn có thể có nhiều cơ hội làm việc với con trỏ. Như bạn đã biết, khi làm việc với con trỏ, bạn thường sử dụng các bước sau:
- Khai báo con trỏ
- Mở con trỏ
- Lặp lại qua các bản ghi con trỏ
- Đóng con trỏ
- Định vị lại con trỏ
Ví dụ, giả sử rằng bạn cần thực hiện một số tác vụ cho mỗi cơ sở dữ liệu trong trường hợp của bạn. Sử dụng con trỏ, thông thường bạn sẽ sử dụng mã tương tự như sau:
DECLARE @dbname AS sysname; DECLARE C CURSOR FORWARD_ONLY STATIC READ_ONLY FOR SELECT name FROM sys.databases; OPEN C; FETCH NEXT FROM C INTO @dbname; WHILE @@FETCH_STATUS = 0 BEGIN PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...'; /* ... do your thing here ... */ FETCH NEXT FROM C INTO @dbname; END; CLOSE C; DEALLOCATE C;
Lệnh CLOSE giải phóng tập hợp kết quả hiện tại và giải phóng các khóa. Lệnh DEALLOCATE loại bỏ một tham chiếu con trỏ và khi tham chiếu cuối cùng được phân bổ, giải phóng cấu trúc dữ liệu bao gồm con trỏ. Nếu bạn thử chạy đoạn mã trên hai lần mà không có lệnh CLOSE và DEALLOCATE, bạn sẽ gặp lỗi sau:
Msg 16915, Level 16, State 1, Line 4 A cursor with the name 'C' already exists. Msg 16905, Level 16, State 1, Line 6 The cursor is already open.
Đảm bảo rằng bạn đã chạy các lệnh CLOSE và DEALLOCATE trước khi tiếp tục.
Nhiều người không nhận ra rằng khi họ chỉ cần làm việc với con trỏ trong một lô, đây là trường hợp phổ biến nhất, thay vì sử dụng con trỏ thông thường, bạn có thể làm việc với một biến con trỏ. Giống như bất kỳ biến nào, phạm vi của một biến con trỏ chỉ là lô nơi nó được khai báo. Điều này có nghĩa là ngay sau khi một lô kết thúc, tất cả các biến sẽ hết hạn. Sử dụng một biến con trỏ, sau khi một loạt kết thúc, SQL Server sẽ tự động đóng và phân bổ nó, giúp bạn không cần phải chạy lệnh CLOSE và DEALLOCATE một cách rõ ràng.
Đây là mã đã sửa đổi lần này bằng cách sử dụng biến con trỏ:
DECLARE @dbname AS sysname, @C AS CURSOR; SET @C = CURSOR FORWARD_ONLY STATIC READ_ONLY FOR SELECT name FROM sys.databases; OPEN @C; FETCH NEXT FROM @C INTO @dbname; WHILE @@FETCH_STATUS = 0 BEGIN PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...'; /* ... do your thing here ... */ FETCH NEXT FROM @C INTO @dbname; END;
Hãy thực hiện nó nhiều lần và lưu ý rằng lần này bạn không gặp bất kỳ lỗi nào. Nó chỉ gọn gàng hơn và bạn không phải lo lắng về việc giữ tài nguyên con trỏ nếu bạn quên đóng và phân bổ con trỏ.
MERGE với OUTPUT
Kể từ khi ra đời mệnh đề OUTPUT cho các câu lệnh sửa đổi trong SQL Server 2005, nó trở thành một công cụ rất thiết thực bất cứ khi nào bạn muốn trả về dữ liệu từ các hàng đã sửa đổi. Mọi người sử dụng tính năng này thường xuyên cho các mục đích như lưu trữ, kiểm tra và nhiều trường hợp sử dụng khác. Tuy nhiên, một trong những điều khó chịu về tính năng này là nếu bạn sử dụng nó với các câu lệnh INSERT, bạn chỉ được phép trả về dữ liệu từ các hàng đã chèn, đặt tiền tố cho các cột đầu ra bằng đã chèn . Bạn không có quyền truy cập vào các cột của bảng nguồn, mặc dù đôi khi bạn cần trả lại các cột từ nguồn cùng với các cột từ mục tiêu.
Ví dụ:hãy xem xét các bảng T1 và T2, mà bạn tạo và điền bằng cách chạy mã sau:
DROP TABLE IF EXISTS dbo.T1, dbo.T2; GO CREATE TABLE dbo.T1(keycol INT NOT NULL IDENTITY PRIMARY KEY, datacol VARCHAR(10) NOT NULL); CREATE TABLE dbo.T2(keycol INT NOT NULL IDENTITY PRIMARY KEY, datacol VARCHAR(10) NOT NULL); INSERT INTO dbo.T1(datacol) VALUES('A'),('B'),('C'),('D'),('E'),('F');
Lưu ý rằng thuộc tính nhận dạng được sử dụng để tạo khóa trong cả hai bảng.
Giả sử rằng bạn cần sao chép một số hàng từ T1 đến T2; giả sử, những cái mà keycol% 2 =1. Bạn muốn sử dụng mệnh đề OUTPUT để trả về các khóa mới được tạo trong T2, nhưng bạn cũng muốn trả lại các khóa nguồn tương ứng từ T1 cùng với các khóa đó. Kỳ vọng trực quan là sử dụng câu lệnh INSERT sau:
INSERT INTO dbo.T2(datacol) OUTPUT T1.keycol AS T1_keycol, inserted.keycol AS T2_keycol SELECT datacol FROM dbo.T1 WHERE keycol % 2 = 1;
Tuy nhiên, rất tiếc, như đã đề cập, mệnh đề OUTPUT không cho phép bạn tham chiếu đến các cột từ bảng nguồn, vì vậy bạn gặp lỗi sau:
Msg 4104, Mức 16, Trạng thái 1, Dòng 2Không thể liên kết số nhận dạng nhiều phần "T1.keycol".
Nhiều người không nhận ra rằng kỳ lạ là giới hạn này không áp dụng cho tuyên bố MERGE. Vì vậy, mặc dù hơi khó xử, bạn có thể chuyển đổi câu lệnh INSERT của mình thành câu lệnh MERGE, nhưng để làm như vậy, bạn cần vị từ MERGE luôn là false. Điều này sẽ kích hoạt mệnh đề KHI KHÔNG PHÙ HỢP và áp dụng hành động CHÈN được hỗ trợ duy nhất ở đó. Bạn có thể sử dụng điều kiện giả giả, chẳng hạn như 1 =2. Đây là mã được chuyển đổi hoàn chỉnh:
MERGE INTO dbo.T2 AS TGT USING (SELECT keycol, datacol FROM dbo.T1 WHERE keycol % 2 = 1) AS SRC ON 1 = 2 WHEN NOT MATCHED THEN INSERT(datacol) VALUES(SRC.datacol) OUTPUT SRC.keycol AS T1_keycol, inserted.keycol AS T2_keycol;
Lần này mã chạy thành công, tạo ra kết quả sau:
T1_keycol T2_keycol ----------- ----------- 1 1 3 2 5 3
Hy vọng rằng Microsoft sẽ tăng cường hỗ trợ cho mệnh đề OUTPUT trong các câu lệnh sửa đổi khác để cho phép trả về các cột từ bảng nguồn.
Kết luận
Đừng cho rằng và RTFM! :-)