Chúng ta thường thấy các truy vấn SQL phức tạp được viết kém chạy trên một bảng hoặc các bảng trong cơ sở dữ liệu. Những câu truy vấn đó làm cho thời gian thực thi rất lâu và gây tiêu tốn rất nhiều tài nguyên CPU và các tài nguyên khác. Tuy nhiên, các truy vấn phức tạp cung cấp thông tin có giá trị cho ứng dụng / người chạy chúng trong nhiều trường hợp. Do đó, chúng là tài sản hữu ích trong tất cả các loại ứng dụng.
Khó gỡ lỗi các truy vấn phức tạp
Nếu chúng ta xem xét kỹ các truy vấn có vấn đề, nhiều truy vấn trong số đó rất phức tạp, đặc biệt là những truy vấn cụ thể được sử dụng trong báo cáo.
Các truy vấn phức tạp thường bao gồm năm bảng lớn trở lên và được nối với nhau bằng nhiều truy vấn con. Mỗi truy vấn con có một mệnh đề WHERE thực hiện các phép tính từ đơn giản đến phức tạp và / hoặc chuyển đổi dữ liệu trong khi nối các bảng có liên quan với nhau.
Các truy vấn như vậy có thể trở nên khó khăn để gỡ lỗi mà không tiêu tốn nhiều tài nguyên. Lý do là, rất khó để xác định xem mỗi truy vấn phụ và / hoặc các truy vấn phụ được kết hợp có tạo ra kết quả chính xác hay không.
Một tình huống điển hình là:họ gọi cho bạn vào ban đêm để giải quyết vấn đề trên một máy chủ cơ sở dữ liệu bận rộn với một truy vấn phức tạp liên quan và bạn cần nhanh chóng khắc phục sự cố đó. Với tư cách là Nhà phát triển hoặc DBA, bạn có thể có rất ít thời gian và tài nguyên hệ thống có sẵn vào một giờ muộn. Do đó, điều đầu tiên bạn cần là một kế hoạch về cách gỡ lỗi truy vấn có vấn đề.
Đôi khi, quy trình gỡ lỗi diễn ra tốt đẹp. Đôi khi, bạn phải mất rất nhiều thời gian và nỗ lực trước khi đạt được mục tiêu và giải quyết được vấn đề.
Viết truy vấn trong cấu trúc CTE
Nhưng điều gì sẽ xảy ra nếu có một cách để viết các truy vấn phức tạp để người ta có thể gỡ lỗi chúng nhanh chóng, từng phần một?
Có một cách như vậy. Nó được gọi là Biểu thức bảng chung hoặc CTE.
Biểu thức bảng chung là một tính năng tiêu chuẩn trong hầu hết các cơ sở dữ liệu hiện đại như SQLServer, MySQL (kể từ phiên bản 8.0), MariaDB (phiên bản 10.2.1), Db2 và Oracle. Nó có cấu trúc đơn giản đóng gói một hoặc nhiều truy vấn con vào một tập kết quả được đặt tên tạm thời. Bạn có thể sử dụng thêm bộ kết quả này trong các CTE hoặc truy vấn phụ được đặt tên khác.
Biểu thức bảng chung, ở một mức độ nhất định, là một CHẾ ĐỘ XEM chỉ tồn tại và được tham chiếu bởi truy vấn tại thời điểm thực thi.
Việc chuyển đổi một truy vấn phức tạp thành một truy vấn kiểu CTE yêu cầu một số tư duy có cấu trúc. Tương tự đối với OOP với tính năng đóng gói khi viết lại một truy vấn phức tạp thành cấu trúc CTE.
Bạn cần suy nghĩ về:
- Từng tập dữ liệu mà bạn đang lấy từ mỗi bảng.
- Cách chúng được kết hợp với nhau để đóng gói các truy vấn con gần nhất vào một tập hợp kết quả được đặt tên tạm thời.
Lặp lại nó cho từng truy vấn phụ và tập dữ liệu còn lại cho đến khi bạn đạt được kết quả cuối cùng của truy vấn. Lưu ý rằng mỗi tập kết quả được đặt tên tạm thời cũng là một truy vấn phụ.
Phần cuối cùng của truy vấn phải là một lựa chọn rất “đơn giản”, trả về kết quả cuối cùng cho ứng dụng. Khi bạn đã đến phần cuối cùng này, bạn có thể trao đổi nó bằng một truy vấn chọn dữ liệu từ một tập kết quả tạm thời được đặt tên riêng.
Bằng cách này, việc gỡ lỗi từng tập kết quả tạm thời trở thành một công việc dễ dàng.
Để hiểu cách chúng tôi có thể xây dựng các truy vấn của mình từ đơn giản đến phức tạp, hãy xem cấu trúc CTE. Mẫu đơn giản nhất như sau:
WITH CTE_1 as (
select .... from some_table where ...
)
select ... from CTE_1
where ...
Đây CTE_1 là tên duy nhất bạn đặt cho tập kết quả được đặt tên tạm thời. Có thể có nhiều bộ kết quả nếu cần. Theo đó, biểu mẫu mở rộng thành, như được hiển thị bên dưới:
WITH CTE_1 as (
select .... from some_table where ...
), CTE_2 as (
select .... from some_other_table where ...
)
select ... from CTE_1 c1,CTE_2 c2
where c1.col1 = c2.col1
....
Lúc đầu, mỗi phần CTE được tạo riêng biệt. Sau đó, nó sẽ tiến triển, vì các CTE được liên kết với nhau để tạo nên tập hợp kết quả cuối cùng của truy vấn.
Bây giờ, chúng ta hãy xem xét một trường hợp khác, truy vấn cơ sở dữ liệu Bán hàng hư cấu. Chúng tôi muốn biết những sản phẩm nào, bao gồm cả số lượng và tổng doanh số, đã được bán trong từng danh mục trong tháng trước và sản phẩm nào có tổng doanh số bán hàng cao hơn so với tháng trước đó.
Chúng tôi xây dựng truy vấn của mình thành nhiều phần CTE, trong đó mỗi phần tham chiếu đến phần trước. Đầu tiên, chúng tôi xây dựng một tập hợp kết quả để liệt kê dữ liệu chi tiết mà chúng tôi cần từ các bảng của mình để tạo thành phần còn lại của truy vấn:
WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
)
select dt.*
from detailed_data dt.
order by dt.order_date desc, dt.category_name, dt.product_name
Bước tiếp theo là tóm tắt số lượng và tổng số liệu bán hàng theo từng danh mục và tên sản phẩm:
WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
), product_sales as (
select year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name,sum(dt.quantity) total_quantity, sum(dt.listprice * (1 - dt.discount)) total_product_sales
from detailed_data dt
group by year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name
)
select ps.*
from product_sales ps
order by ps.year desc, ps.month desc, ps.category_name,ps.product_name
Bước cuối cùng là tạo hai tập kết quả tạm thời đại diện cho dữ liệu của tháng trước và của tháng trước. Sau đó, lọc ra dữ liệu được trả về dưới dạng tập hợp kết quả cuối cùng:
WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
), product_sales as (
select year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name,sum(dt.quantity) total_quantity, sum(dt.listprice * (1 - dt.discount)) total_product_sales
from detailed_data dt
group by year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name
), last_month_data (
select ps.*
from product_sales ps.
where ps.year = year(CURRENT_DATE) -1
and ps.month = month(CURRENT_DATE) -1
), prev_month_data (
select ps.*
from product_sales ps.
where ps.year = year(CURRENT_DATE) -2
and ps.month = month(CURRENT_DATE) -2
)
select lmd.*
from last_month_data lmd, prev_month_data pmd
where lmd.category_name = pmd.category_name
and lmd.product_name = pmd.product_name
and ( lmd.total_quantity > pmd.total_quantity
or lmd.total_product_sales > pmd.total_product_sales )
order by lmd.year desc, lmd.month desc, lmd.category_name,lmd.product_name, lmd.total_product_sales desc, lmd.total_quantity desc
Lưu ý rằng trong SQLServer, bạn đặt getdate () thay vì CURRENT_DATE.
Bằng cách này, chúng ta có thể trao đổi phần cuối cùng với một lựa chọn truy vấn các phần CTE riêng lẻ để xem kết quả của một phần đã chọn. Do đó, chúng tôi có thể nhanh chóng khắc phục sự cố.
Ngoài ra, bằng cách thực hiện giải thích trên từng phần CTE (và toàn bộ truy vấn), chúng tôi ước tính mức độ hiệu quả của từng phần và / hoặc toàn bộ truy vấn trên các bảng và dữ liệu.
Tương ứng, bạn có thể tối ưu hóa từng phần bằng cách viết lại và / hoặc thêm các chỉ mục thích hợp vào các bảng có liên quan. Sau đó, bạn giải thích toàn bộ truy vấn để xem kế hoạch truy vấn cuối cùng và tiến hành tối ưu hóa nếu cần.
Truy vấn đệ quy sử dụng cấu trúc CTE
Một tính năng hữu ích khác của CTE là tạo truy vấn đệ quy.
Truy vấn SQL đệ quy cho phép bạn đạt được những điều bạn không thể tưởng tượng được với loại SQL này và tốc độ của nó. Bạn có thể giải quyết nhiều vấn đề kinh doanh và thậm chí viết lại một số logic ứng dụng / SQL phức tạp thành một lệnh gọi SQL đệ quy đơn giản tới cơ sở dữ liệu.
Có một số thay đổi nhỏ trong việc tạo truy vấn đệ quy giữa các hệ thống cơ sở dữ liệu. Tuy nhiên, mục tiêu là như nhau.
Một vài ví dụ về tính hữu ích của CTE đệ quy:
- Bạn có thể sử dụng nó để tìm khoảng trống trong dữ liệu.
- Bạn có thể tạo sơ đồ tổ chức.
- Bạn có thể tạo dữ liệu được tính toán trước để sử dụng thêm trong một phần CTE khác
- Cuối cùng, bạn có thể tạo dữ liệu thử nghiệm.
Từ đệ quy nói lên tất cả. Bạn có một truy vấn liên tục gọi chính nó với một số điểm bắt đầu và CỰC QUAN TRỌNG một điểm kết thúc (một lối thoát không an toàn như tôi gọi).
Nếu bạn không có lối ra không an toàn, hoặc công thức đệ quy của bạn vượt quá nó, bạn đang gặp rắc rối lớn. Truy vấn sẽ đi vào một vòng lặp vô hạn dẫn đến CPU rất cao và sử dụng LOG rất cao. Nó sẽ dẫn đến cạn kiệt bộ nhớ và / hoặc bộ nhớ.
Nếu truy vấn của bạn bị hỏng, bạn phải suy nghĩ rất nhanh để vô hiệu hóa nó. Nếu bạn không thể làm như vậy, hãy cảnh báo DBA của bạn ngay lập tức, để chúng ngăn hệ thống cơ sở dữ liệu bị nghẹt, giết chết chuỗi chạy.
Xem ví dụ:
with RECURSIVE mydates (level,nextdate) as (
select 1 level, FROM_UNIXTIME(RAND()*2147483647) nextdate from DUAL
union all
select level+1, FROM_UNIXTIME(RAND()*2147483647) nextdate
from mydates
where level < 1000
)
SELECT nextdate from mydates
);
Ví dụ này là một cú pháp CTE đệ quy MySQL / MariaDB. Với nó, chúng tôi tạo ra một nghìn ngày ngẫu nhiên. Mức độ là bộ đếm của chúng tôi và lối ra không an toàn để thoát khỏi truy vấn đệ quy một cách an toàn.
Như đã chứng minh, dòng 2 là điểm bắt đầu của chúng ta, trong khi dòng 4-5 là lệnh gọi đệ quy với điểm kết thúc trong mệnh đề WHERE (dòng 6). Các dòng 8 và 9 là các lệnh gọi trong việc thực hiện truy vấn đệ quy và truy xuất dữ liệu.
Một ví dụ khác:
DECLARE @today as date;
DECLARE @1stjanprevyear as date;
select @today = DATEADD(DAY, 0, DATEDIFF(DAY, 0, getdate())),
@1stjanprevyear = DATEFROMPARTS(YEAR(GETDATE())-1, 1, 1) ;
WITH DatesCTE as (
SELECT @1stjanprevyear as CalendarDate
UNION ALL
SELECT dateadd(day , 1, CalendarDate) AS CalendarDate FROM DatesCTE
WHERE dateadd (day, 1, CalendarDate) < @today
), MaxMinDates as (
SELECT Max(CalendarDate) MaxDate,Min(CalendarDate) MinDate
FROM DatesCTE
)
SELECT i.*
FROM InvoiceTable i, MaxMinDates t
where i.INVOICE_DATE between t.MinDate and t.MaxDate
OPTION (MAXRECURSION 1000);
Ví dụ này là một cú pháp SQLServer. Ở đây, chúng tôi để phần DatesCTE sản xuất tất cả các ngày từ hôm nay đến ngày 1 tháng 1 của năm trước. Chúng tôi sử dụng nó để trả lại tất cả các Hóa đơn thuộc những ngày đó.
Điểm bắt đầu là @ 1stjanprevyear biến và lối thoát không an toàn @today . Tối đa là 730 ngày. Do đó, tùy chọn đệ quy tối đa được đặt thành 1000 để đảm bảo nó dừng.
Chúng tôi thậm chí có thể bỏ qua MaxMinDates một phần và viết phần cuối cùng, như hình dưới đây. Đây có thể là một cách tiếp cận nhanh hơn, vì chúng ta có mệnh đề WHERE phù hợp.
....
SELECT i.*
FROM InvoiceTable i, DatesCTE t
where i.INVOICE_DATE = t.CalendarDate
OPTION (MAXRECURSION 1000);
Kết luận
Nhìn chung, chúng ta đã thảo luận ngắn gọn và chỉ ra cách chuyển một truy vấn phức tạp thành một truy vấn có cấu trúc CTE. Khi một truy vấn được chia thành các phần CTE khác nhau, bạn có thể sử dụng chúng trong các phần khác và gọi một cách độc lập trong truy vấn SQL cuối cùng cho mục đích gỡ lỗi.
Một điểm quan trọng khác là việc sử dụng CTE làm cho việc gỡ lỗi một truy vấn phức tạp trở nên đơn giản hơn khi nó được chia thành các phần có thể quản lý được, để trả về tập kết quả đúng và mong đợi. Điều quan trọng là nhận ra rằng việc chạy giải thích trên từng phần truy vấn và toàn bộ truy vấn là rất quan trọng để đảm bảo rằng truy vấn và DBMS chạy tối ưu nhất có thể.
Tôi cũng đã minh họa việc viết một truy vấn CTE đệ quy mạnh mẽ / một phần trong việc tạo dữ liệu nhanh chóng để sử dụng thêm trong một truy vấn.
Đáng chú ý, khi viết một truy vấn đệ quy, RẤT cẩn thận KHÔNG để quên lối thoát không an toàn . Đảm bảo kiểm tra kỹ các phép tính được sử dụng trong lối ra không an toàn để tạo ra tín hiệu dừng và / hoặc sử dụng maxrecursion tùy chọn mà SQLServer cung cấp.
Tương tự, các DBMS khác có thể sử dụng cte_max_recursion_depth (MySQL 8.0) hoặc max_recursive_iterations (MariaDB 10.3) là các lối thoát an toàn bổ sung.
Đọc thêm
Mọi thứ bạn cần biết về SQL CTE in One Spot