从 CSV 加载时 PostgreSQL/JooQ 批量插入性能问题;我该如何改进流程?

2023-12-10

For 这个项目,我打算制作一个 Web 版本,现在正在制作一个 PostgreSQL (9.x) 后端,Web 应用程序将从该后端进行查询。

现在,跟踪器会生成一个包含两个 CSV 的 zip 文件,在运行时将其加载到 H2 数据库中,其架构如下(是的,我知道 SQL 可以写得更好一点):

create table matchers (
    id integer not null,
    class_name varchar(255) not null,
    matcher_type varchar(30) not null,
    name varchar(1024) not null
);

alter table matchers add primary key(id);

create table nodes (
    id integer not null,
    parent_id integer not null,
    level integer not null,
    success integer not null,
    matcher_id integer not null,
    start_index integer not null,
    end_index integer not null,
    time bigint not null
);

alter table nodes add primary key(id);
alter table nodes add foreign key (matcher_id) references matchers(id);
create index nodes_parent_id on nodes(parent_id);
create index nodes_indices on nodes(start_index, end_index);

现在,由于 PostgreSQL 数据库将能够处理多个跟踪,因此我必须再添加一张表; PostgreSQL 后端的模式看起来像这样(也低于平均 SQL 警报;此外,在parse_info表,该content列包含解析的文件的全文,在 zip 文件中它是单独存储的):

create table parse_info (
    id uuid primary key,
    date timestamp not null,
    content text not null
);

create table matchers (
    parse_info_id uuid references parse_info(id),
    id integer not null,
    class_name varchar(255) not null,
    matcher_type varchar(30) not null,
    name varchar(1024) not null,
    unique (parse_info_id, id)
);

create table nodes (
    parse_info_id uuid references parse_info(id),
    id integer not null,
    parent_id integer not null,
    level integer not null,
    success integer not null,
    matcher_id integer not null,
    start_index integer not null,
    end_index integer not null,
    time bigint not null,
    unique (parse_info_id, id)
);

alter table nodes add foreign key (parse_info_id, matcher_id)
    references matchers(parse_info_id, id);
create index nodes_parent_id on nodes(parent_id);
create index nodes_indices on nodes(start_index, end_index);

现在,我正在做的是将现有的 zip 文件插入到 postgresql 数据库中;我正在使用 JooQ 及其CSV 加载 API.

过程有点复杂...以下是当前步骤:

  • 生成一个UUID;
  • 我从 zip 中读取了必要的信息(解析日期、输入文本)并将记录写入parse_info table;
  • 我创建了 CSV 的临时副本,以便 JooQ 加载 API 能够使用它(请参阅代码摘录后了解原因);
  • 我插入所有匹配器,然后插入所有节点。

这是代码:

public final class Zip2Db2
{
    private static final Pattern SEMICOLON = Pattern.compile(";");
    private static final Function<String, String> CSV_ESCAPE
        = TraceCsvEscaper.ESCAPER::apply;

    // Paths in the zip to the different components
    private static final String INFO_PATH = "/info.csv";
    private static final String INPUT_PATH = "/input.txt";
    private static final String MATCHERS_PATH = "/matchers.csv";
    private static final String NODES_PATH = "/nodes.csv";

    // Fields to use for matchers zip insertion
    private static final List<Field<?>> MATCHERS_FIELDS = Arrays.asList(
        MATCHERS.PARSE_INFO_ID, MATCHERS.ID, MATCHERS.CLASS_NAME,
        MATCHERS.MATCHER_TYPE, MATCHERS.NAME
    );

    // Fields to use for nodes zip insertion
    private static final List<Field<?>> NODES_FIELDS = Arrays.asList(
        NODES.PARSE_INFO_ID, NODES.PARENT_ID, NODES.ID, NODES.LEVEL,
        NODES.SUCCESS, NODES.MATCHER_ID, NODES.START_INDEX, NODES.END_INDEX,
        NODES.TIME
    );

    private final FileSystem fs;
    private final DSLContext jooq;
    private final UUID uuid;

    private final Path tmpdir;

