🎯 本月只剩几天支持 SQLTest.online,帮助我们让 SQL 练习对每个人都免费。 今天捐赠 →
SQL 代码已复制到剪贴板

SQL 最佳实践:可读且易于维护的查询

第 10.1 课 · 阅读时间:约 8 分钟

SQL 最佳实践是一组格式化和命名习惯,能让查询易于阅读、评审和修改。在本课中,你将学习如何排列子句和列,如何为表、列和别名命名,什么时候注释有帮助,哪些习惯能防止错误,以及如何把一条写在一行里的查询变成团队能够维护的代码。

在上一个模块中,你使用了视图和临时表,查询也增长到包含多个连接和多个步骤。到了这个规模,代码质量和正确性同样重要:SQL 被阅读的次数远多于被编写的次数,读者包括评审时的同事,也包括几周后的你自己。

结构良好的代码能让错误显而易见、加快评审,并让查询更容易扩展。这些规则都不会改变数据库的行为,它们改变的是人理解查询的速度。

SQL 代码质量:每行一列的 SELECT 格式、用 snake_case 代替 camelCase 的命名,以及解释过滤条件用意的注释

为什么代码风格很重要

5–10 行的查询一眼就能看懂。包含大量 JOIN、子查询或 CTE 的报表则不然,而一致的风格可以帮助你:

  • 更快发现错误:在整洁的代码中,错误的过滤条件或遗漏的连接条件会很显眼。
  • 安全地扩展查询:新的列或条件都有明确的位置。
  • 团队协作:大家用同样的布局阅读代码,评审关注逻辑而不是风格。

格式化与缩进

SQL 不在意空格和关键字的大小写,所以布局由你决定。下面是最常用的约定。

关键字大写

把 SQL 关键字(SELECT、FROM、WHERE、JOIN、GROUP BY)写成大写,名称写成小写。这样查询的结构就能从它所使用的数据中凸显出来。

-- 难以浏览
select title, rental_rate from film where rating = 'G';

-- 更容易浏览
SELECT title, rental_rate
FROM film
WHERE rating = 'G';

结果:两条查询返回同样的 178 部电影,只是布局不同。

每个子句一行

每个主要子句都另起一行。如果 SELECT 或 GROUP BY 中列出很多列,就把每一列单独放一行并缩进:

SELECT
    customer_id,
    first_name,
    last_name,
    email
FROM customer
WHERE active = 1
ORDER BY last_name;

每行一列时,增加或删除一列只会改动一行,代码评审中的 diff 也只显示这一行。


命名约定

小写与 snake_case

事实上的标准是使用小写名称,并用下划线分隔单词:customer_orders、total_amount,而不是 CustomerOrders 或 TotalAmount。不同数据库对标识符大小写的处理不同:PostgreSQL 会把不带引号的名称转换为小写,而 Linux 上的 MySQL 对表名区分大小写。snake_case 可以避免这些意外。

统一的前缀

有些团队用前缀标记对象类型,例如视图用 v_(v_active_customers),临时表用 tmp_(tmp_monthly_report)。Sakila 不使用前缀:它的视图叫 customer_list、film_list、sales_by_film_category。两种做法都可以,关键是整个数据库只采用一种规则。

SELECT category, total_sales
FROM sales_by_film_category
ORDER BY total_sales DESC
LIMIT 3;

结果:销售额最高的三个分类:Sports、Sci-Fi 和 Animation。


清晰的别名

表别名

使用简短但有意义的别名,例如表名的首字母。t1、t2、a、b 这样的别名会迫使读者去查每个别名代表什么:

-- 不清楚:t1 和 t2 是什么?
SELECT t1.first_name, t2.amount
FROM customer t1
JOIN payment t2 ON t1.customer_id = t2.customer_id;

-- 清楚
SELECT c.first_name, p.amount
FROM customer c
JOIN payment p ON c.customer_id = p.customer_id;

计算列的名称

用 AS 给每个聚合和表达式命名。报表中名为 COUNT(*) 的列很难在应用程序或电子表格中使用:

SELECT
    c.name AS category_name,
    COUNT(*) AS film_count,
    AVG(f.replacement_cost) AS avg_replacement_cost
FROM film f
JOIN film_category fc ON f.film_id = fc.film_id
JOIN category c ON fc.category_id = c.category_id
GROUP BY c.name
ORDER BY film_count DESC;

