使用jOOQ查询倾斜的数据时如何防止执行计划出现问题?

使用jOOQ的最大优势之一是,你只需用几行代码就可以改变你所有复杂的应用程序生成的SQL。在这篇文章中,我们将研究如何像这样解决一些常见的绑定偷看问题,不需要触及你的应用程序代码,不需要向每个团队成员解释这个先进的SQL性能问题,最重要的是:永久解决。

绑定值有什么用?

首先,绑定变量是个好东西它们:

  • 防止SQL注入
  • 确保句法的正确性
  • 提高DBMS中执行计划缓存的性能

后面这一条并不影响所有的方言,就像这篇文章并不影响所有的方言一样。像Oracle、SQL Server等商业DBMS都有一个强大的执行计划缓存。它们被设计用来运行具有非常复杂计划的成千上万的不同查询。规划这些查询需要时间(我见过Oracle SQL被规划了5秒钟!),你不希望DBMS在每次运行查询时重新执行这些规划工作,这可能是每秒数千次。

通常情况下,这个执行计划缓存需要SQL字符串(或者它的哈希值),并将元数据(如其他可能的执行计划)与之关联。当SQL字符串发生变化时,缓存查找失败,"新 "的查询必须重新计划。

我说 "新",是因为对用户来说可能是 "相同 "的查询,即使字符串不同。比如说:

SELECT * FROM book WHERE id = 1;
SELECT * FROM book WHERE id = 2;

现在我们有两次 "相同 "的查询,但每次都是 "新 "的。Oracle将重新计划这两个查询。所以,我们使用绑定变量来代替:

SELECT * FROM book WHERE id = ?;
SELECT * FROM book WHERE id = ?;

什么是Bind Peeking问题?

在某些情况下,缓存的计划不是最佳的。当实际的绑定值对计划很重要时,就会出现这种情况,例如,值1 会产生一个与值2 大不相同的计划,或者更有可能的是,值DELETED 会产生一个与PROCESSEDNEW 不同的计划。

这个问题在我们之前的博文《为什么你应该设计你的数据库来优化统计》中已经讨论过了。

"Bind Peeking "是Oracle数据库的一项技术(其他数据库也有,但可能不叫 "Bind Peeking"),在我们不知道绑定值的情况下,可以 "偷看 "绑定变量以获得比平均计划更准确的计划。这可能有两种情况,好的或坏的,所以在以前的Oracle版本中有许多修复/补丁/解决方法。关于这个主题的一些有趣的文章。

数据库慢慢地进入了真正的自适应查询执行模型,当估计明显错误时,执行计划可以在飞行过程中被修复。Db2在这方面相当强,而Oracle也越来越好。

但即使如此,有些时候计划员还是会出错,仅仅是因为他们不能合理地估计一个简单的谓词所产生的卡值,比如说

WHERE x = ?

......只是因为整个查询非常复杂,一些SQL转换并不适用。

通过避免绑定值来防止问题的发生

再次强调。请默认使用绑定值。*默认情况下,*它们是个好东西。并非所有的数据都像我在另一篇博文中介绍的那样偏斜。但有些数据几乎总是倾斜的。枚举类型。

当你有一个枚举,比如:

enum ProcessingState {
  NEW,
  PROCESSING,
  EXECUTED,
  DELETED
}

或者在PostgreSQL中:

CREATE TYPE processing_state AS ENUM (
  'new',
  'processing',
  'executed',
  'deleted'
);

或者甚至只是编码为一个CHECK 的约束:

CREATE TABLE transaction (
  -- ...
  processing_state VARCHAR(10) CHECK (processing_state IN (
    'new',
    'processing',
    'executed',
    'deleted'
  ))
  -- ...
);

在这种情况下,你将很可能有高度倾斜的 数据。例如,一个快速查询可能会产生:

SELECT processing_state, count(*)
FROM transaction
GROUP BY processing_state

结果是:

+------------------+----------+
| processing_state |    count |
+------------------+----------+
| new              |    10234 |
| processing       |       15 |
| executed         | 17581684 |
| deleted          |    83193 |
+------------------+----------+

现在,你认为在寻找NEWPROCESSING 与寻找EXECUTED 的值时,你会从索引PROCESSING_STATE 中获益吗?你想要相同的计划吗?你想要一个平均的计划吗?这个计划可能不会使用索引,而事实上你应该使用它(寻找PROCESSING )?

不仅如此,你的查询也不可能如此通用,以至于各个PROCESSING_STATE 值可以互换使用。例如,一个寻找DELETED 状态的查询可能是由一个想要永久删除逻辑上被删除的事务的批处理工作运行的。它永远不会查询除了DELETED 状态以外的任何东西。所以,不妨内联,对吗?

现在,如果你写一个这样的查询:

SELECT *
FROM transaction
WHERE processing_state = 'processing';