    public Zip2Db2(final FileSystem fs, final DSLContext jooq, final UUID uuid)
        throws IOException
    {
        this.fs = fs;
        this.jooq = jooq;
        this.uuid = uuid;

        tmpdir = Files.createTempDirectory("zip2db");
    }

    public void removeTmpdir()
        throws IOException
    {
        // From java7-fs-more (https://github.com/fge/java7-fs-more)
        MoreFiles.deleteRecursive(tmpdir, RecursionMode.KEEP_GOING);
    }

    public void run()
    {
        time(this::generateMatchersCsv, "Generate matchers CSV");
        time(this::generateNodesCsv, "Generate nodes CSV");
        time(this::writeInfo, "Write info record");
        time(this::writeMatchers, "Write matchers");
        time(this::writeNodes, "Write nodes");
    }

    private void generateMatchersCsv()
        throws IOException
    {
        final Path src = fs.getPath(MATCHERS_PATH);
        final Path dst = tmpdir.resolve("matchers.csv");

        try (
            final Stream<String> lines = Files.lines(src);
            final BufferedWriter writer = Files.newBufferedWriter(dst,
                StandardOpenOption.CREATE_NEW);
        ) {
            // Throwing below is from throwing-lambdas
            // (https://github.com/fge/throwing-lambdas)
            lines.map(this::toMatchersLine)
                .forEach(Throwing.consumer(writer::write));
        }
    }

    private String toMatchersLine(final String input)
    {
        final List<String> parts = new ArrayList<>();
        parts.add('"' + uuid.toString() + '"');
        Arrays.stream(SEMICOLON.split(input, 4))
            .map(s -> '"' + CSV_ESCAPE.apply(s) + '"')
            .forEach(parts::add);
        return String.join(";", parts) + '\n';
    }

    private void generateNodesCsv()
        throws IOException
    {
        final Path src = fs.getPath(NODES_PATH);
        final Path dst = tmpdir.resolve("nodes.csv");

        try (
            final Stream<String> lines = Files.lines(src);
            final BufferedWriter writer = Files.newBufferedWriter(dst,
                StandardOpenOption.CREATE_NEW);
        ) {
            lines.map(this::toNodesLine)
                .forEach(Throwing.consumer(writer::write));
        }
    }

    private String toNodesLine(final String input)
    {
        final List<String> parts = new ArrayList<>();
        parts.add('"' + uuid.toString() + '"');
        SEMICOLON.splitAsStream(input)
            .map(s -> '"' + CSV_ESCAPE.apply(s) + '"')
            .forEach(parts::add);
        return String.join(";", parts) + '\n';
    }

    private void writeInfo()
        throws IOException
    {
        final Path path = fs.getPath(INFO_PATH);

        try (
            final BufferedReader reader = Files.newBufferedReader(path);
        ) {
            final String[] elements = SEMICOLON.split(reader.readLine());

            final long epoch = Long.parseLong(elements[0]);
            final Instant instant = Instant.ofEpochMilli(epoch);
            final ZoneId zone = ZoneId.systemDefault();
            final LocalDateTime time = LocalDateTime.ofInstant(instant, zone);

            final ParseInfoRecord record = jooq.newRecord(PARSE_INFO);

            record.setId(uuid);
            record.setContent(loadText());
            record.setDate(Timestamp.valueOf(time));

            record.insert();
        }
    }

    private String loadText()
        throws IOException
    {
        final Path path = fs.getPath(INPUT_PATH);

        try (
            final BufferedReader reader = Files.newBufferedReader(path);
        ) {
            return CharStreams.toString(reader);
        }
    }

    private void writeMatchers()
        throws IOException
    {
        final Path path = tmpdir.resolve("matchers.csv");

        try (
            final BufferedReader reader = Files.newBufferedReader(path);
        ) {
            jooq.loadInto(MATCHERS)
                .onErrorAbort()
                .loadCSV(reader)
                .fields(MATCHERS_FIELDS)
                .separator(';')
                .execute();
        }
    }