结果:16 个分类,列名清晰;Sports 的电影最多,共 74 部。


注释

当原因无法从代码本身看出时,注释用来解释代码为什么这样做:业务规则、不寻常的过滤条件、已知的数据问题。

  • 单行注释(--)解释某个过滤条件或公式。
  • 块注释(/* ... */)描述脚本的用途。
/*
  报表:2005 年 7 月客户消费
  用于:每月营销邮件
*/
SELECT
    customer_id,
    SUM(amount) AS july_spent
FROM payment
WHERE payment_date >= '2005-07-01'  -- 整个月,
  AND payment_date < '2005-08-01'   -- 包括 7 月 31 日零点之后
GROUP BY customer_id
ORDER BY july_spent DESC;

结果:599 位客户及其 2005 年 7 月的消费额。

不要注释代码已经说明的内容:在 SELECT customer_id 上方写 -- 选择 customer id 只会增加噪音,而过时的注释比没有注释更糟糕。


让查询更安全的习惯

  • 在报表、视图和应用代码中列出具体的列,而不是使用 SELECT *。这样表中新增的列就不会改变或拖慢你的结果。
  • 使用显式的 JOIN ... ON,而不是在 FROM 中用逗号列出多张表。连接条件紧挨着表,更不容易被遗漏。
  • 对日期时间列,优先使用半开区间(>= start AND < end)而不是 BETWEEN,如上面的示例所示。
  • 约定一种风格并让它自动化。像 SQLFluff 这样的格式化工具和 linter 会替你应用规则,评审时就不必再争论空格。

实践示例:整理一条混乱的查询

整理前:全部写在一行,计算列没有名称:

select f.title,c.name,count(r.rental_id) from film f join film_category fc on f.film_id=fc.film_id join category c on fc.category_id=c.category_id join inventory i on f.film_id=i.film_id join rental r on i.inventory_id=r.inventory_id group by f.title,c.name having count(r.rental_id)>30 order by count(r.rental_id) desc;

整理后:每个子句一行,连接对齐,列有名称:

SELECT
    f.title,
    c.name AS category_name,
    COUNT(r.rental_id) AS rental_count
FROM film f
JOIN film_category fc ON f.film_id = fc.film_id
JOIN category c       ON fc.category_id = c.category_id
JOIN inventory i      ON f.film_id = i.film_id
JOIN rental r         ON i.inventory_id = r.inventory_id
GROUP BY f.title, c.name
HAVING COUNT(r.rental_id) > 30
ORDER BY rental_count DESC;

结果:两个版本都返回同样的 16 部被租借超过 30 次的电影,排在首位的是 BUCKET BROTHERHOOD,共 34 次。

在第二个版本中,一眼就能看出连接了哪些表、如何连接、统计了什么,以及结果如何过滤和排序。


要点:

  • 关键字用大写,名称用小写的 snake_case。
  • 每个子句另起一行,较长的列列表每行一列。
  • 使用有意义的表别名,并用 AS 为每个计算列命名。
  • 注释说明“为什么”,而不是“做什么”:业务规则和不明显的过滤条件。
  • 列出具体的列而不是 SELECT *,使用显式的 JOIN ... ON 和半开日期区间。
  • 团队约定统一的风格,并用格式化工具或 linter 来执行。

常见问题

SQL 关键字必须大写吗?

不必。数据库接受任意大小写的关键字。大写关键字是一种约定,能让长查询的结构更清晰;最重要的是整个团队使用同一种风格。

什么样的 SQL 代码易于维护?

一致的格式、清晰的名称和别名、有名称的计算列,以及在逻辑不明显处的简短注释。这样的代码更容易评审、修改和调试,即使不是原作者也能轻松上手。

为什么在生产查询中要避免 SELECT *?

SELECT * 会返回所有列,包括之后新加入表中的列。这可能破坏依赖固定列集合的应用程序,传输不必要的数据,并妨碍使用覆盖索引。请列出你需要的列。

如何整理一条混乱的 SQL 查询?

把它拆成各自独占一行的子句(SELECT、FROM、JOIN、WHERE、GROUP BY、ORDER BY),为列列表加上缩进,替换含义不清的别名,为计算列命名,并在需要解释的逻辑处添加注释。分别运行旧版本和新版本,确认结果一致。