7 điểm bởi GN⁺ 2024-09-26 | 2 bình luận | Chia sẻ qua WhatsApp

Mục lục

Định dạng/khả năng đọc

  • Dùng dấu phẩy ở đầu khi phân tách các trường
  • Dùng giá trị giả trong mệnh đề WHERE
  • Thụt lề mã hợp lý
  • Cân nhắc CTE khi viết truy vấn phức tạp

Tính năng hữu ích

  • Dùng toán tử :: để chuyển đổi kiểu dữ liệu
  • Tận dụng anti join
  • Dùng QUALIFY để lọc hàm cửa sổ
  • Có thể dùng GROUP BY theo vị trí cột

Những cạm bẫy cần tránh

  • Cẩn thận khi dùng NOT IN với giá trị NULL
  • Đổi tên trường tính toán để tránh mơ hồ
  • Chỉ rõ mỗi cột thuộc về bảng nào
  • Hiểu thứ tự thực thi
  • Thêm chú thích vào mã
  • Đọc toàn bộ tài liệu

Định dạng/khả năng đọc

Dùng dấu phẩy ở đầu khi phân tách các trường

  • Khi phân tách các trường trong mệnh đề SELECT, dùng dấu phẩy ở đầu có thể giúp phân biệt rõ các cột mới
  • Dấu phẩy ở đầu cung cấp tín hiệu trực quan giúp dễ phát hiện việc thiếu dấu phẩy
SELECT
  employee_id,
  employee_name,
  job,
  salary
FROM employees;

Dùng giá trị giả trong mệnh đề WHERE

  • Có thể dùng giá trị giả trong mệnh đề WHERE để thêm và bỏ điều kiện một cách linh hoạt
SELECT *
FROM employees
WHERE 1=1 -- giá trị giả
  AND job IN ('Clerk', 'Manager')
  AND dept_no != 5;

Thụt lề mã hợp lý

  • Thụt lề mã để tăng khả năng đọc và giúp đồng nghiệp cũng như chính bạn trong tương lai dễ hiểu hơn
-- ví dụ tệ:
SELECT
  timeslot_date,
  timeslot_channel,
  overnight_fta_share,
  IFF(DATEDIFF(DAY, timeslot_date, CURRENT_DATE()) > 7,
    LAG(overnight_fta_share, 1) OVER (PARTITION BY timeslot_date, timeslot_channel ORDER BY timeslot_activity),
    NULL) AS C7_fta_share,
  IFF(DATEDIFF(DAY, timeslot_date, CURRENT_DATE()) >= 29,
    LAG(overnight_fta_share, 2) OVER (PARTITION BY timeslot_date, timeslot_channel ORDER BY timeslot_activity),
    NULL) AS C28_fta_share
FROM timeslot_data;

-- ví dụ tốt:
SELECT
  timeslot_date,
  timeslot_channel,
  overnight_fta_share,
  IFF(DATEDIFF(DAY, timeslot_date, CURRENT_DATE()) > 7,
    LAG(overnight_fta_share, 1) OVER (PARTITION BY timeslot_date, timeslot_channel ORDER BY timeslot_activity),
    NULL) AS C7_fta_share,
  IFF(DATEDIFF(DAY, timeslot_date, CURRENT_DATE()) >= 29,
    LAG(overnight_fta_share, 2) OVER (PARTITION BY timeslot_date, timeslot_channel ORDER BY timeslot_activity),
    NULL) AS C28_fta_share
FROM timeslot_data;

Cân nhắc CTE khi viết truy vấn phức tạp

  • Thay vì lồng các inline view, có thể dùng common table expression (CTE) để tăng khả năng đọc và tổ chức mã tốt hơn
-- dùng inline view:
SELECT
  vhs.movie,
  vhs.vhs_revenue,
  cs.cinema_revenue
FROM
  (SELECT
    movie_id,
    SUM(ticket_sales) AS cinema_revenue
  FROM tickets
  GROUP BY movie_id) AS cs
INNER JOIN
  (SELECT
    movie,
    movie_id,
    SUM(revenue) AS vhs_revenue
  FROM blockbuster
  GROUP BY movie, movie_id) AS vhs
ON cs.movie_id = vhs.movie_id;

-- dùng CTE:
WITH cinema_sales AS (
  SELECT
    movie_id,
    SUM(ticket_sales) AS cinema_revenue
  FROM tickets
  GROUP BY movie_id
),
vhs_sales AS (
  SELECT
    movie,
    movie_id,
    SUM(revenue) AS vhs_revenue
  FROM blockbuster
  GROUP BY movie, movie_id
)
SELECT
  vhs.movie,
  vhs.vhs_revenue,
  cs.cinema_revenue
FROM cinema_sales AS cs
INNER JOIN vhs_sales AS vhs
ON cs.movie_id = vhs.movie_id;

Tính năng hữu ích

Dùng toán tử :: để chuyển đổi kiểu dữ liệu

  • Trong một số RDBMS, có thể dùng toán tử :: để chuyển giá trị sang kiểu dữ liệu khác
