blob: 8cec2c9fe4a64467a6a1fe3191f9b98236a14df0 [file]
/*
* Class org.apache.derbyTesting.functionTests.tests.lang.StalePlansTest
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.derbyTesting.functionTests.tests.lang;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import junit.framework.Test;
import org.apache.derbyTesting.functionTests.util.Formatters;
import org.apache.derbyTesting.junit.BaseJDBCTestCase;
import org.apache.derbyTesting.junit.BaseTestSuite;
import org.apache.derbyTesting.junit.CleanDatabaseTestSetup;
import org.apache.derbyTesting.junit.DatabasePropertyTestSetup;
import org.apache.derbyTesting.junit.JDBC;
import org.apache.derbyTesting.junit.RuntimeStatisticsParser;
import org.apache.derbyTesting.junit.SQLUtilities;
/**
* This is the test for stale plan invalidation. The system determines at
* execution whether the tables used by a DML statement have grown or shrunk
* significantly, and if so, causes the statement to be recompiled at the next
* execution.
*/
public class StalePlansTest extends BaseJDBCTestCase {
/**
* The value of derby.language.stalePlanCheckInterval to use in this
* test. The default value is 100, but we use 10 to reduce the number
* of times the test has to execute statements to get to the desired
* state.
*/
private static final int STALE_PLAN_CHECK_INTERVAL = 10;
public StalePlansTest(String name) {
super(name);
}
/**
* Create the test suite. This test is not run in client/server mode since
* it only tests the query plans generated by the embedded driver.
*/
public static Test suite() {
Properties props = new Properties();
// Check for stale plans on every 10th execution (default 100) to
// reduce the number of times we need to execute each statement.
props.setProperty("derby.language.stalePlanCheckInterval",
String.valueOf(STALE_PLAN_CHECK_INTERVAL));
// Disable the index statistics daemon so that it doesn't cause
// recompilation of statements at random times.
props.setProperty("derby.storage.indexStats.auto", "false");
Test suite = new DatabasePropertyTestSetup(
new BaseTestSuite(StalePlansTest.class), props, true);
return new CleanDatabaseTestSetup(suite);
}
/**
* Create tables and indexes needed by the test cases. Enable collection of
* run-time statistics.
*/
protected void setUp() throws SQLException {
getConnection().setAutoCommit(false);
Statement stmt = createStatement();
stmt.executeUpdate("create table t (c1 int, c2 int, c3 varchar(255))");
stmt.executeUpdate("create index idx on t (c1)");
stmt.executeUpdate("call SYSCS_UTIL.SYSCS_SET_RUNTIMESTATISTICS(1)");
stmt.close();
commit();
}
/**
* Drop tables used in the test.
*/
protected void tearDown() throws Exception {
Statement stmt = createStatement();
stmt.executeUpdate("drop table t");
commit();
super.tearDown();
}
/**
* Flush the cache so that row count changes are visible. When a dirty
* page is written to disk, the row count estimate for the container will
* be updated with the number of added/deleted rows on that page since
* the last time the page was read from disk or written to disk. We invoke
* a checkpoint in order to force all dirty pages to be flushed and make
* all row count changes visible.
*/
private void flushRowCount(Statement stmt) throws SQLException {
stmt.execute("CALL SYSCS_UTIL.SYSCS_CHECKPOINT_DATABASE()");
}
/**
* Negative test - set stalePlanCheckInterval to a value out of range.
*/
public void testStalePlanCheckIntervalOutOfRange() throws SQLException {
Statement stmt = createStatement();
assertStatementError("XCY00", stmt,
"call SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY(" +
"'derby.language.stalePlanCheckInterval', '2')");
stmt.close();
}
/**
* Test that the query plan is changed when the size of a small table
* changes.
*
* <p><b>Note:</b> This test is outdated since Derby now tries to use index
* scans whenever possible on small tables (primarily to avoid table locks
* for certain isolation levels, but also because a small table is likely
* to grow).
*/
public void testStalePlansOnSmallTable() throws SQLException {
Statement stmt = createStatement();
PreparedStatement insert =
prepareStatement("insert into t values (?,?,?)");
insert.setInt(1, 1);
insert.setInt(2, 100);
insert.setString(3, Formatters.padString("abc", 255));
insert.executeUpdate();
commit();
// Make sure row count from insert is flushed out
flushRowCount(stmt);
PreparedStatement ps =
prepareStatement("select count(c1 + c2) from t where c1 = 1");
// Expect this to do an index scan
String[][] expected = {{ "1" }};
JDBC.assertFullResultSet(ps.executeQuery(), expected);
assertTrue(SQLUtilities.
getRuntimeStatisticsParser(stmt).usedIndexScan());
// Execute 11 more times, the plan should not change
for (int i = 0; i < 11; i++) {
JDBC.assertFullResultSet(ps.executeQuery(), expected);
}
// Expect index scan
assertTrue(SQLUtilities.
getRuntimeStatisticsParser(stmt).usedIndexScan());
commit();
// Now increase the size of the table
insert.setInt(2, 100);
for (int i = 2; i <= 10; i++) {
insert.setInt(1, i);
insert.executeUpdate();
}
commit();
// Make sure row count from inserts is flushed out
flushRowCount(stmt);
// Execute 11 times, the plan should not change
for (int i = 0; i < 11; i++) {
JDBC.assertFullResultSet(ps.executeQuery(), expected);
}
// Expect this to use index
JDBC.assertFullResultSet(ps.executeQuery(), expected);
assertTrue(SQLUtilities.
getRuntimeStatisticsParser(stmt).usedIndexScan());
commit();
// Now shrink the table back to its original size
stmt.executeUpdate("delete from t where c1 >= 2");
// Execute 11 times, the plan should not change
for (int i = 0; i < 11; i++) {
JDBC.assertFullResultSet(ps.executeQuery(), expected);
}
// Expect this to do an index scan
assertTrue(SQLUtilities.
getRuntimeStatisticsParser(stmt).usedIndexScan());
stmt.close();
ps.close();
insert.close();
}
/**
* Test that the query plan changes when a large table is modified.
*/
public void testStalePlansOnLargeTable() throws SQLException {
Statement stmt = createStatement();
PreparedStatement insert =
prepareStatement("insert into t values (?,?,?)");
insert.setInt(1, 1);
insert.setInt(2, 1);
insert.setString(3, Formatters.padString("abc", 255));
insert.executeUpdate();
PreparedStatement insert2 =
prepareStatement("insert into t select c1+?, c2+?, c3 from t");
for (int i = 1; i <= 512; i *= 2) {
insert2.setInt(1, i);
insert2.setInt(2, i);
insert2.executeUpdate();
}
commit();
// Make sure row count from inserts is flushed out
flushRowCount(stmt);
PreparedStatement ps = prepareStatement(
"select count(c1 + c2) from t where c1 = 1");
// Expect this to use index
String[][] expected = {{ "1" }};
JDBC.assertFullResultSet(ps.executeQuery(), expected);
assertTrue(SQLUtilities.
getRuntimeStatisticsParser(stmt).usedIndexScan());
commit();
// Change the row count a little bit. A recompile will only be
// triggered if the row count changes by 10% or more.
for (int i = 1025; i <= 1250; i++) {
insert.setInt(1, i);
insert.setInt(2, i);
insert.executeUpdate();
}
commit();
// Change the data so a table scan would make more sense.
// Use a qualifier to convince TableScanResultSet not to
// update the row count in the store (which would make it
// hard for this test to control when recompilation takes
// place).
stmt.executeUpdate("update t set c1 = 1 where c1 > 0");
// Make sure row count from inserts is flushed out
flushRowCount(stmt);
// Execute 11 more times, the plan should not change
for (int i = 0; i < 11; i++) {
JDBC.assertSingleValueResultSet(ps.executeQuery(), "1250");
}
// Expect this to use table scan, as the above update has basically
// made all the rows in the table be equal to "1", thus using the index
// does not help if all the rows are going to qualify.
RuntimeStatisticsParser rsp =
SQLUtilities.getRuntimeStatisticsParser(stmt);
if (!rsp.usedTableScan()) {
// Dump the full plan to help debug DERBY-6336.
fail("Expected table scan. Full plan:\n" + rsp.toString());
}
// Change the row count significantly
stmt.executeUpdate("insert into t select c1,c2,c3 from t where c1<128");
// Make sure row count from inserts is flushed out
flushRowCount(stmt);
// Execute 11 times, the plan should change
for (int i = 0; i < 11; i++) {
JDBC.assertSingleValueResultSet(ps.executeQuery(), "2500");
}
// Expect this to do table scan
assertTrue(SQLUtilities.
getRuntimeStatisticsParser(stmt).usedTableScan());
// Change the distribution back to where an index makes sense
stmt.executeUpdate("update t set c1 = c2");
// Change the row count significantly
stmt.executeUpdate("insert into t select c1, c2, c3 from t");
// Make sure row count from inserts is flushed out
flushRowCount(stmt);
// Execute 11 times, the plan should change
for (int i = 0; i < 11; i++) {
JDBC.assertFullResultSet(ps.executeQuery(),
new String[][] { { "4" } });
}
// Expect this to do index to baserow
assertTrue(SQLUtilities.
getRuntimeStatisticsParser(stmt).usedIndexRowToBaseRow());
stmt.close();
insert.close();
insert2.close();
ps.close();
}
/**
* Regression test case for DERBY-6724, where an INSERT statement would
* fail with a NullPointerException if it had fired a trigger, and it
* was detected during execution that the statement plan was stale and
* had to be recompiled.
*/
public void testDerby6724() throws SQLException {
Statement s = createStatement();
s.execute("create table d6724_t(x int)");
s.execute("create trigger d6724_tr after insert on d6724_t values 1");
s.execute("insert into d6724_t values 1");
// Before DERBY-6724 this statement would fail with an NPE in the
// (STALE_PLAN_CHECK_INTERVAL+1)'th execution.
PreparedStatement ps = prepareStatement(
"insert into d6724_t select * from d6724_t");
for (int i = 0; i < STALE_PLAN_CHECK_INTERVAL + 1; i++) {
// Execute the statement and verify that the correct number of
// rows are inserted. The number doubles for each execution.
assertUpdateCount(ps, 1 << i);
}
rollback();
}
}