Skip to content

Commit 063cb3d

Browse files
committed
PR ebean-orm#3144 - FEATURE: NEW: QueryPlanLogger for DB2
1 parent 590cecd commit 063cb3d

File tree

4 files changed

+355
-0
lines changed

4 files changed

+355
-0
lines changed

ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,11 @@ public class DatabaseConfig {
502502
*/
503503
private boolean queryPlanEnable;
504504

505+
/**
506+
* Additional platform specific options for query-plan generation.
507+
*/
508+
private String queryPlanOptions;
509+
505510
/**
506511
* The default threshold in micros for collecting query plans.
507512
*/
@@ -2878,6 +2883,7 @@ protected void loadSettings(PropertiesWrapper p) {
28782883
queryPlanTTLSeconds = p.getInt("queryPlanTTLSeconds", queryPlanTTLSeconds);
28792884
slowQueryMillis = p.getLong("slowQueryMillis", slowQueryMillis);
28802885
queryPlanEnable = p.getBoolean("queryPlan.enable", queryPlanEnable);
2886+
queryPlanOptions = p.get("queryPlan.options", queryPlanOptions);
28812887
queryPlanThresholdMicros = p.getLong("queryPlan.thresholdMicros", queryPlanThresholdMicros);
28822888
queryPlanCapture = p.getBoolean("queryPlan.capture", queryPlanCapture);
28832889
queryPlanCapturePeriodSecs = p.getLong("queryPlan.capturePeriodSecs", queryPlanCapturePeriodSecs);
@@ -3277,6 +3283,20 @@ public void setQueryPlanEnable(boolean queryPlanEnable) {
32773283
this.queryPlanEnable = queryPlanEnable;
32783284
}
32793285

3286+
/**
3287+
* Returns platform specific query plan options.
3288+
*/
3289+
public String getQueryPlanOptions() {
3290+
return queryPlanOptions;
3291+
}
3292+
3293+
/**
3294+
* Set platform specific query plan options.
3295+
*/
3296+
public void setQueryPlanOptions(String queryPlanOptions) {
3297+
this.queryPlanOptions = queryPlanOptions;
3298+
}
3299+
32803300
/**
32813301
* Return the query plan collection threshold in microseconds.
32823302
*/

ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,8 @@ QueryPlanLogger queryPlanLogger(Platform platform) {
597597
return new QueryPlanLoggerSqlServer();
598598
case ORACLE:
599599
return new QueryPlanLoggerOracle();
600+
case DB2:
601+
return new QueryPlanLoggerDb2(config.getQueryPlanOptions());
600602
default:
601603
return new QueryPlanLoggerExplain();
602604
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
2+
package io.ebeaninternal.server.query;
3+
4+
import io.ebean.util.IOUtils;
5+
import io.ebean.util.StringHelper;
6+
import io.ebeaninternal.api.CoreLog;
7+
import io.ebeaninternal.api.SpiDbQueryPlan;
8+
import io.ebeaninternal.api.SpiQueryPlan;
9+
import io.ebeaninternal.server.bind.capture.BindCapture;
10+
11+
import java.io.BufferedReader;
12+
import java.io.IOException;
13+
import java.io.InputStream;
14+
import java.sql.Connection;
15+
import java.sql.PreparedStatement;
16+
import java.sql.ResultSet;
17+
import java.sql.SQLException;
18+
import java.sql.Statement;
19+
import java.util.Map;
20+
import java.util.Random;
21+
22+
import static java.lang.System.Logger.Level.WARNING;
23+
24+
/**
25+
* A QueryPlanLogger for DB2.
26+
* <p>
27+
* To use query plan capturing, you have to install the explain tables with
28+
* <code>SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', CURRENT SCHEMA )</code>.
29+
* To do this in a repeatable script, you may use this statement:
30+
*
31+
* <pre>
32+
* BEGIN
33+
* IF NOT EXISTS (SELECT * FROM SYSCAT.TABLES WHERE TABSCHEMA = CURRENT SCHEMA AND TABNAME = 'EXPLAIN_STREAM') THEN
34+
* call SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', CURRENT SCHEMA );
35+
* END IF;
36+
* END
37+
* </pre>
38+
*
39+
* @author Roland Praml, FOCONIS AG
40+
*/
41+
public final class QueryPlanLoggerDb2 extends QueryPlanLogger {
42+
43+
private Random rnd = new Random();
44+
45+
private final String schema;
46+
47+
private final boolean create;
48+
49+
private static final String GET_PLAN_TEMPLATE = readReasource("QueryPlanLoggerDb2.sql");
50+
51+
private static final String CREATE_TEMPLATE = "BEGIN\n"
52+
+ "IF NOT EXISTS (SELECT * FROM SYSCAT.TABLES WHERE TABSCHEMA = ${SCHEMA} AND TABNAME = 'EXPLAIN_STREAM') THEN\n"
53+
+ " CALL SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', ${SCHEMA} );\n"
54+
+ "END IF;\n"
55+
+ "END";
56+
57+
public QueryPlanLoggerDb2(String opts) {
58+
Map<String, String> map = StringHelper.delimitedToMap(opts, ";", "=");
59+
create = !"false" .equals(map.get("create")); // default is create
60+
String schema = map.get("schema"); // should be null or SYSTOOLS
61+
if (schema == null || schema.isEmpty()) {
62+
this.schema = null;
63+
} else {
64+
this.schema = schema.toUpperCase();
65+
}
66+
}
67+
68+
private static String readReasource(String resName) {
69+
try (InputStream stream = QueryPlanLoggerDb2.class.getResourceAsStream(resName)) {
70+
if (stream == null) {
71+
throw new IllegalStateException("Could not find resource " + resName);
72+
}
73+
BufferedReader reader = IOUtils.newReader(stream);
74+
StringBuilder sb = new StringBuilder();
75+
reader.lines().forEach(line -> sb.append(line).append('\n'));
76+
return sb.toString();
77+
} catch (IOException e) {
78+
throw new IllegalStateException("Could not read resource " + resName, e);
79+
}
80+
}
81+
82+
@Override
83+
public SpiDbQueryPlan collectPlan(Connection conn, SpiQueryPlan plan, BindCapture bind) {
84+
try (Statement stmt = conn.createStatement()) {
85+
if (create) {
86+
// create explain tables if neccessary
87+
if (schema == null) {
88+
stmt.execute(CREATE_TEMPLATE.replace("${SCHEMA}", "CURRENT USER"));
89+
} else {
90+
stmt.execute(CREATE_TEMPLATE.replace("${SCHEMA}", "'" + schema + "'"));
91+
}
92+
conn.commit();
93+
}
94+
95+
try {
96+
int queryNo = rnd.nextInt(Integer.MAX_VALUE);
97+
98+
String sql = "EXPLAIN PLAN SET QUERYNO = " + queryNo + " FOR " + plan.sql();
99+
try (PreparedStatement explainStmt = conn.prepareStatement(sql)) {
100+
bind.prepare(explainStmt, conn);
101+
explainStmt.execute();
102+
}
103+
104+
sql = schema == null
105+
? GET_PLAN_TEMPLATE.replace("${SCHEMA}", conn.getMetaData().getUserName().toUpperCase())
106+
: GET_PLAN_TEMPLATE.replace("${SCHEMA}", schema);
107+
108+
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
109+
pstmt.setInt(1, queryNo);
110+
try (ResultSet rset = pstmt.executeQuery()) {
111+
return readQueryPlan(plan, bind, rset);
112+
}
113+
}
114+
} finally {
115+
conn.rollback(); // do not keep query plans in DB
116+
}
117+
} catch (SQLException e) {
118+
CoreLog.log.log(WARNING, "Could not log query plan", e);
119+
return null;
120+
}
121+
}
122+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
WITH tree(operator_ID, level, path, explain_time, cycle)
2+
AS
3+
(
4+
SELECT 1 operator_id
5+
, 0 level
6+
, CAST('001' AS VARCHAR(1000)) path
7+
, max(explain_time) explain_time
8+
, 0
9+
FROM ${SCHEMA}.EXPLAIN_OPERATOR O
10+
WHERE O.EXPLAIN_REQUESTER = SESSION_USER
11+
12+
UNION ALL
13+
14+
SELECT s.source_id
15+
, level + 1
16+
, tree.path || '/' || LPAD(CAST(s.source_id AS VARCHAR(3)), 3, '0') path
17+
, tree.explain_time
18+
, POSITION('/' || LPAD(CAST(s.source_id AS VARCHAR(3)), 3, '0') || '/' IN path USING OCTETS)
19+
FROM tree
20+
, ${SCHEMA}.EXPLAIN_STREAM S
21+
WHERE s.target_id = tree.operator_id
22+
AND s.explain_time = tree.explain_time
23+
AND S.Object_Name IS NULL
24+
AND S.explain_requester = SESSION_USER
25+
AND tree.cycle = 0
26+
AND level < 100
27+
)
28+
SELECT *
29+
FROM (
30+
SELECT "Explain Plan"
31+
FROM (
32+
SELECT CAST( LPAD(id, MAX(LENGTH(id)) OVER(), ' ')
33+
|| ' | '
34+
|| RPAD(operation, MAX(LENGTH(operation)) OVER(), ' ')
35+
|| ' | '
36+
|| LPAD(rows, MAX(LENGTH(rows)) OVER(), ' ')
37+
|| ' | '
38+
-- Don't show ActualRows columns if there are no actuals available at all
39+
|| CASE WHEN COUNT(ActualRows) OVER () > 1 -- the heading 'ActualRows' is always present, so "1" means no OTHER values
40+
THEN LPAD(ActualRows, MAX(LENGTH(ActualRows)) OVER(), ' ') || ' | '
41+
ELSE ''
42+
END
43+
|| LPAD(cost, MAX(LENGTH(cost)) OVER(), ' ')
44+
AS VARCHAR(100)) "Explain Plan"
45+
, path
46+
FROM (
47+
SELECT 'ID' ID
48+
, 'Operation' Operation
49+
, 'Rows' Rows
50+
, 'ActualRows' ActualRows
51+
, 'Cost' Cost
52+
, '0' Path
53+
FROM SYSIBM.SYSDUMMY1
54+
-- TODO: UNION ALL yields duplicate. where do they come from?
55+
UNION
56+
SELECT CAST(tree.operator_id as VARCHAR(254)) ID
57+
, CAST(LPAD(' ', tree.level, ' ')
58+
|| CASE WHEN tree.cycle = 1
59+
THEN '(cycle) '
60+
ELSE ''
61+
END
62+
|| COALESCE (
63+
TRIM(O.Operator_Type)
64+
|| COALESCE(' (' || argument || ')', '')
65+
|| ' '
66+
|| COALESCE(S.Object_Name,'')
67+
, ''
68+
)
69+
AS VARCHAR(254)) AS OPERATION
70+
, COALESCE(CAST(rows AS VARCHAR(254)), '') Rows
71+
, CAST(ActualRows as VARCHAR(254)) ActualRows -- note: no coalesce
72+
, COALESCE(CAST(CAST(O.Total_Cost AS BIGINT) AS VARCHAR(254)), '') Cost
73+
, path
74+
FROM tree
75+
LEFT JOIN ( SELECT i.source_id
76+
, i.target_id
77+
, CAST(CAST(ROUND(o.stream_count) AS BIGINT) AS VARCHAR(12))
78+
|| ' of '
79+
|| CAST (total_rows AS VARCHAR(12))
80+
|| CASE WHEN total_rows > 0
81+
AND ROUND(o.stream_count) <= total_rows THEN
82+
' ('
83+
|| LPAD(CAST (ROUND(ROUND(o.stream_count)/total_rows*100,2)
84+
AS NUMERIC(5,2)), 6, ' ')
85+
|| '%)'
86+
ELSE ''
87+
END rows
88+
, CASE WHEN act.actual_value is not null then
89+
CAST(CAST(ROUND(act.actual_value) AS BIGINT) AS VARCHAR(12))
90+
|| ' of '
91+
|| CAST (total_rows AS VARCHAR(12))
92+
|| CASE WHEN total_rows > 0 THEN
93+
' ('
94+
|| LPAD(CAST (ROUND(ROUND(act.actual_value)/total_rows*100,2)
95+
AS NUMERIC(5,2)), 6, ' ')
96+
|| '%)'
97+
ELSE NULL
98+
END END ActualRows
99+
, i.object_name
100+
, i.explain_time
101+
FROM (SELECT MAX(source_id) source_id
102+
, target_id
103+
, MIN(CAST(ROUND(stream_count,0) AS BIGINT)) total_rows
104+
, CAST(LISTAGG(object_name) AS VARCHAR(50)) object_name
105+
, explain_time
106+
FROM ${SCHEMA}.EXPLAIN_STREAM
107+
WHERE explain_time = (SELECT MAX(explain_time)
108+
FROM ${SCHEMA}.EXPLAIN_OPERATOR
109+
WHERE EXPLAIN_REQUESTER = SESSION_USER
110+
)
111+
GROUP BY target_id, explain_time
112+
) I
113+
LEFT JOIN ${SCHEMA}.EXPLAIN_STREAM O
114+
ON ( I.target_id=o.source_id
115+
AND I.explain_time = o.explain_time
116+
AND O.EXPLAIN_REQUESTER = SESSION_USER
117+
)
118+
LEFT JOIN ${SCHEMA}.EXPLAIN_ACTUALS act
119+
ON ( act.operator_id = i.target_id
120+
AND act.explain_time = i.explain_time
121+
AND act.explain_requester = SESSION_USER
122+
AND act.ACTUAL_TYPE like 'CARDINALITY%'
123+
)
124+
) s
125+
ON ( s.target_id = tree.operator_id
126+
AND s.explain_time = tree.explain_time
127+
)
128+
LEFT JOIN ${SCHEMA}.EXPLAIN_OPERATOR O
129+
ON ( o.operator_id = tree.operator_id
130+
AND o.explain_time = tree.explain_time
131+
AND o.explain_requester = SESSION_USER
132+
)
133+
LEFT JOIN (SELECT LISTAGG (CASE argument_type
134+
WHEN 'UNIQUE' THEN
135+
CASE WHEN argument_value = 'TRUE'
136+
THEN 'UNIQUE'
137+
ELSE NULL
138+
END
139+
WHEN 'TRUNCSRT' THEN
140+
CASE WHEN argument_value = 'TRUE'
141+
THEN 'TOP-N'
142+
ELSE NULL
143+
END
144+
WHEN 'SCANDIR' THEN
145+
CASE WHEN argument_value != 'FORWARD'
146+
THEN argument_value
147+
ELSE NULL
148+
END
149+
ELSE argument_value
150+
END
151+
, ' ') argument
152+
, operator_id
153+
, explain_time
154+
FROM ${SCHEMA}.EXPLAIN_ARGUMENT EA
155+
WHERE argument_type IN ('AGGMODE' -- GRPBY
156+
, 'UNIQUE', 'TRUNCSRT' -- SORT
157+
, 'SCANDIR' -- IXSCAN, TBSCAN
158+
, 'OUTERJN' -- JOINs
159+
)
160+
AND explain_requester = SESSION_USER
161+
GROUP BY explain_time, operator_id
162+
163+
) A
164+
ON ( a.operator_id = tree.operator_id
165+
AND a.explain_time = tree.explain_time
166+
)
167+
) O
168+
UNION ALL
169+
VALUES ('Explain plan (c) 2014-2017 by Markus Winand - NO WARRANTY - V20171102','Z0')
170+
, ('Modifications by Ember Crooks - NO WARRANTY','Z1')
171+
, ('http://use-the-index-luke.com/s/last_explained','Z2')
172+
, ('', 'A')
173+
, ('', 'Y')
174+
, ('Predicate Information', 'AA')
175+
UNION ALL
176+
SELECT CAST (LPAD(CASE WHEN operator_id = LAG (operator_id)
177+
OVER (PARTITION BY operator_id
178+
ORDER BY pred_order
179+
)
180+
THEN ''
181+
ELSE operator_id || ' - '
182+
END
183+
, MAX(LENGTH(operator_id )+4) OVER()
184+
, ' ')
185+
|| how_applied
186+
|| ' '
187+
|| predicate_text
188+
AS VARCHAR(100)) "Predicate Information"
189+
, 'P' || LPAD(id_order, 5, '0') || pred_order path
190+
FROM (SELECT CAST(operator_id AS VARCHAR(254)) operator_id
191+
, LPAD(trim(how_applied)
192+
, MAX (LENGTH(TRIM(how_applied)))
193+
OVER (PARTITION BY operator_id)
194+
, ' '
195+
) how_applied
196+
-- next: capped to length 80 to avoid
197+
-- SQL0445W Value "..." has been truncated. SQLSTATE=01004
198+
-- error when long literal values may appear (space padded!)
199+
, CAST(substr(predicate_text, 1, 80) AS VARCHAR(80)) predicate_text
200+
, CASE how_applied WHEN 'START' THEN '1'
201+
WHEN 'STOP' THEN '2'
202+
WHEN 'SARG' THEN '3'
203+
ELSE '9'
204+
END pred_order
205+
, operator_id id_order
206+
FROM ${SCHEMA}.EXPLAIN_PREDICATE p
207+
WHERE explain_time = (SELECT max(explain_time) FROM ${SCHEMA}.EXPLAIN_STATEMENT WHERE queryno = ?)
208+
)
209+
)
210+
ORDER BY path
211+
)

0 commit comments

Comments
 (0)