使用Java 8和SQL高效检索每日最新时间戳数据教程

使用Java 8和SQL高效检索每日最新时间戳数据教程

本教程旨在指导您如何利用Java 8 Stream API或原生SQL查询,从包含多条时间戳记录的数据集中,针对每个实体和特定日期,精确筛选出具有最新时间戳的唯一记录。我们将详细讲解如何通过分组、比较和聚合操作实现这一常见的数据处理需求,提供通用及特定场景的解决方案,并辅以代码示例、性能考量和注意事项。

1. 问题描述

在数据处理场景中,我们经常会遇到需要从一系列记录中,根据某些条件(如实体名称、日期)筛选出最新的那条记录。例如,给定一个 currency 实体列表,其中包含货名称(name)和接收时间(lastreceived),我们的目标是针对每种货币每个日期,找出在该日期内接收时间最晚(最新)的那条记录。

考虑以下 Currency 类定义:

import java.time.LocalDateTime;class Currency {    private Integer id;    private String name;    private LocalDateTime lastReceived;    // 构造函数    public Currency(Integer id, String name, LocalDateTime lastReceived) {        this.id = id;        this.name = name;        this.lastReceived = lastReceived;    }    // Getter方法    public Integer getId() { return id; }    public String getName() { return name; }    public LocalDateTime getLastReceived() { return lastReceived; }    @Override    public String toString() {        return "Currency{" +               "id=" + id +               ", name='" + name + ''' +               ", lastReceived=" + lastReceived +               '}';    }}

假设我们有如下数据库记录示例:

ID NAME LAST_RECEIVED

1USD2022-05-18 09:04:01.5452USD2022-05-18 08:04:01.5453USD2022-05-19 08:04:01.5454USD2022-05-20 08:04:01.5455USD2022-05-20 11:04:01.5456BUSD2022-05-18 08:04:01.545

我们期望的结果是针对每种货币的每个日期,只保留时间戳最新的记录。例如,对于 “USD” 货币:

ID NAME LAST_RECEIVED

1USD2022-05-18 09:04:01.5453USD2022-05-19 08:04:01.5455USD2022-05-20 11:04:01.545

2. Java 8 Stream API 解决方案

Java 8引入的Stream API为处理集合数据提供了强大而灵活的工具。我们可以利用其分组、映射和聚合功能来实现上述需求。

立即学习“Java免费学习笔记(深入)”;

2.1 通用场景:按货币和日期分组获取最新记录

此方案适用于从包含多种货币的数据集中,为每种货币的每个日期获取最新记录。核心思路是创建一个复合键,包含货币名称和日期,然后根据这个复合键进行分组,并在每个组内找出 lastReceived 时间戳最大的记录。

import java.time.LocalDateTime;import java.util.ArrayList;import java.util.Arrays;import java.util.Comparator;import java.util.List;import java.util.Optional;import java.util.stream.Collectors;public class CurrencyProcessor {    public static void main(String[] args) {        List allCurrencies = Arrays.asList(            new Currency(1, "USD", LocalDateTime.parse("2022-05-18T09:04:01.545")),            new Currency(2, "USD", LocalDateTime.parse("2022-05-18T08:04:01.545")),            new Currency(3, "USD", LocalDateTime.parse("2022-05-19T08:04:01.545")),            new Currency(4, "USD", LocalDateTime.parse("2022-05-20T08:04:01.545")),            new Currency(5, "USD", LocalDateTime.parse("2022-05-20T11:04:01.545")),            new Currency(6, "BUSD", LocalDateTime.parse("2022-05-18T08:04:01.545")),            new Currency(7, "EUR", LocalDateTime.parse("2022-05-18T10:00:00.000")),            new Currency(8, "EUR", LocalDateTime.parse("2022-05-18T09:30:00.000"))        );        List lastByDateForAllCurrencies = new ArrayList(allCurrencies                .stream()                .collect(Collectors.groupingBy(                    // 复合键:货币名称 + 日期                    curr -> Arrays.asList(curr.getName(), curr.getLastReceived().toLocalDate()),                    // 在每个组内,找出lastReceived最大的Currency对象                    Collectors.collectingAndThen(                        Collectors.maxBy(Comparator.comparing(Currency::getLastReceived)),                        Optional::get // 假设每个组至少有一个元素,因此Optional::get是安全的                    )                ))                .values()); // 获取Map中所有值(即筛选出的最新Currency对象)        System.out.println("--- 所有货币按日期筛选的最新记录 (未排序) ---");        lastByDateForAllCurrencies.forEach(System.out::println);        // 如果需要排序,可以对结果列表进行排序        lastByDateForAllCurrencies.sort(            Comparator.comparing(Currency::getName)                      .thenComparing(Currency::getLastReceived)        );        System.out.println("n--- 所有货币按日期筛选的最新记录 (已排序) ---");        lastByDateForAllCurrencies.forEach(System.out::println);    }}

代码解析:

Collectors.groupingBy(): 这是核心操作,用于将流中的元素按照指定的键进行分组。键 (curr -> Arrays.asList(curr.getName(), curr.getLastReceived().toLocalDate())): 我们创建了一个包含货币名称和日期的 List 作为复合键。toLocalDate() 方法将 LocalDateTime 转换为 LocalDate,从而忽略时间部分,只关注日期。下游收集器 (Collectors.collectingAndThen(…)): 在每个分组内部,我们希望找到 lastReceived 最大的那个 Currency 对象。Collectors.maxBy(Comparator.comparing(Currency::getLastReceived)): 这个收集器会返回一个 Optional<Currencyyoujiankuohaophpcn,表示组内 lastReceived 最大的元素。Optional::get: 由于 groupingBy 的特性,每个分组至少会有一个元素,所以 maxBy 总是会找到一个值,因此 Optional::get 在这里是安全的。如果存在分组为空的可能,则需要更安全的 Optional 处理,例如 orElse(null) 或 orElseThrow()。.values(): groupingBy 返回一个 Map,其值是每个分组筛选出的最新 Currency 对象。我们通过 .values() 获取这些对象。new ArrayList(…): 将 Map 的值集合转换为 ArrayList,以便后续进行排序。.sort(…): 如果需要按特定顺序(例如先按货币名称,再按接收时间)排列结果,可以对 ArrayList 进行排序。

2.2 特定货币场景:筛选后按日期分组获取最新记录

如果我们的需求是只针对某一种特定货币(例如 “USD”)进行处理,可以在分组前先进行过滤,这样可以简化分组键。

import java.time.LocalDateTime;import java.util.ArrayList;import java.util.Arrays;import java.util.Comparator;import java.util.List;import java.util.Optional;import java.util.stream.Collectors;public class SpecificCurrencyProcessor {    public static void main(String[] args) {        List allCurrencies = Arrays.asList(            new Currency(1, "USD", LocalDateTime.parse("2022-05-18T09:04:01.545")),            new Currency(2, "USD", LocalDateTime.parse("2022-05-18T08:04:01.545")),            new Currency(3, "USD", LocalDateTime.parse("2022-05-19T08:04:01.545")),            new Currency(4, "USD", LocalDateTime.parse("2022-05-20T08:04:01.545")),            new Currency(5, "USD", LocalDateTime.parse("2022-05-20T11:04:01.545")),            new Currency(6, "BUSD", LocalDateTime.parse("2022-05-18T08:04:01.545"))        );        String targetCurrencyName = "USD";        List lastUSDByDate = new ArrayList(allCurrencies                .stream()                .filter(curr -> targetCurrencyName.equalsIgnoreCase(curr.getName())) // 过滤出特定货币                .collect(Collectors.groupingBy(                    curr -> curr.getLastReceived().toLocalDate(), // 简化分组键,只需日期                    Collectors.collectingAndThen(                        Collectors.maxBy(Comparator.comparing(Currency::getLastReceived)),                        Optional::get                    )                ))                .values());        // 对结果进行排序        lastUSDByDate.sort(Comparator.comparing(Currency::getLastReceived));        System.out.println("--- USD货币按日期筛选的最新记录 ---");        lastUSDByDate.forEach(System.out::println);    }}

代码解析:

filter(curr -> targetCurrencyName.equalsIgnoreCase(curr.getName())): 在 groupingBy 之前,先通过 filter 操作筛选出目标货币(例如 “USD”)的所有记录。groupingBy(curr -> curr.getLastReceived().toLocalDate(), …): 由于已经过滤了货币,分组键只需包含日期即可。排序:同样,可以通过 sort 方法对结果进行排序,通常按 lastReceived 时间升序排列。

3. 原生SQL查询方案

对于数据库中的大量数据,使用原生SQL查询通常是更高效的选择,尤其是在数据库层面支持窗口函数的情况下。窗口函数可以在不改变查询结果集行数的情况下,对分组内的数据进行计算。

这里以 PostgreSQL 为例,展示如何使用 ROW_NUMBER() 窗口函数实现相同的功能。

SELECT id, name, last_receivedFROM (    SELECT        c.*,        -- 使用 ROW_NUMBER() 窗口函数为每个 (name, date) 分组内的记录编号        -- 按照 last_received 降序排列,最新记录的 rr 值为 1        ROW_NUMBER() OVER (            PARTITION BY name, to_char(last_received, 'yyyy-MM-dd')            ORDER BY last_received DESC        ) AS rr    FROM Currency c    WHERE c.name = :currName -- 可选:如果只需要特定货币) tblWHERE rr = 1 -- 筛选出每个分组中编号为 1 的记录(即最新记录)ORDER BY last_received; -- 对最终结果按接收时间排序

SQL查询解析:

*内层查询 (`SELECT c., ROW_NUMBER() OVER (…) AS rr FROM Currency c WHERE c.name = :currName`)**:ROW_NUMBER() OVER (PARTITION BY name, to_char(last_received, ‘yyyy-MM-dd’) ORDER BY last_received DESC): 这是核心部分。PARTITION BY name, to_char(last_received, ‘yyyy-MM-dd’): 这定义了窗口(分组)的边界。它会为每种货币(name)的每个日期(to_char(last_received, ‘yyyy-MM-dd’) 将时间戳转换为日期字符串)创建一个独立的组。ORDER BY last_received DESC: 在每个窗口(分组)内部,记录会按照 last_received 时间戳降序排列。ROW_NUMBER(): 为每个窗口内的记录分配一个从 1 开始的序列号。因此,每个分组中 last_received 最新的记录将获得 rr = 1。WHERE c.name = :currName: 这是一个可选的过滤条件,用于只查询特定货币的数据。如果需要查询所有货币,可以移除此条件。外层查询 (SELECT id, name, last_received FROM (…) tbl WHERE rr = 1 ORDER BY last_received):WHERE rr = 1: 从内层查询的结果中,我们只选择那些 rr 值为 1 的记录,这正是每个分组中 lastReceived 最新的那条记录。ORDER BY last_received: 对最终结果进行排序,通常按 lastReceived 升序。

JPA集成注意事项:

目前,JPA(Java Persistence API)标准本身对SQL窗口函数的支持并不完善。因此,如果需要使用窗口函数,通常需要通过 @Query(nativeQuery = true, value = “…”) 注解来执行原生SQL查询。

// 假设这是你的JPA Repository接口public interface CurrencyRepository extends JpaRepository {    @Query(nativeQuery = true, value = """        SELECT id, name, last_received        FROM (            SELECT c.*,            ROW_NUMBER() OVER (                PARTITION BY name, to_char(last_received, 'yyyy-MM-dd')                ORDER BY last_received DESC            ) AS rr            FROM Currency c            WHERE c.name = :currName        ) tbl        WHERE rr = 1        ORDER BY last_received    """)    List findLastByDateByCurrencyName(@Param("currName") String currName);    // 如果需要查询所有货币,可以移除 WHERE c.name = :currName    @Query(nativeQuery = true, value = """        SELECT id, name, last_received        FROM (            SELECT c.*,            ROW_NUMBER() OVER (                PARTITION BY name, to_char(last_received, 'yyyy-MM-dd')                ORDER BY last_received DESC            ) AS rr            FROM Currency c        ) tbl        WHERE rr = 1        ORDER BY name, last_received    """)    List findAllLastByDate();}

4. 总结与注意事项

Java 8 Stream API

优点:代码简洁、声明式,适用于内存中的小型到中型数据集。易于理解和维护,充分利用了函数式编程的优势。缺点:对于非常大的数据集,如果数据需要从数据库加载到内存中,可能会消耗大量内存并影响性能。注意事项:Optional::get 的使用需要谨慎,确保分组不会为空。toLocalDate() 方法用于精确到日期,如果需要精确到更小的时间单位(如小时),则需要调整分组键。

原生SQL查询(窗口函数)

优点:在数据库层面执行,通常对大数据集具有更高的性能,减少了数据传输到应用层后的处理负担。尤其适用于数据库是数据源的场景。缺点:SQL语法可能因数据库类型而异(例如 to_char 函数在不同数据库中可能写法不同)。与ORM框架(如JPA)的集成可能需要使用原生查询,失去部分ORM的抽象优势。注意事项:确保 PARTITION BY 和 ORDER BY 子句正确定义了分组和排序逻辑,以准确识别最新记录。

在实际应用中,选择哪种方案取决于具体的数据量、性能要求以及应用架构。对于数据量较小且已在内存中的数据,Java Stream API是简洁高效的选择;而对于大规模数据库数据,原生SQL查询通常能提供更好的性能。

以上就是使用Java 8和SQL高效检索每日最新时间戳数据教程的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/42009.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年11月6日 17:09:20
下一篇 2025年11月6日 17:12:37

相关推荐

  • Golang如何开发简单的问卷调查项目

    答案是使用Golang搭建一个简易问卷系统,通过定义Survey和Response结构体,实现展示问卷、提交回答和查看结果的完整流程。 用Golang开发一个简单的问卷调查项目,核心是搭建HTTP服务、设计数据结构、处理表单提交和展示结果。整个过程不复杂,适合初学者练手。以下是具体实现思路和步骤。 …

    2025年12月16日
    000
  • Go语言:深入理解uint8到string的转换技巧

    本文将详细介绍在go语言中如何将`uint8`类型有效转换为字符串。当从字符串中索引单个字符(其类型为`uint8`)并尝试将其数值转换为字符串表示时,常见的错误是直接使用`strconv.itoa`。我们将阐明`uint8`和`int`之间的区别,并提供正确的类型转换方法,确保代码的健壮性和可读性…

    2025年12月16日
    000
  • Go语言中big.Int到指定进制字符串的转换方法与非导出函数探究

    本文深入探讨了在go语言中将`big.int`类型转换为自定义进制字符串的实践方法,特别是如何避免标准库`base32`包的额外格式。针对用户希望访问`math/big`包中非导出函数`nat.string`的需求,文章阐明了go语言中非导出函数无法直接访问的限制,并提出通过`strconv.for…

    2025年12月16日
    000
  • Golang如何使用container/list管理链表

    Go语言中container/list包提供双向链表,无需手动实现节点结构;通过list.New()创建链表,或直接声明var l list.List即可使用;支持PushBack、PushFront在尾部或头部添加元素,也可用InsertAfter、InsertBefore在指定位置插入;遍历时通…

    2025年12月16日
    000
  • Go 项目测试文件组织:子目录、递归执行与覆盖率实践

    本文深入探讨 go 语言项目中测试文件的组织策略,重点介绍如何在子目录中管理测试、如何使用 `go test ./…` 命令进行递归测试,并分析其对包内容访问权限的影响。此外,文章还详细阐述了 go 1.20 引入的集成测试覆盖率功能,以及 `package_test` 模式的应用,旨在…

    2025年12月16日
    000
  • Go语言中Gzip解压数据不完整问题解析与io.Reader的正确使用姿势

    在使用go语言的`compress/gzip`包进行数据解压时,开发者常遇到单次`gzip.reader.read()`调用无法获取全部原始数据的问题。本文旨在阐明`io.reader`接口的读取机制,指出`read`方法并不保证一次性读取所有可用数据,并提供一个健壮的循环读取方案,确保在处理压缩或…

    2025年12月16日
    000
  • 如何在Golang中使用container/list操作链表

    答案:Golang的container/list包提供双向链表,支持任意类型存储。使用list.New()创建链表,PushFront/PushBack添加元素,Front/Next或Back/Prev遍历,Remove删除元素,直接赋值Value修改内容,还可执行移动、插入等操作,适用于频繁增删场…

    2025年12月16日
    000
  • Go语言在Windows环境下清空控制台的有效方法

    本教程详细介绍了go语言在windows操作系统中清空控制台的正确方法。通过利用`os/exec`包,结合windows命令解释器`cmd`及其`/c`参数执行`cls`命令,可以有效实现控制台屏幕的清空。文章提供了完整的代码示例和详细解释,帮助开发者在go程序中实现跨平台(侧重windows)的控…

    2025年12月16日
    000
  • Go语言并发编程:构建健壮的通道复用器

    本文深入探讨了go语言中通道复用器的实现,旨在将多个输入通道的数据合并到一个输出通道。文章首先剖析了初学者在实现过程中常遇到的闭包中循环变量捕获和并发共享状态管理(如计数器)的常见陷阱,并解释了这些问题如何导致非预期行为。随后,详细介绍了如何利用`sync.waitgroup`和正确的gorouti…

    2025年12月16日
    000
  • Go语言命名返回值:原理、用法与最佳实践

    go语言的命名返回值提供了一种声明函数返回变量的便捷方式,允许通过空 `return` 语句隐式返回这些变量的当前值,或通过显式 `return` 语句覆盖它们。这种机制得益于go在栈上分配参数和返回值的底层实现,使得命名返回值成为函数签名中预定义存储位置的自然表达。理解其工作原理有助于编写更清晰、…

    2025年12月16日
    000
  • Go语言中io.Writer接口的正确初始化与使用

    本文深入探讨了go语言中`io.writer`接口未初始化导致的运行时错误(nil指针解引用)问题。通过分析接口的本质及其与具体实现的关联,文章展示了如何正确地声明和初始化`io.writer`变量,并提供了使用`os.stdout`和`bytes.buffer`等具体类型进行初始化的示例,旨在帮助…

    2025年12月16日
    000
  • Go 语言命名返回值:用法、原理与最佳实践

    go 语言的命名返回值是一项强大特性,它允许在函数签名中声明返回变量,从而简化代码并提高可读性。本文深入探讨了命名返回变量的用法,包括其隐式和显式返回机制,并通过解释 go 函数参数和返回值在栈上的分配原理,揭示了其底层工作方式。我们将通过示例代码和汇编分析,确认其使用的合法性与高效性,并提供实践建…

    2025年12月16日
    000
  • Go并发模式:安全有效地合并多个通道

    本文深入探讨了go语言中如何安全高效地合并多个通道(channel)的数据流到一个单一通道。我们将分析并发编程中常见的陷阱,如循环变量的闭包捕获问题和共享状态的竞态条件,并详细介绍如何利用`sync.waitgroup`机制来优雅地管理并发goroutine的生命周期,从而构建一个健壮的通道复用器。…

    2025年12月16日
    000
  • 理解Go语言中io.Writer接口的空指针运行时错误

    本文深入探讨了Go语言中因未初始化`io.Writer`接口而导致的“panic: runtime error: invalid memory address or nil pointer dereference”运行时错误。文章通过分析示例代码,阐明了Go接口的零值行为,并提供了使用`os.Std…

    2025年12月16日
    000
  • Go 语言命名返回值:深入理解与最佳实践

    go 语言的命名返回值提供了一种简洁的方式来声明和管理函数返回结果。它们不仅可以避免重复声明,还允许使用裸 return 语句隐式返回已命名的变量。这种机制通过在函数调用栈上预留空间实现,确保了代码的清晰性和效率,并且在go标准库中被广泛应用,是一种完全推荐的编程实践。 在 Go 语言中,函数可以返…

    2025年12月16日
    000
  • 深入理解Go语言中io.Writer接口的nil值与运行时错误

    在Go语言中,未初始化的`io.Writer`接口变量默认为`nil`,当尝试对其执行写入操作时,会导致“panic: runtime error: invalid memory address or nil pointer dereference”运行时错误。本文将详细解释这一现象的原因,并通过示…

    2025年12月16日
    000
  • Go语言中io.Writer接口的正确初始化与使用:避免运行时错误

    本文详细解析了go语言中`io.writer`接口因未初始化而导致`nil`指针解引用运行时错误的原因。通过对比接口与具体类型的概念,并提供`os.stdout`和`bytes.buffer`等具体实现示例,指导开发者如何正确初始化并使用`io.writer`接口,从而避免常见的`panic`问题,…

    2025年12月16日
    000
  • Go语言命名返回值的深度解析与实践

    Go语言的命名返回值是一项强大特性,它允许在函数签名中声明返回变量,简化了变量声明并支持隐式返回。本文将深入探讨命名返回值的内部工作机制,包括其在栈上的表示以及与`return`语句的交互,并通过实例代码展示其正确用法和最佳实践,帮助开发者更有效地利用这一特性编写清晰、高效的Go代码。 命名返回值的…

    2025年12月16日
    000
  • Golang如何使用Kubernetes ConfigMap管理配置_Golang Kubernetes ConfigMap管理实践详解

    使用ConfigMap实现Golang应用配置管理,通过环境变量或文件挂载方式解耦配置,结合fsnotify监听实现热更新,提升应用灵活性与可维护性。 在 Kubernetes 环境中,应用配置与代码分离是最佳实践之一。Golang 作为云原生生态中的主流语言,天然适合与 Kubernetes 集成…

    2025年12月16日
    000
  • Golang如何使用text/template生成文本内容_Golang text/template文本生成实践详解

    Go语言中text/template包用于动态生成文本,支持变量替换、条件判断、循环及自定义函数。通过Parse解析模板字符串,Execute将数据写入io.Writer。结构体字段需首字母大写以导出,.Name等占位符对应字段值。使用{{if}}进行条件控制,{{range}}遍历切片或map。可…

    2025年12月16日
    000

发表回复

登录后才能评论
关注微信