Skip to content

Commit bc7c385

Browse files
authored
Add a check to prevent unnecessary where-clauses in index creating queries on not null columns (#617)
* Update SQL queries * Add docs * Add new check
1 parent dabfec2 commit bc7c385

File tree

20 files changed

+402
-24
lines changed

20 files changed

+402
-24
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ All checks can be divided into 2 groups:
7878
| 28 | Columns whose names do not follow naming convention | static | yes | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/columns_not_following_naming_convention.sql) |
7979
| 29 | Primary keys with varchar columns instead of uuids | static | yes | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/primary_keys_with_varchar.sql) |
8080
| 30 | Columns with [varchar(n)](https://www.postgresql.org/docs/current/datatype-character.html) type | static | yes | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/columns_with_fixed_length_varchar.sql) |
81+
| 31 | Indexes with unnecessary where-clause on not null column | static | yes | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/indexes_with_unnecessary_where_clause.sql) |
8182

8283
For raw sql queries see [pg-index-health-sql](https://github.com/mfvanek/pg-index-health-sql) project.
8384

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Проверка наличия индексов, которые имеют избыточный предикат с предложением where
2+
3+
Эта проверка тесно связана с проверкой [indexes_with_null_values](indexes_with_null_values.md).
4+
5+
Бывает, что разработчики начинают копировать и редактировать запросы на создание индексов.
6+
Это приводит к появлению запросов с предикатами вида `where <column> is not null` даже на тех столбцах,
7+
которые изначально имеют характеристику `not null`.
8+
9+
Такие индексы:
10+
- содержат избыточный предикат, который вычисляется каждый раз;
11+
- не могут быть использованы в качестве основы для внешнего ключа, если создаются с ключевым словом `unique`.
12+
13+
## SQL запрос
14+
15+
- [indexes_with_unnecessary_where_clause.sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/indexes_with_unnecessary_where_clause.sql)
16+
17+
## Тип проверки
18+
19+
- **static** (может выполняться на пустой БД в компонентных\интеграционных тестах)
20+
21+
## Поддержка секционированных таблиц
22+
23+
Поддерживает секционированные таблицы.
24+
Проверка выполняется на самой секционированной таблице (родительской). Отдельные секции (потомки) игнорируются.
25+
26+
## Скрипт для воспроизведения
27+
28+
```sql
29+
create schema if not exists demo;
30+
31+
create table if not exists demo.t1(
32+
id bigint not null primary key,
33+
id_ref bigint not null);
34+
35+
create index if not exists idx_t1_id_ref on demo.t1 (id_ref) where id_ref is not null;
36+
37+
create table if not exists demo.t2(
38+
"first-ref" bigint not null,
39+
second_ref bigint not null,
40+
t1_id bigint references demo.t1 (id));
41+
42+
create index if not exists "idx_t2_first-ref_second_ref" on demo.t2 (second_ref, "first-ref") where "first-ref" is not null;
43+
44+
create index if not exists idx_t2_id_ref on demo.t2 (t1_id) where t1_id is not null;
45+
46+
create index if not exists idx_second_ref_t1_id on demo.t2 (t1_id, second_ref) where t1_id is not null;
47+
48+
create table if not exists demo.one_partitioned(
49+
"first-ref" bigint not null,
50+
second_ref bigint not null
51+
) partition by range (second_ref);
52+
53+
create index if not exists "idx_second_ref_first-ref" on demo.one_partitioned (second_ref, "first-ref") where "first-ref" is not null;
54+
55+
create table if not exists demo.one_default partition of demo.one_partitioned default;
56+
```

doc/rus/primary_keys_with_varchar.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ insert into demo.t_varchar_long values (gen_random_uuid());
5050
select id_long from demo.t_varchar_long;
5151

5252
create table if not exists demo.t_link (
53-
id_long varchar(36) not null references demo.t_varchar_long (id_long),
5453
"id-short" varchar(32) not null references demo."t-varchar-short" ("id-short"),
54+
id_long varchar(36) not null references demo.t_varchar_long (id_long),
5555
primary key (id_long, "id-short")
5656
);
5757

pg-index-health-core/src/main/java/io/github/mfvanek/pg/core/checks/common/Diagnostic.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ public enum Diagnostic implements CheckTypeAware {
5353
OBJECTS_NOT_FOLLOWING_NAMING_CONVENTION("objects_not_following_naming_convention.sql"),
5454
COLUMNS_NOT_FOLLOWING_NAMING_CONVENTION("columns_not_following_naming_convention.sql"),
5555
PRIMARY_KEYS_WITH_VARCHAR("primary_keys_with_varchar.sql"),
56-
COLUMNS_WITH_FIXED_LENGTH_VARCHAR("columns_with_fixed_length_varchar.sql");
56+
COLUMNS_WITH_FIXED_LENGTH_VARCHAR("columns_with_fixed_length_varchar.sql"),
57+
INDEXES_WITH_UNNECESSARY_WHERE_CLAUSE("indexes_with_unnecessary_where_clause.sql");
5758

5859
private final ExecutionTopology executionTopology;
5960
private final String sqlQueryFileName;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright (c) 2019-2025. Ivan Vakhrushev and others.
3+
* https://github.com/mfvanek/pg-index-health
4+
*
5+
* This file is a part of "pg-index-health" - a Java library for
6+
* analyzing and maintaining indexes health in PostgreSQL databases.
7+
*
8+
* Licensed under the Apache License 2.0
9+
*/
10+
11+
package io.github.mfvanek.pg.core.checks.host;
12+
13+
import io.github.mfvanek.pg.connection.PgConnection;
14+
import io.github.mfvanek.pg.core.checks.common.Diagnostic;
15+
import io.github.mfvanek.pg.core.checks.extractors.IndexWithColumnsExtractor;
16+
import io.github.mfvanek.pg.model.context.PgContext;
17+
import io.github.mfvanek.pg.model.index.IndexWithColumns;
18+
19+
import java.util.List;
20+
import javax.annotation.Nonnull;
21+
22+
/**
23+
* Check for indexes with unnecessary where-clause on not null column on a specific host.
24+
*
25+
* @author Ivan Vakhrushev
26+
* @since 0.14.6
27+
*/
28+
public class IndexesWithUnnecessaryWhereClauseCheckOnHost extends AbstractCheckOnHost<IndexWithColumns> {
29+
30+
public IndexesWithUnnecessaryWhereClauseCheckOnHost(@Nonnull final PgConnection pgConnection) {
31+
super(IndexWithColumns.class, pgConnection, Diagnostic.INDEXES_WITH_UNNECESSARY_WHERE_CLAUSE);
32+
}
33+
34+
/**
35+
* Returns indexes with unnecessary where-clause on not null column in the specified schema.
36+
*
37+
* @param pgContext check's context with the specified schema
38+
* @return list of indexes with unnecessary where-clause on not null column
39+
*/
40+
@Nonnull
41+
@Override
42+
protected List<IndexWithColumns> doCheck(@Nonnull final PgContext pgContext) {
43+
return executeQuery(pgContext, IndexWithColumnsExtractor.of());
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright (c) 2019-2025. Ivan Vakhrushev and others.
3+
* https://github.com/mfvanek/pg-index-health
4+
*
5+
* This file is a part of "pg-index-health" - a Java library for
6+
* analyzing and maintaining indexes health in PostgreSQL databases.
7+
*
8+
* Licensed under the Apache License 2.0
9+
*/
10+
11+
package io.github.mfvanek.pg.core.checks.host;
12+
13+
import io.github.mfvanek.pg.core.checks.common.DatabaseCheckOnHost;
14+
import io.github.mfvanek.pg.core.checks.common.Diagnostic;
15+
import io.github.mfvanek.pg.core.fixtures.support.DatabaseAwareTestBase;
16+
import io.github.mfvanek.pg.core.fixtures.support.DatabasePopulator;
17+
import io.github.mfvanek.pg.model.column.Column;
18+
import io.github.mfvanek.pg.model.context.PgContext;
19+
import io.github.mfvanek.pg.model.index.IndexWithColumns;
20+
import io.github.mfvanek.pg.model.predicates.SkipTablesByNamePredicate;
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.params.ParameterizedTest;
23+
import org.junit.jupiter.params.provider.ValueSource;
24+
25+
import java.util.List;
26+
27+
import static io.github.mfvanek.pg.core.support.AbstractCheckOnHostAssert.assertThat;
28+
29+
class IndexesWithUnnecessaryWhereClauseCheckOnHostTest extends DatabaseAwareTestBase {
30+
31+
private final DatabaseCheckOnHost<IndexWithColumns> check = new IndexesWithUnnecessaryWhereClauseCheckOnHost(getPgConnection());
32+
33+
@Test
34+
void shouldSatisfyContract() {
35+
assertThat(check)
36+
.hasType(IndexWithColumns.class)
37+
.hasDiagnostic(Diagnostic.INDEXES_WITH_UNNECESSARY_WHERE_CLAUSE)
38+
.hasHost(getHost())
39+
.isStatic();
40+
}
41+
42+
@ParameterizedTest
43+
@ValueSource(strings = {PgContext.DEFAULT_SCHEMA_NAME, "custom"})
44+
void onDatabaseWithThem(final String schemaName) {
45+
executeTestOnDatabase(schemaName, DatabasePopulator::withUnnecessaryWhereClause, ctx -> {
46+
assertThat(check)
47+
.executing(ctx)
48+
.hasSize(2)
49+
.containsExactly(
50+
IndexWithColumns.ofSingle(ctx, "t1", "idx_t1_id_ref", 0L, Column.ofNotNull(ctx, "t1", "id_ref")),
51+
IndexWithColumns.ofColumns(ctx, "t2", "\"idx_t2_first-ref_second_ref\"", 0L, List.of(
52+
Column.ofNotNull(ctx, "t2", "second_ref"), Column.ofNotNull(ctx, "t2", "\"first-ref\"")))
53+
);
54+
55+
assertThat(check)
56+
.executing(ctx, SkipTablesByNamePredicate.of(ctx, List.of("t1", "t2")))
57+
.isEmpty();
58+
});
59+
}
60+
61+
@ParameterizedTest
62+
@ValueSource(strings = {PgContext.DEFAULT_SCHEMA_NAME, "custom"})
63+
void shouldWorkWithPartitionedTables(final String schemaName) {
64+
executeTestOnDatabase(schemaName, DatabasePopulator::withUnnecessaryWhereClauseInPartitionedIndex, ctx ->
65+
assertThat(check)
66+
.executing(ctx)
67+
.hasSize(1)
68+
.containsExactly(
69+
IndexWithColumns.ofColumns(ctx, "one_partitioned", "\"idx_second_ref_first-ref\"", 0L, List.of(
70+
Column.ofNotNull(ctx, "one_partitioned", "second_ref"), Column.ofNotNull(ctx, "one_partitioned", "\"first-ref\"")))
71+
)
72+
);
73+
}
74+
}

pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/core/fixtures/support/DatabasePopulator.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@
4141
import io.github.mfvanek.pg.core.fixtures.support.statements.CreateFunctionsStatement;
4242
import io.github.mfvanek.pg.core.fixtures.support.statements.CreateIndexWithBooleanValuesStatement;
4343
import io.github.mfvanek.pg.core.fixtures.support.statements.CreateIndexWithNullValuesStatement;
44+
import io.github.mfvanek.pg.core.fixtures.support.statements.CreateIndexWithUnnecessaryWhereClauseStatement;
4445
import io.github.mfvanek.pg.core.fixtures.support.statements.CreateIndexesOnArrayColumnStatement;
4546
import io.github.mfvanek.pg.core.fixtures.support.statements.CreateIndexesWithDifferentOpclassStatement;
4647
import io.github.mfvanek.pg.core.fixtures.support.statements.CreateMaterializedViewStatement;
4748
import io.github.mfvanek.pg.core.fixtures.support.statements.CreateNotSuitableIndexForForeignKeyStatement;
49+
import io.github.mfvanek.pg.core.fixtures.support.statements.CreatePartitionedIndexWithUnnecessaryWhereClauseStatement;
4850
import io.github.mfvanek.pg.core.fixtures.support.statements.CreatePartitionedTableWithDroppedColumnStatement;
4951
import io.github.mfvanek.pg.core.fixtures.support.statements.CreatePartitionedTableWithJsonAndSerialColumnsStatement;
5052
import io.github.mfvanek.pg.core.fixtures.support.statements.CreatePartitionedTableWithNullableFieldsStatement;
@@ -371,6 +373,11 @@ public DatabasePopulator withVarcharInPartitionedTable() {
371373
return register(124, new CreatePartitionedTableWithVarcharStatement());
372374
}
373375

376+
@Nonnull
377+
public DatabasePopulator withUnnecessaryWhereClauseInPartitionedIndex() {
378+
return register(125, new CreatePartitionedIndexWithUnnecessaryWhereClauseStatement());
379+
}
380+
374381
@Nonnull
375382
public DatabasePopulator withEmptyTable() {
376383
return register(130, new CreateEmptyTableStatement());
@@ -391,6 +398,11 @@ public DatabasePopulator withDroppedAccountNumberColumn() {
391398
return register(137, new DropColumnStatement("accounts", "account_number"));
392399
}
393400

401+
@Nonnull
402+
public DatabasePopulator withUnnecessaryWhereClause() {
403+
return register(138, new CreateIndexWithUnnecessaryWhereClauseStatement());
404+
}
405+
394406
public void populate() {
395407
try (SchemaNameHolder ignored = SchemaNameHolder.with(schemaName)) {
396408
ExecuteUtils.executeInTransaction(dataSource, statementsToExecuteInSameTransaction.values());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright (c) 2019-2025. Ivan Vakhrushev and others.
3+
* https://github.com/mfvanek/pg-index-health
4+
*
5+
* This file is a part of "pg-index-health" - a Java library for
6+
* analyzing and maintaining indexes health in PostgreSQL databases.
7+
*
8+
* Licensed under the Apache License 2.0
9+
*/
10+
11+
package io.github.mfvanek.pg.core.fixtures.support.statements;
12+
13+
import java.util.List;
14+
import javax.annotation.Nonnull;
15+
16+
public class CreateIndexWithUnnecessaryWhereClauseStatement extends AbstractDbStatement {
17+
18+
@Nonnull
19+
@Override
20+
protected List<String> getSqlToExecute() {
21+
return List.of(
22+
"create table if not exists {schemaName}.t1(" +
23+
"id bigint not null primary key," +
24+
"id_ref bigint not null);",
25+
"create index if not exists idx_t1_id_ref on {schemaName}.t1 (id_ref) " +
26+
"where id_ref is not null;",
27+
"create table if not exists {schemaName}.t2(" +
28+
"\"first-ref\" bigint not null," +
29+
"second_ref bigint not null," +
30+
"t1_id bigint references {schemaName}.t1 (id));",
31+
"create index if not exists \"idx_t2_first-ref_second_ref\" on {schemaName}.t2 (second_ref, \"first-ref\") " +
32+
"where \"first-ref\" is not null;",
33+
"create index if not exists idx_t2_id_ref on {schemaName}.t2 (t1_id) " +
34+
"where t1_id is not null;",
35+
"create index if not exists idx_second_ref_t1_id on {schemaName}.t2 (t1_id, second_ref) " +
36+
"where t1_id is not null;"
37+
);
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) 2019-2025. Ivan Vakhrushev and others.
3+
* https://github.com/mfvanek/pg-index-health
4+
*
5+
* This file is a part of "pg-index-health" - a Java library for
6+
* analyzing and maintaining indexes health in PostgreSQL databases.
7+
*
8+
* Licensed under the Apache License 2.0
9+
*/
10+
11+
package io.github.mfvanek.pg.core.fixtures.support.statements;
12+
13+
import java.util.List;
14+
import javax.annotation.Nonnull;
15+
16+
public class CreatePartitionedIndexWithUnnecessaryWhereClauseStatement extends AbstractDbStatement {
17+
18+
@Nonnull
19+
@Override
20+
protected List<String> getSqlToExecute() {
21+
return List.of(
22+
"create table if not exists {schemaName}.one_partitioned(" +
23+
"\"first-ref\" bigint not null," +
24+
"second_ref bigint not null" +
25+
") partition by range (second_ref);",
26+
"create index if not exists \"idx_second_ref_first-ref\" on {schemaName}.one_partitioned (second_ref, \"first-ref\") " +
27+
"where \"first-ref\" is not null;",
28+
"create table if not exists {schemaName}.one_default partition of {schemaName}.one_partitioned default;"
29+
);
30+
}
31+
}

pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/core/fixtures/support/statements/CreateTableWithFixedLengthVarcharStatement.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ protected List<String> getSqlToExecute() {
2626
"id_long varchar(36) not null primary key);",
2727
"insert into {schemaName}.t_varchar_long values (gen_random_uuid());",
2828
"create table if not exists {schemaName}.t_link (" +
29-
"id_long varchar(36) not null references {schemaName}.t_varchar_long (id_long)," +
3029
"\"id-short\" varchar(32) not null references {schemaName}.\"t-varchar-short\" (\"id-short\")," +
30+
"id_long varchar(36) not null references {schemaName}.t_varchar_long (id_long)," +
3131
"primary key (id_long, \"id-short\"));",
3232
"create table if not exists {schemaName}.t_varchar_long_not_pk (" +
3333
"id_long varchar(36) not null);",

pg-index-health-logger/src/main/java/io/github/mfvanek/pg/health/logger/DatabaseChecksOnCluster.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.github.mfvanek.pg.health.checks.cluster.IndexesWithBloatCheckOnCluster;
2626
import io.github.mfvanek.pg.health.checks.cluster.IndexesWithBooleanCheckOnCluster;
2727
import io.github.mfvanek.pg.health.checks.cluster.IndexesWithNullValuesCheckOnCluster;
28+
import io.github.mfvanek.pg.health.checks.cluster.IndexesWithUnnecessaryWhereClauseCheckOnCluster;
2829
import io.github.mfvanek.pg.health.checks.cluster.IntersectedForeignKeysCheckOnCluster;
2930
import io.github.mfvanek.pg.health.checks.cluster.IntersectedIndexesCheckOnCluster;
3031
import io.github.mfvanek.pg.health.checks.cluster.InvalidIndexesCheckOnCluster;
@@ -105,7 +106,8 @@ public DatabaseChecksOnCluster(@Nonnull final HighAvailabilityPgConnection haPgC
105106
new ObjectsNotFollowingNamingConventionCheckOnCluster(haPgConnection),
106107
new ColumnsNotFollowingNamingConventionCheckOnCluster(haPgConnection),
107108
new PrimaryKeysWithVarcharCheckOnCluster(haPgConnection),
108-
new ColumnsWithFixedLengthVarcharCheckOnCluster(haPgConnection)
109+
new ColumnsWithFixedLengthVarcharCheckOnCluster(haPgConnection),
110+
new IndexesWithUnnecessaryWhereClauseCheckOnCluster(haPgConnection)
109111
);
110112
}
111113

0 commit comments

Comments
 (0)