news 2026/10/9 7:25:37

MyBatis日期查询边界陷阱:从隐式转换到左闭右开区间

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MyBatis日期查询边界陷阱:从隐式转换到左闭右开区间

1. “查询一整天”这个需求,为什么在 MyBatis 里总翻车

1.1 一个五分钟需求背后的数据类型错位

先说个我闭着眼都能背出来的场景:运营后台有个订单列表,产品提的需求是“按日期筛选,选 12 月 1 日就出 12 月 1 日这一天的所有订单”。前端日期组件返回的是2024-12-01这个字符串,后端查询对象里顺手就定义成了String startDate,数据库里订单创建时间create_time是datetime类型,MyBatis 负责把结果映射回 VO。

看起来确实五分钟能写完:先where create_time = #{startDate},不行就换BETWEEN,再不行就上LIKE。等你真把 SQL 塞进数据库跑一遍就会发现,这五分钟写出来的东西,十条订单能漏掉九条半,而且不是稳定漏——有时候能查到几条,有时候一条都没有,最气人的是开发环境数据少,自测时随手查一个日期碰巧能过,上了生产才爆雷。

问题根源不在需求,而在“字符串日期”和“datetime 字段”这两个东西之间的错位。datetime在 MySQL 里是一个精确到秒(或毫秒、微秒,取决于列定义)的时间点,2024-12-01 08:30:00、2024-12-01 23:59:59都是合法值;但2024-12-01不是一个合法的时间点,它只是一个“日期”。把日期字符串丢给 datetime 字段去比较,MySQL 一定会做点什么来弥合这个差异,而你大概率没意识到的,就是它到底做了怎样的转换。

1.2 隐式转换:等值查询只匹配到零点

先说结论:当你在 SQL 里把'2024-12-01'这个字符串和 datetime 字段做等值比较时,MySQL 会做隐式转换,把字符串解释成2024-12-01 00:00:00。换句话说:

WHERE create_time = '2024-12-01'

在语义上等价于:

WHERE create_time = '2024-12-01 00:00:00'

它只能匹配当天零点整那一秒钟的数据。而你的真实需求是“一整天”,也就是从00:00:00到23:59:59,如果列是datetime(3)或datetime(6),还要覆盖到毫秒和微秒。需求和 SQL 从一开始就是两个不同的集合,查不全才是常态。

这种问题在新项目里特别容易发生,因为 MyBatis 的#{}参数帮你把引号、类型这些都处理“干净”了,你很容易忽略 MySQL 内部对这个字符串到底做了什么事。数据量小的时候,偶尔选中一条零点附近的订单,看起来还“挺正常”,侥幸感一上来,坑就留到上线了。

1.3 三层复杂度,缺一层都容易出问题

这个主题表面上是个“日期查询的小问题”,真正拆开其实有三层:

  • SQL 语义层:字符串怎么转换成 datetime,范围边界怎么划,BETWEEN、>、<、LIKE各有什么后果;
  • MyBatis 实现层:XML 里用#{}还是${},<if>判断空字符串,动态 SQL 的语法,参数怎么传才不会在边界场景翻车;
  • 运行性能层:你写出的条件能不能走索引,函数是作用在字段上还是作用在参数上,一张几百万行的表会不会被你一个看似无害的条件打成全表扫描。

下面我按这三层,把这几年的项目里攒下的经验和踩坑复盘完整展开。

2. 最容易踩进去的四种错误写法

2.1 BETWEEN 与“当天日期”:微弱正确感掩盖的大漏

<select id="selectByCreateTime" resultType="OrderDO"> SELECT * FROM t_order WHERE create_time BETWEEN #{startDate} AND #{endDate} </select>

Service 层传入startDate = "2024-12-01"、endDate = "2024-12-01"。表面上一看,BETWEEN 是闭区间,包含等于,逻辑没毛病。但结合前面说的隐式转换,这个条件等价于:

WHERE create_time >= '2024-12-01 00:00:00' AND create_time <= '2024-12-01 00:00:00'

