上面这个脑图可以加文末公众号回复「mysql脑图」获取xmind源文件。
数据库是一个以某种有组织的方式存储的数据集合,可以将其想象为一个文件柜。
MySQL数据库隶属于MySQLAB公司,总部位于瑞典,后被oracle收购。是目前最流行的关系型数据库。
优点:
我这里的命令是:
选择NewConnection之后填一下配置:
就可以看到你数据库里面的内容了。
就可以啦,效果如下图:
废话少说,下面直接开始知识灌体!
主键的概念十分重要,它唯一标识表中每行的单个或者多个列称为主键。主键用来表示一个特定的行。
虽然并不总是都需要主键,但应尽量保证每个表都定义有主键,以便于以后的数据操纵和管理。没有主键,无法将不同的行区分开来,更新或删除表中特定行很困难。
表中的任何列都可以作为主键,只要它满足以下条件:
在使用多列作为主键时,上述条件必须应用到构成主键的所有列,所有列值的组合必须是唯一的(单个列的值可以不唯一)。
几个普遍认可的最好习惯为:
语法规范:
语法特点:
常见的简单命令:
mysql-uroot-p#–h主机名–u用户名-P端口号–p密码,注意-p跟密码之间不能加空格其他可以加可以不加selectversion();#查看mysql服务版本showdatabases;#查看所有数据库,注意最后有screatedatabase[库名];#创建库use[库名];#打开指定的库showtables;#查看当前库的所有表showtablesfrom[库名];#查看其他库的所有表desc[表名];#查看表结构createtable[表名](#创建表列名列类型,列名列类型,);dropdatabase[库名];#删除库droptable[表名];#删除表exit;#退出2.4创建表并填充数据首先我们整点数据,方便后面的代码演示。
如果你用的是navicate,在上一章创建到localhost-mysql的连接后,运行一下即可:
运行之后在命令行中showtables就可以看到库中的表了,如下图:
简单介绍一下刚刚创建好的表。
为了数据分类处理,顾客customers、供应商vendors、订单orders、订单信息orderitems、产品记录productnotes、产品products表分别存储不同的信息。
比如供应商信息表vendors总每个供应商都有一个唯一标识,也就是主键vend_id,而products产品表的每个产品也有一个主键prod_id,还有一个字段vend_id供应商ID和供应商表中的vend_id一一对应,这就是外键。
这样做的好处:
可伸缩性(scale),能够适应不断增加的工作量而不失败。设计良好的数据库或应用程序称之为可伸缩性好(scalewell)。
MySQL数据类型定义了列中可以存储什么数据以及该数据怎样存储的规则。
整型:Tinyint、Smallint、Mediumint、Int(Integer)、Bigint,可以为无符号和有符号,默认有符号。
小数
M为整数部位+小数部位,D为小数部位,M和D都可以省略。如果是decimal,则M默认为10,D默认为0。
用来查询的select语句大概是最常用的了,用来从一个或多个表中检索信息,一条select语句必须至少给出两条信息:想选择什么、从什么地方选择。
#基本语法select[查询列表]from[表名];#查询单个/多个/所有字段selectcust_namefromcustomers;selectcust_name,cust_city,cust_addressfromcustomers;select`select`fromcustomers;#如果某字段是关键字,可以用`符号包起来select*fromcustomers;#*表示所有#查询常量值/表达式/函数select100;select'zhangsan';select100%98;selectversion();
查询出来的结果可能有多个重复值,可以使用distinct关键字来去重
selectorder_numfromorderitems;#有一些重复值selectdistinctorder_numfromorderitems;#将重复值去重3.2限制结果limitselect语句返回所有匹配的行,它们可能是指定表中的每个行。为了返回第一行或前几行,可使用limit子句。
limitm表示返回找出的前m行,limitm,n表示返回第m行开始的n行,也可以使用limitmoffsetn含义和前面一样。
注意,检索出来的第一行的索引为0行。
在某些情况下,语句可能使用完全限定的列明与表名:
selectorderitems.order_numfrommysql_demo1.orderitems;#上面这句等价于selectorder_numfromorderitems;4.排序检索数据orderby上一章从orderitems这个表中检索的数据是没有排序的,一般情况下返回的顺序是在底层表中出现的顺序。可以通过orderby子句来对检索后的数据进行排序。
可以用asc、desc关键字来指定排序方向。orderbyasc升序、orderbydesc降序,不写默认是升序。
orderby子句中可以支持单个字段、多个字段、表达式、函数、别名,一般放在句子的最后面,除了limit之外。
select*fromorderitemsorderbyitem_price;#按item_price升序排列#先按quantity升序排列,quantity的值一样时按item_price的值升序排列select*fromorderitemsorderbyquantity,item_price;#先按quantity降序排列,quantity的值一样时按item_price的值升序排列select*fromorderitemsorderbyquantitydesc,item_price;#找到最贵订单select*fromorderitemsorderbyitem_pricedesclimit1;5.过滤数据where在from子句后使用where关键字可以增加筛选条件,过滤数据。
#基本语法select[查询列表]from[表名]where[筛选条件]orderby[排序条件];按条件表达式来筛选>、=、<、>=、<=、!=、<>、<=>
#找出产品价格为2.5的产品名字selectprod_name,prod_pricefromproductswhereprod_price=2.5;#找出产品价格小于等于10的产品名字,并按产品价格降序排列selectprod_name,prod_pricefromproductswhereprod_price<=10orderbyprod_pricedesc;#找到供应商id不为1003的产品,!=和<>含义一样,都是不等于selectvend_id,prod_namefromproductswherevend_id<>1003;5.1范围检查betweenand使用between...and...指定所需范围的开始值和结束值,可以达到范围查询的效果。
注意betweenand左右数字是按小大的顺序的,调过来不行。
#查询产品价格在3到10内的产品selectprod_name,prod_pricefromproductswhereprod_pricebetween3and10;#单独使用and也可以打到这个效果selectprod_name,prod_pricefromproductswhereprod_price<=10andprod_price>=3;5.2空值检查is(not)null创建表时,可以指定某些列可以不包含值,即可以为null,null表示无值novalue,这与字段包含0、空字符串或仅仅包含空格不同。
使用isnull或isnotnull可以用来判断一个值是否为null。
说明:
前文提到了and操作符,通过这个操作符可以增加限定条件:
#找出供应商为1003提供的价格小于等于10的产品select*fromproductswherevend_id=1003andprod_price<=10;5.4逻辑或操作符oror操作符和and操作符相反,这是逻辑或操作符,返回匹配任一条件的行:
#找出id为1003或1001的供应商select*fromproductswherevend_id=1003orvend_id=1001;在and和or同时出现时,会优先处理and,比如这句:
select*fromproductswherevend_id=1001orvend_id=1003andprod_price>=10;这句会先处理and,表示vend_id为1003且prod_price大于等于10的产品,或者vend_id为1001的产品。
遇到这种情况,可以通过增加圆括号:
select*fromproductswhere(vend_id=1001orvend_id=1003)andprod_price>=10;这样检索的结果就是vend_id为1001或1003的产品里,所有prod_price大于等于10的产品列表了。
任何时候使用具有and和or操作符的where子句,都应该使用圆括号明确地分组操作符。不要过分依赖默认计算次序,即使它确实是你想要的东西也是如此,而且使用圆括号能消除歧义,增加可读性。
使用in操作符可以指定条件范围,范围中的每个条件都可以进行匹配。in要匹配的值放在其后的圆括号中:
#找出id为1003或1001的供应商select*fromproductswherevend_idin(1001,1003);in操作符可以用or来取代,在以下情况建议使用in:
not否操作符可以和前面的in和betweenand一起使用,表示对范围取反:
#找出id不为10011003的产品select*fromproductswherevend_idnotin(1001,1003);#选择产品价格不在5到15之间的产品select*fromproductswhereprod_pricenotbetween5and15;5.7like操作符比如想找出名称中包含anvil的所有产品,可以通过like操作符来进行搜索。like表示后面跟的搜索模式使用通配符匹配而不是直接相等匹配。
最常使用的通配符是%操作符,%表示任意多个字符,包括没有字符。
#找出产品名字以jet开头的产品select*fromproductswhereprod_namelike'jet%';#找出产品名中含有on的产品select*fromproductswhereprod_namelike'%on%';#找出产品名以s开头,以e结束的产品select*fromproductswhereprod_namelike's%e';注意,%是无法匹配null的。
_表示任意单个字符。
select*fromproductswhereprod_namelike'_tonanvil';另外,转译使用\,比如\_
#找到描述中有%的产品select*fromproductswhereprod_desclike'%\%%';注意:
使用regexp关键字来表示匹配其后的正则表达式:
#找到产品名以1000或anvil结尾的产品select*fromproductswhereprod_nameregexp'1000|anvil$';正则表达式中转译使用\\,比如希望查找.这个字符而不是正则中的.通配符,使用\\.,为了转移\这个字符,使用\\\
#找到产品名以.字符开头的产品select*fromproductswhereprod_nameregexp'^\\.';6.计算字段有时候我们需要直接从数据库中检索出转换、计算或格式化过的数据,而不是检索出数据,然后再在客户机应用程序或报告程序中重新格式化,这时我们就需要计算字段了。
查询出来的虚拟表格可以起一个别名,方便理解,可读性好,另外如果查询的字段有重名的情况,可以使用别名as来区分开来。
#使用as关键字selectcust_nameasnamefromcustomers;#as关键字也可以直接省略selectcust_namenamefromcustomers;#可以给不同字段分别起别名selectcust_namename,cust_citylocationfromcustomers;6.2拼接concat想把多个字段连接成一个字段,可以使用到拼接字段函数concat:
#将供应商的名字和地点拼接好后返回,并命名为vendselectconcat(vend_name,'(',vend_country,')')vendfromvendors;注意中间如果有任何一个数据为null,拼接的结果也是null。
所以对某些可能为null的字段要使用ifnull函数判断一下,第一个参数为要判断的字段,第二个参数是如果是null希望返回的结果:
#将顾客信息拼接起来selectconcat(cust_name,'(',ifnull(cust_email,'-'),')')customerInfofromcustomers;如果表中的数据前后有空格,可以使用rtrim()函数去除右边空格,ltrim()去除左边空格,或者trim()去除前后空格:
#将顾客信息处理后拼接起来selectconcat(rtrim(vend_name),'(',trim(vend_country),')')vendfromvendors;6.3算术计算+-*/基本的算术运算符在select语句中也是支持的:
#计算订单每种总额,并按照总金额降序排列selectprod_idasid,quantity,quantity*item_priceastotalPricefromorderitemsorderbytotalPricedesc;基本运算符加减乘除都是支持的+、-、*、/。
前面介绍的去除数据首位空格的trim()函数就是数据处理函数,除此之外还有多种其他类型的数据处理函数:
在不了解如何使用一个函数的时候,可以使用help命令,比如helpsubstr就可以获取substr的使用方式和示例。
示例:
#upper、lower将姓变大写,名变小写,然后拼接selectconcat(upper(last_name),lower(first_name))姓名fromemployees;#姓名中首字符大写,其他字符小写然后用_拼接,显示出来selectconcat(upper(substr(last_name,1,1)),'_',lower(substr(last_name,2)))fromemployees;#substr截取字符串,sql中索引从1开始而不是0selectsubstr('helloworld',3);#lloworldselectsubstr('helloworld',2,3);#ell#instr返回子串第一次出现的索引,没找到返回0selectinstr('abcdefg','cd');#3#trim减去字符串首尾的空格或指定字符selecttrim('hello');#helloselecttrim('aa'from'aaabaabaaaaaa');#abaab#lpad用指定的字符实现左填充指定长度selectlpad('he',5,'-');#---he#rpad用指定的字符实现左填充指定长度selectrpad('he',5,'-*');#he-*-#replace替换selectreplace('abcabcabc','bc','--');#a--a--a--7.2数学函数函数说明round()四舍五入ceil()向上取整floor()向下取整truncate()保留几位小数mod()取余abs()返回绝对值rand()返回一个随机数示例:
聚集函数(aggregatefunction)运行在行组上,计算和返回单个值的函数。
#计算产品价格平均值selectavg(prod_price)asavgPricefromproducts;#计算供应商id为1003提供的产品的平均价格selectavg(prod_price)asavgPricefromproductswherevend_id=1003;#计算价格最大的产品价格selectmax(prod_price)asmaxPricefromproducts;#计算顾客总数selectcount(*)fromcustomers;#计算具有email的顾客数selectcount(cust_email)fromcutomers;#计算产品价格总和selectsum(prod_price)fromproducts;#计算订单为20005的订单总额selectsum(item_price*quantity)totalPricefromorderitemswhereorder_num=20005;#计算产品具有的不同的价格的平均数selectavg(distinctprod_price)avgPricefromproductswherevend_id=1003;#同时计算产品总数、价格最小值、最大值、平均数selectcount(*)prodNums,min(prod_price)priceMin,max(prod_price)priceMax,avg(prod_price)priceAvgfromproducts;8.分组数据之前的聚集函数都是在where子句查询到的所有数据基础上进行的计算,比如查询某个供应商的产品平均价格,但假如希望分别返回每个供应商提供的产品的平均价格,该怎么处理呢。这该用到分组了,分组允许把数据分为多个逻辑组,以便能对每个组进行聚集计算。
使用groupby子句可以指示MySQL按某个数据排序并分组数据,然后对每个组而不是整个结果集进行聚集。
#分别查询每个供应商提供的产品种类数selectvend_id,count(*)num_prodsfromproductsgroupbyvend_id;#查询每个供应商提供的产品的平均价格selectvend_id,avg(prod_price)avg_pricefromproductsgroupbyvend_id;注意:
除了能用groupby分组数据外,MySQL还允许使用having关键字过滤分组,指定包括哪些分组,排除哪些分组。
语法顺序如下:
#语法顺序select[查询列表]from[表名]where[筛选条件]groupby[分组列表]having[分组后筛选]orderby[排序列表]limit[要检索行数];where过滤没有分组的概念,指定的是行而不是分组,针对分组的过滤使用having子句。事实上,目前为止所学过的所有类型的where子句都可以用having来替代。
关于having和where的差别,这里有另一种理解方法,where在数据分组前进行过滤,having在数据分组后进行过滤。where排除的行不包括在分组中,这可能会改变计算值,从而影响having子句中基于这些值过滤掉的分组。
能用分组前筛选where的,优先考虑分组前筛选。
#找到提供大于2个产品的供应商,并列出其提供的产品数量,这里使用having来过滤掉产品数不大于2的供应商selectvend_id,count(*)prodCountfromproductsgroupbyvend_idhavingprodCount>2;#找到供应商提供的商品平均价格大于10的供应商,并且按平均价格降序排列selectvend_id,avg(prod_price)avgPricefromproductsgroupbyvend_idhavingavgPrice>10orderbyavgPricedesc;9.子查询子查询(subquery),嵌套在其他查询中的查询。
当一个查询语句中又嵌套了另一个完整的select语句,则被嵌套的select语句称为子查询或内查询,外面的select语句称为主查询或外查询。
之前所有查询都是在同一张表中的,如果我们想获取的信息分散在两张甚至多张表呢,比如要从订单表orders中获取顾客ID,然后用顾客ID去顾客表custormers找到对应顾客信息。
#首先在orderitems表中找到产品TNT2对应的订单编号selectorder_numfromorderitemswhereprod_id='TNT2'#然后在orders表中找到订单编号对应的顾客idselectcust_idfromorderswhereorder_numin(selectorder_numfromorderitemswhereprod_id='TNT2');#然后去customers表中找到顾客id对应的顾客名字selectcust_id,cust_namefromcustomerswherecust_idin(selectcust_idfromorderswhereorder_numin(selectorder_numfromorderitemswhereprod_id='TNT2'));这里实际上有三条语句,最里边的子查询返回订单号列表,此列表用于其外面的子查询的where子句。外面的子查询返回顾客ID列表,此顾客ID列表用于最外层查询的where子句。最外层查询最终返回所需的数据。
对于能嵌套的子查询的数目没有限制,不过在实际使用时由于性能的限制,不能嵌套太多的子查询。
使用子查询的另一方法是创建计算字段。假如需要显示customers表中每个顾客的订单总数。订单与相应的顾客ID存储在orders表中。
如果要查的数据分散在多个表中,如何使用单条select语句查到数据呢,使用联结可以做到。
联结是一种机制,用来在一条select语句中关联表,因此称之为联结。使用特殊的语法,可以联结多个表返回一组输出,联结在运行时关联表中正确的行。
维护引用完整性:在使用关系表时,仅在关系列中插入合法的数据非常重要。如果在products表中插入拥有没有在vendors表中出现的供应商ID的供应商生产的产品,则这些产品是不可访问的,因为它们没有关联到某个供应商。
为防止这种情况发生,可指示MySQL只允许在products表的供应商ID列中出现合法值(即出现在vendors表中的供应商)。这就是维护引用完整性,它是通过在表的定义中指定主键和外键来实现的。
联结的创建非常简单,规定要联结的所有表以及它们如何关联即可。
#列出产品的供应商及其价格selectvend_name,prod_name,prod_pricefromvendors,productswherevendors.vend_id=products.vend_idorderbyprod_pricedesc;这里在where后面用完全限定列名方式指定MySQL匹配vender表的vend_id列和products表的vend_id字段。
当引用的列可能有歧义时,必须使用完全限定列名的方式,因为MySQL不知道你指的是哪个列。
在联结两个表时,实际上做的是将一个表的每一行与另一个表的每一行配对,所以where子句作为过滤条件,过滤出只包含指定联结条件的列wherevendors.vend_id=products.vend_id,没有where子句,将返回两个表的长度乘积个字段,这叫笛卡尔积(cartesianproduct),可以运行一下这句看看:
#返回两个表长度乘积行selectvend_name,prod_name,prod_pricefromvendors,products;所有联结应该总是使用联结条件,否则会得出笛卡尔积。
一条select语句也可以联结多个表,比如需要把某个订单的产品信息、订单信息、供应商信息都列出来,要找的产品信息分散在供应商、产品、订单信息三个表中。
#将订单20005的产品信息、订单信息、供应商信息找出来selectprod_name,vend_name,prod_price,quantityfromorderitems,products,vendorswhereproducts.vend_id=vendors.vend_idandorderitems.prod_id=products.prod_idandorder_num=20005;这里使用and来连接多个联结条件,定义了3个表之间用什么作为关联。
注意:MySQL在运行时关联多个表以处理联结可能是非常耗费资源的,不要联结不必要的表。联结的表越多,性能下降越厉害。
这里可以使用联结来实现9.1节的例子,之前是使用子查询来实现的,从订单表orders中获取顾客ID,然后用顾客ID去顾客表custormers找到对应顾客信息。
#使用联结来实现9.1的例子selectcustomers.cust_id,cust_namefromorders,customers,orderitemswhereorders.order_num=orderitems.order_numandcustomers.cust_id=orders.cust_idandprod_id='TNT2';#由于三个表中只有一个表有prod_id,所以不需要限定表名这里提一句,不仅仅列可以起别名,表也可以起,用法跟列的别名一样:
#把前面这个句子起别名selectc.cust_id,cust_namefromorderso,customersc,orderitemsoiwhereo.order_num=oi.order_numandc.cust_id=o.cust_idandprod_id='TNT2';这样不仅仅可以缩短SQL语句,也允许在单条select语句中多次使用相同的表,同时起的别名不仅可以用在select子句,也可以使用在where、orderby子句以及语句的其他部分。
目前为止所用的联结称为等值联结(equijoin),它基于两个表之间的相等测试,也称为内部联结。其实,对于这种联结可以使用稍微不同的语法来明确指定联结的类型。下面的select语句返回与前面例子完全相同的数据:
#列出产品的供应商及其价格selectvend_name,prod_name,prod_pricefromvendorsinnerjoinproductsonvendors.vend_id=products.vend_id;这里的联结条件使用on子句而不是where,这两种语法都可以达到效果。尽管使用where子句定义联结的确比较简单,但使用明确的联结语法能够确保不会忘记联结条件,有时候这样做也能影响性能。
比如某个产品出现了质量问题,现在希望找出这个产品的供应商提供的所有产品信息。按照之前介绍的子查询,我们可以先找到对应产品的供应商,然后找到具有这个供应商ID的产品列表:
#先找到产品ID为TNT1的供应商ID,然后找到对应供应商ID提供的产品列表selectprod_id,prod_name,vend_idfromproductswherevend_idin(selectvend_idfromproductswhereprod_id='TNT1');使用子查询确实可以实现,使用联结也可以做到,这就是自联结:
#自联结selectp1.prod_id,p1.prod_name,p1.vend_idfromproductsp1,productsp2wherep1.vend_id=p2.vend_idandp2.prod_id='TNT1';自联结查询的两个表是同一个表,因此products表需要分别起别名,以作为区分,而且select子句中出现的列名也需要限定表明,因为两个表都出现了相同的字段。
自联结通常作为外部语句用来替代从相同表中检索数据时使用的子查询语句。虽然最终的结果是相同的,但有时候处理联结远比处理子查询快得多。应该试一下两种方法,以确定哪一种的性能更好。
无论何时对表进行联结,应该至少有一个列出现在不止一个表中(被联结的列)。标准的联结返回所有数据,甚至相同的列多次出现。自然联结排除多次出现,使每个列只返回一次。
自然联结就是你只选择那些唯一的列,这一般是通过对表使用通配符,对所有其他表的列使用明确的子集来完成的。
比如:
此时联结需要包含哪些没有关联行的那些行。
比如检索所有用户,及其所下的订单,没有订单的也要列举出来:
#内部联结,查找用户对应的订单selectc.cust_id,o.order_numfromcustomerscinnerjoinordersoonc.cust_id=o.cust_id;#左外部联结,将没有下单过的顾客行也列出来selectc.cust_id,o.order_numfromcustomerscleftouterjoinordersoonc.cust_id=o.cust_id;#右外部联结,列出所有订单及其顾客,这样没下单过的顾客就不会被列举出来selectc.cust_id,o.order_numfromcustomerscrightouterjoinordersoonc.cust_id=o.cust_id;在使用outerjoin语法时,必须使用right或left关键字指定包括其所有行的表。right指出的是outerjoin右边的表,而left指出的是outerjoin左边的表。上面使用leftouterjoin从from子句的左边表custermers中选择所有行。为了从右边的表中选择所有行,应该使用rightouterjoin。
左外部联结可通过颠倒from或where子句中表的顺序转换为右外部联结,具体用哪个看你方便。
比如想检索一个顾客下过的订单数量,即使没有也要写0,此时使用分组和count聚集函数来统计数量:
#找到每个顾客所下订单的数量,并降序排列selectc.cust_id,c.cust_name,count(o.order_num)count_ordersfromcustomerscleftouterjoinordersoonc.cust_id=o.cust_idgroupbyc.cust_idorderbycount_ordersdesc;因为即使顾客没有下单,也要在结果里,所以把顾客表放在左边,用左外部联结。
MySQL允许执行多条select语句,并将结果作为单个查询结果集返回。这些组合查询通常称为并(union)或复合查询(compoundquery)。
有两种情况需要使用组合查询:
多数情况下,组合查询可以使用具有多个where子句条件的单条查询代替。具体场景可以尝试一下这两种方式,看看对特定的查询哪一种性能更好。
当查询结果来自于多张表,但多张表之间没有关联,这个时候往往使用组合查询。在每条select语句之间放上关键字union即可。
#比如需要列出商品价格小于等于10而且是供应商ID为1005或1003的产品信息selectprod_id,prod_name,prod_price,vend_idfromproductswhereprod_price<=10unionselectprod_id,prod_name,prod_price,vend_idfromproductswherevend_idin(1005,1003);#实际上这句也可以通过where语句代替selectprod_id,prod_name,prod_pricefromproductswhereprod_price<=10orvend_idin(1005,1003);有些情况下,比如更复杂的过滤条件、需要从多个表中检索数据的情况下,使用union可能会更简单。union每个查询必须包含相同的列、表达式、聚集函数,不过每个列不需要以相同的次序列出。列数据类型必须兼容,类型不必完全相同,但必须是数据库管理系统可以隐式的转换。组合查询的排序orderby只能出现在最后一条select语句之后,不能对不同的select语句分别排序。11.2包含或取消重复的行union(all)两行union分开的语句可能会返回重复的行,但前面那个例子实际结果却并没有包含重复行,这是因为union关键字自动去除了重复的行,如果不希望去重,可以使用unionall关键字。
#不去重重复行selectprod_id,prod_name,prod_price,vend_idfromproductswhereprod_price<=10unionallselectprod_id,prod_name,prod_price,vend_idfromproductswherevend_idin(1005,1003);如果需要出现重复行,此时无法使用where关键字来达成同样的效果了。
前面说的都是数据的查询,这一章将所以说数据的增删改。
数据插入使用insert关键字,它可以插入一行、多行数据,也可以插入某些查询的结果。
#插入一条数据到顾客表中insertintocustomersvalues(null,'ZhangSan','001street','ShangHai','SH','666666','ZH',null,null);这里插入一条数据到顾客表中,存储到每个表列中的数据需要在values子句中给出,按照表在创建的时候的顺序依次给出。如果某个列没值就给null。虽然第一条数据对应cust_id列的属性是notnull的,但是这个列是auto_increment也就是自增的,MySQL会自动忽略你给出的null并将值自动增加再填充。
但使用上面values子句这种方式并不安全,因为这种方式注入数据完全靠输入数据的顺序,如果表结构变动,就会导致输入数据错位。
安全的数据插入方式是这样的:
#安全但繁琐的插入方式insertintocustomers(cust_name,cust_address,cust_city,cust_state,cust_zip,cust_country,cust_contact,cust_email)values('ZhangSan','001street','ShangHai','SH','666666','ZH',null,null);这里在前一个括号给出了后面括号中数据对应的列名,这样的话即使表结构或者顺序发生变化,也能正确插入数据。
可以看到列cust_id被省略了,当满足下面条件时,列可以省略:
如果不能省略却省略了,会报错。
insert操作可能很耗时,特别是有很多索引需要更新时,而且它可能降低等待处理的select语句的性能。如果数据检索是最重要的,你可以通过在insert和into之间添加关键字low_priority,降低insert语句的优先级,这也同样适用于下文提到的update和delete语句。
上面介绍的insert语句可以一次插入一个行,如果想一次插入多个行,每次都列出列名就比较繁琐了,可以使用下面这种方式:
#插入多个行insertintocustomers(cust_name,cust_address,cust_city,cust_state,cust_zip,cust_country,cust_contact,cust_email)values('ZhangSan','001street','ShangHai','SH','666666','ZH',null,null),('LiSi','002street','BeiJing','BJ','878787','ZH',null,'123123@163.com');values子句后面继续用括号将字段括起来添加新行,中间加个逗号。这可以提高数据库处理的性能,因为单条insert语句处理多个插入比使用多条insert语句快。
insert可以将一条select语句的结果插入表中,这就是insertselect。比如你想将另一个表中查询的数据插入到这个表中:
#从别的表中找出数据,并插入customers表中insertintocustomers(cust_id,cust_name,cust_address,cust_city,cust_state,cust_zip,cust_country,cust_contact,cust_email)selectid,name,address,city,state,zip,country,contact,emailfromcustnew;select的新行也可以省略cust_id,这样insert的时候也是可以自动生成新的cust_id的。另外可以看到select语句的列名跟insertinto语句后的列名并不对应,这也没关系,因为insertselect使用的是位置对应,select语句返回的第一列对应cust_id,第二列对应cust_name,依次对应。select语句后也可以加入where子句进行过滤。
update语句用来修改表中的数据,使用update的时候一定要小心,不要忘了添加where子句,因为一不小心就会更新表中所有行。
#更新id为10005的用户的信息updatecustomerssetcust_email='888@qq.com'wherecust_id=10005;如果这里没有使用where子句,update将会更新这个表中的所有行的cust_email字段,所以一定要注意。
要删除某行某列的值,可以将值修改为null。
更新多个字段的方式也很简单:
#更新多个字段updatecustomerssetcust_email='666@qq.com',cust_contact='SZhang'wherecust_id=10005;如果用update语句更新多行,并且在更新这些行中的一行或多行时出一个现错误,则整个update操作被取消(错误发生前更新的所有行被恢复到它们原来的值)。为即使是发生错误,也继续进行更新,可以在update后使用ignore关键字。
update语句可以使用子查询,用select语句检索出的数据来更新列数据。
delete语句可以用来从表中删除特定的行或者所有行。使用delete语句的时候要小心,不要忘了添加where子句,因为一不小心就会删除表中所有行。
#删除顾客表中顾客id为10008的行deletefromcustomerswherecust_id=10008;如果将where子句去掉,那么就是删除这个表中的所有行,但不是删除这个表,删除表使用的是另一个语句drop。另外删除一个表中所有行更快的语句是truncatetable,因为delete是逐行删除数据,而truncate是删除原来的表重新建个表。
注意,在使用update和delete之前,应该非常小心,因为MySQL没有撤销,使用之前建议先使用select进行测试,防止where子句不正确导致数据丢失。
CREATETABLEcustomers(cust_idintNOTNULLAUTO_INCREMENT,cust_namechar(50)NOTNULL,cust_addresschar(50)NULL,cust_citychar(50)NULL,cust_statechar(5)NULL,cust_zipchar(10)NULL,cust_countrychar(50)NULLDEFAULT'ZH',#指定默认值cust_contactchar(50)NULL,cust_emailchar(255)NULL,PRIMARYKEY(cust_id)#指定主键)ENGINE=InnoDB;从这里可以看到createtable的格式。
如果要在一个表不存在时创建,应该在表名前、createtable后加上ifnotexists。这样会先检查表名是否已存在,并且在不存在时进行创建。
对于auto_increment,每个表只能有一个auto_increment,而且它必须被索引。当你使用insert语句插入一个新的值,后续自动增量将从这个值重新开始增加。如果一个表创建新的列需要得到最auto_increment的值,可以使用last_insert_id()来获取最后自增的值。
上面创建语句的列名后null表示这个列在插入和修改时允许不给出值,如果是notnull,那么在插入或修改时就必须给值,否则会报错。默认为null,如果不显式的给出notnull,则会默认为null。
primarykey指示主键的值,在插入时主键值必须是不重复的,主键也可以是多个字段primarykey(cust_id,cust_name)用逗号分开。作为主键的列是不能允许null的。
default关键字可以指定默认值,如果插入行没有指定默认值,那么将默认使用默认值。
最后的engine字段指定了不同的引擎,以下是MySQL支持的几个常用的引擎:
根据不同需要可以选择不同引擎。
#给供应商表增加一列vend_phone字段altertablevendorsaddvend_phonechar(20)default12212341234;#删除这个添加的vend_phone字段altertablevendorsdropcolumnvend_phone;altertable经常用来定义外键foreignkey,用于限制两个表的关系,保证该字段的值必须来自于主表的关联列的值。外键具有保持数据完整性和一致性的机制,对业务处理有着很好的校验作用。
外键用来在从表添加外键约束,用于引用主表中某列的值,比如学生表的专业编号,员工表的部门编号,员工表的工种编号,产品表的供应商编号等等。
#将订单信息表的order_num设置为订单表的外键altertableorderitemsaddconstraintfk_orderitems_ordersforeignkey(order_num)referencesorders(order_num);设置外键之后,如果外键已经有对应数据,就不能直接删除主表的这个外键行了:
#直接删除外键行报错,不允许删deletefromorderswhereorder_num=20009;#先删除orderitems中的关联行再删除orders中的外键行,就可以删了deletefromorderswhereorder_num=20009;deletefromorderitemswhereorder_num=20009;所以在插入数据时,先插入主表,再插入从表。删除数据时,先删除从表,再删除主表。
注意:使用altertable要小心,最好在改动之前做一个完整的备份,数据库表的更改不能撤销。如果不小心增加了不需要的列,可能无法删除它们,如果删除了不该删除的列,可能就丢失了数据。
删除一个表可以使用droptable关键字。
droptablecustomers2;删除表没有确认,也没有撤销,执行后将永久删除该表。
如果删除时不存在这个表会报错,可以在droptable关键字后加上ifexists,这样数据库会先检查这个目标表是不是存在:
#删除一个表,如果没加ifexists表又不存在则会报错droptableifexistscustomers2;创建新表时,指定的表名必须不存在,否则会报错,所以在创建前也可以执行这个句子。
重命名一个表可以使用renametable关键字。
#重命名一个表renametablecustomerstocustomers2;#重命名多个表renametablecustomerstocustomers2,vendorstovendors2;14.视图视图是虚拟存在的表,行和列的数据来自定义视图的查询中使用的表,并且是在使用视图时动态生成的,只保存SQL逻辑,不保存查询结果。
比如说现在要查询购买了TNT2产品的顾客信息,按之前介绍的知识使用联结从三个表中查找:
#找到购买了TNT2的顾客信息selectcust_name,cust_contact,cust_email,prod_idfromcustomersc,orderso,orderitemsoiwherec.cust_id=o.cust_idando.order_num=oi.order_numandprod_id='TNT2';那如果现在要换成找到购买了另一个产品的顾客信息呢,重新写一遍查询语句似乎有点重复。程序员永远不做重复的事,如果有一个虚拟表,名为prod_cust,然后使用select*fromprod_custwhereprod_id='TNT1'就可以轻松找到对应的行了,这就是视图。
#创建视图createviewprod_custasselectcust_name,cust_contact,cust_email,prod_idfromcustomersc,orderso,orderitemsoiwherec.cust_id=o.cust_idando.order_num=oi.order_num;#使用视图查询购买了产品TNT2的顾客信息selectcust_name,cust_emailfromprod_custwhereprod_id='TNT2';使用起来挺简单的,可以根据需要编写出可重复使用的视图,方便查询。
视图的使用如下:
createviewprod_custas...;#创建视图showcreateviewprod_cust;#查看创建视图的语句dropviewprod_cust;#删除视图createorreplaceviewprod_custas...;#更新视图如果要修改视图,可以先删除再新建,也可以直接createorreplaceview,如果存在则会替换,如果不存在则会新建。
视图并不直接包含数据,而是一个SQL查询。视图和普通表的关系,就像临时组建的歌唱团和普通班级的关系。
视图创建后,可以像使用表一样使用视图,对视图进行select、过滤、排序、联结等等操作。
使用视图可以:
顺便说一句,创建视图之后,showtables也会显示视图,所以你可以通过下面方式查询所有基表或者视图:
#显示当前database中不包括视图的所有基表selecttable_namefrominformation_schema.tableswheretable_schema='mysql_demo1'andtable_type='BASETABLE';#显示当前database中的所有视图selecttable_namefrominformation_schema.tableswheretable_schema='mysql_demo1'andtable_type='VIEW';14.2使用视图重新格式化检索出的数据比如某个场景,经常会使用到一些格式化的数据,那么就可以使用视图把数据格式化的形式先拼接好:
视图对于简化计算字段的使用很有用,比如希望查找20008的订单的订单总额:
#查找20008订单的订单总额selectorder_num,sum(quantity*item_price)sum_pricefromorderitemswhereorder_num=20008;那么希望查找另一个订单总额时,可以使用视图来改造一下:
#查找订单总额视图createorreplaceviewsum_ordersasselectorder_num,sum(quantity*item_price)sum_pricefromorderitemsgroupbyorder_num;#找另一个订单的总金额selectorder_num,sum_pricefromsum_orderswhereorder_num=20009;看到视图十分好用,实际使用中按需使用视图可以极大方便数据库操作。
视图也是可以使用insert、update、delete更新数据的,虽然视图只是一个SQL句子而不是实际数据。
更新视图的数据会更新其基表,但并非所有视图都可以更新的,如果数据库不冷确定被更新的基数据,则不允许更新。比如分组、联结、子查询、并、聚集函数、distinct等等。
#比如给上面的email不为null的视图添加一行数据,插入会成功insertintocust_has_email(cust_id,cust_name,cust_address,cust_city,cust_state,cust_zip,cust_country,cust_contact)values(10010,'ZhangSan','001street','ShangHai','SH','9999','ZH','LiS');#给查找订单总额视图增加一行会失败,因为这里有分组,数据库不知道在哪插入insertintosum_orders(order_num,order_item,prod_id,quantity,item_price)values(20009,5,'OL1',2,8.99);不过一般,应该把视图用于检索数据select,而不是增删改数据insert、update、delete。
前面介绍的大部分SQL语句都是对一个或者多个表的单个查询,但是实际情况下一个完整的操作可能是由多个语句组合而成的,比如考虑下面这个下单流程:
可以说存储过程就是数据库SQL语言层面上的代码封装和重用,可以回传值,也可以接受参数。可以将其视为批文件,但作用不仅限于批处理。
存储过程简单、安全、高性能。不过有些数据库管理员会限制存储过程的创建权限,只允许用户使用,但不允许用户创建存储过程。
创建存储过程使用createprocedure,可以设置参数,存储过程体使用begin...end分隔开,调用使用call
#创建一个计算平均价格的存储过程createprocedureproduct_pricing(vend_idint)beginselectavg(prod_price)aspriceaveragefromproductswhereproducts.vend_id=vend_id;end;#查看创建存储过程的语句showcreateprocedureproduct_pricing;#调用存储过程查询平均价格callproduct_pricing(1002);这里的存储过程使用了参数,也可以不使用参数,和其他语言中的函数类似。
删除使用drop关键字,如果不存在这个存储过程会报错,此时可以增加ifexists关键字:
#删除存储过程dropprocedureproduct_pricing;#先检查再删除dropprocedureifexistsproduct_pricing;15.3使用参数变量(variable)内存中一个特定的位置,用来临时存储数据。
存储过程输入了4个参数,一个输入参数,还有三个用来存储的参数,每个参数用in(传递给存储过程)、out(从存储过程传出)、inout(对存储过程传入和传出)指定参数。
MySQL中的变量都必须以@开始,存储过程中检索得到的值使用into保存到相应变量,之后可以就可以查询到变量中存储的值了。
#存储过程输入输出参数createprocedureproduct_pricing(invend_idint,outmin_pricedecimal(8,2),outmax_pricedecimal(8,2),outavg_pricedecimal(8,2))beginselectmin(prod_price)intomin_pricefromproductswhereproducts.vend_id=vend_id;selectmax(prod_price)intomax_pricefromproductswhereproducts.vend_id=vend_id;selectavg(prod_price)intoavg_pricefromproductswhereproducts.vend_id=vend_id;end;#调用存储过程查询产品平均价格callproduct_pricing(1002,@minprice,@maxprice,@avgprice);#查询刚刚输出的变量select@minprice,@maxprice,@avgprice;再试个例子,使用存储过程计算出指定订单号的总价,并输出到变量中:
#计算指定订单号的总价格,并输出到变量中createprocedureorder_pricing(inorder_numint,outtotal_pricedecimal(8,2))beginselectsum(quantity*item_price)intototal_pricefromorderitemswhereorderitems.order_num=order_num;end;#计算订单20005的总价callorder_pricing(20005,@totalprice);#查询总价select@totalprice;15.4使用条件语句存储过程也可以使用if(条件)then...elseif(条件)then...else语句,比如现在要计算折扣后的商品价格,总商品数量3件8折,4件7折,这里使用存储过程:
有时,需要在检索出来的行中前进或后退一行或多行,这就是使用游标的原因。游标(cursor)是一个存储在MySQL服务器上的数据库查询,它不是一条select语句,而是被该语句检索出来的结果集。在存储了游标之后,应用可以根据需要滚动或浏览其中的数据。
游标主要用于交互式应用,其中用户需要滚动屏幕上的数据,并对数据进行浏览或做出更改。
MySQL中的游标只能用于存储过程或函数。
游标处理分为下面几个步骤:
下面直接看例子:
declare语句是有顺序的,局部变量需要在句柄之后定义,句柄必须在游标之后定义,否则会报错。
除了repeat循环外,MySQL还支持loop循环、while循环,基本大同小异,可以自己查询学习一下。
如果你想要某些语句在事件发生时自动执行,可以考虑触发器。
创建触发器使用createtrigger关键字,格式如下:
#语法createtrigger[触发器名][after/before][insert/update/delete]on[表名]foreachrowbegin[sql语句]end;#删除droptrigger[ifexists][触发器名];foreachrow表示对每个插入行执行触发器。只有表
#创建一个触发器,在新的产品插入时给临时变量赋值createtriggernewproductafterinsertonproductsforeachrowselect'Productadded'into@newprod;#插入一条语句试试insertintoproducts(prod_id,vend_id,prod_name,prod_price,prod_desc)values('XP2000',1005,'JetPack200',55,'JetPack200,multi-use');select@newprod;17.2使用触发器触发器要谨慎使用,由于触发器是针对每一行的,对增删改非常频繁的表上切记不要使用触发器,因为会非常消耗资源。
#插入用户后获取这个新用户自动生成的的ID并且赋值给临时变量createtriggernewcustafterinsertoncustomersforeachrowselectnew.cust_idinto@newcust_after;#插入用户insertintocustomers(cust_name,cust_address,cust_city,cust_state,cust_zip,cust_country,cust_contact,cust_email)values('ZhangSan','001street','ShangHai','SH','666666','ZH',null,null);#查看新值select@newcust_after;before经常被用于数据验证。
droptriggerifexistsdeletecustomer;#创建触发器,当从顾客表中删除时将删除的数据插入到另一个存档表中createtriggerdeletecustomerbeforedeleteoncustomersforeachrowbegininsertintocustomers2(cust_id,cust_name,cust_address,cust_city,cust_state,cust_zip,cust_country,cust_contact,cust_email)values(old.cust_id,old.cust_name,old.cust_address,old.cust_city,old.cust_state,old.cust_zip,old.cust_country,old.cust_contact,old.cust_email);end;#删除刚刚创建的顾客数据deletefromcustomerswherecust_id=10013;#查询一下存档表中的顾客数据是否存在select*fromcustomers2;这里使用before而不是after的原因是,如果因为某种原因顾客信息不能存档,delete操作将会放弃,避免信息丢失。
droptriggerifexistsupdatecustomer;#使用触发器,将每次更新的cust_country转化为大写createtriggerupdatecustomerbeforeupdateoncustomersforeachrowbeginsetnew.cust_country=upper(new.cust_country);end;#更改数据updatecustomerssetcust_country='zh'wherecust_id=10005;18.管理事务处理事务处理(transactionprocessing)可以用来维护数据库的完整性,它保证成批的MySQL操作要么完全执行,要么完全不执行。
举个例子,如果我们要转账给别人,首先把别人账户增加钱,再把我们账户上钱扣除,如果中间出现问题,那么麻烦就大了。
或者在当前数据库中,如果我们要添加一个订单信息,分为下面几步:
如果发生了某种数据库故障(超出磁盘限制、安全限制、表锁等),阻止了一个完整的流程,会出现什么情况。如果故障出现在1和2之间,这没什么关系,因为一个顾客没有订单信息是合法的,如果出现在3和4之间,那么就会出现一个空的订单,这个订单没有包含的产品信息,这很严重,如果出现在5时,添加orderitems过程中出现问题,那么可能出现订单信息不完整的情况,也很严重。
使用事务可以避免这个情况,如果中间发生了问题,那么则回退到某个安全的状态。
那么使用事务如何处理这个过程呢:
这里有几个概念:
starttransaction标识事务开始,使用rollback可以进行回退从start到rollback中间的所有语句。
#开始事务starttransaction;deletefromcustomerswherecust_id>10005;#删除几个行select*fromcustomers;#使用rollback回滚delete语句rollback;rollback可以回退insert、update、delete语句,但不能回退create、drop语句,事务处理块中可以使用这两个语句,但rollback无效果。
MySQL中用户的任何一个更新操作(写操作)都被视为一个事务,这就是所谓的隐含提交(implicitcommit),相当于MySQL帮你在后台提交了。
可以针对每个连接使用setautocommit=0来设置MySQL不自动提交更改,设置之后,每个SQL语句或者语句块所在的事务都需要显式commit才能提交事务。
但在事务处理块中,提交不会隐含地进行,需要你自己来显式的调用:
在commit或rollback执行时,事务会被自动关闭。
之前的rollback、commit只能对整个事务处理块整体提交或回滚,某些复杂场景下可能要部分回滚或者部分恢复,比如之前例子,如果订单信息增加失败,可能要回滚到添加用户信息后。
此时可以使用保留点,这样在发生问题时回滚到保留点处即可。保留点使用比较简单:
#创建保留点savepointsav1;#回滚到保留点rollbacktosav1;保留点可以使用多一点,当在事务完成时,他们将会被自动释放,也可以使用releasesavpoint来手动释放。
对数据库来说,用户应该对他们需要的数据具有适当的访问权,既不能多也不能少。
给不同的用户提供不同的访问权,这就是访问控制。
对于root登陆的使用需要十分谨慎小心,仅在绝对需要时才使用,不应在日常MySQL操作中使用root账户。
用户信息存储在MySQL的mysql库中:
#查看用户列表usemysql;selectuserfromuser;创建用户账号:
#创建用户及其密码createuserzhangsanidentifiedby'888888';#更改用户名renameuserzhangsantolisi;#删除用户dropuserifexistslisi;设置权限用grant关键字:
#显示用户张三的权限showgrantsforzhangsan;#设置权限,允许张三在mysql_demo1库上使用selectgrantselectonmysql_demo1.*tozhangsan;#撤销权限,撤销张三在mysql_demo1库上使用select的权限revokeselectonmysql_demo1.*fromzhangsan;权限设置和用户设置还有很多内容,不是本文的重点,可以百度一下或者看文档。
数据库也是经常需要备份的,可以使用以下方法:
记录Mysql运行过程中的Error、Warning、Note等信息,系统出错或者某条记录出问题可以查看错误日志。
通过showvariableslike"log_error";来查看错误日志存放的位置。
记录包括查询、修改、更新等的每条语句。
通过showglobalvariableslike"%genera%";查看日常日志存放的地方,如果general_log是off则不能查询,可以通过setglobalgeneral_log=on;打开查询,然后tail-f/var/lib/mysql/VM-0-17-centos.log;来查看。
包含一些事件,描述了数据库的改动,如建表、数据改动等,主要用于备份恢复、回滚操作等。
可以通过showvariableslike"%log_bin%";来查看Binlog存在哪,会有多个文件,使用showmasterlogs;可以看到查看所有Binlog日志列表,格式是bingo.000008这样,当Binlog日志写满或者数据库重启会产生新文件,使用flushlogs可以手动产生新文件,Binlog十分重要,产生问题要回滚用Binlog就可以了。
记录执行缓慢的任何查询,在优化数据库时比较有用。
可以通过showvariableslike"%slow%";来查看缓慢日志存放的地方。