SELECT CAST('5' AS INTEGER); -- dùng hàm CAST
SELECT '5'::INTEGER; -- dùng cú pháp ::

Tận dụng anti join

  • Anti join rất hữu ích khi cần trả về các hàng chỉ tồn tại ở một bảng
  • Cũng có thể dùng subquery, nhưng nhìn chung anti join thường nhanh hơn
-- anti join:
SELECT
  video_content.*
FROM video_content
LEFT JOIN archive
ON video_content.series_id = archive.series_id
WHERE archive.series_id IS NULL;

-- subquery:
SELECT
  *
FROM video_content
WHERE series_id NOT IN (SELECT DISTINCT series_id FROM archive);

-- correlated subquery:
SELECT
  *
FROM video_content
WHERE NOT EXISTS (
  SELECT 1
  FROM archive a
  WHERE a.series_id = vc.series_id
);

-- EXCEPT:
SELECT series_id
FROM video_content
EXCEPT
SELECT series_id
FROM archive;

Dùng QUALIFY để lọc hàm cửa sổ

  • Có thể dùng QUALIFY để lọc kết quả của hàm cửa sổ
  • Hữu ích để giảm số dòng mã
-- dùng QUALIFY:
SELECT
  product,
  market,
  SUM(revenue) AS market_revenue
FROM sales
GROUP BY product, market
QUALIFY DENSE_RANK() OVER (PARTITION BY product ORDER BY SUM(revenue) DESC) <= 10
ORDER BY product, market_revenue;

-- không dùng QUALIFY:
SELECT
  product,
  market,
  market_revenue
FROM (
  SELECT
    product,
    market,
    SUM(revenue) AS market_revenue,
    DENSE_RANK() OVER (PARTITION BY product ORDER BY SUM(revenue) DESC) AS market_rank
  FROM sales
  GROUP BY product, market
)
WHERE market_rank <= 10
ORDER BY product, market_revenue;

Có thể dùng GROUP BY theo vị trí cột

  • Có thể dùng vị trí cột thay cho tên cột để GROUP BY hoặc ORDER BY
  • Hữu ích cho truy vấn tạm thời, nhưng trong mã production thì luôn nên tham chiếu bằng tên cột
SELECT
  dept_no,
  SUM(salary) AS dept_salary
FROM employees
GROUP BY 1 -- dept_no là cột đầu tiên trong mệnh đề SELECT
ORDER BY 2 DESC;

Những cạm bẫy cần tránh

Cẩn thận khi dùng NOT IN với giá trị NULL

  • NOT IN sẽ không hoạt động khi có giá trị NULL
  • Thay vào đó nên dùng NOT EXISTS
INSERT INTO departments (id)
VALUES (1), (2), (NULL);

-- không hoạt động vì giá trị NULL
SELECT *
FROM employees
WHERE department_id NOT IN (SELECT DISTINCT id FROM departments);

-- cách khắc phục
SELECT *
FROM employees e
WHERE NOT EXISTS (
  SELECT 1
  FROM departments d
  WHERE d.id = e.department_id
);

Đổi tên trường tính toán để tránh mơ hồ

  • Nếu đổi tên một trường tính toán thành tên cột đã có sẵn, có thể phát sinh hành vi không mong muốn
INSERT INTO products (product, revenue)
VALUES ('Shark', 100), ('Robot', 150), ('Alien', 90);

-- hàm cửa sổ xếp sản phẩm 'Robot' ở hạng 1
SELECT
  product,
  CASE product WHEN 'Robot' THEN 0 ELSE revenue END AS revenue,
  RANK() OVER (ORDER BY revenue DESC)
FROM products;

Chỉ rõ mỗi cột thuộc về bảng nào

  • Trong truy vấn phức tạp, việc chỉ rõ mỗi cột thuộc về bảng nào sẽ giúp dễ truy vết vấn đề hơn
SELECT
  vc.video_id,
  vc.series_name,
  metadata.season,
  metadata.episode_number
FROM video_content AS vc
INNER JOIN video_metadata AS metadata
ON vc.video_id = metadata.video_id;

Hiểu thứ tự thực thi

  • Lời khuyên quan trọng nhất cho người học SQL là phải hiểu thứ tự thực thi
  • Khi hiểu thứ tự thực thi, cách bạn viết truy vấn sẽ thay đổi hoàn toàn

Thêm chú thích vào mã

  • Khi viết mã, nên thêm chú thích giải thích lý do
  • Đồng nghiệp và chính bạn trong tương lai sẽ biết ơn điều đó
SELECT
  video_content.*
FROM video_content
LEFT JOIN archive -- CMS mới không thể xử lý định dạng video lưu trữ
ON video_content.series_id = archive.series_id
WHERE archive.series_id IS NULL;

