1. 不使用预编译语句
有趣的是,在jdbc出现多年后,这个错误或错误仍然出现在博客、论坛和邮件列表中,即使这是一件非常简单的事情,需要记住和理解。似乎一些开发人员出于以下任何原因没有使用准备好的陈述:
- 他们不知道预编译语句
- 他们认为预编译语句比较慢
- 他们认为写一份预编译语句需要付出更多的努力
首先,让我们打破上述神话。在96%的案例中,你最好写一份预编译语句,而不是静态的语句。为什么?原因很简单:
- 在内联绑定值时,您可以省略来自不良字符串串联的语法错误。
- 在内联绑定值时,您可以省略不良字符串串联中的sql注入漏洞。
- 在内联更“复杂”的数据类型(如时间戳、二进制数据等)时,您可以避免边缘情况。
- 您可以将准备好的语句保持一段时间,使用新的绑定值重用它们,而不是立即关闭它们(例如,在postgres中有用)。
- 可以在更复杂的数据库中使用自适应游标共享(oracle语言)。这有助于防止对每一组新的绑定值进行硬解析SQL语句。
相信吗?是的。注意,在极少数情况下,您确实需要内联绑定值,以便让数据库的基于成本的优化器了解真正将受到查询影响的数据类型。通常,这会导致“常量”谓词,如:
- deleted = 1
- status = 42
但它不应该导致“可变”谓词,例如:
- first_name like “jon%”
- amount > 19.95
请注意,现代数据库实现了绑定变量窥视。因此,默认情况下,您不妨对所有查询参数使用绑定值。另请注意,在编写嵌入式jpql或嵌入式sql时,更高级别的apis,如jpa标准查询或jooq将帮助您非常轻松地和透明地生成准备好的语句和绑定值。
更多背景信息:
- 绑定价值偷窥的注意事项:甲骨文大师tanel poder关于该主题的一篇有趣的博客文章
- 光标共享。一个有趣的堆栈溢出问题。
解决方法:
默认情况下,始终使用预编译语句而不是静态语句。默认情况下,切勿将值内联绑定到您的sql中。
2. 返回过多列
这个错误非常频繁,可能会导致数据库执行计划和java应用程序中非常糟糕的效果。让我们先看看第二个效果:
对java应用程序的不良影响:
如果您正在选择*(星号)或一组“默认”的50列,并在各种daos中重用,您将大量数据从数据库传输到jdbc结果集。即使您没有读取结果集的数据,它也已通过电线传输并由jdbc驱动程序加载到您的内存中。如果您知道您只需要其中2-3列,这完全浪费io和内存。
这是显而易见的,但也要小心......
对数据库执行计划的不良影响:
这些影响实际上可能比对java应用程序的影响糟糕得多。复杂的数据库在计算查询的最佳执行计划时执行许多sql转换。很可能是查询的某些部分可以“转换”,因为他们知道它们不会对映射(选择子句)或过滤谓词做出贡献。
现在,这真是一头野兽。想想一个复杂的选择,它将结合两种观点:
select *
from customer_view c
join order_view o
on c.cust_id = o.cust_id
连接到上述连接表引用的每个视图都可能再次加入来自数十个表的数据,例如cremanse_address、order_history、order_settlement等。鉴于选择*映射,您的数据库别无选择,只能完全加载所有这些已加入的表,而事实上,您唯一感兴趣的是:
select c.first_name, c.last_name, o.amount
from customer_view c
join order_view o
on c.cust_id = o.cust_id
一个好的数据库将以可以删除大多数“隐藏”连接的方式转换您的sql,从而减少数据库中的io和内存消耗。
治愈方法:
永远不要执行 select * 。永远不要对各种查询重复使用相同的映射。始终尝试将映射简化为您真正需要的数据。
请注意,使用oms可能很难实现。
3.认为join是一个 select 条款
这不是一个对性能或sql正确性有很大影响的错误,但sql开发人员应该意识到,join子句本身不是选择语句的一部分。sql标准1992这样定义了 table reference :
6.3 <table reference>
<table reference> ::=
<table name> [ [ as ] <correlation name>
[ <left paren> <derived column list> <right paren> ] ]
| <derived table> [ as ] <correlation name>
[ <left paren> <derived column list> <right paren> ]
| <joined table>
from子句和连接表可以使用这样的表引用:
7.4 <from clause>
<from clause> ::=
from <table reference> [ { <comma> <table reference> }... ]
7.5 <joined table>
<joined table> ::=
<cross join>
| <qualified join>
| <left paren> <joined table> <right paren>
<cross join> ::=
<table reference> cross join <table reference>
<qualified join> ::=
<table reference> [ natural ] [ <join type> ] join
<table reference> [ <join specification> ]
关系数据库非常以表为中心。许多操作都以这样或那样的方式在物理表、连接表或派生表上执行。为了有效地编写SQL,重要的是要理解select ..From子句需要一个以逗号分隔的表引用列表,无论它们以何种形式提供。
根据表引用的复杂性,一些数据库还接受其他语句中的复杂表引用,例如插入、更新、删除、合并。例如,请参阅甲骨文的手册,解释如何创建可更新的视图。
解决方法:
始终将您的 from 子句视为整个表引用。如果您编写了 join 子句,请将此 join 子句视为复杂表引用的一部分:
select c.first_name, c.last_name, o.amount
from
customer_view c
join order_view o
on c.cust_id = o.cust_id
4.使用pre-ansi连接语法
现在我们已经澄清了表引用的工作原理(见上一点),应该会变得更加明显,应该不惜一切代价避免前ansi连接语法。对于执行计划,如果您在 join .. on 子句或 where 子句中指定join谓词,通常没有区别。但从可读性和维护的角度来看,同时过滤谓词和连接谓词使用 where 子句是一个主要的泥潭。考虑这个简单的例子:
select c.first_name, c.last_name, o.amount
from customer_view c,
order_view o
where o.amount > 100
and c.cust_id = o.cust_id
and c.language = 'en'
你能发现连接谓词吗?如果我们加入几十张表呢?当为外部连接应用专有语法时,情况会变得更糟,例如甲骨文的(+)语法。
解决方法:
始终使用ansi join语法。永远不要将join谓词放入 where 子句中。使用前ansi join语法绝对没有优势
5.忘记从类似谓词的输入中转义
sql标准1992这样指定了 like predicate :
8.5 <like predicate>
<like predicate> ::=
<match value> [ not ] like <pattern>
[ escape <escape character> ]
在允许在sql查询中使用用户输入时,几乎总是应该使用 escape 关键字。虽然百分比符号(%)实际上应该是数据的一部分可能很少,但下划线(_)很可能是:
select *
from t
where t.x like 'some!_prefix%' escape '!'
解决方法:
使用类似谓词时,总是考虑适当的转义。
6.认为不是(a in(x,y))是a in(x,y)的布尔逆
这个很微妙,但对空值非常重要!让我们回顾一下 a in (x, y) 的真正含义:
a in (x, y)
is the same as a = any (x, y)
is the same as a = x or a = y
同时, not (a in (x, y)) 真正意味着:
not (a in (x, y))
is the same as a not in (x, y)
is the same as a != any (x, y)
is the same as a != x and a != y
这看起来像前一个谓词的布尔逆,但它不是!如果 x or y is null,则not in谓词将导致未知,而in谓词仍可能返回布尔值。
或者换句话说,当 a in (x, y) 产生true或 false , not(a in (x, y)) 仍然可以产生 unknown 而不是 false 或 true 。请注意,如果 in 谓词的右侧是子查询,这也是真的。
你不相信吗?它显示以下查询没有结果:
select 1
where 1 in (null)
union all
select 2
where not(1 in (null))
解决方法: 当涉及可空列时,要注意not in谓词
7.认为不是(a is null )和(a not is null)是一样的
对,所以我们记得sql在处理null时实现了三值逻辑。这就是为什么我们可以使用null谓词来检查null值,对吗?没错。
但即使是 null 谓词也是微妙的。请注意,以下两个谓词仅等价于第1度的行值表达式:
not (a is null)
is not the same as a is not null
如果a是一个度数大于1的行值表达式,则真值表被转换为:
- 只有当a中的所有值都是空时,a才产生
null - not(a is null)只有在a中的所有值都是
null时才会产生false - 只有当a中的所有值不是
null时,a才不会产生true - not(a is not null)只有在a中的所有值都不是
null时才会产生false
解决方法:
使用行值表达式时,请注意空谓词,它可能无法正常工作。
8. 没有在支持行值表达式的地方使用行值表达式
行值表达式是一个很棒的sql功能。当sql是一种非常以表为中心的语言时,表也非常以行为中心。行值表达式通过创建可以与相同度和行类型的其他行进行比较的本地临时行,您可以更轻松地描述复杂的谓词。一个简单的例子是同时向客户查询名字和姓氏。
select c.address
from customer c,
where (c.first_name, c.last_name) = (?, ?)
可以看出,这种语法比等效语法稍微简洁一些,在等效语法中,谓词左侧的每一列都与右侧的相应列进行比较。如果许多独立的谓词与和和结合,情况尤其如此。使用行值表达式可以将相关谓词组合成一个。这对于复合外键上的连接表达式最有用:
select c.first_name, c.last_name, a.street
from customer c
join address a
on (c.id, c.tenant_id) = (a.id, a.tenant_id)
不幸的是,并非所有数据库都以同样的方式支持行值表达式。但sql标准早在1992年就定义了它们,如果您使用它们,像甲骨文或postgres这样的复杂数据库可以使用它们来计算更好的执行计划。这在流行的索引卢克页面上得到了解释。
解决方法:
尽可能使用行值表达式。它们将使您的sql更简洁,甚至更快。
9.没有定义足够的约束
所以,我将引用tom kyte并再次使用索引。您的元数据中没有足够的约束。首先,约束可帮助您防止数据损坏,这已经非常有用。但对我来说,更重要的是,约束将帮助数据库执行sql转换,因为数据库可以决定
- 一些值是等价的
- 有些条款是多余的
- 一些子句是“无”的(即它们不会返回任何值)
一些开发人员可能会认为约束很慢。情况正好相反,除非您插入大量数据,在这种情况下,您可以禁用大型操作的约束,或者在没有约束的情况下使用临时“加载表”,将数据离线传输到真实表。
解决方法:
定义尽可能多的约束。它们将帮助您的数据库在查询时更好地执行。
10.认为50ms是快速的查询执行
nosql炒作仍在进行中,许多公司仍然认为他们是twitter或Facebook,迫切需要更快、更可扩展的解决方案,逃离原子关系模型以横向扩展。一些公司可能会成功(例如twitter或facebook),另一些公司可能会遇到这种情况:
在这里找到:twitter.com/codinghorro…
对于其他被迫(或选择)坚持经过验证的关系数据库的人来说,不要被骗以为现代数据库很慢。它们速度超快。事实上,它们速度太快了,它们可以解析您的20kb查询文本,计算2000行执行计划,并在不到一毫秒的时间内实际执行该怪物,如果您和您的dba相处得很好,并将数据库调到最大值。
它们可能很慢,因为您的应用程序滥用了流行的orm,或者因为该orm无法为您的复杂查询逻辑生成快速sql。在这种情况下,您可能想选择一个更以sql为中心的api,如jdbc、jooq或mybatis,这将让您重新控制您的sql。
因此,不要认为50ms的查询执行速度快,甚至是可以接受的。事实并非如此。如果您在开发时获得这些速度,请确保您调查执行计划。这些怪物可能会在生产中爆炸,在那里您的上下文和数据更复杂。