当前位置: 首页 > 科技观察

你离完全理解SQL只有十步之遥_0

时间:2023-03-17 18:30:41 科技观察

很多程序员视SQL为洪水猛兽。SQL是为数不多的声明式语言之一,其运行方式与命令行语言、面向对象的编程语言,甚至我们所知道的函数式语言有很大不同(尽管有些人认为SQL也是一种函数式语言。).我们每天写SQL,在开源软件jOOQ中使用。所以想把SQL的美妙之处介绍给那些还在为它头疼的朋友们,所以这篇文章是专门为以下读者而写的:在工作中会用到SQL但还没有完全理解它的人。能熟练使用SQL但不了解其语法逻辑的人。任何想教别人SQL的人。本文重点介绍SELECT句型,其他DML(DataManipulationLanguage数据操作语言命令)将在其他文章中介绍。1.SQL是一种声明式语言。首先,你必须牢记这个概念:“声明”。SQL语言是一种范例,用于向计算机声明您希望从原始数据中得到什么结果,而不是告诉计算机如何获得结果。不是很好吗?(译者注:简单来说,SQL语言声明了结果集的属性,计算机会根据SQL语句的内容从数据库中选择符合语句的数据,而不是指示计算机如何像传统编程思维一样操作。)SELECTfirst_name,last_nameFROMemployeesWHEREsalary>100000上面的例子很容易理解,我们不关心这些员工记录从哪里来,我们需要的只是那些高薪的数据(译者注:工资>100000)。我们在哪里学这个?如果SQL语言如此简单,那么是什么让人“闻到SQL味”呢?主要原因是:在我们的潜意识里,我们是按照命令式编程的思维方式来思考问题的。就像这样:“计算机,先做这个,再做那个,但在此之前检查条件A和条件B是否满足”。比如用变量传递参数,使用循环语句,迭代,调用函数等等,都是这种命令式编程的思维习惯。2.SQL的语法不按语法顺序执行SQL语句有一个特点让大多数人感到困惑,那就是:SQL语句的执行顺序与其语句的语法顺序不一致。SQL语句的语法顺序为:SELECT[DISTINCT]FROMWHEREGROUPBYHAVINGUNIONORDERBY为了方便理解,上面没有列出所有的SQL语法结构,但是足以说明SQL语句的语法顺序是完全不同的它的执行顺序。以上面的语句为例,它的执行顺序是:FROMWHEREGROUPBYHAVINGSELECTDISTINCTUNIORDERBY关于SQL语句的执行顺序,有三点值得我们注意:(1)FROM是SQL语句执行的第一步,不是SELECT.数据库执行SQL语句的第一步是将数据从硬盘加载到数据缓冲区,从而对数据进行操作。(译者注:原文是“首先发生的事情是将数据从磁盘加载到内存中,以便对这些数据进行操作。”,但事实并非如此。以Oracle等常见数据库为例,(2)SELECT在大部分语句执行完之后执行,严格来说是在FROM和GROUPBY之后执行。理解这一点非常重要,这就是为什么不能使用SELECT中的别名字段作为WHERE中的判断条件的原因。SELECTA.x+A.yASzFROMAWHEREz=10--z在此处不可用,因为SELECT是最后执行的语句!如果您想重复使用别名z,您有两种选择。重写表达式z代表:SELECTA.x+A.yASzFROMAWHERE(A.x+A.y)=10...或求助于派生表、CDE或视图以避免别名重用。请参阅下面的示例。(3)无论语法或执行顺序如何,UNION总是在ORDERBY之前。很多人认为每个UNION段都可以使用ORDERBY排序,但是根据SQL语言标准和各个数据库中SQL实现的差异,并不是这样的。虽然有些数据库允许SQL语句对子查询(subqueries)或派生表(derivedtables)进行排序,但这并不意味着在UNION操作后排序会保持排序后的顺序。注意:并非所有数据库都使用相同的方式解析SQL语句。比如在MySQL、PostgreSQL、SQLite中,就不会按照上面第二点的方式执行。我们学到了什么?由于并非所有数据库都执行上述SQL预测,我们的结论是什么?我们的要点是永远记住SQL语句的语法顺序与其执行顺序不同,这样我们就可以避免常见的错误。如果你能记住SQL语句的语法顺序和执行顺序之间的区别,你就可以很容易地理解一些非常常见的SQL问题。当然,如果一种语言被设计成语法顺序直接反映其语句的执行顺序,那么这种语言对程序员是非常友好的。这种编程语言级别的设计理念已经被微软应用到LINQ语言中。3.SQL语言的核心是对表的引用(tablereferences)。由于SQL语句的语法顺序和执行顺序的不同,很多同学会认为SELECT中的字段信息才是SQL语句的核心。其实真正的核心在于对表的引用。根据SQL标准,FROM语句定义为:::=FROM[{}...]FROM语句的“输出”是一个联合tablefrom维度上所有引用表的并集。我们慢慢分析:FROMa,b上面FROM语句输出的是一张联表,将表a和表b结合在一起。如果a表有3个字段,b表有5个字段,那么这个“输出表”就有8(=5+3)个字段。这个联表中的数据是a*b,是a和b的笛卡尔积。换句话说,a表中的每条数据都必须与b表中的每条数据配对。如果a表有3条数据,b表有5条数据,那么联合表就有15(=5*3)条数据。FROM输出的结果经过WHERE语句过滤后,经过GROUPBY语句处理,形成新的输出结果。我们稍后会讨论这个方面。如果我们从集合论(关系代数)的角度来看,数据库表就是一组数据元素关系,每条SQL语句都会改变一个或几个关系,从而产生新的数据元素关系(即生成新表)).我们学到了什么?在思考问题的时候,站在表的角度思考问题,这样就很容易理解数据在SQL语句的“管道”上发生了怎样的变化。4、灵活的引用表可以让SQL语句更强大灵活的引用表可以让SQL语句更强大。一个简单的例子是JOIN的使用。严格来说,JOIN语句不是SELECT的一部分,而是一种特殊的表引用语句。SQL语言标准中表的连接定义如下:::=|<派生表>|拿前面的例子:FROMa,ba可能进入下表的连接:a1JOINa2ONa1.id=a2.id把这个放到前面的例子中就变成了:FROMa1JOINa2ONa1.id=a2.id,b虽然用逗号放置一个连接表,然后将另一个表连接在一起并不常见,但你可以做到。结果,最终输出表有a1+a2+b字段。(译者注:这里的原词是degree,翻译为维度。如果我们把一张表形象化,我们可以想象每张表都是由水平和垂直两个维度组成的。水平维度就是我们所说的字段。或者列,英文是列;垂直维度代表每条数据,英文是record,根据上下文,笔者这里应该指的是字段数。)SQL语句中派生表的引用比表连接还要厉害,如下我们要说的是表连接。我们学到了什么?思考问题的时候,从表引用的角度出发,这样就很容易理解SQL语句是如何处理数据的,也能帮助你理解那些复杂的表引用是干什么的。更重要的是,一定要了解JOIN是构建连接表的关键字,而不是SELECT语句的一部分。有些数据库允许在INSERT、UPDATE、DELETE中加入JOIN。5、建议在SQL语句中使用表连接。我们来看看刚才的那句话:FROMa,b学习高级SQL的程序员可能会给你忠告:表连接尽量不要用逗号代替JOIN,这样会提高你的SQL语句的可读性,避免错误。使用逗号来简化SQL语句有时会引起混淆。考虑以下语句:FROMa,b,c,d,e,f,g,hWHEREa.a1=b.bxANDa.a2=c。c1ANDd.d1=b.bc--etc...不难看出使用JOIN语句的好处是:安全。JOIN和要连接的表之间非常接近,这样可以避免错误。对于更多的连接方式,JOIN语句可以区分外连接和内连接。我们学到了什么?切记尽量使用JOIN连接表,千万不要在FROM连接表后使用逗号。6、SQL语句中不同的连接操作在SQL语句中,表的连接方式基本分为五种:EQUIJOINSEMIJOINANTIJOINCROSSJOINDIVISIONEQUIJOIN这是最常见的JOIN操作,它包括两种连接方式:INNERJOIN(或JOIN)OUTERJOIN(包括:LEFT、RIGHT、FULLOUTERJOIN)最容易通过示例说明差异:--此表引用包含作者及其书籍。--每本书及其作者都有一条记录。--没有书籍的作者不包括authorJOINbookONauthor.id=book.author_id--此表引用包含作者及其书籍--每本书及其作者都有一条记录。--...或者有一个“empty”recordforauthorswithoutbooks--(“empty”意味着所有的书列都是NULL)使用存在。“SEMI”在拉丁语中是“一半”的意思。这种类型的连接是只连接目标表的一部分。这是什么意思?再想想上面关于作者和标题的链接。让我们想象一下我们不需要作者/书名组合的情况,只需要书名表中那些书的作者信息。那么我们可以这样写:--UsingINFROMauthorWHEREauthor.idIN(SELECTbook.author_idFROMbook)--UsingEXISTSFROMauthorWHEREEXISTS(SELECT1FROMbookWHEREbook.author_id=author.id)虽然没有严格的规范什么时候应该使用IN什么时候应该使用EXISTS,但这里有一些你应该知道的事情:有些数据库,性能差异会很大)因为title表中图书对应的作者信息也可以通过INNERJOIN获取,所以很多初学者认为可以通过DISTINCT去重,然后SEMIJOIN语句是这样写的:--FindonlythoseauthorswhoalsohavebooksSELECTDISTINCTfirst_name,last_nameFROMauthorJOINbookONauthor.id=book.author_id这是一种非常糟糕的写法,原因如下:SQLstatement性能低下:由于去重操作(DISTINCT)需要数据库反复将数据从磁盘读入内存。(译者注:DISTINCT确实是一个非常耗资源的操作,但是每个数据库对DISTINCT的操作可能不同)。这样写也不完全正确:虽然现在这样写可能不是问题,但是随着SQL语句越来越复杂,你去重得到正确的结果会变得非常困难。ANTIJOIN的连接关系与SEMIJOIN正好相反。您可以通过在IN或EXISTS之前添加NOT关键字来使用这种连接。例如,我们列出在标题表中没有一本书的作者:--使用INFROMauthorWHEREauthor.idNOTIN(SELECTbook.author_idFROMbook)--使用EXISTSFROMauthorWHERENOTEXISTS(SELECT1FROMbookWHEREbook.author_id=author.id)CROSSJOIN的连接过程是两个连接表的乘积:第一个表中的每条数据对应第二个表中的每条数据。我们以前见过,这就是在FROM语句中使用逗号的方式。在实际应用中,很少使用CROSSJOIN,但一旦用到,就可以用这条SQL语句来表达:--CombineeveryauthorwitheverybookauthorCROSSJOINbookDIVISIONDIVISION确实是个怪胎。简而言之,如果JOIN是乘法运算,那么DIVISION就是JOIN的逆运算。DIVISION关系很难在SQL中表达,并且由于这是初学者指南,因此解释DIVISION超出了我们的目的。我们学到了什么?学到了很多!让我们回顾一下。SQL是对表的引用,JOIN是引用表的复杂方式。但是,SQL语言的表达方式和我们实际需要的逻辑关系是有区别的。并不是所有的逻辑关系都能找到对应的JOIN操作,所以我们需要在平时多积累和学习关系逻辑,这样你以后在写SQL语句的时候才能选择合适的JOIN操作。7、派生表就像SQL中的变量在这之前,我们了解到SQL是一种声明式语言,SQL语句中不能包含变量。但是可以写类似变量的语句,这些叫做派生表:说白了,所谓派生表就是括号里的子查询:--派生表FROM(SELECT*FROMauthor)注意有时候我们可以定义一个关联派生表的名称(我们称之为别名)。--AderivedtablewithaliasFROM(SELECT*FROMauthor)派生表可以有效避免SQL逻辑带来的问题。例如:如果你想复用一个SELECT和WHERE语句的查询结果,你可以这样写(以Oracle为例):--获取作者的名字和姓氏,以及他们的年龄,以天为单位SELECTfirst_name,last_name,ageFROM(SELECTfirst_name,last_name,current_date-date_of_birthageFROMauthor)--如果年龄大于10000天WHEREage>10000我们需要注意的是:在一些数据库中,以及在SQL:1990标准中,派生表被归类为下一级——公用表语句(commontableexperssion)。这允许您在一个SELECT语句中多次重复使用派生表。上面的例子(几乎)等同于下面的语句:WITHaAS(SELECTfirst_name,last_name,current_date-date_of_birthageFROMauthor)SELECT*FROMaWHEREage>10000当然,你也可以创建一个单独的视图,这样你就可以更广泛地重用派生表。我们学到了什么?我们反复强调,一般情况下,SQL语句是引用表,而不是引用字段。要利用这一点,不要害怕使用派生表或其他更复杂的语句。8.SQL语句中的GROUPBY是对表的引用的操作让我们回忆一下前面的FROM语句:FROMa,b现在,我们将GROUPBY应用到上面的语句中:GROUPBYA.x,A.y,B.z上面的结果该语句的引用是对包含三个字段的新表的引用。仔细理解这句话:当你应用GROUPBY时,SELECT后不使用聚合函数的列必须出现在GROUPBY之后。(译者注:原文大致意思是“当你使用GROUPBY时,你可以进行下一级逻辑操作的列会减少,包括SELECT中的列”)。注意:其他字段可以使用聚合函数:SELECTA.x,A.y,SUM(A.z)FROMAGROUPBYA.x,A.y另外值得注意的一点:MySQL不遵守这个标准,这确实是一个很混乱的地方。(译者注:这并不是说MySQL没有GROUPBY的功能)但是不要被MySQL给骗了。GROUPBY改变了引用表的方式。您既可以在SELECT中引用一个字段,又可以像这样在GROUPBY中对其进行分组。我们学到了什么?GROUPBY,再次对表的引用进行操作,转换成新的引用方式。9、SQL语句中的SELECT本质上是关系的映射。我个人喜欢“映射”这个词,尤其是当它用在关系代数中时。(译者注:原文中使用的词是projection,有两个意思,第一个意思是预测、规划、设计,第二个意思是投影、制图。经过反复推敲,我觉得更直观的是这里使用映射。表达SELECT的作用)。一旦你建立了对表的引用,经过修改和转换,你就可以一步步将它映射到另一个模型。SELECT语句就像一个“投影仪”,我们可以把它理解为一个函数,将源表中的数据按照一定的逻辑转换成目标表中的数据。通过SELECT语句,可以对各个字段进行操作,通过复杂的表达式生成需要的数据。SELECT语句有很多特殊的规则,至少你应该熟悉以下几点:你只能使用那些可以通过表引用获取的字段;如果有GROUPBY语句,则只能使用GROUPBY语句函数之后的字段或聚合;当你的语句中没有GROUPBY时,你可以使用windowfunction而不是aggregatefunction;当你的语句中没有GROUPBY时,不能同时使用聚合函数和其他函数;有一些方法可以将普通函数封装在聚合函数中;...一些更复杂的规则就足够了,另写一篇文章。例如:为什么不能在没有GROUPBY的SELECT语句中将普通函数和聚合函数一起使用?(上面的#4)原因如下:从直觉上讲,这不符合逻辑。如果直觉不能说服你,语法肯定可以。SQL:1999标准引入了GROUPINGSETS,SQL:2003标准引入了组集:GROUPBY()。每当您的语句中出现聚合函数,并且没有明确的GROUPBY语句时,一个不明确的空GROUPINGSET将应用于此SQL。因此,原有的逻辑顺序规则被打破,映射(即SELECT)关系将首先影响逻辑关系,然后才是句法关系。(译者注:这段话原文比较难,可以简单理解为:在同时包含聚合函数和普通函数的SQL语句中,如果没有GROUPBY进行分组,则SQL语句将默认情况下将整个表作为一个组。当聚合函数对某个字段进行聚合统计时,引用表中的每条记录都失去了意义,所有的数据都聚合成一个统计值。你再使用其他函数是没有意义的此时每条记录)。使困惑?我也是。让我们回到更明显的事情上。我们学到了什么?SELECT语句可能是SQL语句中最难的部分,尽管它看起来很简单。其他语句的作用实际上是对表的不同形式的引用。SELECT语句将这些引用整合起来,通过逻辑规则将源表映射到目标表,这个过程是可逆的,所以我们可以清楚的知道目标表中的数据是怎么来的。要想学好SQL语言,在使用SELECT语句之前必须了解其他语句。虽然SELECT是语法结构中的第一个关键字,但它应该是我们掌握的最后一个关键字。10、SQL语句中几个简单的关键字:DISTINCT、UNION、ORDERBY和OFFSET学完了复杂的SELECT,我们来看一些简单的:集合操作(??DISTINCT和UNION),OFFSET...FETCH)集合操作(??setoperation):集合操作主要是对集合进行操作,其实就是指对表的操作。从概念上讲,它们很容易理解:DISTINCT在映射后对数据进行去重UNION连接两个子查询并进行去重UNIONALL连接两个不去重的子查询EXCEPT在第二个子查询中去重通过INTERSECT从第一个子查询中删除结果。保留两个子查询的结果并删除排序操作:排序操作与逻辑关系无关。这是一个特定于SQL的功能。排序操作不仅在SQL语句结束时进行,在SQL语句执行过程中也会进行。使用ORDERBY和OFFSET...FETCH是保持数据有序的最有效方法。所有其他排序都有些随机,尽管它们产生可重现的排序。OFFSET...SET是一个没有统一语法的语句。不同的数据库有不同的表达方式,比如MySQL和PostgreSQL中的LIMIT...OFFSET,SQLServer和Sybase中的TOP...STARTAT等等,让我们在工作中尽情使用SQL吧!与任何语言一样,学好SQL需要大量练习。上面的10个简单步骤可以帮助您更好地理解您每天编写的SQL语句。另一方面,也可以从常见的错误中积累很多经验。