Đọc toàn bộ tài liệu

  • Đọc kỹ toàn bộ tài liệu sẽ giúp tránh những vấn đề ngoài dự kiến
  • Chỉ mất vài phút để đọc tài liệu, và điều đó có thể giúp giải quyết các vấn đề bất ngờ
-- nếu đọc tài liệu kỹ hơn thì đã có thể xử lý vấn đề NULL
SELECT COALESCE(GREATEST(signup_date, consumption_date), signup_date, consumption_date);

-- có thể dùng hàm GREATEST_IGNORE_NULLS
SELECT GREATEST_IGNORE_NULLS(signup_date, consumption_date);

Tổng kết của GN⁺

  • Bài viết này cung cấp nhiều mẹo và thủ thuật khác nhau để viết SQL hiệu quả hơn và dễ đọc hơn
  • Có nhiều thông tin hữu ích cho cả người mới học SQL lẫn các nhà phân tích dữ liệu đã có kinh nghiệm
  • Đặc biệt, việc dùng CTE khi viết truy vấn phức tạp, tận dụng anti join và dùng QUALIFY sẽ rất hữu ích trong công việc thực tế
  • Điều quan trọng là hình thành thói quen hiểu thứ tự thực thi của SQL, thêm chú thích vào mã và đọc tài liệu cẩn thận
  • Các công cụ khác có tính năng tương tự gồm PostgreSQL, MySQL, Oracle

2 bình luận

 
hiyama 2024-09-26

Dấu phẩy đặt ở đầu trong bài viết này lại đều được viết thành dấu phẩy đặt ở cuối. Trong bản gốc, chúng được nhập theo kiểu đặt ở đầu.

-- Good:  
SELECT   
timeslot_date  
, timeslot_channel   
, overnight_fta_share  
, IFF(DATEDIFF(DAY, timeslot_date, CURRENT_DATE()) &gt; 7, -- First argument of IFF.  
	LAG(overnight_fta_share, 1) OVER (PARTITION BY timeslot_date, timeslot_channel ORDER BY timeslot_activity), -- Second argument of IFF.  
		NULL) AS C7_fta_share -- Third argument of IFF.  
, IFF(DATEDIFF(DAY, timeslot_date, CURRENT_DATE()) &gt;= 29,   
		LAG(overnight_fta_share, 2) OVER (PARTITION BY timeslot_date, timeslot_channel ORDER BY timeslot_activity),   
			NULL) AS C28_fta_share  
FROM timeslot_data  
;  
 
GN⁺ 2024-09-26
Ý kiến trên Hacker News
  • Sự cần thiết phải hiểu rõ máy chủ DB và thường xuyên kiểm tra kế hoạch truy vấn

    • Nhiều trường hợp EXISTS nhanh hơn IN
    • NOT EXISTSEXCEPT hoạt động khác nhau khi xử lý giá trị NULL
    • Khuyến nghị dùng cột subquery thay vì table join
    • Cần tránh quét toàn bảng và thêm chỉ mục
    • Khi lọc theo biểu thức có thể dùng cột tính toán và chỉ mục
    • UNION ALL có thể nhanh hơn OR
    • Có thể ép thứ tự lọc thông qua subquery JOIN
  • Mẹo xử lý stored procedure phức tạp

    • Sao chép bảng thường trực sang bảng tạm và chỉ lọc các hàng cần thiết
    • Thao tác trên bảng tạm
    • Cập nhật bảng thường trực trong transaction, rollback nếu có lỗi
    • Cần thận trọng khi làm việc với bảng từ xa, khuyến nghị sao chép sang bảng tạm rồi xử lý
    • Vì kế hoạch truy vấn có thể trở nên rối rắm nên hãy chia nhỏ thành các bước nhỏ để xử lý
    • Luôn kiểm tra kế hoạch truy vấn
  • Ý kiến về tính dễ đọc của mã

    • Hai ví dụ đầu hy sinh tính dễ đọc để tăng sự dễ dàng khi viết
    • Ví dụ cuối không cho thấy thụt lề mang lại nhiều hiệu quả
  • Đề xuất dùng cú pháp FROM-first và piping của SQL

    • Trải nghiệm dùng Kusto query language là một bước tiến lớn
  • Mẹo liên quan đến Anti Join

    • Khuyến nghị dùng EXISTS, có lợi khi kiểm tra sự tồn tại của hàng trong subquery dựa trên điều kiện
  • Ưu điểm của việc dùng dấu phẩy ở đầu dòng trong câu lệnh SELECT

    • Có thể chú thích từng dòng riêng lẻ
    • Cải thiện khả năng đọc nhờ cách thụt lề mã
  • Trong MSSQL, khi dùng chú thích nên dùng /* */ thay vì --

    • Vì query store lưu truy vấn mà không có xuống dòng
  • Khuyến nghị sử dụng window function

  • Tranh luận về việc dùng 1=1 trong mệnh đề WHERE

  • Giới thiệu AI2sql

    • Có thể tạo truy vấn SQL từ prompt tiếng Anh dạng văn bản thường
    • Hữu ích khi viết các truy vấn phức tạp