Hôm nay, để kết thúc chuyến phiêu lưu hoành tráng với PL/pgSQL này, tụi mình sẽ phân tích những lỗi phổ biến nhất mà bạn có thể gặp phải khi debug và tối ưu hóa function và procedure. Biết mấy lỗi này sẽ giúp bạn không chỉ tránh được rắc rối về sau mà còn xử lý bug hiệu quả hơn nếu lỡ có dính phải.
Các lỗi thường gặp khi debug và tối ưu hóa
1. Dùng biến sai cách
Một trong những lỗi hay gặp nhất khi viết và debug function trong PL/pgSQL là khai báo hoặc dùng biến không đúng. Ví dụ, bạn quên không chỉ rõ kiểu biến hoặc bị rối với giá trị truyền qua parameter. Xem thử thực tế nó như nào nhé:
CREATE OR REPLACE FUNCTION calculate_discount(order_total NUMERIC)
RETURNS NUMERIC AS $$
DECLARE
discount_rate NUMERIC;
BEGIN
-- Ối! Quên khởi tạo biến discount_rate rồi
RETURN order_total * discount_rate;
END;
$$ LANGUAGE plpgsql;
Khi gọi function này bạn sẽ bị lỗi liên quan đến việc dùng NULL trong phép tính, vì biến discount_rate ban đầu chưa được gán giá trị.
Làm sao để tránh:
- Luôn gán giá trị mặc định cho biến khi khai báo:
DECLARE
discount_rate NUMERIC := 0.1; -- Giá trị mặc định
- Kiểm tra giá trị biến bằng
RAISE NOTICEđể chắc chắn nó đúng như mong đợi:
RAISE NOTICE 'Giá trị discount_rate: %', discount_rate;
2. Thiếu logging lỗi
Một vấn đề phổ biến khác là không có cơ chế logging. Nếu có gì đó sai mà bạn không log lại quá trình chạy function, thì giống như mò kim đáy bể, nhất là khi bạn còn chưa chắc có cái kim ở đó không nữa.
Ví dụ function không có logging:
CREATE OR REPLACE FUNCTION process_order(order_id INT)
RETURNS VOID AS $$
BEGIN
-- Một logic xử lý đơn hàng khá phức tạp
UPDATE orders SET status = 'processed' WHERE id = order_id;
END;
$$ LANGUAGE plpgsql;
Giả sử order_id truyền vào bị sai thì sao? Hoặc bản ghi trong bảng orders không tồn tại thì sao?
Làm sao để tránh: Thêm RAISE NOTICE hoặc RAISE EXCEPTION để log các bước quan trọng:
CREATE OR REPLACE FUNCTION process_order(order_id INT)
RETURNS VOID AS $$
BEGIN
-- Log dữ liệu đầu vào
RAISE NOTICE 'Xử lý đơn hàng với ID %', order_id;
-- Logic xử lý phức tạp
UPDATE orders SET status = 'processed' WHERE id = order_id;
-- Log kết quả
RAISE NOTICE 'Đã cập nhật trạng thái đơn hàng cho ID %', order_id;
END;
$$ LANGUAGE plpgsql;
Giờ bạn sẽ dễ dàng lần ra lỗi nhờ các thông báo được in ra.
3. Bỏ qua hiệu năng của query
Đây là một trong những "kẻ thù" lớn nhất của dân làm database. Ví dụ, bạn viết function nhìn thì ổn nhưng chạy lại siêu chậm. Một trong những lý do chính là thiếu index hoặc plan query không tối ưu.
Ví dụ query chậm:
CREATE OR REPLACE FUNCTION get_large_orders()
RETURNS TABLE(order_id INT, total NUMERIC) AS $$
BEGIN
RETURN QUERY
SELECT id, total FROM orders WHERE total > 1000;
END;
$$ LANGUAGE plpgsql;
Nếu cột total trong bảng orders không có index, query sẽ quét hết bảng, rất tốn thời gian.
Làm sao để tránh:
- Dùng
EXPLAIN ANALYZEđể kiểm tra hiệu năng query:
EXPLAIN ANALYZE SELECT id, total FROM orders WHERE total > 1000;
- Tạo index cho các cột hay dùng:
CREATE INDEX idx_orders_total ON orders(total);
4. Dùng sai mức isolation của transaction
Khi chạy các procedure phức tạp, đôi khi lỗi xảy ra do hiểu nhầm về mức isolation của transaction. Ví dụ, hai transaction cùng lúc update một bản ghi có thể gây ra deadlock.
Ví dụ có thể gây deadlock:
BEGIN;
UPDATE orders SET status = 'processed' WHERE id = 1;
-- Đợi lock từ transaction khác
UPDATE inventory SET stock = stock - 1 WHERE product_id = 100;
COMMIT;
Nếu transaction khác thực hiện các thao tác này theo thứ tự ngược lại, bạn sẽ bị khóa chéo.
Làm sao để tránh:
- Suy nghĩ kỹ thứ tự thao tác và luôn tuân thủ nó.
- Dùng mức isolation
SERIALIZABLEnếu cần thiết.
5. Không xử lý lỗi
Xử lý lỗi không chỉ là best practice mà còn giúp code của bạn ổn định hơn. Ví dụ, đoạn code sau không có xử lý lỗi:
CREATE OR REPLACE FUNCTION add_order(order_id INT)
RETURNS VOID AS $$
BEGIN
INSERT INTO orders (id, status) VALUES (order_id, 'new');
END;
$$ LANGUAGE plpgsql;
Nếu order_id đã tồn tại, bạn sẽ bị lỗi duplicate key value violates unique constraint.
Làm sao để tránh: Dùng block xử lý exception:
CREATE OR REPLACE FUNCTION add_order(order_id INT)
RETURNS VOID AS $$
BEGIN
INSERT INTO orders (id, status) VALUES (order_id, 'new');
EXCEPTION WHEN unique_violation THEN
RAISE NOTICE 'Đơn hàng với ID % đã tồn tại!', order_id;
END;
$$ LANGUAGE plpgsql;
Ví dụ lỗi và cách sửa
Lỗi 1: Query chạy chậm do thiếu index
Tình huống: Bạn có query lọc bảng theo một cột nhưng cột đó lại không có index.
Cách sửa: Tạo index cho cột phù hợp.
Lỗi 2: Logic function rối và khó debug
Tình huống: Function chứa quá nhiều logic và không chia nhỏ thành subfunction.
Cách sửa: Chia nhỏ function phức tạp thành các subfunction nhỏ hơn. Như vậy code sẽ dễ đọc và dễ debug hơn.
Lỗi 3: Dùng RAISE EXCEPTION sai cách
Tình huống: RAISE EXCEPTION được dùng cho mọi lỗi, kể cả lỗi nhỏ.
Cách sửa: Dùng RAISE NOTICE cho thông báo thông tin và chỉ dùng RAISE EXCEPTION cho lỗi nghiêm trọng.
RAISE NOTICE 'Mọi thứ ổn — đã hoàn thành bước hiện tại của function.';
RAISE EXCEPTION 'Có gì đó hỏng rồi! Kiểm tra lại parameter đầu vào nhé.';
Khuyến nghị để tránh lỗi
- Thêm logging: ở các bước quan trọng trong function, dùng
RAISE NOTICEđể theo dõi tiến trình. - Test function thường xuyên: dùng dữ liệu test để kiểm tra function và procedure.
- Giữ code dễ đọc: chia nhỏ function phức tạp thành subfunction và procedure nhỏ hơn.
- Phân tích hiệu năng: dùng
EXPLAIN ANALYZEđể đảm bảo query chạy hiệu quả. - Chuẩn bị cho bất ngờ: luôn thêm block xử lý exception để xử lý lỗi.
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION 'Đã xảy ra lỗi không lường trước: %', SQLERRM;
Như vậy bạn sẽ tự tin xử lý lỗi và còn phòng tránh được lỗi về sau nữa.
GO TO FULL VERSION