在真实的 Java 项目开发过程中,SELECT 查询的写法不仅影响接口性能,还直接关系到系统的可维护性、可扩展性和数据库负载。作为后端开发者,我们不能只是“能查出数据就行”,而应遵循最佳实践,写出高效、安全、可读性强的 SQL。
以下是我在多个企业级项目(包括电商、金融、SaaS 平台)中总结出的 PostgreSQL SELECT 命令推荐做法与建议,每一条都附有 原因说明和实际影响。
✅ 一、推荐做法与建议(含原因)
1. ❌ 避免使用 SELECT *,明确指定字段
-- ❌ 不推荐
SELECT * FROM users;
-- ✅ 推荐
SELECT id, username, email, created_at FROM users;
原因:
- 性能更好:减少网络传输和内存占用,尤其当表有
TEXT、JSONB、BYTEA大字段时。 - 可维护性强:接口返回字段清晰,避免因表结构变更导致 DTO 映射错误。
- 防止敏感信息泄露:如
password_hash、profile等字段不会被意外返回。 - JPA/Hibernate 更高效:避免加载不需要的字段,减少实体初始化开销。
📌 Java 开发提示:MyBatis 中使用
resultMap或 Spring Data JPA 中使用Projection可精准控制字段。
2. 🔍 使用 LIMIT 和 OFFSET 时,警惕深度分页性能问题
-- ❌ 深度分页慢(跳过 10 万条)
SELECT id, title FROM articles ORDER BY created_at DESC LIMIT 10 OFFSET 100000;
-- ✅ 推荐:游标分页(Cursor-based Pagination)
SELECT id, title, created_at
FROM articles
WHERE created_at < '2024-01-01 00:00:00' -- 上一页最后一条的 created_at
ORDER BY created_at DESC
LIMIT 10;
原因:
OFFSET N会扫描前 N 条数据,N 越大越慢,时间复杂度 O(N)。- 游标分页基于索引条件查询,性能稳定,接近 O(1)。
- 特别适合内容流、日志、订单列表等大数据量场景。
📌 适用场景:信息流、后台管理列表、审计日志等。
3. 🧱 为查询字段创建合适的索引
-- 查询常用字段:username, email
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_email ON users(email);
-- 复合索引:状态 + 时间(如订单筛选)
CREATE INDEX idx_orders_status_created ON orders(status, created_at DESC);
-- JSONB 字段索引(如 profile->>'nick')
CREATE INDEX idx_users_profile_nick ON users USING GIN ((profile->>'nick'));
原因:
- 没有索引的
WHERE、ORDER BY会导致全表扫描(Seq Scan),性能极差。 - 正确的索引可将查询从 秒级降到毫秒级。
- PostgreSQL 支持多种索引类型(B-tree、GIN、BRIN),应根据数据类型选择。
📌 建议:使用
EXPLAIN ANALYZE检查查询是否走索引。
4. 🧩 优先使用 JOIN 而不是在 Java 中拼接数据
-- ✅ 推荐:一次查询完成关联
SELECT u.username, o.order_no, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.id = '123';
而不是:
// ❌ 不推荐:N+1 查询问题
User user = userRepository.findById("123");
List<Order> orders = orderRepository.findByUserId("123"); // 每次查一次数据库
原因:
- 减少数据库连接次数,降低网络延迟。
- 避免 N+1 查询问题,这是性能杀手。
- 数据库擅长关联计算,Java 层做关联效率低。
📌 Spring Data JPA 提示:使用
@EntityGraph或JOIN FETCH避免 N+1。
5. 🧠 复杂统计逻辑尽量在数据库层完成
-- ✅ 推荐:在数据库中完成分组统计
SELECT
status,
COUNT(*) AS count,
SUM(amount) AS total
FROM orders
GROUP BY status;
而不是:
// ❌ 不推荐:拉全量数据到 Java 层处理
List<Order> orders = orderRepository.findAll();
Map<String, Long> countByStatus = orders.stream()
.collect(groupingBy(Order::getStatus, counting()));
原因:
- 数据库的
GROUP BY使用索引和并行计算,远快于 JavaStream。 - 减少网络传输数据量(只传聚合结果,不传原始数据)。
- 降低应用服务器内存压力。
6. 🧩 使用 CTE(公共表表达式)提升复杂 SQL 可读性
WITH recent_orders AS (
SELECT user_id, SUM(amount) AS total
FROM orders
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY user_id
)
SELECT u.username, ro.total
FROM users u
JOIN recent_orders ro ON u.id = ro.user_id
ORDER BY ro.total DESC;
原因:
- 将复杂查询拆分为逻辑块,类似 Java 中的变量命名。
- 提高 SQL 可读性和可维护性。
- 避免嵌套子查询导致的“括号地狱”。
7. 📊 合理使用窗口函数替代 Java 循环逻辑
-- 计算每个用户的订单排名
SELECT
user_id,
order_no,
amount,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS rn
FROM orders;
原因:
- 窗口函数是数据库原生支持的高效计算。
- 替代 Java 中的
for循环 +Map<UserId, List<Order>>+ 排序。 - 性能更好,代码更简洁。
8. 🧲 使用 CASE WHEN 在 SQL 层做数据转换
SELECT
status,
CASE
WHEN status = 'paid' THEN '已支付'
WHEN status = 'shipped' THEN '已发货'
ELSE '其他'
END AS status_text
FROM orders;
原因:
- 减少 Java 中的
if-else或switch判断。 - 接口返回字段直接可用,无需额外转换。
- 适合状态、类型等枚举字段的展示层转换。
9. 🔐 避免在 WHERE 中使用函数包裹字段(破坏索引)
-- ❌ 破坏索引,无法使用 created_at 索引
SELECT * FROM users WHERE DATE(created_at) = '2024-01-01';
-- ✅ 推荐:使用范围查询
SELECT * FROM users
WHERE created_at >= '2024-01-01'
AND created_at < '2024-01-02';
原因:
WHERE DATE(created_at) = ...会导致全表扫描。- 正确写法可利用
B-tree索引,性能提升百倍。
10. 🧪 开发阶段使用 EXPLAIN ANALYZE 分析慢查询
EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = '123' AND status = 'paid';
原因:
- 查看是否走索引(
Index ScanvsSeq Scan) - 发现性能瓶颈(如
Nested Loop、Hash Join成本) - 优化 SQL 和索引设计
📌 建议:在 DataGrip 中开启“Auto Explain”功能,自动分析执行计划。
11. 🧩 使用 JSONB 字段存储灵活数据,但查询要合理
-- ✅ 合理查询 JSONB
SELECT * FROM users WHERE profile->>'age' = '28';
-- ⚠️ 避免频繁查询深层嵌套字段
-- 建议:将高频查询字段提升为独立列(如 age INT)
原因:
JSONB适合存储配置、扩展字段。- 但频繁查询
JSONB字段会影响性能,建议为常用字段建GIN索引或拆分为独立列。
12. 🧩 生产环境避免在 SELECT 中使用 NOW()、CURRENT_USER() 等动态函数
-- ❌ 不推荐:在查询中使用 NOW(),影响缓存和计划复用
SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '1 hour';
-- ✅ 推荐:由应用传参
-- PreparedStatement: WHERE created_at > ?
原因:
- 动态函数导致每次查询的执行计划不同,无法复用执行计划缓存。
- 使用参数化查询更安全(防 SQL 注入)、更高效。
✅ 总结:PostgreSQL SELECT 查询推荐做法一览表
| 建议 | 原因 | 适用场景 |
|---|---|---|
✅ 明确字段,不用 * | 减少传输、避免敏感字段泄露 | 所有查询 |
✅ 使用 LIMIT/OFFSET 或游标分页 | 避免深度分页性能问题 | 列表接口 |
| ✅ 为查询字段建索引 | 避免全表扫描 | 高频查询字段 |
✅ 用 JOIN 替代 N+1 查询 | 减少数据库访问次数 | 关联查询 |
| ✅ 统计在数据库层完成 | 性能高、减少数据传输 | 报表、分析 |
| ✅ 使用 CTE 提升可读性 | 结构清晰,易于维护 | 复杂查询 |
| ✅ 使用窗口函数 | 替代 Java 循环 | 排名、累计 |
✅ 用 CASE WHEN 做转换 | 减少 Java 判断 | 状态展示 |
✅ 避免 WHERE 中用函数 | 保持索引可用 | 时间、字符串处理 |
✅ 用 EXPLAIN ANALYZE 优化 | 定位性能瓶颈 | 慢查询排查 |
✅ JSONB 查询建索引 | 提升半结构化查询性能 | 扩展字段 |
| ✅ 参数化查询 | 安全、可缓存 | 所有动态条件 |
🎁 附加建议:Java 开发者如何落地这些实践?
- 与 DBA 或架构师协作:定期 review 慢查询日志。
- 使用 Spring Boot Actuator + Prometheus:监控 SQL 执行时间。
- 代码审查中加入 SQL 检查项:禁止
SELECT *、N+1 查询。 - 使用 QueryDSL 或 jOOQ:生成类型安全、可优化的 SQL。
- 定期执行
ANALYZE TABLE:更新统计信息,帮助优化器生成更好执行计划。
882

被折叠的 条评论
为什么被折叠?