    private void writeNodes()
        throws IOException
    {
        final Path path = tmpdir.resolve("nodes.csv");

        try (
            final BufferedReader reader = Files.newBufferedReader(path);
        ) {
            jooq.loadInto(NODES)
                .onErrorAbort()
                .loadCSV(reader)
                .fields(NODES_FIELDS)
                .separator(';')
                .execute();
        }
    }

    private void time(final ThrowingRunnable runnable, final String description)
    {
        System.out.println(description + ": start");
        final Stopwatch stopwatch = Stopwatch.createStarted();
        runnable.run();
        System.out.println(description + ": done (" + stopwatch.stop() + ')');
    }

    public static void main(final String... args)
        throws IOException
    {
        if (args.length != 1) {
            System.err.println("missing zip argument");
            System.exit(2);
        }

        final Path zip = Paths.get(args[0]).toRealPath();

        final UUID uuid = UUID.randomUUID();
        final DSLContext jooq = PostgresqlTraceDbFactory.defaultFactory()
            .getJooq();

        try (
            final FileSystem fs = MoreFileSystems.openZip(zip, true);
        ) {
            final Zip2Db2 zip2Db = new Zip2Db2(fs, jooq, uuid);
            try {
                zip2Db.run();
            } finally {
                zip2Db.removeTmpdir();
            }
        }
    }
}

现在,这是我的第一个问题...它比加载到 H2 慢得多。以下是包含 620 个匹配器和 45746 个节点的 CSV 的时序:

Generate matchers CSV: start
Generate matchers CSV: done (45.26 ms)
Generate nodes CSV: start
Generate nodes CSV: done (573.2 ms)
Write info record: start
Write info record: done (311.1 ms)
Write matchers: start
Write matchers: done (4.192 s)
Write nodes: start
Write nodes: done (22.64 s)

给予或接受,并忘记编写专门的 CSV 的部分(见下文),即 25 秒。将其加载到动态的、基于磁盘的 H2 数据库中需要不到5秒!

我遇到的另一个问题是我必须编写专用的 CSV;看来 CSV 加载 API 在接受的内容方面并不真正灵活,例如,我必须转动这一行:

328;SequenceMatcher;COMPOSITE;token

进入这个:

"some-randome-uuid-here";"328";"SequenceMatcher";"COMPOSITE";"token"

但我最大的问题实际上是这个拉链非常小。例如,我有一个 zip,其中不是 620 个匹配器,而是 1532 个匹配器,也不是 45746 个节点,而是超过3400万个节点;即使我们忽略 CSV 生成时间(原始节点 CSV 为 1.2 GiB),由于 H2 注入需要 20 分钟,因此将其乘以 5 会得出 1 小时 30 分钟以南的某个时间点,这是很多!

总而言之,目前这个过程效率很低......


现在,为 PostgreSQL 辩护:

  • 对 PostgreSQL 实例的限制比对 H2 实例的限制要高得多:我不需要在生成的 zip 文件中使用 UUID;
  • H2 针对写入进行“不安全”调整:jdbc:h2:/path/to/db;LOG=0;LOCK_MODE=0;UNDO_LOG=0;CACHE_SIZE=131072.

尽管如此,插入时间的这种差异似乎有点过大,而且我确信它可以更好。但我不知道从哪里开始。

另外,我知道 PostgreSQL 有一个专门的机制来从 CSV 加载,但这里的 CSV 是在一个 zip 文件中开始的,我真的很想避免像我目前正在做的那样创建一个专用的 CSV。理想情况下,我想直接从 zip 中逐行读取(这就是我为 H2 注入所做的),转换行并写入 PostgreSQL 模式。

最后,我还知道我目前在插入之前没有禁用 PostgreSQL 模式的约束;我还没有尝试过这个(会有什么不同吗?)。

那么,您建议我做什么来提高性能?


从 CSV 文件批量插入 PostgreSQL 的最快方法是Copy。 COPY 命令针对插入大量行进行了优化。

通过 Java,您可以使用PostgreSQL JDBC 驱动程序的复制实现

这里有一个关于如何使用它的很好的小例子:如何使用 JDBC 将数据从文件复制到 PostgreSQL?

如果您有一个带有标题的 CSV,您可能需要运行与此类似的命令:

