- [#5960] Duplicate SQL detection - [#7094] Add ParamType.FORCE_INDEXED
This commit is contained in:
parent
972e330b60
commit
b2e4cadf8a
@ -38,6 +38,7 @@
|
||||
package org.jooq;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A parameter object that is passed to {@link DiagnosticsListener} methods.
|
||||
@ -90,4 +91,14 @@ public interface DiagnosticsContext {
|
||||
* {@link #resultSet()}, or <code>-1</code> if there was no result set.
|
||||
*/
|
||||
int resultSetActualColumns();
|
||||
|
||||
/**
|
||||
* The normalised statement that all duplicates correspond to.
|
||||
*/
|
||||
String normalisedStatement();
|
||||
|
||||
/**
|
||||
* The duplicate statements that all correspond to a single normalised statement.
|
||||
*/
|
||||
List<String> duplicateStatements();
|
||||
}
|
||||
|
||||
@ -71,4 +71,43 @@ public interface DiagnosticsListener {
|
||||
*/
|
||||
void tooManyColumnsFetched(DiagnosticsContext ctx);
|
||||
|
||||
/**
|
||||
* The executed JDBC statement has duplicates.
|
||||
* <p>
|
||||
* Many databases maintain an execution plan cache, which remembers
|
||||
* execution plans for a given SQL string. These caches often use the
|
||||
* verbatim SQL string (or a hash thereof) as a key, meaning that "similar"
|
||||
* but not identical statements will produce different keys. This may be
|
||||
* desired in rare cases when querying skewed data, as a hack to force the
|
||||
* optimiser to calculate a new plan for a given "similar" but not identical
|
||||
* query, but mostly, this is not desirable as calculating execution plans
|
||||
* can turn out to be expensive.
|
||||
* <p>
|
||||
* Examples of such similar statements include:
|
||||
* <p>
|
||||
* <h3>Whitespace differences</h3>
|
||||
* <p>
|
||||
* <code><pre>
|
||||
* SELECT * FROM actor;
|
||||
* SELECT * FROM actor;
|
||||
* </pre></code>
|
||||
* <p>
|
||||
* <h3>Inline bind values</h3>
|
||||
* <p>
|
||||
* <code><pre>
|
||||
* SELECT * FROM actor WHERE id = 1;
|
||||
* SELECT * FROM actor WHERE id = 2;
|
||||
* </pre></code>
|
||||
* <p>
|
||||
* <h3>Aliasing and qualification</h3>
|
||||
* <p>
|
||||
* <code><pre>
|
||||
* SELECT a1.* FROM actor a1 WHERE id = ?;
|
||||
* SELECT * FROM actor a2 WHERE a2.id = ?;
|
||||
* </pre></code>
|
||||
* <p>
|
||||
* This event is triggered every time a new duplicate is encountered.
|
||||
*/
|
||||
void duplicateStatements(DiagnosticsContext ctx);
|
||||
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import javax.xml.bind.annotation.XmlType;
|
||||
* <simpleType name="ParamType">
|
||||
* <restriction base="{http://www.w3.org/2001/XMLSchema}string">
|
||||
* <enumeration value="INDEXED"/>
|
||||
* <enumeration value="FORCE_INDEXED"/>
|
||||
* <enumeration value="NAMED"/>
|
||||
* <enumeration value="NAMED_OR_INLINED"/>
|
||||
* <enumeration value="INLINED"/>
|
||||
@ -34,6 +35,7 @@ import javax.xml.bind.annotation.XmlType;
|
||||
public enum ParamType {
|
||||
|
||||
INDEXED,
|
||||
FORCE_INDEXED,
|
||||
NAMED,
|
||||
NAMED_OR_INLINED,
|
||||
INLINED;
|
||||
|
||||
@ -150,6 +150,8 @@ abstract class AbstractContext<C extends Context<C>> extends AbstractScope imple
|
||||
|
||||
this.forcedParamType = SettingsTools.getStatementType(settings()) == StatementType.STATIC_STATEMENT
|
||||
? ParamType.INLINED
|
||||
: SettingsTools.getParamType(settings()) == ParamType.FORCE_INDEXED
|
||||
? ParamType.INDEXED
|
||||
: null;
|
||||
|
||||
ParamCastMode m = settings().getParamCastMode();
|
||||
|
||||
@ -39,6 +39,9 @@ package org.jooq.impl;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.jooq.DiagnosticsContext;
|
||||
|
||||
@ -47,11 +50,22 @@ import org.jooq.DiagnosticsContext;
|
||||
*/
|
||||
final class DefaultDiagnosticsContext implements DiagnosticsContext {
|
||||
|
||||
ResultSet resultSet;
|
||||
int resultSetFetchedColumns;
|
||||
int resultSetActualColumns;
|
||||
int resultSetFetchedRows;
|
||||
int resultSetActualRows;
|
||||
ResultSet resultSet;
|
||||
int resultSetFetchedColumns;
|
||||
int resultSetActualColumns;
|
||||
int resultSetFetchedRows;
|
||||
int resultSetActualRows;
|
||||
final String normalisedStatement;
|
||||
final List<String> duplicateStatements;
|
||||
|
||||
DefaultDiagnosticsContext(String statement) {
|
||||
this(statement, Arrays.asList(statement));
|
||||
}
|
||||
|
||||
DefaultDiagnosticsContext(String normalisedStatement, List<String> duplicateStatements) {
|
||||
this.normalisedStatement = normalisedStatement;
|
||||
this.duplicateStatements = duplicateStatements;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final ResultSet resultSet() {
|
||||
@ -87,4 +101,14 @@ final class DefaultDiagnosticsContext implements DiagnosticsContext {
|
||||
public final int resultSetActualColumns() {
|
||||
return resultSet == null ? -1 : resultSetActualColumns;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String normalisedStatement() {
|
||||
return normalisedStatement;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final List<String> duplicateStatements() {
|
||||
return Collections.unmodifiableList(duplicateStatements);
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,4 +53,10 @@ public class DefaultDiagnosticsListener implements DiagnosticsListener {
|
||||
@Override
|
||||
public void tooManyRowsFetched(DiagnosticsContext ctx) {}
|
||||
|
||||
@Override
|
||||
public void tooManyColumnsFetched(DiagnosticsContext ctx) {}
|
||||
|
||||
@Override
|
||||
public void duplicateStatements(DiagnosticsContext ctx) {}
|
||||
|
||||
}
|
||||
|
||||
@ -37,12 +37,26 @@
|
||||
*/
|
||||
package org.jooq.impl;
|
||||
|
||||
import static org.jooq.conf.ParamType.FORCE_INDEXED;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jooq.Configuration;
|
||||
import org.jooq.Parser;
|
||||
import org.jooq.Queries;
|
||||
import org.jooq.RenderContext;
|
||||
import org.jooq.conf.SettingsTools;
|
||||
import org.jooq.tools.jdbc.DefaultConnection;
|
||||
|
||||
/**
|
||||
@ -50,13 +64,19 @@ import org.jooq.tools.jdbc.DefaultConnection;
|
||||
*/
|
||||
final class DiagnosticsConnection extends DefaultConnection {
|
||||
|
||||
final Configuration configuration;
|
||||
final DiagnosticsListeners listeners;
|
||||
static final Map<String, Set<String>> DUPLICATE_SQL = Collections.synchronizedMap(new LRU());
|
||||
final Configuration configuration;
|
||||
final RenderContext forceIndexed;
|
||||
final Parser parser;
|
||||
final DiagnosticsListeners listeners;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
DiagnosticsConnection(Configuration configuration) {
|
||||
super(configuration.connectionProvider().acquire());
|
||||
|
||||
this.configuration = configuration;
|
||||
this.forceIndexed = configuration.derive(SettingsTools.clone(configuration.settings()).withParamType(FORCE_INDEXED)).dsl().renderContext();
|
||||
this.parser = configuration.dsl().parser();
|
||||
this.listeners = DiagnosticsListeners.get(configuration);
|
||||
}
|
||||
|
||||
@ -77,51 +97,101 @@ final class DiagnosticsConnection extends DefaultConnection {
|
||||
|
||||
@Override
|
||||
public final PreparedStatement prepareStatement(String sql) throws SQLException {
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareStatement(sql));
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareStatement(parse(sql)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareStatement(sql, resultSetType, resultSetConcurrency));
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareStatement(parse(sql), resultSetType, resultSetConcurrency));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability));
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareStatement(parse(sql), resultSetType, resultSetConcurrency, resultSetHoldability));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareStatement(sql, autoGeneratedKeys));
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareStatement(parse(sql), autoGeneratedKeys));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareStatement(sql, columnIndexes));
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareStatement(parse(sql), columnIndexes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareStatement(sql, columnNames));
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareStatement(parse(sql), columnNames));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final CallableStatement prepareCall(String sql) throws SQLException {
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareCall(sql));
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareCall(parse(sql)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareCall(sql, resultSetType, resultSetConcurrency));
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareCall(parse(sql), resultSetType, resultSetConcurrency));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability));
|
||||
return new DiagnosticsStatement(this, getDelegate().prepareCall(parse(sql), resultSetType, resultSetConcurrency, resultSetHoldability));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void close() throws SQLException {
|
||||
configuration.connectionProvider().release(getDelegate());
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
final String parse(String sql) {
|
||||
Queries queries;
|
||||
|
||||
try {
|
||||
queries = parser.parse(sql);
|
||||
}
|
||||
catch (ParserException ignore) {
|
||||
return sql;
|
||||
}
|
||||
|
||||
String normalised = forceIndexed.render(queries);
|
||||
List<String> duplicates = null;
|
||||
|
||||
synchronized (DUPLICATE_SQL) {
|
||||
Set<String> v = DUPLICATE_SQL.get(normalised);
|
||||
|
||||
if (v == null) {
|
||||
v = new HashSet<String>();
|
||||
DUPLICATE_SQL.put(normalised, v);
|
||||
}
|
||||
|
||||
if (v.size() >= DUP_SIZE || (v.add(sql) && v.size() > 1))
|
||||
duplicates = new ArrayList<String>(v);
|
||||
}
|
||||
|
||||
if (duplicates != null)
|
||||
listeners.duplicateStatements(new DefaultDiagnosticsContext(normalised, duplicates));
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
// TODO: Make this configurable
|
||||
static final int LRU_SIZE = 50000;
|
||||
static final int DUP_SIZE = 500;
|
||||
|
||||
// See https://stackoverflow.com/a/1953516/521799
|
||||
static class LRU extends LinkedHashMap<String, Set<String>> {
|
||||
private static final long serialVersionUID = 5287799057535876982L;
|
||||
|
||||
LRU() {
|
||||
super(LRU_SIZE + 1, 1.0f, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Entry<String, Set<String>> eldest) {
|
||||
return size() > LRU_SIZE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,4 +72,10 @@ final class DiagnosticsListeners implements DiagnosticsListener {
|
||||
for (DiagnosticsListener listener : listeners)
|
||||
listener.tooManyColumnsFetched(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void duplicateStatements(DiagnosticsContext ctx) {
|
||||
for (DiagnosticsListener listener : listeners)
|
||||
listener.duplicateStatements(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,16 +67,18 @@ import org.jooq.tools.jdbc.DefaultResultSet;
|
||||
final class DiagnosticsResultSet extends DefaultResultSet {
|
||||
|
||||
final DiagnosticsConnection connection;
|
||||
final String sql;
|
||||
final ResultSetMetaData meta;
|
||||
final BitSet read;
|
||||
final int columns;
|
||||
int current;
|
||||
int rows;
|
||||
|
||||
DiagnosticsResultSet(ResultSet delegate, Statement creator, DiagnosticsConnection connection) throws SQLException {
|
||||
DiagnosticsResultSet(ResultSet delegate, String sql, Statement creator, DiagnosticsConnection connection) throws SQLException {
|
||||
super(delegate, creator);
|
||||
|
||||
this.connection = connection;
|
||||
this.sql = sql;
|
||||
this.meta = delegate.getMetaData();
|
||||
this.columns = meta.getColumnCount();
|
||||
this.read = new BitSet(columns);
|
||||
@ -590,7 +592,7 @@ final class DiagnosticsResultSet extends DefaultResultSet {
|
||||
}
|
||||
|
||||
private final DefaultDiagnosticsContext ctx() {
|
||||
DefaultDiagnosticsContext ctx = new DefaultDiagnosticsContext();
|
||||
DefaultDiagnosticsContext ctx = new DefaultDiagnosticsContext(sql);
|
||||
|
||||
ctx.resultSet = super.getDelegate();
|
||||
ctx.resultSetFetchedColumns = read.cardinality();
|
||||
|
||||
@ -59,76 +59,76 @@ final class DiagnosticsStatement extends DefaultCallableStatement {
|
||||
|
||||
@Override
|
||||
public final ResultSet executeQuery(String sql) throws SQLException {
|
||||
return new DiagnosticsResultSet(super.executeQuery(sql), this, connection);
|
||||
return new DiagnosticsResultSet(super.executeQuery(connection.parse(sql)), sql, this, connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int executeUpdate(String sql) throws SQLException {
|
||||
return super.executeUpdate(sql);
|
||||
return super.executeUpdate(connection.parse(sql));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
|
||||
return super.executeUpdate(sql, autoGeneratedKeys);
|
||||
return super.executeUpdate(connection.parse(sql), autoGeneratedKeys);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int executeUpdate(String sql, int[] columnIndexes) throws SQLException {
|
||||
return super.executeUpdate(sql, columnIndexes);
|
||||
return super.executeUpdate(connection.parse(sql), columnIndexes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int executeUpdate(String sql, String[] columnNames) throws SQLException {
|
||||
return super.executeUpdate(sql, columnNames);
|
||||
return super.executeUpdate(connection.parse(sql), columnNames);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean execute(String sql) throws SQLException {
|
||||
return super.execute(sql);
|
||||
return super.execute(connection.parse(sql));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean execute(String sql, int autoGeneratedKeys) throws SQLException {
|
||||
return super.execute(sql, autoGeneratedKeys);
|
||||
return super.execute(connection.parse(sql), autoGeneratedKeys);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean execute(String sql, int[] columnIndexes) throws SQLException {
|
||||
return super.execute(sql, columnIndexes);
|
||||
return super.execute(connection.parse(sql), columnIndexes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean execute(String sql, String[] columnNames) throws SQLException {
|
||||
return super.execute(sql, columnNames);
|
||||
return super.execute(connection.parse(sql), columnNames);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public final long executeLargeUpdate(String sql) throws SQLException {
|
||||
return super.executeLargeUpdate(sql);
|
||||
return super.executeLargeUpdate(connection.parse(sql));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
|
||||
return super.executeLargeUpdate(sql, autoGeneratedKeys);
|
||||
return super.executeLargeUpdate(connection.parse(sql), autoGeneratedKeys);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException {
|
||||
return super.executeLargeUpdate(sql, columnIndexes);
|
||||
return super.executeLargeUpdate(connection.parse(sql), columnIndexes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final long executeLargeUpdate(String sql, String[] columnNames) throws SQLException {
|
||||
return super.executeLargeUpdate(sql, columnNames);
|
||||
return super.executeLargeUpdate(connection.parse(sql), columnNames);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public final void addBatch(String sql) throws SQLException {
|
||||
super.addBatch(sql);
|
||||
super.addBatch(connection.parse(sql));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -278,6 +278,9 @@ Either <input/> or <inputExpression/> must be provided]]></jxb:javadoc></j
|
||||
<!-- Execute statements with indexed parameters, the way JDBC expects them -->
|
||||
<enumeration value="INDEXED"/>
|
||||
|
||||
<!-- Execute statements with indexed parameters, forcing explicit inlined and named parameters to be indexed as well -->
|
||||
<enumeration value="FORCE_INDEXED"/>
|
||||
|
||||
<!-- Execute statements with named parameters -->
|
||||
<enumeration value="NAMED"/>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user