注意,这个范围里只有一个点,就是零点整。08:30 的订单不在里面,下午三点的订单不在里面,晚上十一点的订单更不在里面。实际表现就是:能查出几条刚好半夜下的单,白天所有单全部消失,或者因为索引选择性问题,MySQL 干脆全表扫,扫完给你一个差不多的结果。

这种错误最大的危害,是它有“微弱的正确感”:数据能出来,只是不全,很容易被误判成数据同步延迟、订单没进来,而不是 SQL 写错了。我之前在代码评审里看到过有人加了个注释“endDate 传当天日期,BETWEEN 查整天”,这就是典型的认知错误。

顺带提一个细节,有人会用endDate = "2024-12-01 23:59:59"来补救。秒级字段下这确实能覆盖一整天,但如果字段是datetime(3)或datetime(6),23:59:59.500这条数据就又漏掉了。用23:59:59.999这种字符串看起来是“补满了”,实际遇到精度更高的列或某些数据库的舍入规则,依然有边界风险。锁死“当天某个最晚时间字符串”的做法本质上是赌精度,不适合作为通用方案。

2.2 单条件 <=:少一天且不易察觉

WHERE create_time &lt;= #{endDate}

这种写法经常出现在“统计 12 月 1 日及之前的累计数据”这类需求里。传一个endDate = "2024-12-01",经过隐式转换变成<= '2024-12-01 00:00:00',那么 12 月 1 日白天的订单又没了。比起 BETWEEN,这个错误更隐蔽,因为它是单条件,出错后你很难第一时间怀疑到它头上。我的经验是,凡是看到datetime 字段 <= 纯日期字符串,第一反应就应该是“少了整整一天”,不用犹豫,直接改成左闭右开区间。

2.3 LIKE 模糊匹配:功能对了,性能和语义崩了

WHERE create_time LIKE '2024-12-01%'

这条 SQL 在功能上的确能覆盖一整天,因为2024-12-01%能以字符串前缀的形式匹配2024-12-01 08:00:00、2024-12-01 23:59:59。问题出在性能和语义上:

  • 性能方面,LIKE '2024-12-01%'因为前缀固定,理论上还有机会走索引,但前提是列类型是字符串;如果列是 datetime,MySQL 会先把 datetime 转成字符串再比较,这等于对每一行做一次格式化,然后逐个匹配前缀,大表上查询时间从毫秒涨到秒级很常见。
  • 更糟的写法是LIKE '%2024-12-01%',前缀用通配符,索引必然失效,全表扫描跑不掉。
  • 语义方面,这种写法把“时间范围查询”变成了“字符串模式匹配”,将来字段类型或格式一变,行为就跟着变,维护成本很高。

我在一个老项目里接手过类似的慢查询,单表七百万行,运营每次拉月报,数据库 CPU 直接飙到 90%,DBA 追过来一看,就是LIKE '2024-12%'这种。改成区间条件之后,查询从 8 秒降到 20 毫秒,索引完全吃上。这里的关键不是“能不能查到”,而是“能不能稳定、高效地查到”。

2.4 Java 层拼 23:59:59:用表象逻辑掩盖精度问题

String start = "2024-12-01" + " 00:00:00"; String end = "2024-12-01" + " 23:59:59";

老项目里这种写法不少,把边界问题从 SQL 挪到 Java,看起来是“两头都想到了”。问题是这是典型的“面向表象编程”:今天字段是秒级没问题,哪天字段升级成datetime(6),23:59:59.999的数据依旧漏;哪天产品说“我要查 12 月 1 日 0 点到 12 月 2 日 0 点”,你的字符串拼接又要改。更尴尬的是,Java 层拼好了字符串,MyBatis 里还是#{}传参,问题绕了一圈,最后还是落在字符串和 datetime 的隐式转换里打转。

3. 根上的解法:左闭右开区间 + 显式日期转换

3.1 为什么“小于次日零点”是最稳的边界