\COPY mytable FROM '/tmp/mydata.csv' DELIMITER ';' CSV HEADER

当您向现有表添加大量数据时,另一个性能提升是删除索引,插入数据,然后重新创建索引。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

从 CSV 加载时 PostgreSQL/JooQ 批量插入性能问题;我该如何改进流程? 的相关文章

  • Mysql 中 UNION 子句的替代方案

    我有两张桌子 表 a 表 b table a ID 1 2 3 4 5 7 table b ID 2 3 4 5 6 我必须得到这样的输出而无需UNION命令 ID 1 2 3 4 5 6 7 注意 我有一个联合解决方案 select fr
  • java中如何连接字符串

    这是我的字符串连接代码 StringSecret java public class StringSecret public static void main String args String s new String abc s co
  • 按第一列排序二维数组,然后按第二列排序

    int arrs 1 100 11 22 1 11 2 12 Arrays sort arrs a b gt a 0 b 0 上面的数组已排序为 1 100 1 11 2 12 11 22 我希望它们按以下方式排序a 0 b 0 首先 如果
  • 当从服务类中调用时,Spring @Transactional 不适用于带注释的方法

    在下面的代码中 当方法内部 是从内部调用的方法外部 应该在交易范围内 但事实并非如此 但当方法内部 直接从调用我的控制器class 它受到事务的约束 有什么解释吗 这是控制器类 Controller public class MyContr
  • 列中差异的数量

    我想检索一列每行中有多少个字母的差异 例如 如果您有一个值 test 而另一行有一个值 testing 则 test 和 testing 之间的差异为 4 个字母 该列的数据值为 4 I have reflected about it an
  • 在一个数据访问层中处理多个连接字符串

    我有一个有趣的困境 我目前有一个数据访问层 它必须与多个域一起使用 并且每个域都有多个数据库存储库 具体取决于所调用的存储过程 目前 我只需使用 SWITCH 语句来确定应用程序正在运行的计算机 并从 Web config 返回适当的连接字
  • Hibernate.createBlob() 方法从 Hibernate 4.0.1 开始已弃用,并移至 Hibernate.getLobCreator(Session session).createBlob()

    Method Hibernate createBlob 已弃用自休眠4 0 1并搬到Hibernate getLobCreator Session session createBlob 任何解决方案我应该在方法内传递什么getLobCrea
  • sql server 按组排名

    问题看似简单 但我却无法理解 这是针对 sql 服务器的 what I have in a table What I need as a output cksum id cksum id 2162514679 204 2162514679
  • hibernate锁等待超时超时;

    我正在使用 Hibernate 尝试模拟对数据库中同一行的 2 个并发更新 编辑 我将 em1 getTransaction commit 移至 em1 flush 之后我没有收到任何 StaleObjectException 两个事务已成
  • 在 Netbeans 8 上配置 JBoss EAP 的问题

    我已经下载了 JBoss EAP 7 并正在 Netbeans 8 上配置它 我已经到达向导 实例属性 其中要求从选择框中选择 域 当我打开选择框时 它是空的 没有什么可以选择的 因此 完成 按钮也处于非活动状态 这使得无法完成配置 我通过
  • Java 中的“Lambdifying”scala 函数

    使用Java和Apache Spark 已用Scala重写 面对旧的API方法 org apache spark rdd JdbcRDD构造函数 其参数为 AbstractFunction1 abstract class AbstractF
  • 很好地处理数据库约束错误

    再一次 它应该很简单 我的任务是在我们的应用程序的域对象中放置一个具有唯一约束的特定字段 这本身并不是一个很大的挑战 我刚刚做了以下事情 public class Location more fields Column unique tru
  • 以编程方式在java的resources/source文件夹中创建文件?

    我有两个资源文件夹 src 这是我的 java 文件 资源 这是我的资源文件 图像 properties 组织在文件夹 包 中 有没有办法以编程方式在该资源文件夹中添加另一个 properties 文件 我尝试过这样的事情 public s
  • 编辑文件名在 JComboBox 中的显示方式,同时保持对文件的访问

    我对 Java 很陌生 对堆栈溢出也很陌生 我正在尝试利用 JMF API 创建一个用 Java 编码的简单媒体播放器 到目前为止 我已经能够设置一个简单的队列 播放列表来使用JComboBox called playListHolder
  • Jersey 客户端请求中未设置 Content-Length-Header

    我正在使用 Jersey Client 访问网络服务 如下所示 response r accept MediaType TEXT PLAIN TYPE header content length 0 post String class 其中
  • 当所有维度值都具有 100% 重要性时处理多对多维度

    我至少会尽力保持简洁 假设我们正在跟踪一段时间内的账户余额 所以我们的事实表将包含诸如 账户余额情况表 FK 账户ID FK 日期ID Balance 显然你有一个账户维度表 and a 日期维度表 所以现在我们可以轻松地过滤帐户或日期 或
  • HQL Hibernate 内连接

    我怎样才能在 Hibernate 中编写这个 SQL 查询 我想使用 Hibernate 来创建查询 而不是创建数据库 SELECT FROM Employee e INNER JOIN Team t ON e Id team t Id t
  • MySQL 按重复项从上到下排序

    我有一个lammer问题 因为我不是mysql专业人士 我有类似的字段 id color 1 red 2 green 3 yellow 4 green 5 green 6 red 我想按重复项进行分组 最常见的重复项先进行分组 所以应该这样
  • javafx android 中的文本字段和组合框问题

    我在简单的 javafx android 应用程序中遇到问题 问题是我使用 gradle javafxmobile plugin 在 netbeans ide 中构建了非常简单的应用程序 其中包含一些文本字段和组合框 我在 android
  • 在 RESTful Web 服务中实现注销

    我正在开发一个需要注销服务的移动应用程序 登录服务是通过数据库验证来完成的 现在我陷入了注销状态 退一步 您没有提供有关如何在应用程序中执行身份验证的详细信息 并且很难猜测您在做什么 但是 需要注意的是 在 REST 应用程序中 不能有会话

