奇怪的结果来自具有索引和PreparedStatement的Oracle查询。

huangapple go评论61阅读模式
英文:

Weird results from oracle query with indexes and PreparedStatement

问题

最近,在升级我们的测试环境并引入新版本的应用程序和一些新功能后,我遇到了一些奇怪的Oracle行为,表现在查询结果中,这些结果看起来没有意义,似乎与索引有关。经过一些调查,我能够创建最小条件来重现这个问题。

我在Docker中尝试了Oracle 21的实验:

docker run --restart always -d -p 1521:1521 -e ORACLE_PASSWORD=system --name oracle-21c-01 gvenzl/oracle-xe:21-slim

然后以system用户(system/system)登录,并创建了另一个带有一些权限的用户:

CREATE USER test PROFILE DEFAULT IDENTIFIED BY test ACCOUNT UNLOCK;
GRANT CONNECT TO test;
GRANT UNLIMITED TABLESPACE TO test;
GRANT CREATE TABLE TO test;

之后,我以这个新用户(test/test)的身份登录,并执行了以下操作:

CREATE TABLE TEST_BUG
(
    ID         NUMBER(10)    NOT NULL
        CONSTRAINT PK_TEST_BUG PRIMARY KEY,
    TENANT     NUMBER(10)    NOT NULL,
    IDENTIFIER VARCHAR2(255) NOT NULL,
    NAME       VARCHAR2(255) NOT NULL
);

INSERT INTO TEST_BUG
VALUES (10, 1, 'IDENTIFIER', 'TESTBUG');

ALTER TABLE TEST_BUG
    ADD CONSTRAINT UK_NAME_TENANT UNIQUE (NAME, TENANT);
ALTER TABLE TEST_BUG
    ADD CONSTRAINT UK_IDENTIFIER_TENANT UNIQUE (IDENTIFIER, TENANT);

ALTER INDEX PK_TEST_BUG REBUILD;

正如您所看到的,我只是创建了一个非常简单的表格,其中包含一些索引,但必须按特定的顺序执行这些语句(只有在最后一行重建索引时,我才能复现我们在实际环境中遇到的问题,尽管在更新脚本中没有任何地方执行这个操作)。

考虑到所有这些,执行以下简单的Java代码会给我奇怪的结果:

public class Main {

    public static void main(String[] args) throws Exception {
        Class.forName("oracle.jdbc.driver.OracleDriver");
        try (Connection connection = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:XE", "test", "test")) {
            try (PreparedStatement preparedStatement = connection.prepareStatement(
                    "select id from test_bug where tenant = ? and name=? and id<>?"
            )) {
                preparedStatement.setInt(1, 1);
                preparedStatement.setString(2, "TESTBUG");
                preparedStatement.setInt(3, 10);
                ResultSet resultSet = preparedStatement.executeQuery();
                System.out.println(resultSet.next());
                System.out.println(resultSet.getInt(1));
            }
        }
    }
}

结果是:

true
10

这意味着尽管我插入了一条id=10的记录,并且在查询中明确要求id<>10,但我仍然得到了该记录作为输出,而不是什么都不得到!

还有一些注意事项:

  • 这只对PreparedStatement有效,如果我尝试在SQL开发者中执行此查询,它可以正常工作。
  • 如果我从查询中明确删除id条件(如select id from test_bug where tenant = ? and name=? and id<>10)并删除preparedStatement.setInt(3, 10);,它也可以正常工作(没有结果)。
  • 如果我使用ALTER INDEX PK_TEST_BUG UNUSABLE;禁用PK索引,它也可以正常工作,没有结果,直到我使用ALTER INDEX PK_TEST_BUG REBUILD;恢复索引。
  • 对于Oracle驱动程序,我在这里使用的是ojdbc11 21.9.0.0,但我尝试了不同版本,没有任何区别。

有任何想法,欢迎提出!

英文:

Recently, after upgrading our test env with a new version of application and introducing some new features I've stumbled upon some weird oracle behavior, which shows itself in query results that doesn't make sense and seem to be index dependant.
After doing some investigation I was able to create a minimal conditions to reproduce the issue.

I was experimenting on Oracle21 started in with docker:

docker run --restart always -d -p 1521:1521 -e ORACLE_PASSWORD=system --name oracle-21c-01 gvenzl/oracle-xe:21-slim

Then logging in as system user (system/system) I created another one with few permissions:

CREATE USER test PROFILE DEFAULT IDENTIFIED BY test ACCOUNT UNLOCK;
GRANT CONNECT TO test;
GRANT UNLIMITED TABLESPACE TO test;
GRANT CREATE TABLE TO test;

After that I logged in under this new user (test/test) and executed the following:

CREATE TABLE TEST_BUG
(
    ID         NUMBER(10)    NOT NULL
        CONSTRAINT PK_TEST_BUG PRIMARY KEY,
    TENANT     NUMBER(10)    NOT NULL,
    IDENTIFIER VARCHAR2(255) NOT NULL,
    NAME       VARCHAR2(255) NOT NULL
);

INSERT INTO TEST_BUG
VALUES (10, 1, &#39;IDENTIFIER&#39;, &#39;TESTBUG&#39;);

ALTER TABLE TEST_BUG
    ADD CONSTRAINT UK_NAME_TENANT UNIQUE (NAME, TENANT);
ALTER TABLE TEST_BUG
    ADD CONSTRAINT UK_IDENTIFIER_TENANT UNIQUE (IDENTIFIER, TENANT);

ALTER INDEX PK_TEST_BUG REBUILD;

As you can see here I'm just creating a very simple table with a couple of indexes, but the statements must be executed in this particular order (Also only with index rebuild on the last line I was able to reproduce the issue we're having on the actual environment, even though we do not do it anywhere in the update scripts).

With all that in mind, executing the following simle java code gives me weird results:

public class Main {

    public static void main(String[] args) throws Exception {
        Class.forName(&quot;oracle.jdbc.driver.OracleDriver&quot;);
        try (Connection connection = DriverManager.getConnection(&quot;jdbc:oracle:thin:@localhost:1521:XE&quot;, &quot;test&quot;, &quot;test&quot;)) {
            try (PreparedStatement preparedStatement = connection.prepareStatement(
                    &quot;select id from test_bug where tenant = ? and name=? and id&lt;&gt;?&quot;
            )) {
                preparedStatement.setInt(1, 1);
                preparedStatement.setString(2, &quot;TESTBUG&quot;);
                preparedStatement.setInt(3, 10);
                ResultSet resultSet = preparedStatement.executeQuery();
                System.out.println(resultSet.next());
                System.out.println(resultSet.getInt(1));
            }
        }
    }
}

And results are:

true
10

Meaning that even though I've inserted a single record with id=10 and in the query asking specifically that id&lt;&gt;10, I still get that recoded as an output instead of getting nothing!

Couple more notes:

  • This only works with PreparedStatement, if I try to execute this query with sql developer - works just fine.
  • If I remove put id condition explicitly in query (like select id from test_bug where tenant = ? and name=? and id&lt;&gt;10) and remove preparedStatement.setInt(3, 10); - works also fine (no results)
  • If I disable PK index with ALTER INDEX PK_TEST_BUG UNUSABLE; - works also fine, no results, until I get index back with ALTER INDEX PK_TEST_BUG REBUILD;
  • For oracle driver I'm using here ojdbc11 21.9.0.0, but I've played with different versions - no difference.

Any ideas kindly appreciated!

答案1

得分: 3

这是一个优化器的错误。如果我保持 Java 代码不变,并对其进行 SQL 跟踪,你会看到以下跟踪文件:

select id 
from
 test_bug where tenant = :1  and name=:2  and id != :3 
call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          1           0
Fetch        1      0.00       0.00          0          3          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        3      0.00       0.00          0          3          1           1

Misses in library cache during parse: 1
Misses in library cache during execute: 1
Optimizer mode: ALL_ROWS
Parsing user id: 109  
Number of plan statistics captured: 1

Rows (1st) Rows (avg) Rows (max)  Row Source Operation
---------- ---------- ----------  ---------------------------------------------------
         1          1          1  TABLE ACCESS BY INDEX ROWID BATCHED TEST_BUG (cr=3 pr=0 pw=0 time=32 us starts=1 cost=0 size=155 card=1)
         1          1          1   BITMAP CONVERSION TO ROWIDS (cr=2 pr=0 pw=0 time=25 us starts=1)
         1          1          1    BITMAP AND  (cr=2 pr=0 pw=0 time=23 us starts=1)
         1          1          1     BITMAP CONVERSION FROM ROWIDS (cr=1 pr=0 pw=0 time=9 us starts=1)
         1          1          1      INDEX RANGE SCAN UK_NAME_TENANT (cr=1 pr=0 pw=0 time=8 us starts=1 cost=0 size=0 card=1)(object id 100690)
         1          1          1     BITMAP CONVERSION FROM ROWIDS (cr=1 pr=0 pw=0 time=13 us starts=1)
         1          1          1      SORT ORDER BY (cr=1 pr=0 pw=0 time=11 us starts=1)
         1          1          1       INDEX RANGE SCAN PK_TEST_BUG (cr=1 pr=0 pw=0 time=3 us starts=1 cost=0 size=0 card=1)(object id 100689)

从 Fetch 行的 Rows 列的值为 1,表示我们找到了一行。如果我通过更改语句来禁用该计划以获得所期望的结果:

