以极高的速度提取行

use*_*501 6 java sql oracle jdbc

我在Oracle中有非常大的表(数亿行,包含数字和字符串),我需要读取该表的所有内容,格式化它并写入文件或任何其他资源.通常我的解决方案如下所示:

package my.odp;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.lang.Throwable;
import java.sql.*;


public class Main {
public static volatile boolean finished = false;

public static void main(final String[] args) throws InterruptedException {
    final ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(10000);
    final Thread writeWorker = new Thread("ODP Writer") {
        public void run() {
            try {
                File targetFile = new File(args[0]);
                FileWriter fileWriter = new FileWriter(targetFile);
                BufferedWriter writer = new BufferedWriter(fileWriter);
                String str;
                try {
                    while (!finished) {
                        str = queue.poll(200, TimeUnit.MILLISECONDS);
                        if (str == null) {
                            Thread.sleep(50);
                            continue;
                        }
                        writer.write(str);
                        writer.write('\n');
                    }
                } catch (InterruptedException e) {
                    writer.close();
                    return;
                }
            }
            catch (Throwable e) {
                e.printStackTrace();
                return;
            }
        }
    };

    final Thread readerThread = new Thread("ODP Reader") {
        public void run() {
            try {
                Class.forName("oracle.jdbc.OracleDriver");
                Connection conn = DriverManager.getConnection("jdbc:oracle:thin:@//xxx.xxx.xxx.xxx:1521/orcl", "user", "pass");

                Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
                stmt.setFetchSize(500000);
                ResultSet rs = stmt.executeQuery("select * from src_schema.big_table_view");
                System.out.println("Fetching result");
                while (rs.next()) {
                    StringBuilder sb = new StringBuilder();
                    sb.append(rs.getString(1)).append('\t');//OWNER
                    sb.append(rs.getString(2)).append('\t');//OBJECT_NAME
                    sb.append(rs.getString(3)).append('\t');//SUBOBJECT_NAME
                    sb.append(rs.getLong(4)).append('\t');//OBJECT_ID
                    sb.append(rs.getLong(5)).append('\t');//DATA_OBJECT_ID
                    sb.append(rs.getString(6)).append('\t');//OBJECT_TYPE
                    sb.append(rs.getString(7)).append('\t');//CREATED
                    sb.append(rs.getString(8)).append('\t');//LAST_DDL_TIME
                    sb.append(rs.getString(9)).append('\t');//TIMESTAMP
                    sb.append(rs.getString(10)).append('\t');//STATUS
                    sb.append(rs.getString(11)).append('\t');//TEMPORARY
                    sb.append(rs.getString(12)).append('\t');//GENERATED
                    sb.append(rs.getString(13)).append('\t');//SECONDARY
                    sb.append(rs.getString(14)).append('\t');//NAMESPACE
                    sb.append(rs.getString(15));//EDITION_NAME
                    queue.put(sb.toString());
                }

                rs.close();
                stmt.close();
                conn.close();
                finished = true;
            } catch (Throwable e) {
                e.printStackTrace();
                return;
            }
        }
    };
    long startTime = System.currentTimeMillis();
    writeWorker.start();
    readerThread.start();
    System.out.println("Waiting for join..");
    writeWorker.join();
    System.out.println("Exit:"+ (System.currentTimeMillis() - startTime));
}
Run Code Online (Sandbox Code Playgroud)

}

有两个线程:一个用于从结果集中获取行,另一个用于写入字符串值.测量的加载速度大约是10Mb/s,在我的情况下,我需要加快10倍.Profiler显示最耗时的方法是

oracle.jdbc.driver.OracleResultSetImpl.getString()

和

oracle.net.ns.Packet.receive()

您是否有任何想法如何使jdbc更快地加载数据?任何关于查询优化,字符串加载优化,调整JDBC驱动程序或使用另一个,直接使用oracle JDBC实现,调整Oracle的想法都值得赞赏.

更新: 我编译并列出了下面的讨论结果:

  1. 我无法访问DBMS服务器,除了连接到Oracle数据库和服务器无法连接到任何外部资源.使用服务器或远程文件系统的任何转储和提取工具都无法应用,也无法在服务器上安装和使用任何外部Java或PL/SQL例程.只有连接执行查询 - 这就是全部.

  2. 我使用了探查器并挖掘了Oracle JDBC驱动程序.我发现最昂贵的操作是读取数据,即Socket.read().所有字符串字段都表示为一个char数组,并且对性能几乎没有影响.通常,我使用分析器检查整个应用程序,而Socket.read()绝对是最昂贵的操作.提取字段,构建字符串,写入数据几乎不消耗任何东西.问题仅在于阅读数据.

  3. 服务器端的数据表示中的任何优化都没有实际效果.连接字符串和转换时间戳对性能没有影响.

  4. 应用程序被重写为有几个读取器线程,将准备好的数据放入编写器队列中.每个线程都有自己的连接,没有使用池,因为它们减慢了提取速度(我使用了oracle推荐的UCP池,它占用了大约10%的执行时间,所以我放弃了它).结果集fetchSize也增加了,因为从默认值(10)切换到50000可以提高50%的性能增长.

  5. 我测试了多线程版本如何与4个读取线程一起工作,并发现增加读者数量只会减慢提取速度.我尝试启动2个实例,其中每个实例都有两个读取器,并且两个实例同时作为单个实例工作,即双数据提取需要与单个实例相同的时间.不知道为什么会发生这种情况,但看起来oracle驱动程序有一些性能限制.具有4个独立连接的应用程序比2个具有2个连接的App实例慢.(Profiler用于确保驱动程序的Socket.read()仍然是主要问题,所有其他部分在多线程模式下工作正常).

  6. 我尝试使用SAS获取所有数据,并且它可以比JDBC快2倍地执行相同的提取,它们都使用单个连接到Oracle并且不能使用任何转储操作.Oracle确保JDBC瘦驱动程序与本机驱动程序一样快.

也许Oracle有另一种方法可以通过ODBC或其他方式快速提取到远程主机?

Ste*_*n C 1

看起来您已经找到并调整了行预取参数。然而,根据 Oracle 文档:

“没有最大预取设置,但经验证据表明 10 是有效的。Oracle 从未观察到将预取设置为高于 50 会带来性能优势。如果没有为连接设置默认的行预取​​值,则 10 就是默认。”

您将其设置为 500000。尝试将其回绕到 50 左右...按照 Oracle 的建议。(为什么?可能是预取大小过大导致服务器或客户端使用过多的内存来缓冲预取数据。这可能会对其他事情产生“连锁效应”,导致吞吐量降低。)

参考(来自Oracle 10g文档):


通过在多个 Java 线程中运行同时查询(例如,在表的单独“部分”上),将每个结果集写入单独的流/文件,您也许能够获得更大的吞吐量。但是,您会遇到将输出流/文件拼接在一起的问题。(是否获得整体改进将取决于客户端和服务器端核心的数量、网络和网卡容量以及磁盘 I/O 容量。)

除此之外,我想不出任何方法可以在 Java 中更快地做到这一点。但您可以尝试 PL/SQL 或较低级别的东西。(我不是 Oracle 专家。请咨询您的 DBA。)

Java 速度提高 10 倍是……雄心勃勃的。