一些SQL标准优化方案,这是 SQL Server 必考级、实战级 的东西
SQL Server BETWEEN 日期范围优化(实战规范版)
适用场景:Orders / Products / 报表 / 对账
级别:SQL Server 必考级 · 生产级 · 架构规范级
一、为什么 BETWEEN 日期 AND 日期 容易慢、还容易错
❌ 常见写法(问题很多)
WHERE OrderDate BETWEEN '2026-01-09'
AND '2026-01-09 23:59:59.000'
问题 1️⃣:精度陷阱
-
datetime / datetime2具有 毫秒 / 100ns 精度 -
实际数据可能是:
23:59:59.99723:59:59.9999999
👉 会被 BETWEEN 漏掉
问题 2️⃣:参数化不友好
- 执行计划难以复用
- ORM / 存储过程容易抖动(Plan Cache Pollution)
问题 3️⃣:索引利用率下降
BETWEEN是 闭区间- 优化器有时会扩大扫描范围
- Index Seek 不稳定
二、SQL Server 官方推荐:唯一正确的日期写法
✅ 半开区间(SARGABLE)
WHERE OrderDate >= @DateStart
AND OrderDate < DATEADD(DAY, 1, @DateStart)
如果你有结束时间:
WHERE OrderDate >= @DateStart
AND OrderDate < @DateEnd
🔑 核心原则
左闭右开区间
[start, end)
永远不要用 BETWEEN 做日期范围
三、你这个场景的“标准模板”
📌 查询某一天(最常用)
DECLARE @Date date = '2026-01-09';
WHERE OrderDate >= @Date
AND OrderDate < DATEADD(DAY, 1, @Date)
优点
- ✔ 不漏数据
- ✔ 可走索引
- ✔ 执行计划稳定
📌 查询日期范围(报表 / 对账)
WHERE OrderDate >= @DateFrom
AND OrderDate < DATEADD(DAY, 1, @DateTo)
❌ 不要用 BETWEEN
四、索引设计(性能真正起飞的关键)
✅ 正确的索引顺序(非常重要)
CREATE NONCLUSTERED INDEX IX_Orders_OrderDate_Status
ON dbo.Orders (OrderDate, Status)
INCLUDE (OrderID, CustomerID, SubTotal, Receivd);
为什么 OrderDate 必须放第一?
- 范围查询(
>= <)必须在 索引最左 Status是等值过滤,放第二- INCLUDE 列避免回表
五、千万别这么写(全部是坑)
❌ 1️⃣ 对列做函数
WHERE CONVERT(date, OrderDate) = '2026-01-09'
👉 索引直接失效
❌ 2️⃣ BETWEEN + 减一秒
BETWEEN '2026-01-09'
AND DATEADD(second, -1, '2026-01-10')
- 老写法
- 不精确
- 不稳定
❌ 3️⃣ 字符串比较
WHERE OrderDate BETWEEN '2026-01-09' AND '2026-01-10'
- 隐式转换
- 执行计划不可控
六、不能改 SQL?(只能加索引的补救方案)
⚠️ 这是兜底,不是最佳实践
CREATE NONCLUSTERED INDEX IX_Orders_OrderDateOnly
ON dbo.Orders (OrderDate);
你仍然会遇到:
- 扫描行数多
- Sort
- 性能抖动
七、执行计划对比(本质差异)
| 写法 | Index Seek | Sort |
|---|---|---|
| BETWEEN | ⚠️ 不稳定 | 常见 |
>= AND < |
✅ 稳定 | ❌ |
CONVERT(date, OrderDate) |
❌ | ❌ |
八、一句话总结(团队规范)
SQL Server 中:
❌ 禁用BETWEEN 日期 AND 日期
✅ 统一使用:col >= start AND col < end
九、使用日期字符串比较的正确姿势
❌ 当前写法(有索引问题)
WHERE a.OrderDate = CONVERT(char(10), GETDATE(), 120)
AND a.Status = N'已结账'
问题:
- 字符串比较
- 隐式转换
- 无法 Index Seek
✅ 正确写法(可走索引)
WHERE a.OrderDate >= CONVERT(date, GETDATE())
AND a.OrderDate < DATEADD(day, 1, CONVERT(date, GETDATE()))
AND a.Status = N'已结账'
十、聚合查询的推荐索引(SUM 场景)
CREATE NONCLUSTERED INDEX IX_Orders_OrderDate_Status
ON dbo.Orders (OrderDate, Status)
INCLUDE (SubTotal, VISA);
👉 执行计划会变成:
- Index Seek
- Stream Aggregate
- 无回表
十一、完整优化后的聚合 SQL
SELECT
SUM(a.SubTotal) AS TotalSubTotal,
SUM(ISNULL(a.VISA, 0)) AS TotalVISA,
SUM(a.SubTotal - ISNULL(a.VISA, 0)) AS TotalOther
FROM dbo.Orders a
WHERE a.OrderDate >= CONVERT(date, GETDATE())
AND a.OrderDate < DATEADD(day, 1, CONVERT(date, GETDATE()))
AND a.Status = N'已结账';
十二、搜索备注字段(LIKE 前缀优化)
WHERE Status = N'XXX'
AND Remark LIKE N'新网络订单%'
常见问题
NVARCHAR(MAX)/TEXT不能做索引键- 计算列派生自 MAX 仍不可索引
✅ 标准改造方案(生产级)
1️⃣ 新增可索引列
ALTER TABLE dbo.Orders
ADD RemarkIndex NVARCHAR(50);
2️⃣ 初始化历史数据
UPDATE dbo.Orders
SET RemarkIndex = LEFT(Remark, 50);
3️⃣ 建索引
CREATE NONCLUSTERED INDEX IX_Orders_Status_RemarkIndex
ON dbo.Orders (Status, RemarkIndex);
4️⃣ 同步机制(应用层或触发器)
CREATE TRIGGER trg_Orders_RemarkIndex
ON dbo.Orders
AFTER INSERT, UPDATE
AS
BEGIN
UPDATE o
SET RemarkIndex = LEFT(o.Remark, 50)
FROM dbo.Orders o
JOIN inserted i ON o.OrderID = i.OrderID;
END;
十三、WHERE LEN(a.VIPID) > 0 的索引优化
❌ 当前问题
WHERE LEN(a.VIPID) > 0
- 函数包列
- 逐行计算
- 索引失效
✅ 等价且可走索引的写法
WHERE a.VIPID IS NOT NULL
AND a.VIPID <> N''
🚀 过滤索引(VIPID 大多为空时)
CREATE NONCLUSTERED INDEX IX_Customers_VIPID_NotEmpty
ON dbo.Customers (VIPID)
INCLUDE (
CustomerID,
ConsumerOnOrder,
ConsumerOnPurchase,
Discount
)
WHERE VIPID IS NOT NULL
AND VIPID <> N'';
十四、最终结论(可贴墙)
❌
BETWEEN+ 日期
❌LEN(column)
❌ 字符串比较日期✅
>= AND <
✅ 合理索引顺序
✅ 覆盖索引 / 过滤索引这就是 SQL Server 高性能查询的底层逻辑
如果你愿意,我可以帮你 再整理一版「公司级 SQL 性能规范 PDF / Markdown 模板」,直接给新人用。
关联项目
FreeSql QQ群:4336577
BA & Blazor QQ群:795206915
Maui Blazor 中文社区 QQ群:645660665
知识共享许可协议
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名AlexChow(包含链接: https://github.com/densen2014 ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系 。
转载声明
本文来自博客园,作者:周创琳 AlexChow,转载请注明原文链接:https://www.cnblogs.com/densen2014/p/19463245
AlexChow
今日头条 | 博客园 | 知乎 | Gitee | GitHub


浙公网安备 33010602011771号