在jOOQ中,你可以在每个查询的基础上创建一个 "内联",使用 [DSL.inline("processing")](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/impl/DSL.html#inline(java.lang.String))(与之相对的是 [DSL.val("processing")](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/impl/DSL.html#val(java.lang.String))来创建一个 "内联",它是默认使用的,隐含的:

// These generate a ? bind value
ctx.selectFrom(TRANSACTION)
   .where(TRANSACTION.PROCESSING_STATE.eq("processing"))
   .fetch();

ctx.selectFrom(TRANSACTION)
   .where(TRANSACTION.PROCESSING_STATE.eq(val("processing")))
   .fetch();

// This creates an inline 'processing' literal
ctx.selectFrom(TRANSACTION)
   .where(TRANSACTION.PROCESSING_STATE.eq(inline("processing")))
   .fetch();

像往常一样,假设这个静态导入:

import static org.jooq.impl.DSL.*;

但是现在,你必须考虑在每次查询PROCESSING_STATE ,以及所有其他类似的列时都要这样做。

妥善防止它

更好的办法是永远防止它。你可以创建一个非常简单的Binding ,像这样:

class AlwaysInlineStringBinding implements Binding<String, String> {

    final Binding<?, String> delegate = VARCHAR.getBinding();

    @Override
    public Converter<String, String> converter() {
        return Converters.identity(String.class);
    }

    @Override
    public void sql(BindingSQLContext<String> ctx) 
    throws SQLException {
        ctx.render().visit(inline(ctx.value()));
    }

    @Override
    public void register(BindingRegisterContext<String> ctx) 
    throws SQLException {
        delegate.register(ctx);
    }

    // No need to set anything
    @Override
    public void set(BindingSetStatementContext<String> ctx) 
    throws SQLException {}

    @Override
    public void set(BindingSetSQLOutputContext<String> ctx) 
    throws SQLException {
        delegate.set(ctx);
    }

    @Override
    public void get(BindingGetResultSetContext<String> ctx) 
    throws SQLException {
        delegate.get(ctx);
    }

    @Override
    public void get(BindingGetStatementContext<String> ctx) 
    throws SQLException {
        delegate.get(ctx);
    }

    @Override
    public void get(BindingGetSQLInputContext<String> ctx) 
    throws SQLException {
        delegate.get(ctx);
    }
}

或者,从jOOQ 3.15开始,甚至更简单,而且通用:

class AlwaysInlineStringBinding 
extends DefaultBinding<String, String> {
    public AlwaysInlineStringBinding() {
        super(DefaultBinding.binding(VARCHAR));
    }

    @Override
    public void sql(BindingSQLContext<String> ctx) 
    throws SQLException {
        ctx.render().visit(inline(ctx.value()));
    }

    // No need to set anything
    @Override
    public void set(BindingSetStatementContext<T> ctx) 
    throws SQLException {}
}

或者甚至是通用的:

class AlwaysInlineBinding<T> extends DefaultBinding<T, T> {
    public AlwaysInlineBinding(DataType<T> type) {
        super(DefaultBinding.binding(type));
    }

    @Override
    public void sql(BindingSQLContext<T> ctx) 
    throws SQLException {
        ctx.render().visit(inline(ctx.value()));
    }

    // No need to set anything
    @Override
    public void set(BindingSetStatementContext<T> ctx) 
    throws SQLException {}
}

这所做的只是生成内联值,而不是? 绑定参数标记,并跳过对JDBCPreparedStatement (或反应式R2DBC Statement ,从jOOQ 3.15开始。这将同样工作!)的任何值。

你自己试试吧,非常容易(使用jOOQ 3.15版本):

@Test
public void testAlwaysInlineBinding() {
    DSLContext ctx = DSL.using(DEFAULT);
    DataType<Integer> t = INTEGER.asConvertedDataType(
        new AlwaysInlineBinding<>(INTEGER));

    Field<Integer> i = field("i", INTEGER);
    Field<Integer> j = field("j", t);
    Param<Integer> a = val(1);
    Param<Integer> b = val(1, INTEGER.asConvertedDataType(
        new AlwaysInlineBinding<>(INTEGER)));

    // Bind value by default
    assertEquals("?", ctx.render(a));
    assertEquals("1", ctx.renderInlined(a));
    assertEquals("1", ctx.render(b));
    assertEquals("1", ctx.renderInlined(b));

    // Bind value by default in predicates
    assertEquals("i = ?", ctx.render(i.eq(a)));
    assertEquals("i = 1", ctx.renderInlined(i.eq(a)));
    assertEquals("i = 1", ctx.render(i.eq(b)));
    assertEquals("i = 1", ctx.renderInlined(i.eq(b)));
    assertEquals("i = ?", ctx.render(i.eq(1)));
    assertEquals("i = 1", ctx.renderInlined(i.eq(1)));

    // No more bind values in predicates!
    assertEquals("j = 1", ctx.render(j.eq(a)));
    assertEquals("j = 1", ctx.renderInlined(j.eq(a)));
    assertEquals("j = 1", ctx.render(j.eq(b)));
    assertEquals("j = 1", ctx.renderInlined(j.eq(b)));
    assertEquals("j = 1", ctx.render(j.eq(1)));
    assertEquals("j = 1", ctx.renderInlined(j.eq(1)));
}

当然,你将使用代码生成器的强制类型配置将这个Binding ,而不是以编程方式进行上述操作,附在所有相关的列上。

结论

请默认使用绑定值。无论是在jOOQ还是其他地方。这是一个非常好的默认值。

但是有时候,你的数据是有偏差的,你作为一个开发者,你可能知道。在这些情况下,有时,我们所说的 "内联值"(或常量、字面意义等)可能是更好的选择,可以帮助优化器更好地进行估计。即使优化器第一次估计得很好,计划在生产中也可能因为一些奇怪的原因而改变,包括一些计划因为缓存已满而被清除,或者DBA点击了一个按钮,或者其他什么原因。

而这时你的查询可能会突然变得不必要的慢。没有更多的需要。当你有enum 类型,或者类似的,只要使用上面的简单技巧,在有意义的地方应用到你所有的模式,就可以永远忘记这个问题了。

题外话

当然,另一种方式也同样简单。当你有内联字词想切换到绑定值时,你也可以用同样的方法,例如,当你使用jOOQ的解析连接在方言之间进行翻译,或者修补你错误的ORM生成的SQL时

猜你喜欢

转载自juejin.im/post/7126370700566200351