diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 000000000..aa3455c0a --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,124 @@ +name: Benchmark + +on: + workflow_dispatch: + inputs: + clickhouse: + description: "ClickHouse version" + required: true + default: "latest" + java: + description: "Java version" + required: true + default: "8" + baseline: + description: "Baseline to compare" + required: true + default: "0.2.6" + driver: + description: "Driver version" + required: true + default: "0.3.0-SNAPSHOT" + options: + description: "Benchmark options" + required: true + default: "-prof gc 'Query|Insertion'" + pr: + description: "Pull request#" + required: false + +jobs: + benchmark: + runs-on: ubuntu-latest + name: Benchmark on demand + if: github.event_name == 'workflow_dispatch' + steps: + - name: Check out repository + uses: actions/checkout@v2 + - name: Check out PR + run: | + git fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 \ + origin pull/${{ github.event.inputs.pr }}/merge:merged-pr && git checkout merged-pr + if: github.event.inputs.pr != '' + - name: Set up JDK ${{ github.event.inputs.java }} + uses: actions/setup-java@v1 + with: + java-version: ${{ github.event.inputs.java }} + continue-on-error: true + - name: Update version and group id + run: | + find . -type f -name "pom.xml" -exec sed -i -e 's|${revision}|${{ github.event.inputs.driver }}|g' \ + -e 's|${parent.groupId}|tech.clickhouse|g' '{}' \; + sed -i -e 's|^\( \).*\(\)$|\1${{ github.event.inputs.driver }}\2|' pom.xml + continue-on-error: true + - name: Install driver as needed + run: mvn --batch-mode --update-snapshots -q -DskipTests install + if: endsWith(github.event.inputs.driver, '-SNAPSHOT') + continue-on-error: true + - name: Build project + run: | + cd clickhouse-benchmark + mvn --batch-mode --update-snapshots -Drevision=${{ github.event.inputs.driver }} \ + -DclickhouseVersion=${{ github.event.inputs.clickhouse }} install + java -jar target/benchmarks.jar -rf json ${{ github.event.inputs.options }} > output.txt + echo "BENCHMARK_REPORT<> $GITHUB_ENV + tail -n +$(grep -n '^REMEMBER:' output.txt | tail -1 | awk -F: '{print $1+6}') output.txt | head -n -2 | grep -v ':ยท' >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + cd - + id: benchmark + continue-on-error: true + - name: Record benchmark results + run: | + mv -fv clickhouse-benchmark/jmh-result.json \ + clickhouse-benchmark/docs/results/v${{ github.event.inputs.driver }}.json + if: github.event.inputs.pr == '' + continue-on-error: true + - name: Record pull request benchmark results + run: | + mv -fv clickhouse-benchmark/jmh-result.json \ + clickhouse-benchmark/docs/results/pull-request_${{github.event.inputs.pr}}.json + if: github.event.inputs.pr != '' + continue-on-error: true + - name: Commit benchmark result + uses: zhicwu/add-and-commit@v7 + with: + add: 'clickhouse-benchmark/docs/results/*.json' + author_name: zhicwu + author_email: 4270380+zhicwu@users.noreply.github.com + branch: develop + message: 'Record benchmark results' + push: true + continue-on-error: true + - name: Get commit hash + run: | + echo ::set-output name=hash::$(git log --pretty=format:'%H' -n 1) + id: commit + continue-on-error: true + - name: 'Comment PR' + uses: actions/github-script@v3 + if: github.event.inputs.pr != '' + env: + PR_NO: ${{ github.event.inputs.pr }} + COMMIT_HASH: ${{ steps.commit.outputs.hash }} + COMPARE_TO: ${{ github.event.inputs.baseline }} + CLICKHOUSE_VRESION: ${{ github.event.inputs.clickhouse }} + DRIVER_VRESION: ${{ github.event.inputs.driver }} + JAVA_VERSION: ${{ github.event.inputs.java }} + PREV_STEP_RESULT: '${{ steps.benchmark.outcome }}' + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const issue_number = process.env.PR_NO; + const { repo: { owner, repo } } = context; + const result = process.env.PREV_STEP_RESULT; + const compareUrl = `http://jmh.morethan.io/?sources=https://raw.githubusercontent.com/${owner}/${repo}/develop/clickhouse-benchmark/docs/results/v${process.env.COMPARE_TO}.json,https://raw.githubusercontent.com/${owner}/${repo}/${process.env.COMMIT_HASH || 'develop'}/clickhouse-benchmark/docs/results/${issue_number ? 'pull-request_' + issue_number : 'v' + process.env.DRIVER_VERSION}.json`; + // const benchmarkUrl = `http://jmh.morethan.io/?source=https://raw.githubusercontent.com/${owner}/${repo}/${process.env.COMMIT_HASH || 'develop'}/clickhouse-benchmark/docs/results/${issue_number ? 'pull-request_' + issue_number : 'v' + process.env.DRIVER_VERSION}.json`; + const buildUrl = `https://github.com/${owner}/${repo}/actions/runs/${context.runId}`; + const flag = result === 'success' + ? ':green_circle:' + : (result === 'failure' ? ':red_circle:' : ':yellow_circle:'); + const msg = `${flag} [benchmark](${compareUrl}) using JDK [${process.env.JAVA_VERSION}] and ClickHouse [${process.env.CLICKHOUSE_VRESION}]: [${result}](${buildUrl})` + + '
\nExpand to see details...\n\n```\n' + + process.env.BENCHMARK_REPORT || '' + + '\n```\n
'; + github.issues.createComment({ issue_number, owner, repo, body: msg }); diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 74f431ac3..bee23239a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,9 +5,11 @@ on: branches: - master - develop + - 0.2.x paths-ignore: - - "CHANGELOG" - "**.md" + - "docs/**" + - "**/CHANGELOG" pull_request: types: @@ -15,17 +17,18 @@ on: - synchronize - reopened paths-ignore: - - "CHANGELOG" - "**.md" + - "docs/**" + - "**/CHANGELOG" jobs: build: runs-on: ubuntu-latest strategy: matrix: - java: [8, 9, 11] - # most recent LTS releases and latest stable build - clickhouse: ["19.14", "20.3", "20.8", "latest"] + java: [8, 11] + # most recent LTS releases as well as latest stable builds + clickhouse: ["19.14", "20.3", "20.8", "20.10", "20.12", "21.2", "latest"] name: Build using JDK ${{ matrix.java }} against ClickHouse ${{ matrix.clickhouse }} steps: - name: Check out Git repository diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d13b22284..1f158a1ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: version: description: "Release version" required: true - default: "0.2.6-SNAPSHOT" + default: "0.3.0-SNAPSHOT" jobs: release: @@ -19,13 +19,14 @@ jobs: - name: Install Java and Maven uses: actions/setup-java@v1 with: - java-version: 1.8 - - run: sed -i -e 's|^\( \).*\(\)$|\1${{ github.event.inputs.version }}\2|' pom.xml + java-version: 8 + - run: find . -type f -name "pom.xml" -exec sed -i -e 's|${revision}|${{ github.event.inputs.version }}|g' -e 's|${parent.groupId}|tech.clickhouse|g' '{}' \; + - run: find . -type f -name "log4j.properties" -exec sed -i -e 's|DEBUG|WARN|g' '{}' \; - name: Release Maven package uses: samuelmeuli/action-maven-publish@v1 with: maven_profiles: release - maven_args: --batch-mode + maven_args: -q --batch-mode gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} nexus_username: ${{ secrets.SONATYPE_USER }} diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 229a4aeee..f509ff779 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -14,23 +14,126 @@ on: pr: description: "Pull request#" required: false + pull_request: + types: + - opened + - synchronize + - reopened + paths-ignore: + - "**.md" + - "docs/**" + - "**/CHANGELOG" + issue_comment: + types: + - created + - edited jobs: - verify: + verify-commented-pr: runs-on: ubuntu-latest - name: Verify branch/PR using JDK ${{ github.event.inputs.java }} against ClickHouse ${{ github.event.inputs.clickhouse }} + name: Verify commented PR + if: github.event_name == 'issue_comment' && github.event.issue.pull_request steps: + - uses: zhicwu/pull-request-comment-trigger@master + id: check + with: + trigger: '@verify' + reaction: rocket + env: + GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + - name: Extra parameters from PR comment + if: steps.check.outputs.triggered == 'true' + uses: actions/github-script@v3 + id: commented + env: + COMMENT_BODY: '${{ github.event.comment.body }}' + with: + script: | + const keyword = '@verify'; + let buildArgs = process.env.COMMENT_BODY; + core.info(`Got commented body: ${buildArgs}`); + buildArgs = buildArgs.substring(buildArgs.lastIndexOf(keyword) + keyword.length); + const args = buildArgs.match(/[^\s]+/g); + core.info(`Got commented arguments: ${args}`); + + return { + pr: context.issue.number, + clickhouse: args && args.length > 0 ? args[0] : "latest", + java: args && args.length > 1 ? args[1] : "8" + }; - name: Check out repository uses: actions/checkout@v2 - if: github.event.inputs.pr == '' - - name: Check out PR ${{ github.event.inputs.pr }} + - name: Check out commented PR + if: steps.check.outputs.triggered == 'true' + run: | + git fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin pull/${{ fromJSON(steps.commented.outputs.result).pr }}/merge:merged-pr && git checkout merged-pr + - name: Set up JDK + if: steps.check.outputs.triggered == 'true' + uses: actions/setup-java@v1 + with: + java-version: ${{ fromJSON(steps.commented.outputs.result).java }} + continue-on-error: true + - name: Verify with Maven + if: steps.check.outputs.triggered == 'true' + run: mvn --batch-mode --update-snapshots -DclickhouseVersion=${{ fromJSON(steps.commented.outputs.result).clickhouse }} verify + id: maven + continue-on-error: true + - name: Comment PR + uses: actions/github-script@v3 + if: steps.check.outputs.triggered == 'true' + env: + CLICKHOUSE_VRESION: ${{ fromJSON(steps.commented.outputs.result).clickhouse }} + JAVA_VERSION: ${{ fromJSON(steps.commented.outputs.result).java }} + PREV_STEP_RESULT: '${{ steps.maven.outcome }}' + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { issue: { number: issue_number }, repo: { owner, repo } } = context; + const result = process.env.PREV_STEP_RESULT; + const buildUrl = `https://github.com/${owner}/${repo}/actions/runs/${context.runId}`; + const flag = result === 'success' + ? ':green_circle:' + : (result === 'failure' ? ':red_circle:' : ':yellow_circle:'); + const msg = `${flag} verify using JDK [${process.env.JAVA_VERSION}] and ClickHouse [${process.env.CLICKHOUSE_VRESION}]: [${result}](${buildUrl})`; + github.issues.createComment({ issue_number, owner, repo, body: msg }); + + verify-on-demand: + runs-on: ubuntu-latest + name: Verify on demand + if: github.event_name == 'workflow_dispatch' + steps: + - name: Check out repository uses: actions/checkout@v2 + - name: Check out PR + run: | + git fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin pull/${{ github.event.inputs.pr }}/merge:merged-pr && git checkout merged-pr if: github.event.inputs.pr != '' - with: - ref: refs/remotes/pull/${{ github.event.inputs.pr }}/merge - name: Set up JDK ${{ github.event.inputs.java }} uses: actions/setup-java@v1 with: java-version: ${{ github.event.inputs.java }} + continue-on-error: true - name: Verify with Maven run: mvn --batch-mode --update-snapshots -DclickhouseVersion=${{ github.event.inputs.clickhouse }} verify + id: maven + continue-on-error: true + - name: 'Comment PR' + uses: actions/github-script@v3 + if: github.event.inputs.pr != '' + env: + PR_NO: ${{ github.event.inputs.pr }} + CLICKHOUSE_VRESION: ${{ github.event.inputs.clickhouse }} + JAVA_VERSION: ${{ github.event.inputs.java }} + PREV_STEP_RESULT: '${{ steps.maven.outcome }}' + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const issue_number = process.env.PR_NO; + const { repo: { owner, repo } } = context; + const result = process.env.PREV_STEP_RESULT; + const buildUrl = `https://github.com/${owner}/${repo}/actions/runs/${context.runId}`; + const flag = result === 'success' + ? ':green_circle:' + : (result === 'failure' ? ':red_circle:' : ':yellow_circle:'); + const msg = `${flag} verify using JDK [${process.env.JAVA_VERSION}] and ClickHouse [${process.env.CLICKHOUSE_VRESION}]: [${result}](${buildUrl})`; + github.issues.createComment({ issue_number, owner, repo, body: msg }); diff --git a/.gitignore b/.gitignore index a3764496c..285aa2d41 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ # VSCode .vscode +.factorypath # Eclipse .classpath diff --git a/clickhouse-benchmark/docs/results/v0.2.4.json b/clickhouse-benchmark/docs/results/v0.2.4.json new file mode 100644 index 000000000..e69de29bb diff --git a/clickhouse-benchmark/docs/results/v0.2.5.json b/clickhouse-benchmark/docs/results/v0.2.5.json new file mode 100644 index 000000000..e69de29bb diff --git a/clickhouse-benchmark/docs/results/v0.2.6.json b/clickhouse-benchmark/docs/results/v0.2.6.json new file mode 100644 index 000000000..e69de29bb diff --git a/clickhouse-benchmark/pom.xml b/clickhouse-benchmark/pom.xml new file mode 100644 index 000000000..c967c9eb6 --- /dev/null +++ b/clickhouse-benchmark/pom.xml @@ -0,0 +1,189 @@ + + 4.0.0 + + tech.clickhouse + clickhouse-benchmark + ${revision} + jar + + clickhouse-benchmark + + + 0.3.0-SNAPSHOT + 1.4.4 + 2.5.3 + 2.7.2 + 8.0.23 + 2.5.3 + 1.15.2 + UTF-8 + 1.27 + 1.8 + benchmarks + + + + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + provided + + + + + ru.yandex.clickhouse + clickhouse-jdbc + ${revision} + shaded + + + * + * + + + + + cc.blynk.clickhouse + clickhouse4j + ${clickhouse4j-driver.version} + + + * + * + + + + + org.mariadb.jdbc + mariadb-java-client + ${mariadb-driver.version} + + + * + * + + + + + mysql + mysql-connector-java + ${mysql-driver.version} + + + * + * + + + + + com.github.housepower + clickhouse-native-jdbc-shaded + ${native-driver.version} + + + * + * + + + + + org.testcontainers + testcontainers + ${testcontainers.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + ${javac.target} + ${javac.target} + ${javac.target} + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + package + + shade + + + ${shade.name} + + + org.openjdk.jmh.Main + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + + maven-clean-plugin + 2.5 + + + maven-deploy-plugin + 2.8.1 + + + maven-install-plugin + 2.5.1 + + + maven-jar-plugin + 2.4 + + + maven-javadoc-plugin + 2.9.1 + + + maven-resources-plugin + 2.6 + + + maven-site-plugin + 3.3 + + + maven-source-plugin + 2.2.1 + + + maven-surefire-plugin + 2.17 + + + + + diff --git a/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/Basic.java b/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/Basic.java new file mode 100644 index 000000000..da543ee77 --- /dev/null +++ b/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/Basic.java @@ -0,0 +1,34 @@ +package tech.clickhouse.benchmark; + +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.Collections; + +import org.openjdk.jmh.annotations.Benchmark; + +public class Basic extends JdbcBenchmark { + @Benchmark + public int selectOneRandomNumber(ClientState state) throws Throwable { + final int num = (int) (Math.random() * 1000); + + try (Statement stmt = executeQuery(state, "select ? as n", num)) { + ResultSet rs = stmt.getResultSet(); + + rs.next(); + + if (num != rs.getInt(1)) { + throw new IllegalStateException(); + } + + return num; + } + } + + @Benchmark + public int insertOneRandomNumber(ClientState state) throws Throwable { + final int num = (int) (Math.random() * 1000); + + return executeInsert(state, "insert into test_insert(i) values(?)", + Collections.enumeration(Collections.singletonList(new Object[] { num }))); + } +} diff --git a/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/ClientState.java b/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/ClientState.java new file mode 100644 index 000000000..cc22324e7 --- /dev/null +++ b/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/ClientState.java @@ -0,0 +1,61 @@ +package tech.clickhouse.benchmark; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Properties; + +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; + +@State(Scope.Thread) +public class ClientState { + @Param(value = { "clickhouse4j", Constants.CLICKHOUSE_DRIVER, "clickhouse-native-jdbc-shaded", + "mariadb-java-client", "mysql-connector-java" }) + private String client; + + @Param(value = { Constants.NORMAL_STATEMENT, Constants.PREPARED_STATEMENT }) + private String statement; + + private Connection conn; + + @Setup(Level.Trial) + public void doSetup(ServerState serverState) throws Exception { + JdbcDriver driver = JdbcDriver.from(client); + + try { + conn = ((java.sql.Driver) Class.forName(driver.getClassName()).getDeclaredConstructor().newInstance()) + .connect(String.format(driver.getUrlTemplate(), serverState.getHost(), + serverState.getPort(driver.getDefaultPort()), serverState.getDatabase(), + serverState.getUser(), serverState.getPassword()), new Properties()); + + try (Statement s = conn.createStatement()) { + s.execute( + "create table if not exists test_insert(i Nullable(UInt64), s Nullable(String), t Nullable(DateTime))engine=Memory"); + } + } catch (SQLException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + @TearDown(Level.Trial) + public void doTearDown() throws SQLException { + try (Statement s = conn.createStatement()) { + s.execute("drop table if exists test_insert"); + } + conn.close(); + } + + public Connection getConnection() { + return this.conn; + } + + public boolean usePreparedStatement() { + return Constants.PREPARED_STATEMENT.equals(this.statement); + } +} diff --git a/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/Constants.java b/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/Constants.java new file mode 100644 index 000000000..e2873579b --- /dev/null +++ b/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/Constants.java @@ -0,0 +1,18 @@ +package tech.clickhouse.benchmark; + +public interface Constants { + public static final String CLICKHOUSE_DRIVER = "clickhouse-jdbc"; + + public static final String DEFAULT_HOST = "127.0.0.1"; + public static final String DEFAULT_DB = "system"; + public static final String DEFAULT_USER = "default"; + public static final String DEFAULT_PASSWD = ""; + + public static final int GRPC_PORT = 9100; + public static final int HTTP_PORT = 8123; + public static final int MYSQL_PORT = 3307; + public static final int NATIVE_PORT = 9000; + + public static final String NORMAL_STATEMENT = "normal"; + public static final String PREPARED_STATEMENT = "prepared"; +} diff --git a/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/Insertion.java b/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/Insertion.java new file mode 100644 index 000000000..15fa43f49 --- /dev/null +++ b/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/Insertion.java @@ -0,0 +1,76 @@ +package tech.clickhouse.benchmark; + +import java.sql.Timestamp; +// import java.util.Collections; +import java.util.Enumeration; + +import org.openjdk.jmh.annotations.Benchmark; + +public class Insertion extends JdbcBenchmark { + // @Benchmark + // public int insertOneNumber(ClientState state) throws Throwable { + // return executeInsert(state, "insert into test_insert(i) values(?)", + // Collections.enumeration(Collections.singletonList(new Object[] { (int) + // (Math.random() * 1000) }))); + // } + + @Benchmark + public int insert10kUInt64Rows(ClientState state) throws Throwable { + final int rows = 10000; + final int num = (int) (Math.random() * rows); + + return executeInsert(state, "insert into test_insert(i) values(?)", new Enumeration() { + int counter = 0; + + @Override + public boolean hasMoreElements() { + return counter < rows; + } + + @Override + public Object[] nextElement() { + return new Object[] { num + (counter++) }; + } + }); + } + + @Benchmark + public int insert10kStringRows(ClientState state) throws Throwable { + final int rows = 10000; + final int num = (int) (Math.random() * rows); + + return executeInsert(state, "insert into test_insert(s) values(?)", new Enumeration() { + int counter = 0; + + @Override + public boolean hasMoreElements() { + return counter < rows; + } + + @Override + public Object[] nextElement() { + return new Object[] { String.valueOf(num + (counter++)) }; + } + }); + } + + @Benchmark + public int insert10kTimestampRows(ClientState state) throws Throwable { + final int rows = 10000; + final int num = (int) (Math.random() * rows); + + return executeInsert(state, "insert into test_insert(t) values(?)", new Enumeration() { + int counter = 0; + + @Override + public boolean hasMoreElements() { + return counter < rows; + } + + @Override + public Object[] nextElement() { + return new Object[] { new Timestamp(num + (counter++)) }; + } + }); + } +} diff --git a/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/JdbcBenchmark.java b/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/JdbcBenchmark.java new file mode 100644 index 000000000..e1e4afdc3 --- /dev/null +++ b/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/JdbcBenchmark.java @@ -0,0 +1,119 @@ +package tech.clickhouse.benchmark; + +import org.openjdk.jmh.annotations.*; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Enumeration; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +@State(Scope.Benchmark) +@Warmup(iterations = 10, timeUnit = TimeUnit.SECONDS, time = 1) +@Measurement(iterations = 10, timeUnit = TimeUnit.SECONDS, time = 1) +@Fork(value = 2) +@Threads(value = -1) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +public abstract class JdbcBenchmark { + // batch size for mutation + private final int batchSize = Integer.parseInt(System.getProperty("batchSize", "1000")); + // fetch size for query + private final int fetchSize = Integer.parseInt(System.getProperty("fetchSize", "1000")); + + protected PreparedStatement setParameters(PreparedStatement s, Object... values) throws SQLException { + if (values != null && values.length > 0) { + int index = 1; + for (Object v : values) { + s.setObject(index++, v); + } + } + + return s; + } + + protected String replaceParameters(String sql, Object... values) { + if (values != null && values.length > 0) { + for (Object v : values) { + int index = sql.indexOf('?'); + if (index == -1) { + break; + } + + String expr = null; + if (v instanceof Number) { + expr = String.valueOf(v); + } else { + expr = "'" + v + "'"; // without escaping... + } + + sql = sql.substring(0, index) + expr + sql.substring(index + 1); + } + } + + return sql; + } + + private int processBatch(Statement s, String sql, Enumeration generator) throws SQLException { + int rows = 0; + int counter = 0; + PreparedStatement ps = s instanceof PreparedStatement ? (PreparedStatement) s : null; + while (generator.hasMoreElements()) { + Object[] values = generator.nextElement(); + if (ps != null) { + setParameters(ps, values).addBatch(); + } else { + s.addBatch(replaceParameters(sql, values)); + } + if (++counter % batchSize == 0) { + rows += s.executeBatch().length; + } + } + + if (counter % batchSize != 0) { + rows += s.executeBatch().length; + } + + return rows; + } + + protected int executeInsert(ClientState state, String sql, Enumeration generator) throws SQLException { + Objects.requireNonNull(generator); + + final Connection conn = state.getConnection(); + int rows = 0; + + if (state.usePreparedStatement()) { + try (PreparedStatement s = conn.prepareStatement(sql)) { + rows = processBatch(s, sql, generator); + } + } else { + try (Statement s = conn.createStatement()) { + rows = processBatch(s, sql, generator); + } + } + + return rows; + } + + protected Statement executeQuery(ClientState state, String sql, Object... values) throws SQLException { + final Statement stmt; + + final Connection conn = state.getConnection(); + + if (state.usePreparedStatement()) { + PreparedStatement s = conn.prepareStatement(sql); + s.setFetchSize(fetchSize); + setParameters(s, values).executeQuery(); + stmt = s; + } else { + stmt = conn.createStatement(); + stmt.setFetchSize(fetchSize); + stmt.executeQuery(replaceParameters(sql, values)); + } + + return stmt; + } +} diff --git a/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/JdbcDriver.java b/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/JdbcDriver.java new file mode 100644 index 000000000..5476eb9cd --- /dev/null +++ b/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/JdbcDriver.java @@ -0,0 +1,69 @@ +package tech.clickhouse.benchmark; + +public enum JdbcDriver { + // ClickHouse4j + Clickhouse4j("cc.blynk.clickhouse.ClickHouseDriver", + "jdbc:clickhouse://%s:%s/%s?ssl=false&user=%s&password=%s&use_server_time_zone=false&use_time_zone=UTC", + Constants.HTTP_PORT), + // ClickHouse JDBC Driver + ClickhouseJdbc("ru.yandex.clickhouse.ClickHouseDriver", + "jdbc:clickhouse://%s:%s/%s?ssl=false&user=%s&password=%s&use_server_time_zone=false&use_time_zone=UTC", + Constants.HTTP_PORT), + // ClickHouse Native JDBC Driver + ClickhouseNativeJdbcShaded("com.github.housepower.jdbc.ClickHouseDriver", + "jdbc:clickhouse://%s:%s/%s?ssl=false&user=%s&password=%s&use_server_time_zone=false&use_time_zone=UTC", + Constants.NATIVE_PORT), + + // MariaDB Java Client + MariadbJavaClient("org.mariadb.jdbc.Driver", + "jdbc:mariadb://%s:%s/%s?user=%s&password=%s&useSSL=false&useCompression=true&useServerPrepStmts=false&rewriteBatchedStatements=true&cachePrepStmts=true&serverTimezone=UTC", + Constants.MYSQL_PORT), + + // MySQL Connector/J + MysqlConnectorJava("com.mysql.cj.jdbc.Driver", + "jdbc:mysql://%s:%s/%s?user=%s&password=%s&useSSL=false&useCompression=true&useServerPrepStmts=false&rewriteBatchedStatements=true&cachePrepStmts=true&connectionTimeZone=UTC", + Constants.MYSQL_PORT); + + private final String className; + private final String urlTemplate; + private final int defaultPort; + + public static JdbcDriver from(String driver) { + if (driver == null || driver.isEmpty()) { + throw new IllegalArgumentException("Non-empty driver is needed"); + } + + String[] parts = driver.split(" "); + if (parts.length > 2) { + throw new IllegalArgumentException("Only format ' [version]' is supported!"); + } + + String name = parts[0].replace("-", ""); + + for (JdbcDriver d : JdbcDriver.values()) { + if (d.name().equalsIgnoreCase(name)) { + return d; + } + } + + throw new IllegalArgumentException("Unsupported driver: " + name); + } + + JdbcDriver(String className, String urlTemplate, int defaultPort) { + this.className = className; + this.urlTemplate = urlTemplate; + this.defaultPort = defaultPort; + } + + public String getClassName() { + return this.className; + } + + public String getUrlTemplate() { + return this.urlTemplate; + } + + public int getDefaultPort() { + return this.defaultPort; + } +} diff --git a/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/Query.java b/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/Query.java new file mode 100644 index 000000000..174b2943e --- /dev/null +++ b/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/Query.java @@ -0,0 +1,78 @@ +package tech.clickhouse.benchmark; + +import java.sql.ResultSet; +import java.sql.Statement; +import java.sql.Timestamp; + +import org.openjdk.jmh.annotations.Benchmark; + +public class Query extends JdbcBenchmark { + @Benchmark + public int select10kUInt64Rows(ClientState state) throws Throwable { + int rows = 10000; + int num = (int) (Math.random() * rows); + try (Statement stmt = executeQuery(state, "select * from system.numbers where number > ? limit " + rows, num)) { + ResultSet rs = stmt.getResultSet(); + + float avg = 0.0F; + int count = 0; + while (rs.next()) { + avg = (rs.getInt(1) + avg * count) / (++count); + } + + if (count != rows) { + throw new IllegalStateException(); + } + + return count; + } + } + + @Benchmark + public int select10kStringRows(ClientState state) throws Throwable { + int rows = 10000; + int num = (int) (Math.random() * rows); + try (Statement stmt = executeQuery(state, + "select toString(number) as s from system.numbers where number > ? limit " + rows, num)) { + ResultSet rs = stmt.getResultSet(); + + int count = 0; + String str = null; + while (rs.next()) { + str = rs.getString(1); + count++; + } + + if (count != rows) { + throw new IllegalStateException(); + } + + return count; + } + } + + @Benchmark + public int select10kTimestampRows(ClientState state) throws Throwable { + int rows = 10000; + int num = (int) (Math.random() * rows); + try (Statement stmt = executeQuery(state, + "select toDateTime('2021-02-20 13:15:20') + number as d from system.numbers where number > ? limit " + + rows, + num)) { + ResultSet rs = stmt.getResultSet(); + + int count = 0; + Timestamp ts = null; + while (rs.next()) { + ts = rs.getTimestamp(1); + count++; + } + + if (count != rows) { + throw new IllegalStateException(); + } + + return count; + } + } +} diff --git a/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/ServerState.java b/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/ServerState.java new file mode 100644 index 000000000..71c2ab9c4 --- /dev/null +++ b/clickhouse-benchmark/src/main/java/tech/clickhouse/benchmark/ServerState.java @@ -0,0 +1,117 @@ +package tech.clickhouse.benchmark; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.time.Duration; +import java.util.Enumeration; + +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.ImageFromDockerfile; + +import static java.time.temporal.ChronoUnit.SECONDS; + +@State(Scope.Benchmark) +public class ServerState { + static String getLocalIpAddress() { + String localIpAddress = null; + + try { + for (Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); interfaces + .hasMoreElements();) { + NetworkInterface i = interfaces.nextElement(); + if (i.isUp() && !i.isLoopback() && !i.isPointToPoint() && !i.isVirtual()) { + for (InterfaceAddress addr : i.getInterfaceAddresses()) { + InetAddress inetAddr = addr.getAddress(); + + if (!(inetAddr instanceof Inet4Address)) { + continue; + } + + localIpAddress = inetAddr.getHostAddress(); + break; + } + } + + if (localIpAddress != null) { + break; + } + } + } catch (Exception e) { + // ignore exception + } + + return localIpAddress != null ? localIpAddress : Constants.DEFAULT_HOST; + } + + private final String host = System.getProperty("dbHost", Constants.DEFAULT_HOST); + private final String user = System.getProperty("dbUser", Constants.DEFAULT_USER); + private final String passwd = System.getProperty("dbPasswd", Constants.DEFAULT_PASSWD); + private final String db = System.getProperty("dbName", Constants.DEFAULT_DB); + + private final String localIpAddress = getLocalIpAddress(); + + private GenericContainer container = null; + + @Setup(Level.Trial) + public void doSetup() throws Exception { + if (System.getProperty("dbHost") != null) { + return; + } + + String imageTag = System.getProperty("clickhouseVersion"); + + if (imageTag == null || (imageTag = imageTag.trim()).isEmpty()) { + imageTag = ""; + } else { + imageTag = ":" + imageTag; + } + + final String imageNameWithTag = "yandex/clickhouse-server" + imageTag; + + container = new GenericContainer<>(new ImageFromDockerfile().withDockerfileFromBuilder(builder -> builder + .from(imageNameWithTag) + .run("echo '0.0.0.091003307' > /etc/clickhouse-server/config.d/custom.xml"))) + .withExposedPorts(Constants.GRPC_PORT, Constants.HTTP_PORT, Constants.MYSQL_PORT, + + Constants.NATIVE_PORT) + .waitingFor(Wait.forHttp("/ping").forPort(Constants.HTTP_PORT).forStatusCode(200) + .withStartupTimeout(Duration.of(60, SECONDS))); + + container.start(); + } + + @TearDown(Level.Trial) + public void doTearDown() throws Exception { + if (container != null) { + container.stop(); + } + } + + public String getHost() { + return container != null ? localIpAddress : host; + } + + public int getPort(int defaultPort) { + return container != null ? container.getMappedPort(defaultPort) : defaultPort; + } + + public String getUser() { + return container != null ? Constants.DEFAULT_USER : user; + } + + public String getPassword() { + return container != null ? Constants.DEFAULT_PASSWD : passwd; + } + + public String getDatabase() { + return container != null ? Constants.DEFAULT_DB : db; + } +} diff --git a/pom.xml b/pom.xml index 99d86ee8f..e85ec3844 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,7 @@ 2.9.10.8 29.0-jre 2.3.1 - 1.7 + 1.8 1.15.1 6.14.3 1.10.19 diff --git a/src/main/java/ru/yandex/clickhouse/domain/ClickHouseFormat.java b/src/main/java/ru/yandex/clickhouse/domain/ClickHouseFormat.java index 6a8f9371b..ef8614cb2 100644 --- a/src/main/java/ru/yandex/clickhouse/domain/ClickHouseFormat.java +++ b/src/main/java/ru/yandex/clickhouse/domain/ClickHouseFormat.java @@ -37,7 +37,10 @@ public enum ClickHouseFormat { Native, Null, XML, - CapnProto; + CapnProto, + Parquet, + ORC + ; public static boolean containsFormat(String statement) { if (statement == null || statement.isEmpty()) { diff --git a/src/test/java/ru/yandex/clickhouse/ClickHouseContainerForTest.java b/src/test/java/ru/yandex/clickhouse/ClickHouseContainerForTest.java index 05c2c1b2a..911d14843 100644 --- a/src/test/java/ru/yandex/clickhouse/ClickHouseContainerForTest.java +++ b/src/test/java/ru/yandex/clickhouse/ClickHouseContainerForTest.java @@ -1,10 +1,10 @@ package ru.yandex.clickhouse; import java.time.Duration; - import org.testcontainers.containers.BindMode; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.ImageFromDockerfile; import org.testng.annotations.AfterSuite; import org.testng.annotations.BeforeSuite; @@ -23,6 +23,7 @@ public class ClickHouseContainerForTest { static { String imageTag = System.getProperty("clickhouseVersion"); + if (imageTag == null || (imageTag = imageTag.trim()).isEmpty()) { clickhouseVersion = imageTag = ""; } else { @@ -33,7 +34,15 @@ public class ClickHouseContainerForTest { } imageTag = ":" + imageTag; } - clickhouseContainer = new GenericContainer<>("yandex/clickhouse-server" + imageTag) + + final String imageNameWithTag = "yandex/clickhouse-server" + imageTag; + + clickhouseContainer = new GenericContainer<>( new ImageFromDockerfile() + .withDockerfileFromBuilder(builder -> + builder + .from( imageNameWithTag ) + .run("apt-get update && apt-get install tzdata") + )) .withExposedPorts(HTTP_PORT, NATIVE_PORT, MYSQL_PORT) .withClasspathResourceMapping( "ru/yandex/clickhouse/users.d", diff --git a/src/test/java/ru/yandex/clickhouse/integration/StreamSQLTest.java b/src/test/java/ru/yandex/clickhouse/integration/StreamSQLTest.java index 210e75f4e..c3798f35e 100644 --- a/src/test/java/ru/yandex/clickhouse/integration/StreamSQLTest.java +++ b/src/test/java/ru/yandex/clickhouse/integration/StreamSQLTest.java @@ -8,8 +8,10 @@ import ru.yandex.clickhouse.ClickHouseDataSource; import ru.yandex.clickhouse.domain.ClickHouseCompression; import ru.yandex.clickhouse.domain.ClickHouseFormat; -import ru.yandex.clickhouse.settings.ClickHouseProperties; +import ru.yandex.clickhouse.util.ClickHouseVersionNumberUtil; + import java.io.*; +import java.math.BigDecimal; import java.nio.charset.Charset; import java.sql.ResultSet; import java.sql.SQLException; @@ -216,4 +218,164 @@ public void CSVInsertCompressedIntoTable() throws SQLException, IOException { Assert.assertEquals(rs.getLong("uniq"), 1); } + @Test + public void ORCInsertCompressedIntoTable() throws SQLException { + // clickhouse-client -q "select number int, toString(number) str, 1/number flt, toDecimal64( 1/(number+1) , 9) dcml, + // toDateTime('2020-01-01 00:00:00') + number time from numbers(100) format ORC"|gzip > test_sample.orc.gz + + String version = connection.getServerVersion(); + if (version.compareTo("20.8") < 0) { + return; + } + + connection.createStatement().execute("DROP TABLE IF EXISTS test.orc_stream_compressed"); + connection.createStatement().execute( + "CREATE TABLE test.orc_stream_compressed (int Int64, str String, flt Float64, " + + "dcml Decimal64(9), time DateTime) ENGINE = Log();" + ); + + InputStream inputStream = StreamSQLTest.class.getResourceAsStream("/data_samples/test_sample.orc.gz"); + + connection.createStatement(). + write() + .table("test.orc_stream_compressed") + .format(ClickHouseFormat.ORC) + .dataCompression(ClickHouseCompression.gzip) + .data(inputStream) + .send(); + + ResultSet rs = connection.createStatement().executeQuery( + "SELECT count() AS cnt, " + + "sum(int) sum_int, " + + "round(sum(flt),2) AS sum_flt, " + + "uniqExact(str) uniq_str, " + + "max(dcml) max_dcml, " + + "min(time) min_time, " + + "max(time) max_time " + + "FROM test.orc_stream_compressed"); + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getInt("cnt"), 100); + Assert.assertEquals(rs.getLong("sum_int"), 4950); + Assert.assertEquals(rs.getFloat("sum_flt"), Float.POSITIVE_INFINITY); + Assert.assertEquals(rs.getLong("uniq_str"), 100); + Assert.assertEquals(rs.getBigDecimal("max_dcml"), new BigDecimal("1.000000000")); + Assert.assertEquals(rs.getString("min_time"), "2020-01-01 00:00:00"); + Assert.assertEquals(rs.getString("max_time"), "2020-01-01 00:01:39"); + } + + @Test + public void ORCInsertCompressedIntoTable1() throws SQLException { + // clickhouse-client -q "select number int, toString(number) str, 1/number flt, toDecimal64( 1/(number+1) , 9) dcml, + // toDateTime('2020-01-01 00:00:00') + number time from numbers(100) format ORC"|gzip > test_sample.orc.gz + + String version = connection.getServerVersion(); + if (version.compareTo("20.8") < 0) { + return; + } + + connection.createStatement().execute("DROP TABLE IF EXISTS test.orc1_stream_compressed"); + connection.createStatement().execute( + "CREATE TABLE test.orc1_stream_compressed (int Int64, str String, flt Float64, " + + "dcml Decimal64(9), time DateTime) ENGINE = Log();" + ); + + InputStream inputStream = StreamSQLTest.class.getResourceAsStream("/data_samples/test_sample.orc.gz"); + + connection.createStatement(). + write() + .sql("insert into test.orc1_stream_compressed format ORC") + .dataCompression(ClickHouseCompression.gzip) + .data(inputStream) + .send(); + + ResultSet rs = connection.createStatement().executeQuery( + "select * from test.orc1_stream_compressed where int=42"); + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getInt("int"), 42); + Assert.assertEquals(rs.getString("str"), "42"); + Assert.assertTrue( Math.abs(rs.getFloat("flt") - 0.023809524) < 0.0001); + Assert.assertTrue( Math.abs(rs.getFloat("dcml") - 0.023255813) < 0.0001); + Assert.assertEquals(rs.getString("time"), "2020-01-01 00:00:42"); + } + + @Test + public void ParquetInsertCompressedIntoTable() throws SQLException { + // clickhouse-client -q "select number int, toString(number) str, 1/number flt, toDecimal64( 1/(number+1) , 9) dcml, + // toDateTime('2020-01-01 00:00:00') + number time from numbers(100) format Parquet"|gzip > test_sample.parquet.gz + + String version = connection.getServerVersion(); + if (version.compareTo("20.8") < 0) { + return; + } + + connection.createStatement().execute("DROP TABLE IF EXISTS test.parquet_stream_compressed"); + connection.createStatement().execute( + "CREATE TABLE test.parquet_stream_compressed (int Int64, str String, flt Float64, " + + "dcml Decimal64(9), time DateTime) ENGINE = Log();" + ); + + InputStream inputStream = StreamSQLTest.class.getResourceAsStream("/data_samples/test_sample.parquet.gz"); + + connection.createStatement(). + write() + .table("test.parquet_stream_compressed") + .format(ClickHouseFormat.Parquet) + .dataCompression(ClickHouseCompression.gzip) + .data(inputStream) + .send(); + + ResultSet rs = connection.createStatement().executeQuery( + "SELECT count() AS cnt, " + + "sum(int) sum_int, " + + "round(sum(flt),2) AS sum_flt, " + + "uniqExact(str) uniq_str, " + + "max(dcml) max_dcml, " + + "min(time) min_time, " + + "max(time) max_time " + + "FROM test.parquet_stream_compressed"); + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getInt("cnt"), 100); + Assert.assertEquals(rs.getLong("sum_int"), 4950); + Assert.assertEquals(rs.getFloat("sum_flt"), Float.POSITIVE_INFINITY); + Assert.assertEquals(rs.getLong("uniq_str"), 100); + Assert.assertEquals(rs.getBigDecimal("max_dcml"), new BigDecimal("1.000000000")); + Assert.assertEquals(rs.getString("min_time"), "2020-01-01 00:00:00"); + Assert.assertEquals(rs.getString("max_time"), "2020-01-01 00:01:39"); + } + + @Test + public void ParquetInsertCompressedIntoTable1() throws SQLException { + // clickhouse-client -q "select number int, toString(number) str, 1/number flt, toDecimal64( 1/(number+1) , 9) dcml, + // toDateTime('2020-01-01 00:00:00') + number time from numbers(100) format Parquet"|gzip > test_sample.parquet.gz + + String version = connection.getServerVersion(); + if (version.compareTo("20.8") < 0) { + return; + } + + connection.createStatement().execute("DROP TABLE IF EXISTS test.parquet1_stream_compressed"); + connection.createStatement().execute( + "CREATE TABLE test.parquet1_stream_compressed (int Int64, str String, flt Float64, " + + "dcml Decimal64(9), time DateTime) ENGINE = Log();" + ); + + InputStream inputStream = StreamSQLTest.class.getResourceAsStream("/data_samples/test_sample.parquet.gz"); + + connection.createStatement(). + write() + .sql("insert into test.parquet1_stream_compressed format Parquet") + .dataCompression(ClickHouseCompression.gzip) + .data(inputStream) + .send(); + + ResultSet rs = connection.createStatement().executeQuery( + "select * from test.parquet1_stream_compressed where int=42"); + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getInt("int"), 42); + Assert.assertEquals(rs.getString("str"), "42"); + Assert.assertTrue( Math.abs(rs.getFloat("flt") - 0.023809524) < 0.0001); + Assert.assertTrue( Math.abs(rs.getFloat("dcml") - 0.023255813) < 0.0001); + Assert.assertEquals(rs.getString("time"), "2020-01-01 00:00:42"); + } + } diff --git a/src/test/resources/data_samples/test_sample.orc.gz b/src/test/resources/data_samples/test_sample.orc.gz new file mode 100644 index 000000000..a387cd591 Binary files /dev/null and b/src/test/resources/data_samples/test_sample.orc.gz differ diff --git a/src/test/resources/data_samples/test_sample.parquet.gz b/src/test/resources/data_samples/test_sample.parquet.gz new file mode 100644 index 000000000..eab2be518 Binary files /dev/null and b/src/test/resources/data_samples/test_sample.parquet.gz differ