Unverified 提交 80b0aabc authored 作者: Noel Grandin's avatar Noel Grandin 提交者: GitHub

Merge pull request #785 from katzyn/MVSecondaryIndex

Optimize NULL handling in MVSecondaryIndex.add()
...@@ -9,7 +9,6 @@ import java.util.Collections; ...@@ -9,7 +9,6 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.h2.util.New;
import org.h2.util.StringUtils; import org.h2.util.StringUtils;
/** /**
...@@ -22,6 +21,29 @@ public class Mode { ...@@ -22,6 +21,29 @@ public class Mode {
REGULAR, DB2, Derby, MSSQLServer, HSQLDB, MySQL, Oracle, PostgreSQL, Ignite, REGULAR, DB2, Derby, MSSQLServer, HSQLDB, MySQL, Oracle, PostgreSQL, Ignite,
} }
/**
* Determines how rows with {@code NULL} values in indexed columns are handled
* in unique indexes.
*/
public enum UniqueIndexNullsHandling {
/**
* Multiple identical indexed columns with at least one {@code NULL} value are
* allowed in unique index.
*/
ALLOW_DUPLICATES_WITH_ANY_NULL,
/**
* Multiple identical indexed columns with all {@code NULL} values are allowed
* in unique index.
*/
ALLOW_DUPLICATES_WITH_ALL_NULLS,
/**
* Multiple identical indexed columns are not allowed in unique index.
*/
FORBID_ANY_DUPLICATES;
}
private static final HashMap<String, Mode> MODES = new HashMap<>(); private static final HashMap<String, Mode> MODES = new HashMap<>();
// Modes are also documented in the features section // Modes are also documented in the features section
...@@ -87,17 +109,10 @@ public class Mode { ...@@ -87,17 +109,10 @@ public class Mode {
public boolean systemColumns; public boolean systemColumns;
/** /**
* For unique indexes, NULL is distinct. That means only one row with NULL * Determines how rows with {@code NULL} values in indexed columns are handled
* in one of the columns is allowed. * in unique indexes.
*/
public boolean uniqueIndexSingleNull;
/**
* When using unique indexes, multiple rows with NULL in all columns
* are allowed, however it is not allowed to have multiple rows with the
* same values otherwise.
*/ */
public boolean uniqueIndexSingleNullExceptAllColumnsAreNull; public UniqueIndexNullsHandling uniqueIndexNullsHandling = UniqueIndexNullsHandling.ALLOW_DUPLICATES_WITH_ANY_NULL;
/** /**
* Empty strings are treated like NULL values. Useful for Oracle emulation. * Empty strings are treated like NULL values. Useful for Oracle emulation.
...@@ -208,7 +223,7 @@ public class Mode { ...@@ -208,7 +223,7 @@ public class Mode {
mode = new Mode(ModeEnum.Derby.name()); mode = new Mode(ModeEnum.Derby.name());
mode.aliasColumnName = true; mode.aliasColumnName = true;
mode.uniqueIndexSingleNull = true; mode.uniqueIndexNullsHandling = UniqueIndexNullsHandling.FORBID_ANY_DUPLICATES;
mode.supportOffsetFetch = true; mode.supportOffsetFetch = true;
mode.sysDummy1 = true; mode.sysDummy1 = true;
mode.isolationLevelInSelectOrInsertStatement = true; mode.isolationLevelInSelectOrInsertStatement = true;
...@@ -220,7 +235,7 @@ public class Mode { ...@@ -220,7 +235,7 @@ public class Mode {
mode.aliasColumnName = true; mode.aliasColumnName = true;
mode.convertOnlyToSmallerScale = true; mode.convertOnlyToSmallerScale = true;
mode.nullConcatIsNull = true; mode.nullConcatIsNull = true;
mode.uniqueIndexSingleNull = true; mode.uniqueIndexNullsHandling = UniqueIndexNullsHandling.FORBID_ANY_DUPLICATES;
mode.allowPlusForStringConcat = true; mode.allowPlusForStringConcat = true;
// HSQLDB does not support client info properties. See // HSQLDB does not support client info properties. See
// http://hsqldb.org/doc/apidocs/ // http://hsqldb.org/doc/apidocs/
...@@ -232,7 +247,7 @@ public class Mode { ...@@ -232,7 +247,7 @@ public class Mode {
mode = new Mode(ModeEnum.MSSQLServer.name()); mode = new Mode(ModeEnum.MSSQLServer.name());
mode.aliasColumnName = true; mode.aliasColumnName = true;
mode.squareBracketQuotedNames = true; mode.squareBracketQuotedNames = true;
mode.uniqueIndexSingleNull = true; mode.uniqueIndexNullsHandling = UniqueIndexNullsHandling.FORBID_ANY_DUPLICATES;
mode.allowPlusForStringConcat = true; mode.allowPlusForStringConcat = true;
mode.swapConvertFunctionParameters = true; mode.swapConvertFunctionParameters = true;
mode.supportPoundSymbolForColumnNames = true; mode.supportPoundSymbolForColumnNames = true;
...@@ -260,7 +275,7 @@ public class Mode { ...@@ -260,7 +275,7 @@ public class Mode {
mode = new Mode(ModeEnum.Oracle.name()); mode = new Mode(ModeEnum.Oracle.name());
mode.aliasColumnName = true; mode.aliasColumnName = true;
mode.convertOnlyToSmallerScale = true; mode.convertOnlyToSmallerScale = true;
mode.uniqueIndexSingleNullExceptAllColumnsAreNull = true; mode.uniqueIndexNullsHandling = UniqueIndexNullsHandling.ALLOW_DUPLICATES_WITH_ALL_NULLS;
mode.treatEmptyStringsAsNull = true; mode.treatEmptyStringsAsNull = true;
mode.regexpReplaceBackslashReferences = true; mode.regexpReplaceBackslashReferences = true;
mode.supportPoundSymbolForColumnNames = true; mode.supportPoundSymbolForColumnNames = true;
......
...@@ -9,7 +9,6 @@ import java.util.HashSet; ...@@ -9,7 +9,6 @@ import java.util.HashSet;
import org.h2.api.ErrorCode; import org.h2.api.ErrorCode;
import org.h2.engine.Constants; import org.h2.engine.Constants;
import org.h2.engine.DbObject; import org.h2.engine.DbObject;
import org.h2.engine.Mode;
import org.h2.engine.Session; import org.h2.engine.Session;
import org.h2.message.DbException; import org.h2.message.DbException;
import org.h2.message.Trace; import org.h2.message.Trace;
...@@ -302,34 +301,34 @@ public abstract class BaseIndex extends SchemaObjectBase implements Index { ...@@ -302,34 +301,34 @@ public abstract class BaseIndex extends SchemaObjectBase implements Index {
} }
/** /**
* Check if one of the columns is NULL and multiple rows with NULL are * Check if this row may have duplicates with the same indexed values in the
* allowed using the current compatibility mode for unique indexes. Note: * current compatibility mode. Duplicates with {@code NULL} values are
* NULL behavior is complicated in SQL. * allowed in some modes.
* *
* @param newRow the row to check * @param searchRow
* @return true if one of the columns is null and multiple nulls in unique * the row to check
* indexes are allowed * @return {@code true} if specified row may have duplicates,
* {@code false otherwise}
*/ */
protected boolean containsNullAndAllowMultipleNull(SearchRow newRow) { protected boolean mayHaveNullDuplicates(SearchRow searchRow) {
Mode mode = database.getMode(); switch (database.getMode().uniqueIndexNullsHandling) {
if (mode.uniqueIndexSingleNull) { case ALLOW_DUPLICATES_WITH_ANY_NULL:
for (int index : columnIds) {
if (searchRow.getValue(index) == ValueNull.INSTANCE) {
return true;
}
}
return false; return false;
} else if (mode.uniqueIndexSingleNullExceptAllColumnsAreNull) { case ALLOW_DUPLICATES_WITH_ALL_NULLS:
for (int index : columnIds) { for (int index : columnIds) {
Value v = newRow.getValue(index); if (searchRow.getValue(index) != ValueNull.INSTANCE) {
if (v != ValueNull.INSTANCE) {
return false; return false;
} }
} }
return true; return true;
default:
return false;
} }
for (int index : columnIds) {
Value v = newRow.getValue(index);
if (v == ValueNull.INSTANCE) {
return true;
}
}
return false;
} }
/** /**
......
...@@ -116,7 +116,7 @@ public abstract class PageBtree extends Page { ...@@ -116,7 +116,7 @@ public abstract class PageBtree extends Page {
comp = index.compareRows(row, compare); comp = index.compareRows(row, compare);
if (comp == 0) { if (comp == 0) {
if (add && index.indexType.isUnique()) { if (add && index.indexType.isUnique()) {
if (!index.containsNullAndAllowMultipleNull(compare)) { if (!index.mayHaveNullDuplicates(compare)) {
throw index.getDuplicateKeyException(compare.toString()); throw index.getDuplicateKeyException(compare.toString());
} }
} }
......
...@@ -66,7 +66,7 @@ public class TreeIndex extends BaseIndex { ...@@ -66,7 +66,7 @@ public class TreeIndex extends BaseIndex {
int compare = compareRows(row, r); int compare = compareRows(row, r);
if (compare == 0) { if (compare == 0) {
if (indexType.isUnique()) { if (indexType.isUnique()) {
if (!containsNullAndAllowMultipleNull(row)) { if (!mayHaveNullDuplicates(row)) {
throw getDuplicateKeyException(row.toString()); throw getDuplicateKeyException(row.toString());
} }
} }
......
...@@ -143,7 +143,9 @@ public final class MVSecondaryIndex extends BaseIndex implements MVIndex { ...@@ -143,7 +143,9 @@ public final class MVSecondaryIndex extends BaseIndex implements MVIndex {
array[keyColumns - 1] = ValueLong.get(Long.MIN_VALUE); array[keyColumns - 1] = ValueLong.get(Long.MIN_VALUE);
ValueArray unique = ValueArray.get(array); ValueArray unique = ValueArray.get(array);
SearchRow row = convertToSearchRow(rowData); SearchRow row = convertToSearchRow(rowData);
checkUnique(row, dataMap, unique); if (!mayHaveNullDuplicates(row)) {
requireUnique(row, dataMap, unique);
}
} }
dataMap.putCommitted(rowData, ValueNull.INSTANCE); dataMap.putCommitted(rowData, ValueNull.INSTANCE);
...@@ -193,25 +195,26 @@ public final class MVSecondaryIndex extends BaseIndex implements MVIndex { ...@@ -193,25 +195,26 @@ public final class MVSecondaryIndex extends BaseIndex implements MVIndex {
// this will detect committed entries only // this will detect committed entries only
unique = convertToKey(row); unique = convertToKey(row);
unique.getList()[keyColumns - 1] = ValueLong.get(Long.MIN_VALUE); unique.getList()[keyColumns - 1] = ValueLong.get(Long.MIN_VALUE);
checkUnique(row, map, unique); if (mayHaveNullDuplicates(row)) {
// No further unique checks required
unique = null;
} else {
requireUnique(row, map, unique);
}
} }
try { try {
map.put(array, ValueNull.INSTANCE); map.put(array, ValueNull.INSTANCE);
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
throw mvTable.convertException(e); throw mvTable.convertException(e);
} }
if (indexType.isUnique()) { if (unique != null) {
// This code expects that mayHaveDuplicates(row) == false
Iterator<Value> it = map.keyIterator(unique, true); Iterator<Value> it = map.keyIterator(unique, true);
while (it.hasNext()) { while (it.hasNext()) {
ValueArray k = (ValueArray) it.next(); ValueArray k = (ValueArray) it.next();
SearchRow r2 = convertToSearchRow(k); if (compareRows(row, convertToSearchRow(k)) != 0) {
if (compareRows(row, r2) != 0) {
break; break;
} }
if (containsNullAndAllowMultipleNull(r2)) {
// this is allowed
continue;
}
if (map.isSameTransaction(k)) { if (map.isSameTransaction(k)) {
continue; continue;
} }
...@@ -224,18 +227,13 @@ public final class MVSecondaryIndex extends BaseIndex implements MVIndex { ...@@ -224,18 +227,13 @@ public final class MVSecondaryIndex extends BaseIndex implements MVIndex {
} }
} }
private void checkUnique(SearchRow row, TransactionMap<Value, Value> map, ValueArray unique) { private void requireUnique(SearchRow row, TransactionMap<Value, Value> map, ValueArray unique) {
Iterator<Value> it = map.keyIterator(unique, true); Iterator<Value> it = map.keyIterator(unique);
while (it.hasNext()) { if (it.hasNext()) {
ValueArray k = (ValueArray) it.next(); ValueArray k = (ValueArray) it.next();
SearchRow r2 = convertToSearchRow(k); if (compareRows(row, convertToSearchRow(k)) == 0) {
if (compareRows(row, r2) != 0) { // committed
break; throw getDuplicateKeyException(k.toString());
}
if (map.get(k) != null) {
if (!containsNullAndAllowMultipleNull(r2)) {
throw getDuplicateKeyException(k.toString());
}
} }
} }
} }
......
...@@ -11,7 +11,9 @@ import java.sql.ResultSet; ...@@ -11,7 +11,9 @@ import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Random; import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import org.h2.api.ErrorCode; import org.h2.api.ErrorCode;
import org.h2.result.SortOrder; import org.h2.result.SortOrder;
...@@ -44,6 +46,7 @@ public class TestIndex extends TestBase { ...@@ -44,6 +46,7 @@ public class TestIndex extends TestBase {
testHashIndexOnMemoryTable(); testHashIndexOnMemoryTable();
testErrorMessage(); testErrorMessage();
testDuplicateKeyException(); testDuplicateKeyException();
testConcurrentUpdate();
testNonUniqueHashIndex(); testNonUniqueHashIndex();
testRenamePrimaryKey(); testRenamePrimaryKey();
testRandomized(); testRandomized();
...@@ -187,6 +190,103 @@ public class TestIndex extends TestBase { ...@@ -187,6 +190,103 @@ public class TestIndex extends TestBase {
stat.execute("drop table test"); stat.execute("drop table test");
} }
private class ConcurrentUpdateThread extends Thread {
private final AtomicInteger concurrentUpdateId, concurrentUpdateValue;
private final PreparedStatement psInsert, psDelete;
boolean haveDuplicateKeyException;
ConcurrentUpdateThread(Connection c, AtomicInteger concurrentUpdateId,
AtomicInteger concurrentUpdateValue) throws SQLException {
this.concurrentUpdateId = concurrentUpdateId;
this.concurrentUpdateValue = concurrentUpdateValue;
psInsert = c.prepareStatement("insert into test(id, value) values (?, ?)");
psDelete = c.prepareStatement("delete from test where value = ?");
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
try {
if (Math.random() > 0.05) {
psInsert.setInt(1, concurrentUpdateId.incrementAndGet());
psInsert.setInt(2, concurrentUpdateValue.get());
psInsert.executeUpdate();
} else {
psDelete.setInt(1, concurrentUpdateValue.get());
psDelete.executeUpdate();
}
} catch (SQLException ex) {
switch (ex.getErrorCode()) {
case 23505:
haveDuplicateKeyException = true;
break;
case 90131:
// Unlikely but possible
break;
default:
ex.printStackTrace();
}
}
if (Math.random() > 0.95)
concurrentUpdateValue.incrementAndGet();
}
}
}
private void testConcurrentUpdate() throws SQLException {
Connection c = getConnection("index");
Statement stat = c.createStatement();
stat.execute("create table test(id int primary key, value int)");
stat.execute("create unique index idx_value_name on test(value)");
PreparedStatement check = c.prepareStatement("select value from test");
ConcurrentUpdateThread[] threads = new ConcurrentUpdateThread[4];
AtomicInteger concurrentUpdateId = new AtomicInteger(), concurrentUpdateValue = new AtomicInteger();
// The same connection
for (int i = 0; i < threads.length; i++) {
threads[i] = new ConcurrentUpdateThread(c, concurrentUpdateId, concurrentUpdateValue);
}
testConcurrentUpdateRun(threads, check);
// Different connections
Connection[] connections = new Connection[threads.length];
for (int i = 0; i < threads.length; i++) {
Connection c2 = getConnection("index");
connections[i] = c2;
threads[i] = new ConcurrentUpdateThread(c2, concurrentUpdateId, concurrentUpdateValue);
}
testConcurrentUpdateRun(threads, check);
for (Connection c2 : connections) {
c2.close();
}
stat.execute("drop table test");
c.close();
}
void testConcurrentUpdateRun(ConcurrentUpdateThread[] threads, PreparedStatement check) throws SQLException {
for (ConcurrentUpdateThread t : threads) {
t.start();
}
boolean haveDuplicateKeyException = false;
for (ConcurrentUpdateThread t : threads) {
try {
t.join();
haveDuplicateKeyException |= t.haveDuplicateKeyException;
} catch (InterruptedException e) {
}
}
assertTrue("haveDuplicateKeys", haveDuplicateKeyException);
HashSet<Integer> set = new HashSet<>();
try (ResultSet rs = check.executeQuery()) {
while (rs.next()) {
if (!set.add(rs.getInt(1))) {
fail("unique index violation");
}
}
}
}
private void testNonUniqueHashIndex() throws SQLException { private void testNonUniqueHashIndex() throws SQLException {
reconnect(); reconnect();
stat.execute("create memory table test(id bigint, data bigint)"); stat.execute("create memory table test(id bigint, data bigint)");
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论