SELECT 查询推荐做法总结

【投稿赢 iPhone 17】「我的第一个开源项目」故事征集:用代码换C位出道! 10w+人浏览 1.6k人参与

在真实的 Java 项目开发过程中,SELECT 查询的写法不仅影响接口性能,还直接关系到系统的可维护性、可扩展性和数据库负载。作为后端开发者,我们不能只是“能查出数据就行”,而应遵循最佳实践,写出高效、安全、可读性强的 SQL。

以下是我在多个企业级项目(包括电商、金融、SaaS 平台)中总结出的 PostgreSQL SELECT 命令推荐做法与建议,每一条都附有 原因说明和实际影响


✅ 一、推荐做法与建议(含原因)

1. ❌ 避免使用 SELECT *,明确指定字段

-- ❌ 不推荐
SELECT * FROM users;

-- ✅ 推荐
SELECT id, username, email, created_at FROM users;

原因:

  • 性能更好:减少网络传输和内存占用,尤其当表有 TEXTJSONBBYTEA 大字段时。
  • 可维护性强:接口返回字段清晰,避免因表结构变更导致 DTO 映射错误。
  • 防止敏感信息泄露:如 password_hashprofile 等字段不会被意外返回。
  • JPA/Hibernate 更高效:避免加载不需要的字段,减少实体初始化开销。

📌 Java 开发提示:MyBatis 中使用 resultMap 或 Spring Data JPA 中使用 Projection 可精准控制字段。


2. 🔍 使用 LIMITOFFSET 时,警惕深度分页性能问题

-- ❌ 深度分页慢(跳过 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'));

原因:

  • 没有索引的 WHEREORDER 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 提示:使用 @EntityGraphJOIN 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 使用索引和并行计算,远快于 Java Stream
  • 减少网络传输数据量(只传聚合结果,不传原始数据)。
  • 降低应用服务器内存压力。

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-elseswitch 判断。
  • 接口返回字段直接可用,无需额外转换。
  • 适合状态、类型等枚举字段的展示层转换。

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 Scan vs Seq Scan
  • 发现性能瓶颈(如 Nested LoopHash 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 开发者如何落地这些实践?

  1. 与 DBA 或架构师协作:定期 review 慢查询日志。
  2. 使用 Spring Boot Actuator + Prometheus:监控 SQL 执行时间。
  3. 代码审查中加入 SQL 检查项:禁止 SELECT *、N+1 查询。
  4. 使用 QueryDSL 或 jOOQ:生成类型安全、可优化的 SQL。
  5. 定期执行 ANALYZE TABLE:更新统计信息,帮助优化器生成更好执行计划。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值