关于日期范围查询,行业里逐渐形成的共识是左闭右开区间:条件写成create_time >= 开始日期的 00:00:00且create_time < 结束日期+1 天的 00:00:00。查 12 月 1 日一整天,就是:

WHERE create_time >= '2024-12-01 00:00:00' AND create_time < '2024-12-02 00:00:00'

为什么是“小于次日零点”,而不是“小于当天 23:59:59”?因为“当天最后一刻”不是一个可以被精确表达的单点:秒级是 23:59:59,毫秒级是 23:59:59.999,微秒级是 23:59:59.999999。不管你写哪一个,都存在比它更晚、但仍属于“当天”的数据。而“次日零点”是一个精确、无歧义的点,任何属于当天的数据都严格小于它。把 end 定义为第二天的开始,边界问题就被彻底消解了。

这个思路其实和数组切片、价格区间是同一套逻辑:[0, length)永远比[0, lastIndex]健壮,因为你不需要脑补最后一个元素的位置,边界天然由下一个起点划定。

3.2 STR_TO_DATE 显式转换字符串日期

当参数是字符串'2024-12-01',最稳妥的做法是不要依赖隐式转换,而是显式用STR_TO_DATE把它转成 datetime:

SELECT * FROM t_order WHERE create_time >= STR_TO_DATE('2024-12-01', '%Y-%m-%d') AND create_time < STR_TO_DATE('2024-12-01', '%Y-%m-%d') + INTERVAL 1 DAY;

STR_TO_DATE('2024-12-01', '%Y-%m-%d')返回2024-12-01 00:00:00,加INTERVAL 1 DAY后变成2024-12-02 00:00:00,两个条件刚好圈出 12 月 1 日的一整天。

为什么不直接写create_time >= '2024-12-01'?直接写也能跑,但显式转型有三个好处:

  1. 可读性好,后来的人一看就知道你是想把字符串转成当天零点,而不是在那里碰运气;
  2. 避免隐式转换的规则差异,不同 MySQL 版本、不同sql_mode(比如NO_ZERO_DATE)下,隐式转换行为可能不一样;
  3. 函数作用在参数上而不作用在字段上,不会影响 create_time 字段的索引使用,这一点在第 5 部分会重点展开。

3.3 DATE() 字段函数写法,以及为什么不能推广

还有一种常见写法:

WHERE DATE(create_time) = '2024-12-01'

语义直观,一眼就懂“把这天的数据拿出来”。我认可它在小表、低频后台查询里可以用。但请注意,DATE(create_time)是对字段做函数运算,函数一旦套上去,MySQL 基本无法在 create_time 索引上做范围扫描。它需要先把每一行的 create_time 取出来,算一遍 DATE(),再和字符串比较。表一大,这条 SQL 就开始拖后腿。

MySQL 8.0 里可以通过生成列加表达式索引的方式,让DATE(create_time)命中二级索引,这是另一个进阶话题。普通项目不建议为了一个查询条件去动表结构,通用做法仍然是把函数写在参数侧,保持条件 Sargable(可索引)。

3.4 四种写法放在一起看

