Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: NEW: QueryPlanLogger for DB2 #3144

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions ebean-api/src/main/java/io/ebean/DatabaseBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -2079,6 +2079,11 @@ default DatabaseBuilder queryPlanEnable(boolean queryPlanEnable) {
@Deprecated
DatabaseBuilder setQueryPlanEnable(boolean queryPlanEnable);

/**
* Set platform specific query plan options.
*/
public void setQueryPlanOptions(String queryPlanOptions);

/**
* Set the query plan collection threshold in microseconds.
* <p>
Expand Down Expand Up @@ -3061,6 +3066,11 @@ interface Settings extends DatabaseBuilder {
*/
boolean isQueryPlanEnable();

/**
* Returns platform specific query plan options.
*/
public String getQueryPlanOptions();

/**
* Return the query plan collection threshold in microseconds.
*/
Expand Down
25 changes: 25 additions & 0 deletions ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,11 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
*/
private boolean queryPlanEnable;

/**
* Additional platform specific options for query-plan generation.
*/
private String queryPlanOptions;

/**
* The default threshold in micros for collecting query plans.
*/
Expand Down Expand Up @@ -2146,6 +2151,7 @@ protected void loadSettings(PropertiesWrapper p) {
queryPlanTTLSeconds = p.getInt("queryPlanTTLSeconds", queryPlanTTLSeconds);
slowQueryMillis = p.getLong("slowQueryMillis", slowQueryMillis);
queryPlanEnable = p.getBoolean("queryPlan.enable", queryPlanEnable);
queryPlanOptions = p.get("queryPlan.options", queryPlanOptions);
queryPlanThresholdMicros = p.getLong("queryPlan.thresholdMicros", queryPlanThresholdMicros);
queryPlanCapture = p.getBoolean("queryPlan.capture", queryPlanCapture);
queryPlanCapturePeriodSecs = p.getLong("queryPlan.capturePeriodSecs", queryPlanCapturePeriodSecs);
Expand Down Expand Up @@ -2461,6 +2467,25 @@ public DatabaseConfig setQueryPlanEnable(boolean queryPlanEnable) {
return this;
}

/**
* Returns platform specific query plan options.
*/
@Override
public String getQueryPlanOptions() {
return queryPlanOptions;
}

/**
* Set platform specific query plan options.
*/
@Override
public void setQueryPlanOptions(String queryPlanOptions) {
this.queryPlanOptions = queryPlanOptions;
}

/**
* Return the query plan collection threshold in microseconds.
*/
@Override
public long getQueryPlanThresholdMicros() {
return queryPlanThresholdMicros;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,8 @@ QueryPlanLogger queryPlanLogger(Platform platform) {
return new QueryPlanLoggerSqlServer();
case ORACLE:
return new QueryPlanLoggerOracle();
case DB2:
return new QueryPlanLoggerDb2(config.getQueryPlanOptions());
case POSTGRES:
return new QueryPlanLoggerExplain("explain (analyze, buffers) ");
case YUGABYTE:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@

package io.ebeaninternal.server.query;

import io.ebean.util.IOUtils;
import io.ebean.util.StringHelper;
import io.ebeaninternal.api.CoreLog;
import io.ebeaninternal.api.SpiDbQueryPlan;
import io.ebeaninternal.api.SpiQueryPlan;
import io.ebeaninternal.server.bind.capture.BindCapture;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Map;
import java.util.Random;

import static java.lang.System.Logger.Level.WARNING;

/**
* A QueryPlanLogger for DB2.
* <p>
* To use query plan capturing, you have to install the explain tables with
* <code>SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', CURRENT SCHEMA )</code>.
* To do this in a repeatable script, you may use this statement:
*
* <pre>
* BEGIN
* IF NOT EXISTS (SELECT * FROM SYSCAT.TABLES WHERE TABSCHEMA = CURRENT SCHEMA AND TABNAME = 'EXPLAIN_STREAM') THEN
* call SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', CURRENT SCHEMA );
* END IF;
* END
* </pre>
*
* @author Roland Praml, FOCONIS AG
*/
public final class QueryPlanLoggerDb2 extends QueryPlanLogger {

private Random rnd = new Random();

private final String schema;

private final boolean create;

private static final String GET_PLAN_TEMPLATE = readReasource("QueryPlanLoggerDb2.sql");

private static final String CREATE_TEMPLATE = "BEGIN\n"
+ "IF NOT EXISTS (SELECT * FROM SYSCAT.TABLES WHERE TABSCHEMA = ${SCHEMA} AND TABNAME = 'EXPLAIN_STREAM') THEN\n"
+ " CALL SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', ${SCHEMA} );\n"
+ "END IF;\n"
+ "END";

public QueryPlanLoggerDb2(String opts) {
Map<String, String> map = StringHelper.delimitedToMap(opts, ";", "=");
create = !"false" .equals(map.get("create")); // default is create
String schema = map.get("schema"); // should be null or SYSTOOLS
if (schema == null || schema.isEmpty()) {
this.schema = null;
} else {
this.schema = schema.toUpperCase();
}
}

private static String readReasource(String resName) {
try (InputStream stream = QueryPlanLoggerDb2.class.getResourceAsStream(resName)) {
if (stream == null) {
throw new IllegalStateException("Could not find resource " + resName);
}
BufferedReader reader = IOUtils.newReader(stream);
StringBuilder sb = new StringBuilder();
reader.lines().forEach(line -> sb.append(line).append('\n'));
return sb.toString();
} catch (IOException e) {
throw new IllegalStateException("Could not read resource " + resName, e);
}
}

@Override
public SpiDbQueryPlan collectPlan(Connection conn, SpiQueryPlan plan, BindCapture bind) {
try (Statement stmt = conn.createStatement()) {
if (create) {
// create explain tables if neccessary
if (schema == null) {
stmt.execute(CREATE_TEMPLATE.replace("${SCHEMA}", "CURRENT USER"));
} else {
stmt.execute(CREATE_TEMPLATE.replace("${SCHEMA}", "'" + schema + "'"));
}
conn.commit();
}

try {
int queryNo = rnd.nextInt(Integer.MAX_VALUE);

String sql = "EXPLAIN PLAN SET QUERYNO = " + queryNo + " FOR " + plan.sql();
try (PreparedStatement explainStmt = conn.prepareStatement(sql)) {
bind.prepare(explainStmt, conn);
explainStmt.execute();
}

sql = schema == null
? GET_PLAN_TEMPLATE.replace("${SCHEMA}", conn.getMetaData().getUserName().toUpperCase())
: GET_PLAN_TEMPLATE.replace("${SCHEMA}", schema);

try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, queryNo);
try (ResultSet rset = pstmt.executeQuery()) {
return readQueryPlan(plan, bind, rset);
}
}
} finally {
conn.rollback(); // do not keep query plans in DB
}
} catch (SQLException e) {
CoreLog.log.log(WARNING, "Could not log query plan", e);
return null;
}
}
}
Loading