译者 | 李睿
审校 | 孙淑娟
SQL开发商Data Geeker公司首席执行官Lukas Eder日前表示,他在博客文章列出了Java开发人员在编写SQL时常犯的10个错误。这篇文章得到广泛关注,这让他感到非常惊讶。这种受欢迎程度说明了几点:

一个有趣的事实是,用户甚至在slick的邮件列表中提到了他写的博客文章。Slick是Scala中的一个不以SQL为中心的数据库访问库。和LINQ(以及LINQ-TO-SQL)一样,它关注的是语言集成,而不是SQL代码生成。
无论如何,Eder列出的一些错误还远远不够,下面将介绍Java开发人员在编写SQL时常犯的10个错误。
有趣的是,在JDBC出现多年之后,这种错误或误解仍然出现在博客、论坛和邮件列表中,即使它是关于一个在记忆和理解方面非常简单的的事情。一些开发人员似乎因为以下原因而避免使用预处理语句:
首先需要打破以上误区。在96%的情况下,编写预处理语句要比编写静态语句更好。为什么?其原因很简单:
需要注意的是,在极少数情况下,确实需要内联绑定值,以便让数据库的基于成本的优化器了解真正将受到查询影响的数据类型。通常,这会导致“常量”谓词,例如:
但它不应该导致“变量”谓词,例如:
需要注意的是,现代数据库实现了绑定变量窥视。因此,在默认情况下,还可以为所有查询参数使用绑定值。另外,在编写嵌入式JPQL或嵌入式SQL时,诸如JPA CriteriaQuery或jOOQ等高级API将帮助您生成预处理语句并非常容易和透明地绑定值。
解决办法:
在默认情况下,总是使用预处理语句而不是静态语句,并且永远不要将绑定值内联到SQL中。
这种错误非常常见,可能会在数据库的执行计划和Java应用程序中导致非常糟糕的影响。先看看第二个效果:
(1)对Java应用程序的不良影响
如果选择*(星号)或50列的“默认”集合(在各种数据访问对象之间重用),则需要将大量数据从数据库传输到JDBC结果集。即使没有从结果集中读取数据,它也已经通过网络传输,并由JDBC驱动程序加载到内存中。如果知道只需要2~3个这样的列,这相当浪费IO和内存。
这是显而易见的,但也要小心。
(2)对数据库执行计划的不良影响
这些影响实际上可能比对Java应用程序的影响要严重得多。复杂的数据库在为查询计算最佳执行计划时执行大量SQL转换。很可能查询的某些部分可以被转换掉,因为知道它们不会对投影(选择子句)或过滤谓词产生影响。
考虑一个复杂的选择,它将连接两个视图:
select *
from customer_view c
join order_view o
on c.cust_id = o.cust_id
onc.cust_id=o.cust_id连接到上述连接表引用的每个视图可能再次连接来自几十个表的数据,例如customeraddress、order history、order settlement等。考虑到select*投影,数据库别无选择,只能完全加载所有这些联接表,而实际上,唯一感兴趣的是:
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*。不要为不同的查询重用相同的投影。总是尝试减少投影到真正需要的数据。
注意,用对象关系映射(ORM)很难实现这一点。
这并不是一个对性能或SQL正确性有很大影响的错误,但是,SQL开发人员应该意识到这样一个事实:join子句本身不是select语句的一部分。sql standard 1992这样定义表引用:
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子句作为一个整体来考虑表引用。如果写一个连接子句,把这个连接子句想象成一个复杂表引用的一部分:
select c.first_name, c.last_name, o.amount
from
customer_view c
join order_view o
on c.cust_id = o.cust_id既然已经阐明了表引用是如何工作的,那么无论如何都要避免使用pre-ansi连接语法对于执行计划,如果在join..on子句或where子句中指定连接谓词,通常没有区别。但从可读性和维护的角度来看,对过滤谓词和连接谓词都使用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'能发现连接谓词吗?如果加入几十张表呢?当为外部连接应用专有语法(例如oracle的(+)语法)时,情况会变得更糟。
解决方法:
始终使用ansi-join语法。永远不要将连接谓词放在where子句中。使用pre-ansi连接语法绝对没有好处。
SQL标准1992指定like谓词如下:
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 '!'解决方法:
在使用like谓词时,始终要考虑适当的转义。
这一点很微妙,但对于null来说非常重要!以下回顾一下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或y中的任何一个为null,则not-in谓词将导致未知,而in谓词可能仍然返回布尔值。
换句话说,当in(x,y)产生true或false时,not(a in(x、y))仍可能产生unknown,而不是false或true。注意,如果in谓词的右侧是子查询,这也是正确的。
不相信吗?看看这个sql fiddle。它表明以下查询没有产生结果:
select 1
where 1 in (null)
union all
select 2
where not(1 in (null))解决方法:
当涉及nullable列时,要注意not in谓词!
人们记得SQL在处理null值时实现了三值逻辑。这就是为什么可以使用null谓词来检查null的原因。
但即使是null谓词也很微妙。注意,以下两个谓词仅对度数为1的行值表达式等效:
not (a is null)
is not the same as a is not null如果a是一个度数大于1的行值表达式,那么真值表转换为:
解决方法:
使用行值表达式时,请注意null谓词,它可能无法按预期工作。
行值表达式是一个很好的SQL特性。当SQL是一种以表为中心的语言时,表也以行为中心。行值表达式通过创建可以与具有相同度数和行类型的其他行进行比较的本地特殊行,从而更容易地描述复杂谓词。一个简单的例子是同时查询客户的姓和名。
select c.address
from customer c,
where (c.first_name, c.last_name) = (?, ?)
Where(c.first_name,c.last_name)=(?,?)可以看到,这种语法比等价语法(谓词左边的每一列都与右边的相应列进行比较)稍微简洁一些。如果许多独立谓词与and组合在一起,则尤其如此。使用行值表达式允许将相关谓词组合为一个谓词。这对于复合外键上的连接表达式非常有用:
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年就已经定义了它们,如果使用它们,像Oracle或postgres这样复杂的数据库可以使用它们来计算更好的执行计划。
解决方法:
尽可能使用行值表达式。它们将使SQL更简洁,甚至可能更快。
要引用TomKyte,再使用索引和Luke。元数据中不能有足够的约束。首先,约束帮助防止数据损坏,这已经非常有用了。但更重要的是,约束将帮助数据库执行SQL转换,因为数据库可以决定:
一些开发人员可能认为约束是缓慢的。与其相反,除非插入大量大量的数据,在这种情况下,可以禁用大型操作的约束,或者使用没有约束的临时“加载表”,不受约束地将数据脱机传输到实际表。
解决方法:
定义尽可能多的约束。它们将帮助数据库在查询时更好地执行。
对于NoSQL的炒作仍在继续,许多企业仍然认为他们是Twitter或Facebook,迫切需要更快、更可扩展的解决方案,逃避ACID和关系模型来横向扩展。有些可能会成功(如Twitter或Facebook)。
对于那些被迫或选择坚持使用经过验证的关系数据库的人来说,如果要和DBA相处得很好并将数据库调到最大的话,不要误以为现代数据库很慢,事实上它们非常快,并在不到一毫秒的时间内解析20kb的查询文本,计算2000行执行计划。
它们可能会变慢,因为应用程序误用了流行的对象关系映射(ORM),或者因为这一ORM不能为复杂的查询逻辑生成快速SQL。在这种情况下,可能想要选择一个更以SQL为中心的API,例如JDBC,jOOQ或MyBatis,这将重新控制SQL。
所以,不要认为50毫秒的查询执行速度很快,甚至可以接受。事实并非如此。如果在开发时获得了这些速度,确保研究了执行计划。这些数据可能会在生产环境中爆发式增长,因为生产环境中有更复杂的场景和数据。
SQL非常有趣,但在许多方面也非常微妙。正如Eder之前关于10个常见错误的博客文章所表明的那样,要做到这一点并不容易。但是SQL是可以掌握的。数据是最宝贵的资产。因此需要尊重数据,并编写更好的SQL。
原文标题:10 More Common Mistakes Java Developers Make when Writing SQL,作者:Lukas Eder
大约一年前,我决定确保每个包含非唯一文本的Flash通知都将从模块中的方法中获取文本。我这样做的最初原因是为了避免一遍又一遍地输入相同的字符串。如果我想更改措辞,我可以在一个地方轻松完成,而且一遍又一遍地重复同一件事而出现拼写错误的可能性也会降低。我最终得到的是这样的:moduleMessagesdefformat_error_messages(errors)errors.map{|attribute,message|"Error:#{attribute.to_s.titleize}#{message}."}enddeferror_message_could_not_find(obje
我想用ruby编写一个小的命令行实用程序并将其作为gem分发。我知道安装后,Guard、Sass和Thor等某些gem可以从命令行自行运行。为了让gem像二进制文件一样可用,我需要在我的gemspec中指定什么。 最佳答案 Gem::Specification.newdo|s|...s.executable='name_of_executable'...endhttp://docs.rubygems.org/read/chapter/20 关于ruby-在Ruby中编写命令行实用程序
我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当
我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/
我遵循MichaelHartl的“RubyonRails教程:学习Web开发”,并创建了检查用户名和电子邮件长度有效性的测试(名称最多50个字符,电子邮件最多255个字符)。test/helpers/application_helper_test.rb的内容是:require'test_helper'classApplicationHelperTest在运行bundleexecraketest时,所有测试都通过了,但我看到以下消息在最后被标记为错误:ERROR["test_full_title_helper",ApplicationHelperTest,1.820016791]test
我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm
我是rails的新手,想在form字段上应用验证。myviewsnew.html.erb.....模拟.rbclassSimulation{:in=>1..25,:message=>'Therowmustbebetween1and25'}end模拟Controller.rbclassSimulationsController我想检查模型类中row字段的整数范围,如果不在范围内则返回错误信息。我可以检查上面代码的范围,但无法返回错误消息提前致谢 最佳答案 关键是您使用的是模型表单,一种显示ActiveRecord模型实例属性的表单。c
我正在尝试编写一个将文件上传到AWS并公开该文件的Ruby脚本。我做了以下事情:s3=Aws::S3::Resource.new(credentials:Aws::Credentials.new(KEY,SECRET),region:'us-west-2')obj=s3.bucket('stg-db').object('key')obj.upload_file(filename)这似乎工作正常,除了该文件不是公开可用的,而且我无法获得它的公共(public)URL。但是当我登录到S3时,我可以正常查看我的文件。为了使其公开可用,我将最后一行更改为obj.upload_file(file
我克隆了一个rails仓库,我现在正尝试捆绑安装背景:OSXElCapitanruby2.2.3p173(2015-08-18修订版51636)[x86_64-darwin15]rails-v在您的Gemfile中列出的或native可用的任何gem源中找不到gem'pg(>=0)ruby'。运行bundleinstall以安装缺少的gem。bundleinstallFetchinggemmetadatafromhttps://rubygems.org/............Fetchingversionmetadatafromhttps://rubygems.org/...Fe
在Cooper的书BeginningRuby中,第166页有一个我无法重现的示例。classSongincludeComparableattr_accessor:lengthdef(other)@lengthother.lengthenddefinitialize(song_name,length)@song_name=song_name@length=lengthendenda=Song.new('Rockaroundtheclock',143)b=Song.new('BohemianRhapsody',544)c=Song.new('MinuteWaltz',60)a.betwee