写法能否覆盖一整天索引友好可读性我的判断
create_time = #{date}否,只匹配零点是高不推荐
DATE(create_time) = #{date}是否高小表可用
DATE_FORMAT(create_time, '%Y-%m-%d') = #{date}是否中不推荐
create_time >= STR_TO_DATE(#{date}, '%Y-%m-%d') AND create_time < STR_TO_DATE(#{date}, '%Y-%m-%d') + INTERVAL 1 DAY是是高推荐

DATE_FORMAT那行我直接标不推荐:它把 datetime 转成字符串之后再做字符串比较,精度丢失和全表扫描双重叠加,比DATE()更糟。

4. MyBatis 中的落地方案:从 XML 到 Service 层

4.1 Mapper 接口、查询对象与核心 XML 代码

先定义查询对象,日期字段保持字符串:

@Data public class OrderQuery { /** 开始日期,格式 yyyy-MM-dd,例如 2024-12-01,包含当天 */ private String startDate; /** 结束日期,格式 yyyy-MM-dd,例如 2024-12-31,包含当天 */ private String endDate; private Integer status; }

Mapper 接口:

public interface OrderMapper { List<OrderDO> selectByDateRange(@Param("query") OrderQuery query); }

XML 里的核心写法:

<select id="selectByDateRange" resultType="com.example.OrderDO"> SELECT id, order_no, create_time, status FROM t_order <where> <if test="query.startDate != null and query.startDate != ''"> AND create_time &gt;= STR_TO_DATE(#{query.startDate}, '%Y-%m-%d') </if> <if test="query.endDate != null and query.endDate != ''"> AND create_time &lt; STR_TO_DATE(#{query.endDate}, '%Y-%m-%d') + INTERVAL 1 DAY </if> <if test="query.status != null"> AND status = #{query.status} </if> </where> ORDER BY create_time DESC </select>

这里有几个细节值得注意。

<where>标签会自动去掉第一个条件的AND,避免生成WHERE AND这种语法错误;如果所有条件都为空,它会生成一个没有 WHERE 的语句,这在全表查询场景下是合理的默认行为。

<if test>判断字符串空值是必须的。很多新手只写!= null,结果前端传了空字符串,SQL 变成create_time >= STR_TO_DATE('', '%Y-%m-%d'),MySQL 对空字符串做日期转换会返回 NULL 或者直接报Incorrect datetime value,整个查询直接挂掉。这个问题在 MyBatis 里出现频率非常高。

XML 中大于号、小于号必须转义,>可以用&gt;,<必须写成&lt;。一旦漏了转义,XML 解析阶段直接报错,根本轮不到数据库。上面代码里两个比较符号我都做了转义,这是很基本的规范,但 review 代码时能看到不少漏网之鱼。

4.2 动态 SQL 中空值判断与 XML 转义事项

补充一个和日期无关但经常一起踩的坑:如果你把参数直接写在<if>的判断条件里,注意用query.xxx这种 OGNL 表达式的路径。之前遇到过有人写成:

<if test="startDate != null and startDate != ''">

而实际参数是@Param("query"),正确写法应该是query.startDate。写错路径后,MyBatis 在解析阶段不会立刻报错,运行时才报There is no getter for property named 'startDate',排查时容易被我忽略。

另外,<if>判断里不要把字符串比较写反,比如'' != query.startDate,OGNL 对这种写法的解析结果有时候和直觉不一致。统一写成query.startDate != null and query.startDate != '',最稳。

4.3 Service 层日期字符串的归一化处理

前端传来的日期不一定规整。日期组件可能传2024/12/01,运营贴进 Excel 的数据可能变成2024-12-1,还有人会传2024.12.01。这些字符串直接进STR_TO_DATE,靠%Y-%m-%d格式去猜,能不能解析就看数据库的容错度了。稳妥的做法是在 Service 层统一归一到yyyy-MM-dd:

public List<OrderDO> queryOrders(OrderQuery query) { if (StringUtils.hasText(query.getStartDate())) { query.setStartDate(normalizeDate(query.getStartDate())); } if (StringUtils.hasText(query.getEndDate())) { query.setEndDate(normalizeDate(query.getEndDate())); } return orderMapper.selectByDateRange(query); } private String normalizeDate(String raw) { String normalized = raw.trim() .replaceAll("[/.]", "-"); LocalDate date = LocalDate.parse(normalized, DateTimeFormatter.ofPattern("yyyy-M-d")); return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); }

DateTimeFormatter.ofPattern("yyyy-M-d")能同时解析2024-1-5和2024-01-05,格式化回去统一成2024-01-05。这样传到 XML 里的一定是干净的yyyy-MM-dd,STR_TO_DATE基本不会踩解析坑。

有的团队会把这个约束推给前端,要求前端保证格式。可以,但后端做一次归一化成本很低,收益是出问题时不用扯皮,我建议后端还是自己堵住这一层。

4.4 在 XML 里转,还是在 Service 层转?两种写法的取舍

前面 4.1 的方案是在 XML 里用STR_TO_DATE和INTERVAL 1 DAY,把字符串边界计算交给数据库。另一种做法是在 Service 层直接把日期算成LocalDateTime,XML 只保留最朴素的>=和<:

public List<OrderDO> queryOrders(OrderQuery query) { if (StringUtils.hasText(query.getStartDate())) { LocalDate start = LocalDate.parse(query.getStartDate()); query.setStartDateTime(start.atStartOfDay()); // 2024-12-01T00:00 } if (StringUtils.hasText(query.getEndDate())) { LocalDate end = LocalDate.parse(query.getEndDate()); query.setEndDateTime(end.plusDays(1).atStartOfDay()); // 2024-12-02T00:00 } return orderMapper.selectByDateRange(query); }

DTO 里加两个LocalDateTime字段,XML 变成:

<if test="query.startDateTime != null"> AND create_time &gt;= #{query.startDateTime} </if> <if test="query.endDateTime != null"> AND create_time &lt; #{query.endDateTime} </if>

这条方案的好处是 XML 更干净,SQL 连字符串格式都不用关心,MyBatis 的 TypeHandler 会把LocalDateTime转成 JDBC 的 TIMESTAMP,比较语义清晰;坏处是 DTO 里不能只放朴素的字符串,要多维护两个时间字段,而且“+1 天”的逻辑写在了 Java 里,DBA 排查慢 SQL 时要多跨一层。

两条方案没有绝对优劣,取决于团队规范。如果你们习惯“SQL 尽量朴素、业务计算放 Java”,选 Service 层转换;如果希望“日期边界逻辑都在 SQL 里,DBA 拿着一条 SQL 就能复现问题”,选 XML 转换。关键是选一种固化下来,别一个项目里两种混着写。

5. 从“查得到”到“查得快”:索引、时区与跨数据库

5.1 函数作用在字段上还是参数上,决定索引的生死

SQL 性能优化里有个术语叫 Sargable(Search Argument Able),核心规则一句话:查询条件里函数作用在字段上,通常会阻断索引;函数作用在参数上,不影响索引。

拿推荐的写法对照:

-- 好:函数作用在参数上,字段裸露 create_time >= STR_TO_DATE('2024-12-01', '%Y-%m-%d') -- 差:函数作用在字段上,索引失效 DATE(create_time) >= '2024-12-01'

第一条执行时,MySQL 先把STR_TO_DATE('2024-12-01', '%Y-%m-%d')算成常量2024-12-01 00:00:00,然后对 create_time 做正常的范围扫描。只要 create_time 上有索引,就能从索引定位到对应位置顺序读,效率很高。

第二条要把每一行的 create_time 先做一次 DATE() 运算再参与比较。即使 create_time 字段有索引,也救不回来,因为索引里存的是原始时间值,不是 DATE() 的结果。这就是“一条条件,两种命运”的典型例子。

5.2 隐式转换的可预测性:类型越乱,执行计划越难猜

项目里常见两类隐患:一类是 datetime 字段和字符串直接比较,靠隐式转换;另一类是 VARCHAR 字段和 Date 参数比较,又是另一套隐式规则。单看某一处,数据库都能“猜”出个结果,但一旦查询条件里混合了多种类型,执行计划的可预测性就下降,优化器可能选错索引甚至放弃索引。

我的建议是:datetime 字段就用日期类型,或者在明确格式的字符串上显式STR_TO_DATE,不要同一个 SQL 里一会儿STR_TO_DATE,一会儿依赖隐式转换。线上出问题的时候,这种“跑在隐式转换边缘”的 SQL 是最难排查的,因为它不是稳定报错,而是时好时坏。

5.3 时区错位:关于“日期少一天”的另一个源头

假设业务库是 MySQL,字段是 datetime,应用跑在 Docker 容器里。镜像默认时区是 UTC,宿主机是东八区,数据库配置成东八区。这时如果你在 Service 层用new Date()或LocalDateTime.now()构造边界条件,再通过#{startDateTime}传给 JDBC,驱动会按 JVM 时区把时间转成 UTC 存进去,再按数据库会话时区转回来。两边时区不一致,就会出现“传了 12 月 1 日,查出来的却是 11 月 30 日数据”的诡异现象。

我自己就踩过这个坑,当时应用查“昨天的数据”永远差 8 小时,排查了半天才发现是容器时区问题。此类问题建议按顺序查三处:JVM 默认时区、JDBC 连接 URL 里的serverTimezone参数、MySQL 的system_time_zone和会话time_zone。

缓解办法:数据库、应用、JDBC 连接参数三者统一时区;URL 里显式写serverTimezone=Asia/Shanghai;Docker 里设置环境变量TZ=Asia/Shanghai并挂载/etc/localtime。这个问题排查优先级很高,因为时区偏差会导致日期边界整体平移,看起来像 SQL 写法错了,实际是环境配置错了。

5.4 不同数据库下同一语义的实现对照

左闭右开的思路跨数据库通用,但字符串转日期的函数各不相同:

数据库字符串转日期次日零点表达
MySQLSTR_TO_DATE('2024-12-01', '%Y-%m-%d')+ INTERVAL 1 DAY
OracleTO_DATE('2024-12-01', 'YYYY-MM-DD')+ 1
SQL ServerCONVERT(datetime, '2024-12-01', 120)DATEADD(day, 1, ...)
PostgreSQL'2024-12-01'::date+ INTERVAL '1 day'

Oracle 的+ 1表示加一天,因为 Oracle 的 DATE 类型默认带时间部分,语义正好落在次日零点。SQL Server 的CONVERT第三个参数 120 表示yyyy-mm-dd hh:mi:ss样式,也可以直接CAST('2024-12-01' AS datetime)。PostgreSQL 的::date是类型转换语法,最直接。

不管哪个数据库,我最后给 DBA 看的 SQL 都是同一套语义:

WHERE create_time >= TO_DATE('2024-12-01', 'YYYY-MM-DD') AND create_time < TO_DATE('2024-12-01', 'YYYY-MM-DD') + 1

人话翻译:从 12 月 1 日零点开始,到 12 月 2 日零点之前。这种边界不依赖当天最后一刻的 SQL,换库也不容易出错。

5.5 顺路复习 #{} 与 ${}:日期参数也不能用拼接

上面所有示例都用的是#{}占位符。如果有人图省事改成字符串拼接:

AND create_time >= STR_TO_DATE('${query.startDate}', '%Y-%m-%d')

startDate 直接进 SQL 文本,一方面带来 SQL 注入风险,用户传一个'2024-12-01'; DROP TABLE t_order;--进去后果不堪设想;另一方面预编译失效,每次执行都要重新解析 SQL,性能也会受影响。MyBatis 面试里经常问#{}和${}的区别,实际项目里这俩的选择对安全性的影响非常直接。日期参数一样,永远用#{},别让用户输入直接进 SQL 文本。

6. 一次“少查一天”线上问题的完整排查链路

6.1 现象:凌晨订单莫名消失

有次线上反馈,运营说“今天查昨天的订单,凌晨那批数据没出来”。业务方第一反应是数据没落库,让后端查表;后端查了库,凌晨订单在表里好好的,但通过管理后台接口查不到。于是问题被定性为“查询接口 bug”。

我接手时的第一件事,不是改代码,而是把接口最终执行的 SQL 从 MyBatis 日志里拉出来,手动在数据库里跑一遍。这里说明一下,MyBatis 的日志通常会打印预编译前的 SQL 和参数占位符,需要把参数代进去还原出最终 SQL。还原后的条件长这样:

WHERE create_time >= '2024-12-01 00:00:00' AND create_time <= '2024-12-01 23:59:59'

6.2 逐步定位:从日志、执行计划到表结构

第一步,验证 SQL 本身。拿一个凌晨 00:30 的订单去套条件,create_time <= '2024-12-01 23:59:59'应该成立,但查询结果确实没有。于是怀疑边界,把条件改成:

WHERE create_time = '2024-12-01 00:30:00'

能查到。这说明数据没问题、字段映射没问题,问题缩小到时间范围表达式。

第二步,看表结构。执行SHOW CREATE TABLE t_order,发现create_time的定义是datetime(6),精度到微秒。问题就清楚了:凌晨 00:30:00.000000 的数据,精度没问题;但如果一条订单的时间是2024-12-01 00:30:00.123456,它依然属于当天,却大于23:59:59吗?不是,问题不在这。继续看:日志里还原出的 SQL 中,endDate 参数来自 Service 层拼的字符串"2024-12-01 23:59:59",通过 JDBC 传给 datetime(6) 字段比较时,MySQL 会把这个字符串转成2024-12-01 23:59:59.000000。注意,23:59:59.500000这种带小数秒的数据,大于23:59:59.000000,所以被漏掉了。看起来只差 1 微秒,但确实不在范围内。

第三步,看执行计划。EXPLAIN显示走了全表扫描,因为 LIKE 或边界函数导致了低选择性。这里更关键的结论是:无论有没有索引,这个查询的语义边界本身就是错的,“补齐 23:59:59”这个方案在 datetime(6) 下天然有缝隙。

6.3 修复与防复发:把边界规则写进团队规范

修复不复杂,Service 层不再拼 “23:59:59”,直接算次日零点,XML 里改成:

AND create_time &gt;= #{startDateTime} AND create_time &lt; #{endDateTime}

其中endDateTime是LocalDate.parse("2024-12-01").plusDays(1).atStartOfDay()。

真正难的是防复发。团队里不止一个人写过“拼 23:59:59”的代码,我后来把日期范围查询的规范固化成了三条,写进项目的开发手册:

  • 时间范围统一用左闭右开区间,结束时间总是“次日零点”;
  • 日期字符串一律用yyyy-MM-dd,转换用数据库的STR_TO_DATE/TO_DATE,不依赖隐式转换;
  • 条件里的函数只作用在参数上,不对字段做任何包裹。

这三条看着简单,但能把一整类“日期边界”问题从根上解决。后来新人接手,遇到日期查询就翻这三条,很少再犯同样的错。

6.4 一点个人经验:日期查询的三个习惯

最后分享几个我长期养成的习惯,不算技术,但很管用。

第一,写完日期条件先问自己一句:“这个查询覆盖的区间边界是哪两个时间点?”如果你答不出精确到秒的边界,SQL 基本是有问题的。

第二,代码 Review 时看到“23:59:59”这个字符串,我第一反应是提一个“改成左闭右开”的评论,不用看上下文,因为这个写法本身就带着精度风险的基因。

第三,查日期范围之前,先看一眼表字段的类型和精度。datetime和datetime(6)对边界的要求完全不一样,前者拼 23:59:59 顶多算丑,后者会直接漏数据。把表结构看清楚,再决定用哪种写法,能省掉后面一整轮排查的时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 7:21:42

仓库管理六大核心KPI:从数据指标到实时风控的落地指南

1. 为什么这6个KPI是仓库管理的“命脉”&#xff0c;而不是可有可无的数字&#xff1f;干了十多年仓储物流系统咨询和现场优化&#xff0c;我经手过从200平米社区前置仓到30万平米智能分拨中心的各类项目。见过太多仓库主管把日报表当摆设&#xff0c;也见过不少企业花几百万上…

作者头像 李华
网站建设 2026/10/9 7:15:49

jmeter-如果(If)控制器

满足指定的条件后&#xff0c;才会执行控制器下的http请求&#xff0c;表达式支持&#xff1a;是否等于&#xff0c;如 ${__jexl3(${VAR}1,)} 判断${VAR}变量是否等于1 !  不等于&#xff0c;如 ${__jexl3(${VAR}!1,)} 判断${VAR}变量是否不等于1 &#xff01;  非…

作者头像 李华