随机推荐

  • chdir() 可以接受相对路径吗?

    在Linux上的C中 chdir 函数可以接受相对路径吗 是的 当前工作目录是进程的一个属性 稍微扩展一下 这里有一些相关的POSIX定义 The 当前工作目录被定义为 与进程关联的目录 用于不以斜杠字符开头的路径名的路径名解析 有关更多详
  • 如何更改自定义轨道生成器的源? (雷神)

    我正在制作一个自定义生成器来生成新的 Rails 应用程序 我是这样做的 require thor require rails generators rails app app generator class AppBuilder lt R
  • ORACLE SQL 使用窗口函数运行 TOTAL 和 daytotal

    从 EMPLOYEE 表中 我想对记录数量 雇用的员工 进行分组 并且还有每天的运行总数 输入的格式是这样的 rownum Hired date time 1 1 10 2012 11 00 2 1 10 2012 13 00 3 20 1
  • 如何比较 if 语句中的多个字符串?

    我正在尝试检查 if 语句中的多种可能性 用户输入一个字符串 然后我检查该字符串是否有多种可能性 if theString Seven seven 7 theInt 7 cout lt lt You chose lt lt theInt l
  • Scala isInstanceOf 和类型擦除

    我很困惑如何isInstanceOf在 Scala 中工作 如果我做这样的事情 val x Int 5 x isInstanceOf Int 鉴于 Scala 确实进行了类型擦除 那么 JVM 不应该在运行时删除所有类型信息吗 它不是all
  • 如何获取Windows启动时间? [复制]

    这个问题在这里已经有答案了 如何使用某些 WinAPI 函数在 C 中获取 Windows 启动时间 我目前正在使用命令 systeminfo 但我正在寻找更合适的解决方案 我检查了任务管理器是如何做到这一点的 结果发现它使用ZwQuery
  • C++ 优化中的代码重新排序何时停止?

    我已经阅读了很多关于优化代码中的代码执行保证的问题 所以我试图编译一个关于允许 阻止编译器重新排序代码序列的原因列表 我已经从通常正确的内容开始回答 但我没有添加标准中的引号 这是根据我的经验得出的 如果有任何忘记的内容或对现有的要点进行扩
  • 使用Python多处理在worker之间共享变量[重复]

    这个问题在这里已经有答案了 如何读取和更新Python中多个worker之间共享的变量 例如 我正在使用 Python 中的多个进程扫描文件列表 并且想检查父目录是否已被扫描 def readFile filename Add the pa
  • R中Leaflet中聚集标记的聚合加权线串

    我正在尝试绘制位置和加权连接线串 当我放大或缩小时 标记的聚类会调整得很好 显示的集群标签是聚合的node val的标记 我想对线串做类似的事情 这样 该图不显示连接单个标记的蓝线 而是显示连接标记簇的线 并且 连接标记簇的新线串的宽度根据
  • 如何访问(转发)docker compose 网络内的公共 HTTP 请求 IP 地址?

    通过以下设置 只有 YARP 容器已发布端口 它正确设置了X Forward 供其他容器使用的标头 但不幸的是它是 docker compose 网关地址 当我想记录 公共 客户端的 IP 地址时 我得到 ffff 172 18 0 1这是
  • 如何创建具有依赖注入和接口的连接类?

    我正在读这个问题 PHP 多个不同的数据库依赖注入类 最佳答案 我理解这里使用接口背后的概念 但我不知道如何使用它 这是最重要的答案 抱歉 如果我不应该将其复制到这里 您应该首先为所有数据库操作创建一个接口 interface IDatab
  • d3 v4嵌套数据和堆积条形图

    I am trying to make a chart that looks like this 我有一个 D3 nest 数据结构 如下所示 key Area 1 values key 5 Validation Complete valu
  • Extjs 4通过ajax调用下载文件

    问题很简单 当我提交表单时 我必须下载一个文件 这是提交表单时的一个ajax调用 它允许我使用从表单 服务器端获取的数据构建一个文件 然后将其作为链接到警报 事实上 我的老板希望直接下载文件 而不是通过警报中的链接 所以我必须确保该文件可通
  • 如何在单元格更改上执行 JTable 选择所有文本

    我看过一些这样做的例子 但我仍然无法理解并且无法实现它 我想做的是单元格更改 焦点 下一个选定的单元格将选择所有文本 准备好让用户完全更改它 关于如何做有什么想法吗 更新 不知怎的 我设法完成了以下课程 但是 实施这个tblLayers s
  • 如何打印java txt文件中的列

    我正在尝试在一个 txt 文件上打印多个数组 其中有一个数组打印 然后有另一列装箱并有另一个数组打印 我如何格式化它才能工作 我不记得执行此操作的格式化命令 我需要所有列对齐 现在我有这个 private static void makeF
  • 包装python超类的所有方法

    如果我无法更改超类的代码 有没有办法包装超类的所有方法 作为一个最小的工作示例 请考虑这个基类Base 它有许多返回自身新实例的方法 以及派生类Child class Base def init self val self val val
  • Javascript appendChild onload 事件

    我将动态创建的图像元素附加到文档中 var img new Image img src test jpg img onload function var addedImg container appendChild img console
  • 基于 @AspectJ 的 Spring 3.1 AOP

    我正在尝试使用 Spring 3 1 运行基于 AspectJ 的 AOP 但无法正确配置切入点 切入点和建议方法是 切入点 Pointcut execution point public void selectAll 建议后 After
  • IMPORTHTML 或 IMPORTXML 从站点收集数据

    我已经多次尝试收集此表中的数据 我评论的两个函数的简单方法 我都尝试过 但没有成功 我想知道是否有人知道在 Google 表格中收集这些数据的任何其他方法 网站链接 https www onlinebettingacademy com st
  • 从 CSV 加载时 PostgreSQL/JooQ 批量插入性能问题;我该如何改进流程?

    For 这个项目 我打算制作一个 Web 版本 现在正在制作一个 PostgreSQL 9 x 后端 Web 应用程序将从该后端进行查询 现在 跟踪器会生成一个包含两个 CSV 的 zip 文件 在运行时将其加载到 H2 数据库中 其架构如