con.prepareStatement("select /*+ full(t) */ id from test_bug t where tenant = ? and name=? and id != ?");

然后,通过修改后的计划,我们获得了您期望的结果:

select /*+ full(t) */ id 
from
 test_bug t where tenant = :1  and name=:2  and id != :3 
call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          1           0
Fetch        1      0.00       0.00          0          7          0           0
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        3      0.00       0.00          0          7          1           0

Misses in library cache during parse: 1
Misses in library cache during execute: 1
Optimizer mode: ALL_ROWS
Parsing user id: 109  
Number of plan statistics captured: 1

Rows (1st) Rows (avg) Rows (max)  Row Source Operation
---------- ---------- ----------  ---------------------------------------------------
         0          0          0  TABLE ACCESS FULL TEST_BUG (cr=7 pr=0 pw=0 time=32 us starts=1 cost=3 size=155 card=1)

这并不是说你应该只加入一个 FULL 提示然后忘记它 奇怪的结果来自具有索引和PreparedStatement的Oracle查询。 但值得记录一下 SR,看看是否存在这个 bug 或是否有可用的修补程序。

如果你想要一个快速的解决办法,可以使用以下语句:

select /*+ opt_param('_b_tree_bitmap_plans','false') */ id from test_bug where tenant = :b1 and name= :b2 and id<> :b3;

这应该可以解决问题。

英文:

This is an optimizer bug. If I run the java unchanged and do a SQL trace on it, you see the following the trace file

select id 
from
 test_bug where tenant = :1  and name=:2  and id != :3 


call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          1           0
Fetch        1      0.00       0.00          0          3          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        3      0.00       0.00          0          3          1           1

Misses in library cache during parse: 1
Misses in library cache during execute: 1
Optimizer mode: ALL_ROWS
Parsing user id: 109  
Number of plan statistics captured: 1

Rows (1st) Rows (avg) Rows (max)  Row Source Operation
---------- ---------- ----------  ---------------------------------------------------
         1          1          1  TABLE ACCESS BY INDEX ROWID BATCHED TEST_BUG (cr=3 pr=0 pw=0 time=32 us starts=1 cost=0 size=155 card=1)
         1          1          1   BITMAP CONVERSION TO ROWIDS (cr=2 pr=0 pw=0 time=25 us starts=1)
         1          1          1    BITMAP AND  (cr=2 pr=0 pw=0 time=23 us starts=1)
         1          1          1     BITMAP CONVERSION FROM ROWIDS (cr=1 pr=0 pw=0 time=9 us starts=1)
         1          1          1      INDEX RANGE SCAN UK_NAME_TENANT (cr=1 pr=0 pw=0 time=8 us starts=1 cost=0 size=0 card=1)(object id 100690)
         1          1          1     BITMAP CONVERSION FROM ROWIDS (cr=1 pr=0 pw=0 time=13 us starts=1)
         1          1          1      SORT ORDER BY (cr=1 pr=0 pw=0 time=11 us starts=1)
         1          1          1       INDEX RANGE SCAN PK_TEST_BUG (cr=1 pr=0 pw=0 time=3 us starts=1 cost=0 size=0 card=1)(object id 100689)

with the Fetch line Rows column of 1 meaning we found a row. If I disable that plan from use by changing the statement to:

con.prepareStatement(&quot;select /*+ full(t) */ id from test_bug t where tenant = ? and name=? and id != ?&quot;);

then with the altered plan, we get the result you expect

select /*+ full(t) */ id 
from
 test_bug t where tenant = :1  and name=:2  and id != :3 


call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          1           0
Fetch        1      0.00       0.00          0          7          0           0
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        3      0.00       0.00          0          7          1           0

Misses in library cache during parse: 1
Misses in library cache during execute: 1
Optimizer mode: ALL_ROWS
Parsing user id: 109  
Number of plan statistics captured: 1

Rows (1st) Rows (avg) Rows (max)  Row Source Operation
---------- ---------- ----------  ---------------------------------------------------
         0          0          0  TABLE ACCESS FULL TEST_BUG (cr=7 pr=0 pw=0 time=32 us starts=1 cost=3 size=155 card=1)

This is not to say that you should just put in a FULL hint and forget about it 奇怪的结果来自具有索引和PreparedStatement的Oracle查询。 but its worth logging an SR to see if that's an existing bug or if patches are available.

If you want a quick workaround, then:

select /*+ opt_param(&#39;_b_tree_bitmap_plans&#39;,&#39;false&#39;) */ id from test_bug where tenant = :b1 and name= :b2 and id&lt;&gt; :b3;

should take care of it.

huangapple
  • 本文由 发表于 2023年2月16日 01:59:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/75463745.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定