首页> 搜索结果页
"广东高性能SQL Server合适" 检索
共 8 条结果
亿级Web系统搭建:单机到分布式集群
当一个Web系统从日访问量10万逐步增长到1000万,甚至超过1亿的过程中,Web系统承受的压力会越来越大,在这个过程中,我们会遇到很多的问题。为了解决这些性能压力带来问题,我们需要在Web系统架构层面搭建多个层次的缓存机制。在不同的压力阶段,我们会遇到不同的问题,通过搭建不同的服务和架构来解决。 Web负载均衡 Web负载均衡(Load Balancing),简单地说就是给我们的服务器集群分配“工作任务”,而采用恰当的分配方式,对于保护处于后端的Web服务器来说,非常重要。 负载均衡的策略有很多,我们从简单的讲起哈。 1. HTTP重定向 当用户发来请求的时候,Web服务器通过修改HTTP响应头中的Location标记来返回一个新的url,然后浏览器再继续请求这个新url,实际上就是页面重定向。通过重定向,来达到“负载均衡”的目标。例如,我们在下载PHP源码包的时候,点击下载链接时,为了解决不同国家和地域下载速度的问题,它会返回一个离我们近的下载地址。重定向的HTTP返回码是302,如下图: 亿级Web系统搭建——单机到分布式集群 – hansionxu – 技术的天空 如果使用PHP代码来实现这个功能,方式如下: 这个重定向非常容易实现,并且可以自定义各种策略。但是,它在大规模访问量下,性能不佳。而且,给用户的体验也不好,实际请求发生重定向,增加了网络延时。 2. 反向代理负载均衡 反向代理服务的核心工作主要是转发HTTP请求,扮演了浏览器端和后台Web服务器中转的角色。因为它工作在HTTP层(应用层),也就是网络七层结构中的第七层,因此也被称为“七层负载均衡”。可以做反向代理的软件很多,比较常见的一种是Nginx。 Nginx是一种非常灵活的反向代理软件,可以自由定制化转发策略,分配服务器流量的权重等。反向代理中,常见的一个问题,就是Web服务器存储的session数据,因为一般负载均衡的策略都是随机分配请求的。同一个登录用户的请求,无法保证一定分配到相同的Web机器上,会导致无法找到session的问题。 解决方案主要有两种: 配置反向代理的转发规则,让同一个用户的请求一定落到同一台机器上(通过分析cookie),复杂的转发规则将会消耗更多的CPU,也增加了代理服务器的负担。 将session这类的信息,专门用某个独立服务来存储,例如redis/memchache,这个方案是比较推荐的。 反向代理服务,也是可以开启缓存的,如果开启了,会增加反向代理的负担,需要谨慎使用。这种负载均衡策略实现和部署非常简单,而且性能表现也比较好。但是,它有“单点故障”的问题,如果挂了,会带来很多的麻烦。而且,到了后期Web服务器继续增加,它本身可能成为系统的瓶颈。 3. IP负载均衡 IP负载均衡服务是工作在网络层(修改IP)和传输层(修改端口,第四层),比起工作在应用层(第七层)性能要高出非常多。原理是,他是对IP层的数据包的IP地址和端口信息进行修改,达到负载均衡的目的。这种方式,也被称为“四层负载均衡”。常见的负载均衡方式,是LVS(Linux Virtual Server,Linux虚拟服务),通过IPVS(IP Virtual Server,IP虚拟服务)来实现。 在负载均衡服务器收到客户端的IP包的时候,会修改IP包的目标IP地址或端口,然后原封不动地投递到内部网络中,数据包会流入到实际Web服务器。实际服务器处理完成后,又会将数据包投递回给负载均衡服务器,它再修改目标IP地址为用户IP地址,最终回到客户端。 上述的方式叫LVS-NAT,除此之外,还有LVS-RD(直接路由),LVS-TUN(IP隧道),三者之间都属于LVS的方式,但是有一定的区别,篇幅问题,不赘叙。 IP负载均衡的性能要高出Nginx的反向代理很多,它只处理到传输层为止的数据包,并不做进一步的组包,然后直接转发给实际服务器。不过,它的配置和搭建比较复杂。 4. DNS负载均衡 DNS(Domain Name System)负责域名解析的服务,域名url实际上是服务器的别名,实际映射是一个IP地址,解析过程,就是DNS完成域名到IP的映射。而一个域名是可以配置成对应多个IP的。因此,DNS也就可以作为负载均衡服务。 这种负载均衡策略,配置简单,性能极佳。但是,不能自由定义规则,而且,变更被映射的IP或者机器故障时很麻烦,还存在DNS生效延迟的问题。 5. DNS/GSLB负载均衡 我们常用的CDN(Content Delivery Network,内容分发网络)实现方式,其实就是在同一个域名映射为多IP的基础上更进一步,通过GSLB(Global Server Load Balance,全局负载均衡)按照指定规则映射域名的IP。一般情况下都是按照地理位置,将离用户近的IP返回给用户,减少网络传输中的路由节点之间的跳跃消耗。 图中的“向上寻找”,实际过程是LDNS(Local DNS)先向根域名服务(Root Name Server)获取到顶级根的Name Server(例如.com的),然后得到指定域名的授权DNS,然后再获得实际服务器IP。 CDN在Web系统中,一般情况下是用来解决大小较大的静态资源(html/Js/Css/图片等)的加载问题,让这些比较依赖网络下载的内容,尽可能离用户更近,提升用户体验。 例如,我访问了一张imgcache.gtimg.cn上的图片(腾讯的自建CDN,不使用qq.com域名的原因是防止http请求的时候,带上了多余的cookie信息),我获得的IP是183.60.217.90。 这种方式,和前面的DNS负载均衡一样,不仅性能极佳,而且支持配置多种策略。但是,搭建和维护成本非常高。互联网一线公司,会自建CDN服务,中小型公司一般使用第三方提供的CDN。 Web系统的缓存机制的建立和优化 刚刚我们讲完了Web系统的外部网络环境,现在我们开始关注我们Web系统自身的性能问题。我们的Web站点随着访问量的上升,会遇到很多的挑战,解决这些问题不仅仅是扩容机器这么简单,建立和使用合适的缓存机制才是根本。 最开始,我们的Web系统架构可能是这样的,每个环节,都可能只有1台机器。 我们从最根本的数据存储开始看哈。 一、 MySQL数据库内部缓存使用 MySQL的缓存机制,就从先从MySQL内部开始,下面的内容将以最常见的InnoDB存储引擎为主。 1. 建立恰当的索引 最简单的是建立索引,索引在表数据比较大的时候,起到快速检索数据的作用,但是成本也是有的。首先,占用了一定的磁盘空间,其中组合索引最突出,使用需要谨慎,它产生的索引甚至会比源数据更大。其次,建立索引之后的数据insert/update/delete等操作,因为需要更新原来的索引,耗时会增加。当然,实际上我们的系统从总体来说,是以select查询操作居多,因此,索引的使用仍然对系统性能有大幅提升的作用。 2. 数据库连接线程池缓存 如果,每一个数据库操作请求都需要创建和销毁连接的话,对数据库来说,无疑也是一种巨大的开销。为了减少这类型的开销,可以在MySQL中配置thread_cache_size来表示保留多少线程用于复用。线程不够的时候,再创建,空闲过多的时候,则销毁。 其实,还有更为激进一点的做法,使用pconnect(数据库长连接),线程一旦创建在很长时间内都保持着。但是,在访问量比较大,机器比较多的情况下,这种用法很可能会导致“数据库连接数耗尽”,因为建立连接并不回收,最终达到数据库的max_connections(最大连接数)。因此,长连接的用法通常需要在CGI和MySQL之间实现一个“连接池”服务,控制CGI机器“盲目”创建连接数。 建立数据库连接池服务,有很多实现的方式,PHP的话,我推荐使用swoole(PHP的一个网络通讯拓展)来实现。 3. Innodb缓存设置(innodb_buffer_pool_size) innodb_buffer_pool_size这是个用来保存索引和数据的内存缓存区,如果机器是MySQL独占的机器,一般推荐为机器物理内存的80%。在取表数据的场景中,它可以减少磁盘IO。一般来说,这个值设置越大,cache命中率会越高。 4. 分库/分表/分区。 MySQL数据库表一般承受数据量在百万级别,再往上增长,各项性能将会出现大幅度下降,因此,当我们预见数据量会超过这个量级的时候,建议进行分库/分表/分区等操作。最好的做法,是服务在搭建之初就设计为分库分表的存储模式,从根本上杜绝中后期的风险。不过,会牺牲一些便利性,例如列表式的查询,同时,也增加了维护的复杂度。不过,到了数据量千万级别或者以上的时候,我们会发现,它们都是值得的。 二、 MySQL数据库多台服务搭建 1台MySQL机器,实际上是高风险的单点,因为如果它挂了,我们Web服务就不可用了。而且,随着Web系统访问量继续增加,终于有一天,我们发现1台MySQL服务器无法支撑下去,我们开始需要使用更多的MySQL机器。当引入多台MySQL机器的时候,很多新的问题又将产生。 1. 建立MySQL主从,从库作为备份 这种做法纯粹为了解决“单点故障”的问题,在主库出故障的时候,切换到从库。不过,这种做法实际上有点浪费资源,因为从库实际上被闲着了。 2. MySQL读写分离,主库写,从库读。 两台数据库做读写分离,主库负责写入类的操作,从库负责读的操作。并且,如果主库发生故障,仍然不影响读的操作,同时也可以将全部读写都临时切换到从库中(需要注意流量,可能会因为流量过大,把从库也拖垮)。 3. 主主互备。 两台MySQL之间互为彼此的从库,同时又是主库。这种方案,既做到了访问量的压力分流,同时也解决了“单点故障”问题。任何一台故障,都还有另外一套可供使用的服务。 不过,这种方案,只能用在两台机器的场景。如果业务拓展还是很快的话,可以选择将业务分离,建立多个主主互备。 三、 MySQL数据库机器之间的数据同步 每当我们解决一个问题,新的问题必然诞生在旧的解决方案上。当我们有多台MySQL,在业务高峰期,很可能出现两个库之间的数据有延迟的场景。并且,网络和机器负载等,也会影响数据同步的延迟。我们曾经遇到过,在日访问量接近1亿的特殊场景下,出现,从库数据需要很多天才能同步追上主库的数据。这种场景下,从库基本失去效用了。 于是,解决同步问题,就是我们下一步需要关注的点。 1. MySQL自带多线程同步 MySQL5.6开始支持主库和从库数据同步,走多线程。但是,限制也是比较明显的,只能以库为单位。MySQL数据同步是通过binlog日志,主库写入到binlog日志的操作,是具有顺序的,尤其当SQL操作中含有对于表结构的修改等操作,对于后续的SQL语句操作是有影响的。因此,从库同步数据,必须走单进程。 2. 自己实现解析binlog,多线程写入。 以数据库的表为单位,解析binlog多张表同时做数据同步。这样做的话,的确能够加快数据同步的效率,但是,如果表和表之间存在结构关系或者数据依赖的话,则同样存在写入顺序的问题。这种方式,可用于一些比较稳定并且相对独立的数据表。 国内一线互联网公司,大部分都是通过这种方式,来加快数据同步效率。还有更为激进的做法,是直接解析binlog,忽略以表为单位,直接写入。但是这种做法,实现复杂,使用范围就更受到限制,只能用于一些场景特殊的数据库中(没有表结构变更,表和表之间没有数据依赖等特殊表)。 四、 在Web服务器和数据库之间建立缓存 实际上,解决大访问量的问题,不能仅仅着眼于数据库层面。根据“二八定律”,80%的请求只关注在20%的热点数据上。因此,我们应该建立Web服务器和数据库之间的缓存机制。这种机制,可以用磁盘作为缓存,也可以用内存缓存的方式。通过它们,将大部分的热点数据查询,阻挡在数据库之前。 1. 页面静态化 用户访问网站的某个页面,页面上的大部分内容在很长一段时间内,可能都是没有变化的。例如一篇新闻报道,一旦发布几乎是不会修改内容的。这样的话,通过CGI生成的静态html页面缓存到Web服务器的磁盘本地。除了第一次,是通过动态CGI查询数据库获取之外,之后都直接将本地磁盘文件返回给用户。 在Web系统规模比较小的时候,这种做法看似完美。但是,一旦Web系统规模变大,例如当我有100台的Web服务器的时候。那样这些磁盘文件,将会有100份,这个是资源浪费,也不好维护。这个时候有人会想,可以集中一台服务器存起来,呵呵,不如看看下面一种缓存方式吧,它就是这样做的。 2. 单台内存缓存 通过页面静态化的例子中,我们可以知道将“缓存”搭建在Web机器本机是不好维护的,会带来更多问题(实际上,通过PHP的apc拓展,可通过Key/value操作Web服务器的本机内存)。因此,我们选择搭建的内存缓存服务,也必须是一个独立的服务。 内存缓存的选择,主要有redis/memcache。从性能上说,两者差别不大,从功能丰富程度上说,Redis更胜一筹。 3. 内存缓存集群 当我们搭建单台内存缓存完毕,我们又会面临单点故障的问题,因此,我们必须将它变成一个集群。简单的做法,是给他增加一个slave作为备份机器。但是,如果请求量真的很多,我们发现cache命中率不高,需要更多的机器内存呢?因此,我们更建议将它配置成一个集群。例如,类似redis cluster。 Redis cluster集群内的Redis互为多组主从,同时每个节点都可以接受请求,在拓展集群的时候比较方便。客户端可以向任意一个节点发送请求,如果是它的“负责”的内容,则直接返回内容。否则,查找实际负责Redis节点,然后将地址告知客户端,客户端重新请求。 对于使用缓存服务的客户端来说,这一切是透明的。 内存缓存服务在切换的时候,是有一定风险的。从A集群切换到B集群的过程中,必须保证B集群提前做好“预热”(B集群的内存中的热点数据,应该尽量与A集群相同,否则,切换的一瞬间大量请求内容,在B集群的内存缓存中查找不到,流量直接冲击后端的数据库服务,很可能导致数据库宕机)。 4. 减少数据库“写” 上面的机制,都实现减少数据库的“读”的操作,但是,写的操作也是一个大的压力。写的操作,虽然无法减少,但是可以通过合并请求,来起到减轻压力的效果。这个时候,我们就需要在内存缓存集群和数据库集群之间,建立一个修改同步机制。 先将修改请求生效在cache中,让外界查询显示正常,然后将这些sql修改放入到一个队列中存储起来,队列满或者每隔一段时间,合并为一个请求到数据库中更新数据库。 除了上述通过改变系统架构的方式提升写的性能外,MySQL本身也可以通过配置参数innodb_flush_log_at_trx_commit来调整写入磁盘的策略。如果机器成本允许,从硬件层面解决问题,可以选择老一点的RAID(Redundant Arrays of independent Disks,磁盘列阵)或者比较新的SSD(Solid State Drives,固态硬盘)。 5. NoSQL存储 不管数据库的读还是写,当流量再进一步上涨,终会达到“人力有穷时”的场景。继续加机器的成本比较高,并且不一定可以真正解决问题的时候。这个时候,部分核心数据,就可以考虑使用NoSQL的数据库。NoSQL存储,大部分都是采用key-value的方式,这里比较推荐使用上面介绍过Redis,Redis本身是一个内存cache,同时也可以当做一个存储来使用,让它直接将数据落地到磁盘。 这样的话,我们就将数据库中某些被频繁读写的数据,分离出来,放在我们新搭建的Redis存储集群中,又进一步减轻原来MySQL数据库的压力,同时因为Redis本身是个内存级别的Cache,读写的性能都会大幅度提升。 国内一线互联网公司,架构上采用的解决方案很多是类似于上述方案,不过,使用的cache服务却不一定是Redis,他们会有更丰富的其他选择,甚至根据自身业务特点开发出自己的NoSQL服务。 6. 空节点查询问题 当我们搭建完前面所说的全部服务,认为Web系统已经很强的时候。我们还是那句话,新的问题还是会来的。空节点查询,是指那些数据库中根本不存在的数据请求。例如,我请求查询一个不存在人员信息,系统会从各级缓存逐级查找,最后查到到数据库本身,然后才得出查找不到的结论,返回给前端。因为各级cache对它无效,这个请求是非常消耗系统资源的,而如果大量的空节点查询,是可以冲击到系统服务的。 在我曾经的工作经历中,曾深受其害。因此,为了维护Web系统的稳定性,设计适当的空节点过滤机制,非常有必要。 我们当时采用的方式,就是设计一张简单的记录映射表。将存在的记录存储起来,放入到一台内存cache中,这样的话,如果还有空节点查询,则在缓存这一层就被阻挡了。   异地部署(地理分布式) 完成了上述架构建设之后,我们的系统是否就已经足够强大了呢?答案当然是否定的哈,优化是无极限的。Web系统虽然表面上看,似乎比较强大了,但是给予用户的体验却不一定是最好的。因为东北的同学,访问深圳的一个网站服务,他还是会感到一些网络距离上的慢。这个时候,我们就需要做异地部署,让Web系统离用户更近。 一、 核心集中与节点分散 有玩过大型网游的同学都会知道,网游是有很多个区的,一般都是按照地域来分,例如广东专区,北京专区。如果一个在广东的玩家,去北京专区玩,那么他会感觉明显比在广东专区卡。实际上,这些大区的名称就已经说明了,它的服务器所在地,所以,广东的玩家去连接地处北京的服务器,网络当然会比较慢。 当一个系统和服务足够大的时候,就必须开始考虑异地部署的问题了。让你的服务,尽可能离用户更近。我们前面已经提到了Web的静态资源,可以存放在CDN上,然后通过DNS/GSLB的方式,让静态资源的分散“全国各地”。但是,CDN只解决的静态资源的问题,没有解决后端庞大的系统服务还只集中在某个固定城市的问题。 这个时候,异地部署就开始了。异地部署一般遵循:核心集中,节点分散。 核心集中:实际部署过程中,总有一部分的数据和服务存在不可部署多套,或者部署多套成本巨大。而对于这些服务和数据,就仍然维持一套,而部署地点选择一个地域比较中心的地方,通过网络内部专线来和各个节点通讯。 节点分散:将一些服务部署为多套,分布在各个城市节点,让用户请求尽可能选择近的节点访问服务。 例如,我们选择在上海部署为核心节点,北京,深圳,武汉,上海为分散节点(上海自己本身也是一个分散节点)。我们的服务架构如图: 需要补充一下的是,上图中上海节点和核心节点是同处于一个机房的,其他分散节点各自独立机房。 国内有很多大型网游,都是大致遵循上述架构。它们会把数据量不大的用户核心账号等放在核心节点,而大部分的网游数据,例如装备、任务等数据和服务放在地区节点里。当然,核心节点和地域节点之间,也有缓存机制。 二、 节点容灾和过载保护 节点容灾是指,某个节点如果发生故障时,我们需要建立一个机制去保证服务仍然可用。毫无疑问,这里比较常见的容灾方式,是切换到附近城市节点。假如系统的天津节点发生故障,那么我们就将网络流量切换到附近的北京节点上。考虑到负载均衡,可能需要同时将流量切换到附近的几个地域节点。另一方面,核心节点自身也是需要自己做好容灾和备份的,核心节点一旦故障,就会影响全国服务。 过载保护,指的是一个节点已经达到最大容量,无法继续接接受更多请求了,系统必须有一个保护的机制。一个服务已经满负载,还继续接受新的请求,结果很可能就是宕机,影响整个节点的服务,为了至少保障大部分用户的正常使用,过载保护是必要的。 解决过载保护,一般2个方向: 拒绝服务,检测到满负载之后,就不再接受新的连接请求。例如网游登入中的排队。 分流到其他节点。这种的话,系统实现更为复杂,又涉及到负载均衡的问题。 小结 Web系统会随着访问规模的增长,渐渐地从1台服务器可以满足需求,一直成长为“庞然大物”的大集群。而这个Web系统变大的过程,实际上就是我们解决问题的过程。在不同的阶段,解决不同的问题,而新的问题又诞生在旧的解决方案之上。 系统的优化是没有极限的,软件和系统架构也一直在快速发展,新的方案解决了老的问题,同时也带来新的挑战。
文章
Web App开发  ·  存储  ·  关系型数据库  ·  数据库  ·  索引
2015-09-24
腾讯看点基于 Flink 构建万亿数据量下的实时数仓及实时查询系统
本文由社区志愿者路培杰整理,腾讯看点数据团队高级工程师王展雄在 Flink Forward Asia 2020 分享的议题《腾讯看点基于 Flink 构建万亿数据量下的实时数仓及实时查询系统》。内容包括:背景介绍架构设计实时数仓实时数据查询系统实时系统应用成果总结GitHub 地址 https://github.com/apache/flink欢迎大家给 Flink 点赞送 star~一、背景介绍1. 需要解决的业务痛点推荐系统对于推荐同学来说,想知道一个推荐策略在不同人群中的推荐效果是怎么样的。运营对于运营的同学来说,想知道在广东省的用户中,最火的广东地域内容是哪些?方便做地域 push。审核对于审核的同学,想知道过去 5 分钟游戏类被举报最多的内容和账号是哪些,方便能够及时处理。内容创作对于内容的作者,想知道今天到目前为止,内容被多少个用户观看,收到了多少个点赞和转发,方便能够及时调整他的策略。老板决策对于老板来说,想知道过去 10 分钟有多少用户消费了内容,对消费人群有一个宏观的了解。以上这几点都是我们日常工作中经常遇到的业务场景,后面的篇幅中会给出对应的解决方案。2. 开发前调研在进行开发之前我们做了如下这些调研。2.1 离线数据分析平台能否满足这些需求调研的结论是不能满足离线数据分析平台,不行的原因如下:首先用户的消费行为数据上报需要经过 Spark 的多层离线计算,最终结果出库到 MySQL 或者 ES 提供给离线分析平台查询。这个过程的延时至少是 3-6 个小时,目前比较常见的都是提供隔天的查询,所以很多实时性要求高的业务场景都不能满足。另一个问题是腾讯看点的数据量太大,带来的不稳定性也比较大,经常会有预料不到的延迟,所以离线分析平台是无法满足这些需求的。2.2 准实时数据分析平台在腾讯内部提供了准实时数据查询的功能,底层技术用的是 Kudu + Impala,Impala 虽然是 MPP 架构的大数据计算引擎,并且访问以列式存储数据的 Kudu。但是对于实时数据的分析场景来说,它的查询响应速度和数据的延迟都还是比较高的。比如说查询一次实时的 DAU 返回结果的耗时至少是几分钟,无法提供良好的交互式的用户体验。所以 Kudu+Impala 这种通用的大数据处理框架的速度优势,更多的是相比 Spark 加 HDFS 这种离线分析框架来说的,对于我们实时性要求更高的场景是无法满足的。因此需要进行开发,这就涉及到了方案选型和架构设计。3. 腾讯看点信息流的业务流程在大家介绍一下腾讯看点信息流的业务流程,了解了业务的流程,就能够更好的理解技术架构的方案。第 1 步,内容创作者发布内容;第 2 步,内容会经过内容审核系统启用或者下架;第 3 步,启用的内容给到推荐系统和运营系统,分发给 C 侧用户;第 4 步,内容分发给 C 侧用户之后,用户会产生各种行为,比如说曝光、点击举报等,这些行为数据通过埋点上报,实时接入到消息队列中;第 5 步,构建实时数据仓库;第 6 步,构建实时数据查询系统。我们做的工作主要就在第 5 步和第 6 步,可以看一下我们的业务流程图来进一步的了解。在业务流程图中,我们主要做的两部分工作,就是图中有颜色的这两部分:橙色部分,我们构建了一个腾讯看点的实时数据仓库;绿色部分,我们基于了 OLAP 的存储计算引擎,开发了实时数据分析系统。为什么要构建实时数据仓库?因为原始的数据上报数据量非常大,一天上报的峰值就有上万亿条,而且上报的格式非常混乱,缺乏了内容的维度、信息用户的画像信息,下游就根本没有办法直接使用。而我们提供的实时数据仓库,是根据腾讯看点信息流的业务场景,进行了内容维度的关联,用户画像的关联和各种粒度的聚合,下游可以非常方便的使用实时数据,而且实时数据仓库可以提供给下游的用户反复的消费使用,可以大量的减少重复的工作。绿色部分的多维实时数据分析系统,消费了我们提供的实时数据仓库,利用了 OLAP 存储计算引擎,将海量的数据进行高效的存储,再提供高性能的多维实时分析功能。二、架构设计1. 设计的目标与难点首先来看一下数据分析系统的设计目标与难点。我们的实时数据分析系统分为四大模块:实时计算引擎;实时存储引擎;后台服务层;前端展示层。难点主要在于前两个模块,实时计算引擎和实时存储引擎。千万级每秒的海量数据如何实时的接入,并且进行极低延迟的维表关联是有难度的;实时存储引擎如何支持高并发的写入。高可用分布式和高性能的索引查询是比较难的,可以看一下我们的系统架构设计来了解这几个模块的具体实现。2. 系统架构设计关于系统架构的设计,主要从以下几方面来讲。2.1 实时计算接入层主要是从千万级每秒的原始消息队列中拆分出不同业务不同行为数据的微队列。拿 QQ 看点的视频内容来说,拆分过后的数据就只有百万级每秒了。实时计算层主要是负责多行行为流水数据进行 "行转列" 的操作,实时关联用户画像数据和内容维度数据。实时数仓存储层主要就是设计出符合看点的业务,下游好用的实时消息队列。我们暂时提供了两个消息队列,作为实时数仓的两层:第一层是 DWM 层,它是内容 ID 和用户 ID 粒度聚合的,就是说一条数据包含了内容 ID 和用户 ID,然后还有 B 侧的内容维度数据,C 侧的用户行为数据,还有用户画像数据。第二层是 DWS 层,这一层是内容 ID 粒度聚合的,就是一条数据包含了内容 ID、B 侧数据和 C 侧数据。可以看到内容 ID 和用户 ID 粒度的消息,队列流量进一步减小到了 10 万级每秒,内容 ID 粒度更是减小到了万级每秒,并且格式更加清晰,维度信息更加丰富。2.2 实时存储实时写入层主要是负责 Hash 路由,将数据写入;OLAP 存储层是利用 MPP 的存储引擎,设计出符合业务的索引和物化视图,高效存储海量数据;后台接口层是提供了高效的多维实时查询接口。2.3 后台服务后台服务是基于腾讯自研的 RPC 后台服务框架写的,并且会进行一些二级缓存。2.4 前端服务前端采用的是开源组件 Ant Design,利用了 Nginx,反向代理了浏览器的请求到后台服务器上。3. 方案选型关于架构设计的方案选型,我们对比了业内的领先方案,最终选择了最符合我们业务场景的方案。3.1 实时数仓的选型我们选择的是业内比较成熟的 Lambda 架构,它的优点是成熟度高,灵活性高,迁移成本低等等。但是它有一个缺点,实时和离线用了两套代码,可能会存在一个口径修改了数据,但另一个没有修改从而造成数据不一致的问题。我们的解决方案每天都有做数据对账的工作,如果有异常会进行告警。3.2 实时计算引擎的选型我们选择了 Flink 作为实时计算引擎,是因为 Flink 在设计之初就是为了流处理来设计的,Sparks Streaming 严格来说还是微批处理,storm 现在用的已经不是很多了。并且, Flink 还有 exactly-once 的准确性,轻量级的容错机制,低延迟高吞吐,应用性高的特点,所以我们选择了 Flink 作为实时计算引擎。3.3 实时存储引擎我们的要求是需要有维度索引,支持高并发的写入和高性能的多维实时 OLAP 查询。可以看到 HBase,TiDB 和 ES 都不能满足要求。Druid 有一个缺陷,它是按照时序划分 Segment,也就说明无法将同一个内容全部存放在同一个 Segment 上,所以在计算全局的 Top N 的时候就只能够计算近似值。于是我们选择了最近两年大火的 MPP 数据库引擎 Clickhouse,后面我会结合我们的具体使用场景和 Clickhouse 的内核原理,介绍一下 Clickhouse 的优势。三、实时数仓实时数仓也分为三块来介绍:第一是如何构建实时数仓;第二是实时数仓的优点;第三是基于实时数仓,利用 Flink 开发实时应用时候遇到的一些问题。实时数仓这一部分的难度在于它处于一个比较新的领域,并且各个公司各个业务的差距都比较大,怎么样能够设计出方便好用,符合看点信息流业务场景的实时数仓是有难度的。1. 如何构建实时数仓先看一下实时数仓要做什么。实时数仓对外来说就是几个消息队列,不同的消息队列里面存放的是不同聚合粒度的实时数据,包括了内容 ID、用户 ID、C 侧用户行为数据,B 侧内容维度数据和用户画像数据等。搭建实时数仓可以分为三步。1.1数据清洗首先从海量的原始消息队列中进行复杂的数据清洗操作,可以获得格式清晰的实时数据。它的具体操作其实就是在 Flink 的实时计算环节,先按照一分钟的粒度进行了窗口的聚合,在窗口内原本多行的行为数据被转成了一行多列的数据格式。1.2 高性能维表关联第二步是进行高性能的实时维表关联,补充用户画像数据和内容维度数据等。但是海量的用户画像数据是存在于 HDFS 上的,内容维度数据又是存在于 HBase 上的,所以想要极低延迟的维表关联是有技术挑战的。这一块在后文会单独介绍。1.3 不同粒度聚合第三步是将算好的实时数据按照不同的粒度进行聚合,然后放到对应的消息队列中进行保存,可以提供给下游多用户复用,到这里实时数仓就搭建完成了。接下来详细介绍一下第二步中高性能实时维表关联是怎么处理的。几十亿的用户画像数据存放在 HDFS 上,肯定是无法进行高性能的维表关联的,所以需要进行缓存。由于数据量太大,本地缓存的代价不合理,我们采用的是 Redis 进行缓存,具体实现是通过 Spark 批量读取 HDFS 上的画像数据,每天更新 Redis 缓存,内容维度数据存放在 HBase 中。为了不影响线上的业务,我们访问的是 HBase 的备库,而且由于内容维度变化的频率远高于用户画像,所以维度关联的时候,我们需要尽量的关联到实时的 HBase 数据。一分钟窗口的数据,如果直接关联 HBase 的话,耗时是十几分钟,这样会导致任务延迟。我们发现 1000 条数据访问 HBase 是秒级的,而访问 Redis 的话只是毫秒级的,访问 Redis 的速度基本上是访问 HBase 的 1000 倍,所以我们在访问 HBase 的内容之前设置了一层 Redis 缓存,然后通过了监听 HBase-proxy 写流水,通过这样来保证缓存的一致性。这样一分钟的窗口数据,原本关联内容维度数据耗时需要十几分钟,现在就变成了秒级。我们为了防止过期的数据浪费缓存,缓存的过期时间我们设置成了 24 个小时。最后还有一些小的优化,比如说内容数据上报过程中会上报不少非常规的内容 ID,这些内容 ID 在 HBase 中是不存储的,会造成缓存穿透的问题。所以在实时计算的时候,我们直接过滤掉这些内容 ID,防止缓存穿透,又减少了一些时间。另外,因为设置了定时缓存,会引入一个缓存雪崩的问题,所以我们在实时计算的过程中进行了削峰填谷的操作,错开了设置缓存的时间,来缓解缓存雪崩的问题。2. 实时数仓的优点我们可以看一下,在我们建设实时数仓的前后,开发一个实时应用的区别。没有数仓的时候,我们需要消费千万级每秒的原始队列,进行复杂的数据清洗,然后再进行用户画像关联、内容维度关联,才能够拿到符合要求格式的实时数据。开发和扩展的成本都会比较高。如果想开发一个新的应用,又要走一遍流程。现在有了实时数仓之后,如果再想开发一个内容 ID 粒度的实时应用,就直接申请 TPS 万级每秒的 DWS 层消息对列即可,开发成本变低很多,资源消耗小了很多,可扩展性也强了很多。我们看一个实际的例子,开发我们系统的实时数据大屏,原本需要进行如上的所有操作才能够拿到数据,现在只需要消费 DWS 层消息队列写一条 Flink SQL 即可,仅仅会消耗 2 个 CPU 核心和 1GB 的内存。以 50 个消费者为例,建立实时数仓的前后,下游开发一个实时应用,可以减少 98% 的资源消耗,包括了计算资源、存储资源、人力成本和开发人员的学习接入成本等等,并且随着消费者越多节省的就越多,就拿 Redis 存储这一部分来说,一个月就能够省下上百万的人民币。3. Flink 开发过程中遇到的问题总结在利用 Flink 开发实时应用的过程中遇到过不少问题,这里选择几个比较有代表性的给大家分享一下。3.1 实时数据大屏第一个是开发实时数据大屏的时候,开始是通过 Flink SQL 来实现的,功能非常简单,就是计算当天截止到当前累计的点击数,实现的方式也非常简单,输入的 source table 是实时数据仓库的消息队列。输出的 sink table 就是 Redis。 SQL 就是:select sum(click) from sourceTable Group by day time。这个任务看起来是没有问题的,但是实际跑起来数据却无法实时更新,是因为 source table 每到达一条点击数据,累计值都会加一,然后就会往 Redis 中写一条最新的数据。所以当数据量太大的时候,它就会频繁的写 Redis,所以这样就会导致写 Redis 的网络延迟会显得非常高,从而会导致背压数据无法实时更新。我们做了一个简单的优化,用 table API 执行完 SQL 之后,转化成 DataStream,然后通过一个一秒钟的数据窗口,每秒钟仅仅会输出最新的累计值到 Redis 中,这样的数据就可以实时更新了。3.2 Flink state 的 TTLFlink 的 1.6 版本开始引入了 state TTL,开启了 state TTL 之后,Flink 就会为每一个 keyed state 增加一个时间戳字段,通过时间戳字段就可以判断 state 是不是过期,是否需要进行清理。但是如果仅仅从字面意思上理解就会遇到一些问题,在 1.10 版本之前,虽然开启了 state TTL,但是 Flink 默认是不会自动清理过期的 state 的。所以如果是 heap memory backend,就会导致 OOM 的问题;如果是 rocksDB backend,就会导致 state 的状态越来越大,最终会导致重启的时候耗费的时间过长。后面经过调研,我们发现有两种方式可以清理 Flink 的过期的 state。第一种是手动清理,第二种的话是自动清理。我们最终选择的是以手动触发的方式来清理过期的 state。每天在深夜,也就是业务低谷期的时候,我们会对 state 中的数据进行遍历的访问,访问到过期的数据,就会进行清理。为什么我们没有选择 Flink 的自动清理策略,是因为 Flink 在 1.8 版本之前,只有一种自动清理策略,clean up in full snapshot。这种清理策略从名字上来看就知道他是在做全量 snapshot 的时候会进行清理,但是有一个致命的缺陷,它并不会减少本身 state 的大小,而是仅仅把清理过后的 state 做到 snapshot 里面,最终还是会 OOM。并且,它重启之后才能够加载到之前清理过的 state,会导致它频繁的重启。虽然在 1.8 版本之后,增加了两种自动清理的策略,但是因为它是异步清理,所以他的清理时机和使用方式都不如手动清理那么灵活,所以最终我们还是选择了手动触发的方式进行清理。在 1.10 版本之后,默认是选择了自动清理的策略,但是这就要求用户对自动清理策略的时机和策略 有比较好的了解,这样才能够更好的满足业务的需求。3.3 使用 Flink valueState 和 mapState 经验总结虽然通过 valueState 也可以存储 map 结构的数据,但是能够使用 mapState 的地方尽量使用 mapState,最好不要通过 valueState 来存储 map 结构的数据,因为 Flink 对 mapState 是进行了优化的,效率会比 valuState 中存储 map 结构的数据更加高效。比如我们遇到过的一个问题就是使用 valueState 存储了 map 结构的数据,选择的是 rocksDB backend。我们发现磁盘的 IO 变得越来越高,延迟也相应的增加。后面发现是因为 valueState 中修改 map 中的任意一个 key 都会把整个 map 的数据给读出来,然后再写回去,这样会导致 IO 过高。但是 mapState,它每一个 key 在 rocksDB 中都是一条单独的 key,磁盘 IO 的代价就会小很多。3.4 Checkpoint 超时问题我们还遇到过一些问题,比如说 Checkpoint 超时了,当时我们第一个想法就是计算资源不足,并行度不够导致的超时,所以我们直接增加了计算资源,增大了并行度,但是超时的情况并没有得到缓解。后面经过研究才发现是数据倾斜,导致某个节点的 barrier 下发不及时导致的,通过 rebalance 之后才能够解决。总的来说 Flink 功能还是很强的,它文档比较完善,网上资料非常丰富,社区也很活跃,一般遇到问题都能够比较快的找到解决方案。四、实时数据查询系统我们的实时查询系统,多维实时查询系统用的是 Clickhouse 来实现的,这块分为三个部分来介绍。第一是分布式高可用,第二是海量数据的写入,第三是高性能的查询。Click house 有很多表引擎,表引擎决定了数据以什么方式存储,以什么方式加载,以及数据表拥有什么样的特性?目前 Clickhouse 拥有 merge tree、replaceingMerge Tree、AggregatingMergeTree、外存、内存、IO 等 20 多种表引擎,其中最体现 Clickhouse 性能特点的是 merge tree 及其家族表引擎,并且当前 Clickhouse 也只有 merge 及其家族表引擎支持了主键索引、数据分区、数据副本等优秀的特性。我们当前使用的也是 Clickhouse 的 merge tree 及其家族表引擎,接下来的介绍都是基于引擎展开的。1. 分布式高可用先看分布式高可用,不管单节点的性能多强,随着业务的增长,早晚都会有遇到瓶颈的一天,而且意外的宕机在计算机的运行中是无法避免的。Clickhouse 通过分片来水平扩展集群,将总的数据水平分成 m 分,然后每个分片中保存一份数据,避开了单节点的性能瓶颈,然后通过副本即每个分片拥有若干个数据一样的副本来保障集群的高可用。再看看 Clickhouse 默认的高可用方案,数据写入是通过分布式表写入,然后分布式表会将数据同时写入到同一个分片的所有副本里面。这里会有一个问题,如果副本 0 写入成功,副本 1 写入失败,那么就会造成同一个分片的不同副本数据不一致的问题,所以默认的高可用方案是不能够用于生产环境的。我们这里听取的是 Clickhouse 官方的建议,借助了 Zookeeper 实现高可用的方案,数据写入一个分片的时候,仅仅写入一个副本,然后再写 Zookeeper,通过 Zookeeper 告诉同一个分片的其他副本,再过来拉取数据,保证数据的一致性。接下来看一下 Clickhouse 实现这种高可用方案的底层原理,这种高可用的方案需要通过 Clickhouse 的 replicated merge tree 表引擎来实现,其中在 replicated merge tree 表引擎的核心代码中,有大量跟 Zookeeper 进行交互的逻辑,从而实现了多个副本的协同,包括主副本的选举写入任务队列的变更和副本状态的变化等等。可以看到外部数据写入 Clickhouse 的一个分片,会先写入一个副本的内存中,在内存中按照指定的条件排好序,再写入磁盘的一个临时目录。最后将临时目录重命名为最终目录的名字,写完之后通过 Zookeeper 进行一系列的交互,实现数据的复制。这里没有选用消息队列进行数据的同步,是因为 Zookeeper 更加轻量级,而且写的时候任意写一个副本,其他的副本都能够通过读 Zookeeper 获得一致性的数据,而且就算其他节点第一次来获取数据失败了,后面只要发现它跟 Zookeeper 上的数据记录不一致,就会再次尝试获取数据,保证数据的一致性。2. 海量数据的写入2.1 Append + Merge数据写入遇到的第一个问题是海量数据直接写 Clickhouse 是会失败的。Clickhouse 的 merge tree 家族表引擎的底层原理类似于 LSM tree,数据是通过 append 的方式写入,后续再启动 merge 线程,将小的数据文件进行合并。了解了 Clickhouse merge tree 家族表引擎的写入过程,我们就会发现以下两个问题。如果一次写入的数据太少,比如一条数据只写一次,就会产生大量的文件目录。当后台合并线程来不及合并的时候,文件目录的数量就会越来越多,这会导致 Clickhouse 抛出 too many parts 的异常,写入失败。另外,之前介绍的每一次写入除了数据本身,Clickhouse 还会需要跟 Zookeeper 进行 10 来次的数据交互,而我们知道 Zookeeper 本身是不能够承受很高的并发的,所以可以看到写入 Clickhouse QPS 过高,导致 zookeeper 的崩溃。我们采用的解决方案是改用 batch 的方式写入,写入 zookeeper 一个 batch 的数据,产生一个数据目录,然后再与 Zookeeper 进行一次数据交互。那么 batch 设置多大?如果 batch 太小的话,就缓解不了 Zookeeper 的压力;但是 batch 也不能设置的太大,要不然上游的内存压力以及数据的延迟都会比较大。所以通过实验,最终我们选择了大小几十万的 batch,这样可以避免了 QPS 太高带来的问题。其实当前的方案还是有优化空间的,比如说 Zookeeper 无法线性扩展,我有了解到业内有些团队就把 Mark 和 date part 相关的信息不写入 Zookeeper。这样能够减少 Zookeeper 的压力。不过这样涉及到了对源代码的修改,对于一般的业务团队来说,实现的成本就会比较高。2.2 分布式表写入如果数据写入通过分布式表写入会遇到单点的磁盘问题,先介绍一下分布式表,分布式表实际上是一张逻辑表,它本身并不存储真实的数据,可以理解为一张代理表,比如用户查询分布式表,分布式表会将查询请求下发到每一个分片的本地表上进行查询,然后再收集每一个本地表的查询结果,汇总之后再返回给用户。那么用户写入分布式表的场景,是用户将一个大的 batch 的数据写入分布式表,然后分布式表示按照一定的规则,将大的 batch 的数据划分为若干个 mini batch 的数据,存储到不同的分片上。这里有一个很容易误解的地方,我们最开始也是以为分布式表只是按照一定的规则做一个网络的转发,以为万兆网卡的带宽就足够,不会出现单点的性能瓶颈。但是实际上 Clickhouse 是这样做的,我们看一个例子,有三个分片 shard1,shard2 和 shard3,其中分布式表建立在 shard2 的节点上。第一步,我们给分布式表写入 300 条数据,分布式表会根据路由规则把数据进行分组,假设 shard1 分到 50 条,shard2 分到 150 条,shard3 分到 100 条。第二步,因为分布式表跟 shard2 是在同一台机器上,所以 shard2 的 150 条就直接写入磁盘了。然后 shard1 的 50 条和 shard3 的 100 条,并不是直接转发给他们的,而是也会在分布式表的机器上先写入磁盘的临时目录。第三步,分布式表节点 shard2 会向 shard1 节点和 shard3 节点分别发起远程连接的请求,将对应临时目录的数据发送给 shard1 和 shard3。这里可以看到分布式表所在的节点 shard2 全量数据都会先落在磁盘上,我们知道磁盘的读写速度都是不够快的,很容易就会出现单点的磁盘性能瓶颈。比如单 QQ 看点的视频内容,每天可能写入百亿级的数据,如果写一张分布式表,很容易就会造成单台机器出现磁盘的瓶颈,尤其是 Clickhouse 的底层运用的是 merge tree,它在合并的过程中会存在写放大的问题,这样会加重磁盘的压力。我们做的两个优化方案:第一个就是对磁盘做了 RAID 提升了磁盘的 IO;第二就是在写入之前,上游进行了数据的划分分表操作,直接分开写入到不同的分片上,磁盘的压力直接变为了原来的 n 分之一,这样就很好的避免了磁盘的单点的瓶颈。2.3 局部 Top 并非全局 Top虽然我们的写入是按照分片进行了划分,但是这里引入了一个分布式系统常见的问题,就是局部的 Top 并非全局 Top。比如说同一个内容 x 的数据落在了不同的分片上,计算全局 Top100 点击内容的时候,之前说到分布式表会将查询请求下发到各个分片上,计算局部的 Top100 点击的内容,然后将结果进行汇总。举个例子,内容 x 在分片一和分片二上不是 Top100,所以在汇总数据的时候就会丢失掉分片一和分片二上的内容 x 的点击数据。第二是会造成数据错误,我们做的优化就是在写入之前加上了一层路由,我们将同一个内容 ID 的数据全部路由到了同一个分片上,解决了该问题。这里需要多说一下,现在最新版的 Clickhouse 都是不存在这样这个问题的,对于有 group by 和 limit 的 SQL 命令,只把 group by 语句下发到本地表进行执行,然后各个本地表执行完的全量结果都会传到分布式表,在分布式表再进行一次全局的 group by,最后再做 limit 的操作。这样虽然能够保证全局 top N 的正确性,但代价就是牺牲了一部分的执行性能。如果想要恢复到更高的执行性能,我们可以通过 Clickhouse 提供的 distributed_group_by_no_merge 参数来选择执行的方式。然后再将同一个内容 ID 的记录全部路由到同一个分片上,这样在本地表也能够执行 limit 操作。3. 高性能的存储和查询Clickhouse 高性能查询的一个关键点,就是稀疏索引。稀疏索引这个设计很有讲究,设计的好可以加速查询,设计的不好反而会影响查询效率。因为我们的查询大部分都是时间和内容 ID 相关的,比如说某个内容过去 n 分钟在各个人群的表现如何,我按照日期分钟粒度时间和内容 ID 建立了稀疏索引,针对某个内容的查询,建立稀疏索引之后,可以减少 99% 的文件扫描。Clickhouse 高性能查询的第二点,就是我们现在的数据量太大,维度太多,拿 QQ 看点的视频内容来说,一天入库的流水就有上百亿条,有些维度有几百个类别,如果一次性把所有的维度进行预聚合查询反而会变慢,并且索引会占用大量的存储空间。我们的优化就是针对不同的维度建立对应的预聚合和物化视图,用空间换时间,这样可以缩短查询的时间。举个例子,通过 summary merge tree 建立一个内容 ID 粒度聚合的累积,累加 pv 的物化视图,这样相当于提前进行了 group by 的计算,等真正需要查询聚合结果的时候,就直接查询物化视图,数据都是已经聚合计算过的,且数据的扫描量只是原始流水的千分之一。分布式表查询还会有一个问题,就是查询单个内容 ID 的时候,分布式表会将查询请求下发到所有的分片上,然后再返回给查询结果进行汇总。实际上因为做过路由,一个内容 ID 只存在于一个分片上,剩下的分片其实都是在空跑。针对这类的查询,我们的优化就是后台按照同样的规则先进行路由,然后再查询目标分片,这样减少了 n 分之 n -1 的负载,可以大量的缩短查询时间。而且由于我们提供的是 OLAP 的查询,数据满足最终的一致性即可。所以通过主从副本的读写分离,也可以进一步的提升性能。我们在后台还做了一个一分钟的数据缓存,这样针对相同条件的查询,后台就可以直接返回。4. Clickhouse 扩容方案我们调研了业内一些常见的方案:比如说 HBase 原始数据是存放在 HDFS 上的,扩容只是 region server 的扩容,并不涉及到原始数据的迁移。但是 Clickhouse 的每个分片数据都是在本地,更像是 RocksDB 的底层存储引擎,不能像 HBase 那样方便的扩容。然后是 Redis,Redis 是 Hash 槽这一种,类似于一致性 Hash 的方式,是比较经典的分布式缓存方案。Redis slot 在 Hash 的过程中,虽然会存在短暂的 ASK 不可用,但是总体上来说迁移还是比较方便的。就从原来的 h0 迁移迁移到 h1,最后再删除 h0,但是 Clickhouse 大部分都是 OLAP 的批量查询,而且由于列式存储不支持删除的特性,一致性 hash 的方案也不是很合适。我们目前的扩容方案就是从实时数仓另外消费一份数据写入新的 Clickhouse 集群,两个集群一起跑一段时间,因为实时数据我们现在就保存了三天,等三天之后,后台服务就直接访问新的 Clickhouse 集群。五、实时系统应用成果总结我们输出了腾讯看点的实时数据仓库,DWM 层和 DWS 层两个消息队列,上线了腾讯看点的实时数据分析系统,该系统能够亚秒级的响应多维条件查询请求。在未命中缓存的情况下:过去 30 分钟的内容查询,99% 的请求耗时在一秒内;过去 24 小时的内容查询 90% 的请求耗时在 5 秒内,99% 的请求耗时在 10 秒内。热点推荐Flink Forward Asia 2021 正式启动!议题火热征集中!Flink 1.14 新特性预览Apache Flink 在汽车之家的应用与实践37 手游基于 Flink CDC + Hudi 湖仓一体方案实践更多 Flink 相关技术问题,可扫码加入社区钉钉交流群第一时间获取最新技术文章和社区动态,请关注公众号~活动推荐阿里云基于 Apache Flink 构建的企业级产品-实时计算Flink版现开启活动:99 元试用 实时计算Flink版(包年包月、10CU)即有机会获得 Flink 独家定制T恤;另包 3 个月及以上还有 85 折优惠!了解活动详情:https://www.aliyun.com/product/bigdata/sc
文章
存储  ·  消息中间件  ·  缓存  ·  NoSQL  ·  搜索推荐  ·  分布式数据库  ·  Redis  ·  流计算  ·  索引  ·  Hbase
2021-09-29
很不错的文章---【问底】徐汉彬:亿级Web系统搭建——单机到分布式集群
原文:很不错的文章---【问底】徐汉彬:亿级Web系统搭建——单机到分布式集群 【导读】徐汉彬曾在阿里巴巴和腾讯从事4年多的技术研发工作,负责过日请求量过亿的Web系统升级与重构,目前在小满科技创业,从事SaaS服务技术建设。  大规模流量的网站架构,从来都是慢慢“成长”而来。而这个过程中,会遇到很多问题,在不断解决问题的过程中,Web系统变得越来越大。并且,新的挑战又往往出现在旧的解决方案之上。希望这篇文章能够为技术人员提供一定的参考和帮助。  以下为原文 当一个Web系统从日访问量10万逐步增长到1000万,甚至超过1亿的过程中,Web系统承受的压力会越来越大,在这个过程中,我们会遇到很多的问题。为了解决这些性能压力带来问题,我们需要在Web系统架构层面搭建多个层次的缓存机制。在不同的压力阶段,我们会遇到不同的问题,通过搭建不同的服务和架构来解决。 Web负载均衡  Web负载均衡(Load Balancing),简单地说就是给我们的服务器集群分配“工作任务”,而采用恰当的分配方式,对于保护处于后端的Web服务器来说,非常重要。   负载均衡的策略有很多,我们从简单的讲起哈。 1. HTTP重定向 当用户发来请求的时候,Web服务器通过修改HTTP响应头中的Location标记来返回一个新的url,然后浏览器再继续请求这个新url,实际上就是页面重定向。通过重定向,来达到“负载均衡”的目标。例如,我们在下载PHP源码包的时候,点击下载链接时,为了解决不同国家和地域下载速度的问题,它会返回一个离我们近的下载地址。重定向的HTTP返回码是302,如下图:   如果使用PHP代码来实现这个功能,方式如下:   这个重定向非常容易实现,并且可以自定义各种策略。但是,它在大规模访问量下,性能不佳。而且,给用户的体验也不好,实际请求发生重定向,增加了网络延时。 2. 反向代理负载均衡 反向代理服务的核心工作主要是转发HTTP请求,扮演了浏览器端和后台Web服务器中转的角色。因为它工作在HTTP层(应用层),也就是网络七层结构中的第七层,因此也被称为“七层负载均衡”。可以做反向代理的软件很多,比较常见的一种是Nginx。   Nginx是一种非常灵活的反向代理软件,可以自由定制化转发策略,分配服务器流量的权重等。反向代理中,常见的一个问题,就是Web服务器存储的session数据,因为一般负载均衡的策略都是随机分配请求的。同一个登录用户的请求,无法保证一定分配到相同的Web机器上,会导致无法找到session的问题。 解决方案主要有两种:   配置反向代理的转发规则,让同一个用户的请求一定落到同一台机器上(通过分析cookie),复杂的转发规则将会消耗更多的CPU,也增加了代理服务器的负担。 将session这类的信息,专门用某个独立服务来存储,例如redis/memchache,这个方案是比较推荐的。   反向代理服务,也是可以开启缓存的,如果开启了,会增加反向代理的负担,需要谨慎使用。这种负载均衡策略实现和部署非常简单,而且性能表现也比较好。但是,它有“单点故障”的问题,如果挂了,会带来很多的麻烦。而且,到了后期Web服务器继续增加,它本身可能成为系统的瓶颈。 3. IP负载均衡 IP负载均衡服务是工作在网络层(修改IP)和传输层(修改端口,第四层),比起工作在应用层(第七层)性能要高出非常多。原理是,他是对IP层的数据包的IP地址和端口信息进行修改,达到负载均衡的目的。这种方式,也被称为“四层负载均衡”。常见的负载均衡方式,是LVS(Linux Virtual Server,Linux虚拟服务),通过IPVS(IP Virtual Server,IP虚拟服务)来实现。   在负载均衡服务器收到客户端的IP包的时候,会修改IP包的目标IP地址或端口,然后原封不动地投递到内部网络中,数据包会流入到实际Web服务器。实际服务器处理完成后,又会将数据包投递回给负载均衡服务器,它再修改目标IP地址为用户IP地址,最终回到客户端。    上述的方式叫LVS-NAT,除此之外,还有LVS-RD(直接路由),LVS-TUN(IP隧道),三者之间都属于LVS的方式,但是有一定的区别,篇幅问题,不赘叙。 IP负载均衡的性能要高出Nginx的反向代理很多,它只处理到传输层为止的数据包,并不做进一步的组包,然后直接转发给实际服务器。不过,它的配置和搭建比较复杂。 4. DNS负载均衡 DNS(Domain Name System)负责域名解析的服务,域名url实际上是服务器的别名,实际映射是一个IP地址,解析过程,就是DNS完成域名到IP的映射。而一个域名是可以配置成对应多个IP的。因此,DNS也就可以作为负载均衡服务。   这种负载均衡策略,配置简单,性能极佳。但是,不能自由定义规则,而且,变更被映射的IP或者机器故障时很麻烦,还存在DNS生效延迟的问题。 5. DNS/GSLB负载均衡 我们常用的CDN(Content Delivery Network,内容分发网络)实现方式,其实就是在同一个域名映射为多IP的基础上更进一步,通过GSLB(Global Server Load Balance,全局负载均衡)按照指定规则映射域名的IP。一般情况下都是按照地理位置,将离用户近的IP返回给用户,减少网络传输中的路由节点之间的跳跃消耗。    图中的“向上寻找”,实际过程是LDNS(Local DNS)先向根域名服务(Root Name Server)获取到顶级根的Name Server(例如.com的),然后得到指定域名的授权DNS,然后再获得实际服务器IP。   CDN在Web系统中,一般情况下是用来解决大小较大的静态资源(html/Js/Css/图片等)的加载问题,让这些比较依赖网络下载的内容,尽可能离用户更近,提升用户体验。 例如,我访问了一张imgcache.gtimg.cn上的图片(腾讯的自建CDN,不使用qq.com域名的原因是防止http请求的时候,带上了多余的cookie信息),我获得的IP是183.60.217.90。    这种方式,和前面的DNS负载均衡一样,不仅性能极佳,而且支持配置多种策略。但是,搭建和维护成本非常高。互联网一线公司,会自建CDN服务,中小型公司一般使用第三方提供的CDN。      Web系统的缓存机制的建立和优化 刚刚我们讲完了Web系统的外部网络环境,现在我们开始关注我们Web系统自身的性能问题。我们的Web站点随着访问量的上升,会遇到很多的挑战,解决这些问题不仅仅是扩容机器这么简单,建立和使用合适的缓存机制才是根本。 最开始,我们的Web系统架构可能是这样的,每个环节,都可能只有1台机器。    我们从最根本的数据存储开始看哈。 一、 MySQL数据库内部缓存使用 MySQL的缓存机制,就从先从MySQL内部开始,下面的内容将以最常见的InnoDB存储引擎为主。 1. 建立恰当的索引 最简单的是建立索引,索引在表数据比较大的时候,起到快速检索数据的作用,但是成本也是有的。首先,占用了一定的磁盘空间,其中组合索引最突出,使用需要谨慎,它产生的索引甚至会比源数据更大。其次,建立索引之后的数据insert/update/delete等操作,因为需要更新原来的索引,耗时会增加。当然,实际上我们的系统从总体来说,是以select查询操作居多,因此,索引的使用仍然对系统性能有大幅提升的作用。 2. 数据库连接线程池缓存 如果,每一个数据库操作请求都需要创建和销毁连接的话,对数据库来说,无疑也是一种巨大的开销。为了减少这类型的开销,可以在MySQL中配置thread_cache_size来表示保留多少线程用于复用。线程不够的时候,再创建,空闲过多的时候,则销毁。    其实,还有更为激进一点的做法,使用pconnect(数据库长连接),线程一旦创建在很长时间内都保持着。但是,在访问量比较大,机器比较多的情况下,这种用法很可能会导致“数据库连接数耗尽”,因为建立连接并不回收,最终达到数据库的max_connections(最大连接数)。因此,长连接的用法通常需要在CGI和MySQL之间实现一个“连接池”服务,控制CGI机器“盲目”创建连接数。    建立数据库连接池服务,有很多实现的方式,PHP的话,我推荐使用swoole(PHP的一个网络通讯拓展)来实现。 3. Innodb缓存设置(innodb_buffer_pool_size) innodb_buffer_pool_size这是个用来保存索引和数据的内存缓存区,如果机器是MySQL独占的机器,一般推荐为机器物理内存的80%。在取表数据的场景中,它可以减少磁盘IO。一般来说,这个值设置越大,cache命中率会越高。 4. 分库/分表/分区。 MySQL数据库表一般承受数据量在百万级别,再往上增长,各项性能将会出现大幅度下降,因此,当我们预见数据量会超过这个量级的时候,建议进行分库/分表/分区等操作。最好的做法,是服务在搭建之初就设计为分库分表的存储模式,从根本上杜绝中后期的风险。不过,会牺牲一些便利性,例如列表式的查询,同时,也增加了维护的复杂度。不过,到了数据量千万级别或者以上的时候,我们会发现,它们都是值得的。 二、 MySQL数据库多台服务搭建 1台MySQL机器,实际上是高风险的单点,因为如果它挂了,我们Web服务就不可用了。而且,随着Web系统访问量继续增加,终于有一天,我们发现1台MySQL服务器无法支撑下去,我们开始需要使用更多的MySQL机器。当引入多台MySQL机器的时候,很多新的问题又将产生。 1. 建立MySQL主从,从库作为备份 这种做法纯粹为了解决“单点故障”的问题,在主库出故障的时候,切换到从库。不过,这种做法实际上有点浪费资源,因为从库实际上被闲着了。   2. MySQL读写分离,主库写,从库读。 两台数据库做读写分离,主库负责写入类的操作,从库负责读的操作。并且,如果主库发生故障,仍然不影响读的操作,同时也可以将全部读写都临时切换到从库中(需要注意流量,可能会因为流量过大,把从库也拖垮)。    3. 主主互备。 两台MySQL之间互为彼此的从库,同时又是主库。这种方案,既做到了访问量的压力分流,同时也解决了“单点故障”问题。任何一台故障,都还有另外一套可供使用的服务。    不过,这种方案,只能用在两台机器的场景。如果业务拓展还是很快的话,可以选择将业务分离,建立多个主主互备。 三、 MySQL数据库机器之间的数据同步 每当我们解决一个问题,新的问题必然诞生在旧的解决方案上。当我们有多台MySQL,在业务高峰期,很可能出现两个库之间的数据有延迟的场景。并且,网络和机器负载等,也会影响数据同步的延迟。我们曾经遇到过,在日访问量接近1亿的特殊场景下,出现,从库数据需要很多天才能同步追上主库的数据。这种场景下,从库基本失去效用了。 于是,解决同步问题,就是我们下一步需要关注的点。 1. MySQL自带多线程同步 MySQL5.6开始支持主库和从库数据同步,走多线程。但是,限制也是比较明显的,只能以库为单位。MySQL数据同步是通过binlog日志,主库写入到binlog日志的操作,是具有顺序的,尤其当SQL操作中含有对于表结构的修改等操作,对于后续的SQL语句操作是有影响的。因此,从库同步数据,必须走单进程。 2. 自己实现解析binlog,多线程写入。 以数据库的表为单位,解析binlog多张表同时做数据同步。这样做的话,的确能够加快数据同步的效率,但是,如果表和表之间存在结构关系或者数据依赖的话,则同样存在写入顺序的问题。这种方式,可用于一些比较稳定并且相对独立的数据表。    国内一线互联网公司,大部分都是通过这种方式,来加快数据同步效率。还有更为激进的做法,是直接解析binlog,忽略以表为单位,直接写入。但是这种做法,实现复杂,使用范围就更受到限制,只能用于一些场景特殊的数据库中(没有表结构变更,表和表之间没有数据依赖等特殊表)。 四、 在Web服务器和数据库之间建立缓存 实际上,解决大访问量的问题,不能仅仅着眼于数据库层面。根据“二八定律”,80%的请求只关注在20%的热点数据上。因此,我们应该建立Web服务器和数据库之间的缓存机制。这种机制,可以用磁盘作为缓存,也可以用内存缓存的方式。通过它们,将大部分的热点数据查询,阻挡在数据库之前。    1. 页面静态化 用户访问网站的某个页面,页面上的大部分内容在很长一段时间内,可能都是没有变化的。例如一篇新闻报道,一旦发布几乎是不会修改内容的。这样的话,通过CGI生成的静态html页面缓存到Web服务器的磁盘本地。除了第一次,是通过动态CGI查询数据库获取之外,之后都直接将本地磁盘文件返回给用户。   在Web系统规模比较小的时候,这种做法看似完美。但是,一旦Web系统规模变大,例如当我有100台的Web服务器的时候。那样这些磁盘文件,将会有100份,这个是资源浪费,也不好维护。这个时候有人会想,可以集中一台服务器存起来,呵呵,不如看看下面一种缓存方式吧,它就是这样做的。 2. 单台内存缓存 通过页面静态化的例子中,我们可以知道将“缓存”搭建在Web机器本机是不好维护的,会带来更多问题(实际上,通过PHP的apc拓展,可通过Key/value操作Web服务器的本机内存)。因此,我们选择搭建的内存缓存服务,也必须是一个独立的服务。 内存缓存的选择,主要有redis/memcache。从性能上说,两者差别不大,从功能丰富程度上说,Redis更胜一筹。    3. 内存缓存集群 当我们搭建单台内存缓存完毕,我们又会面临单点故障的问题,因此,我们必须将它变成一个集群。简单的做法,是给他增加一个slave作为备份机器。但是,如果请求量真的很多,我们发现cache命中率不高,需要更多的机器内存呢?因此,我们更建议将它配置成一个集群。例如,类似redis cluster。 Redis cluster集群内的Redis互为多组主从,同时每个节点都可以接受请求,在拓展集群的时候比较方便。客户端可以向任意一个节点发送请求,如果是它的“负责”的内容,则直接返回内容。否则,查找实际负责Redis节点,然后将地址告知客户端,客户端重新请求。    对于使用缓存服务的客户端来说,这一切是透明的。   内存缓存服务在切换的时候,是有一定风险的。从A集群切换到B集群的过程中,必须保证B集群提前做好“预热”(B集群的内存中的热点数据,应该尽量与A集群相同,否则,切换的一瞬间大量请求内容,在B集群的内存缓存中查找不到,流量直接冲击后端的数据库服务,很可能导致数据库宕机)。 4. 减少数据库“写” 上面的机制,都实现减少数据库的“读”的操作,但是,写的操作也是一个大的压力。写的操作,虽然无法减少,但是可以通过合并请求,来起到减轻压力的效果。这个时候,我们就需要在内存缓存集群和数据库集群之间,建立一个修改同步机制。 先将修改请求生效在cache中,让外界查询显示正常,然后将这些sql修改放入到一个队列中存储起来,队列满或者每隔一段时间,合并为一个请求到数据库中更新数据库。    除了上述通过改变系统架构的方式提升写的性能外,MySQL本身也可以通过配置参数innodb_flush_log_at_trx_commit来调整写入磁盘的策略。如果机器成本允许,从硬件层面解决问题,可以选择老一点的RAID(Redundant Arrays of independent Disks,磁盘列阵)或者比较新的SSD(Solid State Drives,固态硬盘)。 5. NoSQL存储 不管数据库的读还是写,当流量再进一步上涨,终会达到“人力有穷时”的场景。继续加机器的成本比较高,并且不一定可以真正解决问题的时候。这个时候,部分核心数据,就可以考虑使用NoSQL的数据库。NoSQL存储,大部分都是采用key-value的方式,这里比较推荐使用上面介绍过Redis,Redis本身是一个内存cache,同时也可以当做一个存储来使用,让它直接将数据落地到磁盘。 这样的话,我们就将数据库中某些被频繁读写的数据,分离出来,放在我们新搭建的Redis存储集群中,又进一步减轻原来MySQL数据库的压力,同时因为Redis本身是个内存级别的Cache,读写的性能都会大幅度提升。    国内一线互联网公司,架构上采用的解决方案很多是类似于上述方案,不过,使用的cache服务却不一定是Redis,他们会有更丰富的其他选择,甚至根据自身业务特点开发出自己的NoSQL服务。 6. 空节点查询问题 当我们搭建完前面所说的全部服务,认为Web系统已经很强的时候。我们还是那句话,新的问题还是会来的。空节点查询,是指那些数据库中根本不存在的数据请求。例如,我请求查询一个不存在人员信息,系统会从各级缓存逐级查找,最后查到到数据库本身,然后才得出查找不到的结论,返回给前端。因为各级cache对它无效,这个请求是非常消耗系统资源的,而如果大量的空节点查询,是可以冲击到系统服务的。   在我曾经的工作经历中,曾深受其害。因此,为了维护Web系统的稳定性,设计适当的空节点过滤机制,非常有必要。 我们当时采用的方式,就是设计一张简单的记录映射表。将存在的记录存储起来,放入到一台内存cache中,这样的话,如果还有空节点查询,则在缓存这一层就被阻挡了。    异地部署(地理分布式) 完成了上述架构建设之后,我们的系统是否就已经足够强大了呢?答案当然是否定的哈,优化是无极限的。Web系统虽然表面上看,似乎比较强大了,但是给予用户的体验却不一定是最好的。因为东北的同学,访问深圳的一个网站服务,他还是会感到一些网络距离上的慢。这个时候,我们就需要做异地部署,让Web系统离用户更近。 一、 核心集中与节点分散 有玩过大型网游的同学都会知道,网游是有很多个区的,一般都是按照地域来分,例如广东专区,北京专区。如果一个在广东的玩家,去北京专区玩,那么他会感觉明显比在广东专区卡。实际上,这些大区的名称就已经说明了,它的服务器所在地,所以,广东的玩家去连接地处北京的服务器,网络当然会比较慢。 当一个系统和服务足够大的时候,就必须开始考虑异地部署的问题了。让你的服务,尽可能离用户更近。我们前面已经提到了Web的静态资源,可以存放在CDN上,然后通过DNS/GSLB的方式,让静态资源的分散“全国各地”。但是,CDN只解决的静态资源的问题,没有解决后端庞大的系统服务还只集中在某个固定城市的问题。 这个时候,异地部署就开始了。异地部署一般遵循:核心集中,节点分散。 核心集中:实际部署过程中,总有一部分的数据和服务存在不可部署多套,或者部署多套成本巨大。而对于这些服务和数据,就仍然维持一套,而部署地点选择一个地域比较中心的地方,通过网络内部专线来和各个节点通讯。 节点分散:将一些服务部署为多套,分布在各个城市节点,让用户请求尽可能选择近的节点访问服务。 例如,我们选择在上海部署为核心节点,北京,深圳,武汉,上海为分散节点(上海自己本身也是一个分散节点)。我们的服务架构如图:    需要补充一下的是,上图中上海节点和核心节点是同处于一个机房的,其他分散节点各自独立机房。 国内有很多大型网游,都是大致遵循上述架构。它们会把数据量不大的用户核心账号等放在核心节点,而大部分的网游数据,例如装备、任务等数据和服务放在地区节点里。当然,核心节点和地域节点之间,也有缓存机制。 二、 节点容灾和过载保护 节点容灾是指,某个节点如果发生故障时,我们需要建立一个机制去保证服务仍然可用。毫无疑问,这里比较常见的容灾方式,是切换到附近城市节点。假如系统的天津节点发生故障,那么我们就将网络流量切换到附近的北京节点上。考虑到负载均衡,可能需要同时将流量切换到附近的几个地域节点。另一方面,核心节点自身也是需要自己做好容灾和备份的,核心节点一旦故障,就会影响全国服务。 过载保护,指的是一个节点已经达到最大容量,无法继续接接受更多请求了,系统必须有一个保护的机制。一个服务已经满负载,还继续接受新的请求,结果很可能就是宕机,影响整个节点的服务,为了至少保障大部分用户的正常使用,过载保护是必要的。 解决过载保护,一般2个方向: 拒绝服务,检测到满负载之后,就不再接受新的连接请求。例如网游登入中的排队。 分流到其他节点。这种的话,系统实现更为复杂,又涉及到负载均衡的问题。 小结 Web系统会随着访问规模的增长,渐渐地从1台服务器可以满足需求,一直成长为“庞然大物”的大集群。而这个Web系统变大的过程,实际上就是我们解决问题的过程。在不同的阶段,解决不同的问题,而新的问题又诞生在旧的解决方案之上。 系统的优化是没有极限的,软件和系统架构也一直在快速发展,新的方案解决了老的问题,同时也带来新的挑战。 关于作者:徐汉彬,曾经在阿里巴巴和腾讯有过4年的技术研发工作经历,目前在小满科技(创业)。      
文章
Web App开发  ·  存储  ·  关系型数据库  ·  数据库  ·  索引
2015-07-28
2022最新MySQL高频面试题汇总
本文已经收录到Github仓库,该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点,欢迎star~Github地址:https://github.com/Tyson0314/Java-learning事务的四大特性?事务特性ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。一致性是指一个事务执行之前和执行之后都必须处于一致性状态。比如a与b账户共有1000块,两人之间转账之后无论成功还是失败,它们的账户总和还是1000。隔离性。跟隔离级别相关,如read committed,一个事务只能读到已经提交的修改。持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。数据库的三大范式第一范式1NF确保数据库表字段的原子性。比如字段 userInfo: 广东省 10086' ,依照第一范式必须拆分成 userInfo: 广东省 userTel: 10086两个字段。第二范式2NF首先要满足第一范式,另外包含两部分内容,一是表必须有一个主键;二是非主键列必须完全依赖于主键,而不能只依赖于主键的一部分。举个例子。假定选课关系表为student_course(student_no, student_name, age, course_name, grade, credit),主键为(student_no, course_name)。其中学分完全依赖于课程名称,姓名年龄完全依赖学号,不符合第二范式,会导致数据冗余(学生选n门课,姓名年龄有n条记录)、插入异常(插入一门新课,因为没有学号,无法保存新课记录)等问题。应该拆分成三个表:学生:student(stuent_no, student_name, 年龄);课程:course(course_name, credit);选课关系:student_course_relation(student_no, course_name, grade)。第三范式3NF首先要满足第二范式,另外非主键列必须直接依赖于主键,不能存在传递依赖。即不能存在:非主键列 A 依赖于非主键列 B,非主键列 B 依赖于主键的情况。假定学生关系表为Student(student_no, student_name, age, academy_id, academy_telephone),主键为"学号",其中学院id依赖于学号,而学院地点和学院电话依赖于学院id,存在传递依赖,不符合第三范式。可以把学生关系表分为如下两个表:学生:(student_no, student_name, age, academy_id);学院:(academy_id, academy_telephone)。2NF和3NF的区别?2NF依据是非主键列是否完全依赖于主键,还是依赖于主键的一部分。3NF依据是非主键列是直接依赖于主键,还是直接依赖于非主键。事务隔离级别有哪些?先了解下几个概念:脏读、不可重复读、幻读。脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。不可重复读是指在对于数据库中的某行记录,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,另一个事务修改了数据并提交了。幻读是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录。对幻读的正确理解是一个事务内的读取操作的结论不能支撑之后业务的执行。假设事务要新增一条记录,主键为id,在新增之前执行了select,没有发现id为xxx的记录,但插入时出现主键冲突,这就属于幻读,读取不到记录却发现主键冲突是因为记录实际上已经被其他的事务插入了,但当前事务不可见。不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。事务隔离就是为了解决上面提到的脏读、不可重复读、幻读这几个问题。MySQL数据库为我们提供的四种隔离级别:Serializable (串行化):通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。Repeatable read (可重复读):MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行,解决了不可重复读的问题。Read committed (读已提交):一个事务只能看见已经提交事务所做的改变。可避免脏读的发生。Read uncommitted (读未提交):所有事务都可以看到其他未提交事务的执行结果。查看隔离级别:select @@transaction_isolation;设置隔离级别:set session transaction isolation level read uncommitted;生产环境数据库一般用的什么隔离级别呢?生产环境大多使用RC。为什么不是RR呢?可重复读(Repeatable Read),简称为RR读已提交(Read Commited),简称为RC缘由一:在RR隔离级别下,存在间隙锁,导致出现死锁的几率比RC大的多!缘由二:在RR隔离级别下,条件列未命中索引会锁表!而在RC隔离级别下,只锁行!也就是说,RC的并发性高于RR。并且大部分场景下,不可重复读问题是可以接受的。毕竟数据都已经提交了,读出来本身就没有太大问题!互联网项目中mysql应该选什么事务隔离级别编码和字符集的关系我们平时可以在编辑器上输入各种中文英文字母,但这些都是给人读的,不是给计算机读的,其实计算机真正保存和传输数据都是以二进制0101的格式进行的。那么就需要有一个规则,把中文和英文字母转化为二进制。其中d对应十六进制下的64,它可以转换为01二进制的格式。于是字母和数字就这样一一对应起来了,这就是ASCII编码格式。它用一个字节,也就是8位来标识字符,基础符号有128个,扩展符号也是128个。也就只能表示下英文字母和数字。这明显不够用。于是,为了标识中文,出现了GB2312的编码格式。为了标识希腊语,出现了greek编码格式,为了标识俄语,整了cp866编码格式。为了统一它们,于是出现了Unicode编码格式,它用了2~4个字节来表示字符,这样理论上所有符号都能被收录进去,并且它还完全兼容ASCII的编码,也就是说,同样是字母d,在ASCII用64表示,在Unicode里还是用64来表示。但不同的地方是ASCII编码用1个字节来表示,而Unicode用则两个字节来表示。同样都是字母d,unicode比ascii多使用了一个字节,如下:D ASCII: 01100100 D Unicode: 00000000 01100100可以看到,上面的unicode编码,前面的都是0,其实用不上,但还占了个字节,有点浪费。如果我们能做到该隐藏时隐藏,这样就能省下不少空间,按这个思路,就是就有了UTF-8编码。总结一下,按照一定规则把符号和二进制码对应起来,这就是编码。而把n多这种已经编码的字符聚在一起,就是我们常说的字符集。比如utf-8字符集就是所有utf-8编码格式的字符的合集。想看下mysql支持哪些字符集。可以执行 show charset;utf8和utf8mb4的区别上面提到utf-8是在unicode的基础上做的优化,既然unicode有办法表示所有字符,那utf-8也一样可以表示所有字符,为了避免混淆,我在后面叫它大utf8。mysql支持的字符集中有utf8和utf8mb4。先说utf8mb4编码,mb4就是most bytes 4的意思,从上图最右边的Maxlen可以看到,它最大支持用4个字节来表示字符,它几乎可以用来表示目前已知的所有的字符。再说mysql字符集里的utf8,它是数据库的默认字符集。但注意,此utf8非彼utf8,我们叫它小utf8字符集。为什么这么说,因为从Maxlen可以看出,它最多支持用3个字节去表示字符,按utf8mb4的命名方式,准确点应该叫它utf8mb3。utf8 就像是阉割版的utf8mb4,只支持部分字符。比如emoji表情,它就不支持。而mysql支持的字符集里,第三列,collation,它是指字符集的比较规则。比如,"debug"和"Debug"是同一个单词,但它们大小写不同,该不该判为同一个单词呢。这时候就需要用到collation了。通过SHOW COLLATION WHERE Charset = 'utf8mb4';可以查看到utf8mb4下支持什么比较规则。如果collation = utf8mb4_general_ci,是指使用utf8mb4字符集的前提下,挨个字符进行比较(general),并且不区分大小写(_ci,case insensitice)。这种情况下,"debug"和"Debug"是同一个单词。如果改成collation=utf8mb4_bin,就是指挨个比较二进制位大小。于是"debug"和"Debug"就不是同一个单词。那utf8mb4对比utf8有什么劣势吗?我们知道数据库表里,字段类型如果是char(2)的话,里面的2是指字符个数,也就是说不管这张表用的是什么编码的字符集,都能放上2个字符。而char又是固定长度,为了能放下2个utf8mb4的字符,char会默认保留2*4(maxlen=4)= 8个字节的空间。如果是utf8mb3,则会默认保留 2 * 3 (maxlen=3) = 6个字节的空间。也就是说,在这种情况下,utf8mb4会比utf8mb3多使用一些空间。索引什么是索引?索引是存储引擎用于提高数据库表的访问速度的一种数据结构。索引的优缺点?优点:加快数据查找的速度为用来排序或者是分组的字段添加索引,可以加快分组和排序的速度加快表与表之间的连接缺点:建立索引需要占用物理空间会降低表的增删改的效率,因为每次对表记录进行增删改,需要进行动态维护索引,导致增删改时间变长索引的作用?数据是存储在磁盘上的,查询数据时,如果没有索引,会加载所有的数据到内存,依次进行检索,读取磁盘次数较多。有了索引,就不需要加载所有数据,因为B+树的高度一般在2-4层,最多只需要读取2-4次磁盘,查询速度大大提升。什么情况下需要建索引?经常用于查询的字段经常用于连接的字段建立索引,可以加快连接的速度经常需要排序的字段建立索引,因为索引已经排好序,可以加快排序查询速度什么情况下不建索引?where条件中用不到的字段不适合建立索引表记录较少。比如只有几百条数据,没必要加索引。需要经常增删改。需要评估是否适合加索引参与列计算的列不适合建索引区分度不高的字段不适合建立索引,如性别,只有男/女/未知三个值。加了索引,查询效率也不会提高。索引的数据结构索引的数据结构主要有B+树和哈希表,对应的索引分别为B+树索引和哈希索引。InnoDB引擎的索引类型有B+树索引和哈希索引,默认的索引类型为B+树索引。B+树索引 B+ 树是基于B 树和叶子节点顺序访问指针进行实现,它具有B树的平衡性,并且通过顺序访问指针来提高区间查询的性能。在 B+ 树中,节点中的 key 从左到右递增排列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。进行查找操作时,首先在根节点进行二分查找,找到key所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出key所对应的数据项。MySQL 数据库使用最多的索引类型是BTREE索引,底层基于B+树数据结构来实现。mysql> show index from blog\G; *************************** 1. row *************************** Table: blog Non_unique: 0 Key_name: PRIMARY Seq_in_index: 1 Column_name: blog_id Collation: A Cardinality: 4 Sub_part: NULL Packed: NULL Null: Index_type: BTREE Comment: Index_comment: Visible: YES Expression: NULL哈希索引哈希索引是基于哈希表实现的,对于每一行数据,存储引擎会对索引列进行哈希计算得到哈希码,并且哈希算法要尽量保证不同的列值计算出的哈希码值是不同的,将哈希码的值作为哈希表的key值,将指向数据行的指针作为哈希表的value值。这样查找一个数据的时间复杂度就是O(1),一般多用于精确查找。Hash索引和B+树索引的区别?哈希索引不支持排序,因为哈希表是无序的。哈希索引不支持范围查找。哈希索引不支持模糊查询及多列索引的最左前缀匹配。因为哈希表中会存在哈希冲突,所以哈希索引的性能是不稳定的,而B+树索引的性能是相对稳定的,每次查询都是从根节点到叶子节点。为什么B+树比B树更适合实现数据库索引?由于B+树的数据都存储在叶子结点中,叶子结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,而在数据库中基于范围的查询是非常频繁的,所以通常B+树用于数据库索引。B+树的节点只存储索引key值,具体信息的地址存在于叶子节点的地址中。这就使以页为单位的索引中可以存放更多的节点。减少更多的I/O支出。B+树的查询效率更加稳定,任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。索引有什么分类?1、主键索引:名为primary的唯一非空索引,不允许有空值。2、唯一索引:索引列中的值必须是唯一的,但是允许为空值。唯一索引和主键索引的区别是:唯一索引字段可以为null且可以存在多个null值,而主键索引字段不可以为null。唯一索引的用途:唯一标识数据库表中的每条记录,主要是用来防止数据重复插入。创建唯一索引的SQL语句如下:ALTER TABLE table_name ADD CONSTRAINT constraint_name UNIQUE KEY(column_1,column_2,...);3、组合索引:在表中的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用,使用组合索引时需遵循最左前缀原则。4、全文索引:只能在CHAR、VARCHAR和TEXT类型字段上使用全文索引。5、普通索引:普通索引是最基本的索引,它没有任何限制,值可以为空。什么是最左匹配原则?如果 SQL 语句中用到了组合索引中的最左边的索引,那么这条 SQL 语句就可以利用这个组合索引去进行匹配。当遇到范围查询(>、<、between、like)就会停止匹配,后面的字段不会用到索引。对(a,b,c)建立索引,查询条件使用 a/ab/abc 会走索引,使用 bc 不会走索引。对(a,b,c,d)建立索引,查询条件为a = 1 and b = 2 and c > 3 and d = 4,那么a、b和c三个字段能用到索引,而d无法使用索引。因为遇到了范围查询。如下图,对(a, b) 建立索引,a 在索引树中是全局有序的,而 b 是全局无序,局部有序(当a相等时,会根据b进行排序)。直接执行b = 2这种查询条件无法使用索引。当a的值确定的时候,b是有序的。例如a = 1时,b值为1,2是有序的状态。当a = 2时候,b的值为1,4也是有序状态。 当执行a = 1 and b = 2时a和b字段能用到索引。而执行a > 1 and b = 2时,a字段能用到索引,b字段用不到索引。因为a的值此时是一个范围,不是固定的,在这个范围内b值不是有序的,因此b字段无法使用索引。什么是聚集索引?InnoDB使用表的主键构造主键索引树,同时叶子节点中存放的即为整张表的记录数据。聚集索引叶子节点的存储是逻辑上连续的,使用双向链表连接,叶子节点按照主键的顺序排序,因此对于主键的排序查找和范围查找速度比较快。聚集索引的叶子节点就是整张表的行记录。InnoDB 主键使用的是聚簇索引。聚集索引要比非聚集索引查询效率高很多。对于InnoDB来说,聚集索引一般是表中的主键索引,如果表中没有显示指定主键,则会选择表中的第一个不允许为NULL的唯一索引。如果没有主键也没有合适的唯一索引,那么InnoDB内部会生成一个隐藏的主键作为聚集索引,这个隐藏的主键长度为6个字节,它的值会随着数据的插入自增。什么是覆盖索引?select的数据列只用从索引中就能够取得,不需要回表进行二次查询,也就是说查询列要被所使用的索引覆盖。对于innodb表的二级索引,如果索引能覆盖到查询的列,那么就可以避免对主键索引的二次查询。不是所有类型的索引都可以成为覆盖索引。覆盖索引要存储索引列的值,而哈希索引、全文索引不存储索引列的值,所以MySQL使用b+树索引做覆盖索引。对于使用了覆盖索引的查询,在查询前面使用explain,输出的extra列会显示为using index。比如user_like 用户点赞表,组合索引为(user_id, blog_id),user_id和blog_id都不为null。explain select blog_id from user_like where user_id = 13;explain结果的Extra列为Using index,查询的列被索引覆盖,并且where筛选条件符合最左前缀原则,通过索引查找就能直接找到符合条件的数据,不需要回表查询数据。explain select user_id from user_like where blog_id = 1;explain结果的Extra列为Using where; Using index, 查询的列被索引覆盖,where筛选条件不符合最左前缀原则,无法通过索引查找找到符合条件的数据,但可以通过索引扫描找到符合条件的数据,也不需要回表查询数据。索引的设计原则?对于经常作为查询条件的字段,应该建立索引,以提高查询速度为经常需要排序、分组和联合操作的字段建立索引索引列的区分度越高,索引的效果越好。比如使用性别这种区分度很低的列作为索引,效果就会很差。避免给"大字段"建立索引。尽量使用数据量小的字段作为索引。因为MySQL在维护索引的时候是会将字段值一起维护的,那这样必然会导致索引占用更多的空间,另外在排序的时候需要花费更多的时间去对比。尽量使用短索引,对于较长的字符串进行索引时应该指定一个较短的前缀长度,因为较小的索引涉及到的磁盘I/O较少,查询速度更快。索引不是越多越好,每个索引都需要额外的物理空间,维护也需要花费时间。频繁增删改的字段不要建立索引。假设某个字段频繁修改,那就意味着需要频繁的重建索引,这必然影响MySQL的性能利用最左前缀原则。索引什么时候会失效?导致索引失效的情况:对于组合索引,不是使用组合索引最左边的字段,则不会使用索引以%开头的like查询如%abc,无法使用索引;非%开头的like查询如abc%,相当于范围查询,会使用索引查询条件中列类型是字符串,没有使用引号,可能会因为类型不同发生隐式转换,使索引失效判断索引列是否不等于某个值时对索引列进行运算查询条件使用or连接,也会导致索引失效什么是前缀索引?有时需要在很长的字符列上创建索引,这会造成索引特别大且慢。使用前缀索引可以避免这个问题。前缀索引是指对文本或者字符串的前几个字符建立索引,这样索引的长度更短,查询速度更快。创建前缀索引的关键在于选择足够长的前缀以保证较高的索引选择性。索引选择性越高查询效率就越高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的数据行。建立前缀索引的方式:// email列创建前缀索引 ALTER TABLE table_name ADD KEY(column_name(prefix_length));索引下推参考我的另一篇文章:图解索引下推!常见的存储引擎有哪些?MySQL中常用的四种存储引擎分别是: MyISAM、InnoDB、MEMORY、ARCHIVE。MySQL 5.5版本后默认的存储引擎为InnoDB。InnoDB存储引擎InnoDB是MySQL默认的事务型存储引擎,使用最广泛,基于聚簇索引建立的。InnoDB内部做了很多优化,如能够自动在内存中创建自适应hash索引,以加速读操作。优点:支持事务和崩溃修复能力;引入了行级锁和外键约束。缺点:占用的数据空间相对较大。适用场景:需要事务支持,并且有较高的并发读写频率。MyISAM存储引擎数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,可以使用MyISAM引擎。MyISAM会将表存储在两个文件中,数据文件.MYD和索引文件.MYI。优点:访问速度快。缺点:MyISAM不支持事务和行级锁,不支持崩溃后的安全恢复,也不支持外键。适用场景:对事务完整性没有要求;表的数据都会只读的。MEMORY存储引擎MEMORY引擎将数据全部放在内存中,访问速度较快,但是一旦系统奔溃的话,数据都会丢失。MEMORY引擎默认使用哈希索引,将键的哈希值和指向数据行的指针保存在哈希索引中。优点:访问速度较快。缺点:哈希索引数据不是按照索引值顺序存储,无法用于排序。不支持部分索引匹配查找,因为哈希索引是使用索引列的全部内容来计算哈希值的。只支持等值比较,不支持范围查询。当出现哈希冲突时,存储引擎需要遍历链表中所有的行指针,逐行进行比较,直到找到符合条件的行。ARCHIVE存储引擎ARCHIVE存储引擎非常适合存储大量独立的、作为历史记录的数据。ARCHIVE提供了压缩功能,拥有高效的插入速度,但是这种引擎不支持索引,所以查询性能较差。MyISAM和InnoDB的区别?存储结构的区别。每个MyISAM在磁盘上存储成三个文件。文件的名字以表的名字开始,扩展名指出文件类型。 .frm文件存储表定义。数据文件的扩展名为.MYD (MYData)。索引文件的扩展名是.MYI (MYIndex)。InnoDB所有的表都保存在同一个数据文件中(也可能是多个文件,或者是独立的表空间文件),InnoDB表的大小只受限于操作系统文件的大小,一般为2GB。存储空间的区别。MyISAM支持支持三种不同的存储格式:静态表(默认,但是注意数据末尾不能有空格,会被去掉)、动态表、压缩表。当表在创建之后并导入数据之后,不会再进行修改操作,可以使用压缩表,极大的减少磁盘的空间占用。InnoDB需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引。可移植性、备份及恢复。MyISAM数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作。对于InnoDB,可行的方案是拷贝数据文件、备份 binlog,或者用mysqldump,在数据量达到几十G的时候就相对麻烦了。是否支持行级锁。MyISAM 只支持表级锁,用户在操作myisam表时,select,update,delete,insert语句都会给表自动加锁,如果加锁以后的表满足insert并发的情况下,可以在表的尾部插入新的数据。而InnoDB 支持行级锁和表级锁,默认为行级锁。行锁大幅度提高了多用户并发操作的性能。是否支持事务和崩溃后的安全恢复。 MyISAM 不提供事务支持。而InnoDB 提供事务支持,具有事务、回滚和崩溃修复能力。是否支持外键。MyISAM不支持,而InnoDB支持。是否支持MVCC。MyISAM不支持,InnoDB支持。应对高并发事务,MVCC比单纯的加锁更高效。是否支持聚集索引。MyISAM不支持聚集索引,InnoDB支持聚集索引。全文索引。MyISAM支持 FULLTEXT类型的全文索引。InnoDB不支持FULLTEXT类型的全文索引,但是innodb可以使用sphinx插件支持全文索引,并且效果更好。表主键。MyISAM允许没有任何索引和主键的表存在,索引都是保存行的地址。对于InnoDB,如果没有设定主键或者非空唯一索引,就会自动生成一个6字节的主键(用户不可见)。表的行数。MyISAM保存有表的总行数,如果select count(*) from table;会直接取出该值。InnoDB没有保存表的总行数,如果使用select count(*) from table;就会遍历整个表,消耗相当大,但是在加了where条件后,MyISAM和InnoDB处理的方式都一样。MySQL有哪些锁?按锁粒度分类,有行级锁、表级锁和页级锁。行级锁是mysql中锁定粒度最细的一种锁。表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开销也最大。行级锁的类型主要有三类:Record Lock,记录锁,也就是仅仅把一条记录锁上;Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身;Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。表级锁是mysql中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分mysql引擎支持。最常使用的MyISAM与InnoDB都支持表级锁定。页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。按锁级别分类,有共享锁、排他锁和意向锁。共享锁又称读锁,是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。排他锁又称写锁、独占锁,如果事务T对数据A加上排他锁后,则其他事务不能再对A加任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。意向锁是表级锁,其设计目的主要是为了在一个事务中揭示下一行将要被请求锁的类型。InnoDB 中的两个表锁:意向共享锁(IS):表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的IS锁;意向排他锁(IX):类似上面,表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须先取得该表的IX锁。意向锁是 InnoDB 自动加的,不需要用户干预。对于INSERT、UPDATE和DELETE,InnoDB 会自动给涉及的数据加排他锁;对于一般的SELECT语句,InnoDB 不会加任何锁,事务可以通过以下语句显式加共享锁或排他锁。共享锁:SELECT … LOCK IN SHARE MODE;排他锁:SELECT … FOR UPDATE;MVCC 实现原理?MVCC(Multiversion concurrency control) 就是同一份数据保留多版本的一种方式,进而实现并发控制。在查询的时候,通过read view和版本链找到对应版本的数据。作用:提升并发性能。对于高并发场景,MVCC比行级锁开销更小。MVCC 实现原理如下:MVCC 的实现依赖于版本链,版本链是通过表的三个隐藏字段实现。DB_TRX_ID:当前事务id,通过事务id的大小判断事务的时间顺序。DB_ROLL_PTR:回滚指针,指向当前行记录的上一个版本,通过这个指针将数据的多个版本连接在一起构成undo log版本链。DB_ROW_ID:主键,如果数据表没有主键,InnoDB会自动生成主键。每条表记录大概是这样的:使用事务更新行记录的时候,就会生成版本链,执行过程如下:用排他锁锁住该行;将该行原本的值拷贝到undo log,作为旧版本用于回滚;修改当前行的值,生成一个新版本,更新事务id,使回滚指针指向旧版本的记录,这样就形成一条版本链。下面举个例子方便大家理解。1、初始数据如下,其中DB_ROW_ID和DB_ROLL_PTR为空。2、事务A对该行数据做了修改,将age修改为12,效果如下:3、之后事务B也对该行记录做了修改,将age修改为8,效果如下:4、此时undo log有两行记录,并且通过回滚指针连在一起。接下来了解下read view的概念。read view可以理解成将数据在每个时刻的状态拍成“照片”记录下来。在获取某时刻t的数据时,到t时间点拍的“照片”上取数据。在read view内部维护一个活跃事务链表,表示生成read view的时候还在活跃的事务。这个链表包含在创建read view之前还未提交的事务,不包含创建read view之后提交的事务。不同隔离级别创建read view的时机不同。read committed:每次执行select都会创建新的read_view,保证能读取到其他事务已经提交的修改。repeatable read:在一个事务范围内,第一次select时更新这个read_view,以后不会再更新,后续所有的select都是复用之前的read_view。这样可以保证事务范围内每次读取的内容都一样,即可重复读。read view的记录筛选方式前提:DATA_TRX_ID 表示每个数据行的最新的事务ID;up_limit_id表示当前快照中的最先开始的事务;low_limit_id表示当前快照中的最慢开始的事务,即最后一个事务。如果DATA_TRX_ID < up_limit_id:说明在创建read view时,修改该数据行的事务已提交,该版本的记录可被当前事务读取到。如果DATA_TRX_ID >= low_limit_id:说明当前版本的记录的事务是在创建read view之后生成的,该版本的数据行不可以被当前事务访问。此时需要通过版本链找到上一个版本,然后重新判断该版本的记录对当前事务的可见性。如果up_limit_id <= DATA_TRX_ID < low_limit_i:需要在活跃事务链表中查找是否存在ID为DATA_TRX_ID的值的事务。如果存在,因为在活跃事务链表中的事务是未提交的,所以该记录是不可见的。此时需要通过版本链找到上一个版本,然后重新判断该版本的可见性。如果不存在,说明事务trx_id 已经提交了,这行记录是可见的。总结:InnoDB 的MVCC是通过 read view 和版本链实现的,版本链保存有历史版本记录,通过read view 判断当前版本的数据是否可见,如果不可见,再从版本链中找到上一个版本,继续进行判断,直到找到一个可见的版本。快照读和当前读表记录有两种读取方式。快照读:读取的是快照版本。普通的SELECT就是快照读。通过mvcc来进行并发控制的,不用加锁。当前读:读取的是最新版本。UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE是当前读。快照读情况下,InnoDB通过mvcc机制避免了幻读现象。而mvcc机制无法避免当前读情况下出现的幻读现象。因为当前读每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。下面举个例子说明下:1、首先,user表只有两条记录,具体如下:2、事务a和事务b同时开启事务start transaction;3、事务a插入数据然后提交;insert into user(user_name, user_password, user_mail, user_state) values('tyson', 'a', 'a', 0);4、事务b执行全表的update;update user set user_name = 'a';5、事务b然后执行查询,查到了事务a中插入的数据。(下图左边是事务b,右边是事务a。事务开始之前只有两条记录,事务a插入一条数据之后,事务b查询出来是三条数据)以上就是当前读出现的幻读现象。那么MySQL是如何避免幻读?在快照读情况下,MySQL通过mvcc来避免幻读。在当前读情况下,MySQL通过next-key来避免幻读(加行锁和间隙锁来实现的)。next-key包括两部分:行锁和间隙锁。行锁是加在索引上的锁,间隙锁是加在索引之间的。Serializable隔离级别也可以避免幻读,会锁住整张表,并发性极低,一般不会使用。共享锁和排他锁SELECT 的读取锁定主要分为两种方式:共享锁和排他锁。select * from table where id<6 lock in share mode;--共享锁 select * from table where id<6 for update;--排他锁这两种方式主要的不同在于LOCK IN SHARE MODE 多个事务同时更新同一个表单时很容易造成死锁。申请排他锁的前提是,没有线程对该结果集的任何行数据使用排它锁或者共享锁,否则申请会受到阻塞。在进行事务操作时,MySQL会对查询结果集的每行数据添加排它锁,其他线程对这些数据的更改或删除操作会被阻塞(只能读操作),直到该语句的事务被commit语句或rollback语句结束为止。SELECT... FOR UPDATE 使用注意事项:for update 仅适用于innodb,且必须在事务范围内才能生效。根据主键进行查询,查询条件为like或者不等于,主键字段产生表锁。根据非索引字段进行查询,会产生表锁。bin log/redo log/undo logMySQL日志主要包括查询日志、慢查询日志、事务日志、错误日志、二进制日志等。其中比较重要的是 bin log(二进制日志)和 redo log(重做日志)和 undo log(回滚日志)。bin logbin log是MySQL数据库级别的文件,记录对MySQL数据库执行修改的所有操作,不会记录select和show语句,主要用于恢复数据库和同步数据库。redo logredo log是innodb引擎级别,用来记录innodb存储引擎的事务日志,不管事务是否提交都会记录下来,用于数据恢复。当数据库发生故障,innoDB存储引擎会使用redo log恢复到发生故障前的时刻,以此来保证数据的完整性。将参数innodb_flush_log_at_tx_commit设置为1,那么在执行commit时会将redo log同步写到磁盘。undo log除了记录redo log外,当进行数据修改时还会记录undo log,undo log用于数据的撤回操作,它保留了记录修改前的内容。通过undo log可以实现事务回滚,并且可以根据undo log回溯到某个特定的版本的数据,实现MVCC。bin log和redo log有什么区别?bin log会记录所有日志记录,包括InnoDB、MyISAM等存储引擎的日志;redo log只记录innoDB自身的事务日志。bin log只在事务提交前写入到磁盘,一个事务只写一次;而在事务进行过程,会有redo log不断写入磁盘。bin log是逻辑日志,记录的是SQL语句的原始逻辑;redo log是物理日志,记录的是在某个数据页上做了什么修改。讲一下MySQL架构?MySQL主要分为 Server 层和存储引擎层:Server 层:主要包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块 binglog 日志模块。存储引擎: 主要负责数据的存储和读取。server 层通过api与存储引擎进行通信。Server 层基本组件连接器: 当客户端连接 MySQL 时,server层会对其进行身份认证和权限校验。查询缓存: 执行查询语句的时候,会先查询缓存,先校验这个 sql 是否执行过,如果有缓存这个 sql,就会直接返回给客户端,如果没有命中,就会执行后续的操作。分析器: 没有命中缓存的话,SQL 语句就会经过分析器,主要分为两步,词法分析和语法分析,先看 SQL 语句要做什么,再检查 SQL 语句语法是否正确。优化器: 优化器对查询进行优化,包括重写查询、决定表的读写顺序以及选择合适的索引等,生成执行计划。执行器: 首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会根据执行计划去调用引擎的接口,返回结果。分库分表当单表的数据量达到1000W或100G以后,优化索引、添加从库等可能对数据库性能提升效果不明显,此时就要考虑对其进行切分了。切分的目的就在于减少数据库的负担,缩短查询的时间。数据切分可以分为两种方式:垂直划分和水平划分。垂直划分垂直划分数据库是根据业务进行划分,例如购物场景,可以将库中涉及商品、订单、用户的表分别划分出成一个库,通过降低单库的大小来提高性能。同样的,分表的情况就是将一个大表根据业务功能拆分成一个个子表,例如商品基本信息和商品描述,商品基本信息一般会展示在商品列表,商品描述在商品详情页,可以将商品基本信息和商品描述拆分成两张表。优点:行记录变小,数据页可以存放更多记录,在查询时减少I/O次数。缺点:主键出现冗余,需要管理冗余列;会引起表连接JOIN操作,可以通过在业务服务器上进行join来减少数据库压力;依然存在单表数据量过大的问题。水平划分水平划分是根据一定规则,例如时间或id序列值等进行数据的拆分。比如根据年份来拆分不同的数据库。每个数据库结构一致,但是数据得以拆分,从而提升性能。优点:单库(表)的数据量得以减少,提高性能;切分出的表结构相同,程序改动较少。缺点:分片事务一致性难以解决跨节点join性能差,逻辑复杂数据分片在扩容时需要迁移什么是分区表?分区是把一张表的数据分成N多个区块。分区表是一个独立的逻辑表,但是底层由多个物理子表组成。当查询条件的数据分布在某一个分区的时候,查询引擎只会去某一个分区查询,而不是遍历整个表。在管理层面,如果需要删除某一个分区的数据,只需要删除对应的分区即可。分区一般都是放在单机里的,用的比较多的是时间范围分区,方便归档。只不过分库分表需要代码实现,分区则是mysql内部实现。分库分表和分区并不冲突,可以结合使用。分区表类型range分区,按照范围分区。比如按照时间范围分区CREATE TABLE test_range_partition( id INT auto_increment, createdate DATETIME, primary key (id,createdate) ) PARTITION BY RANGE (TO_DAYS(createdate) ) ( PARTITION p201801 VALUES LESS THAN ( TO_DAYS('20180201') ), PARTITION p201802 VALUES LESS THAN ( TO_DAYS('20180301') ), PARTITION p201803 VALUES LESS THAN ( TO_DAYS('20180401') ), PARTITION p201804 VALUES LESS THAN ( TO_DAYS('20180501') ), PARTITION p201805 VALUES LESS THAN ( TO_DAYS('20180601') ), PARTITION p201806 VALUES LESS THAN ( TO_DAYS('20180701') ), PARTITION p201807 VALUES LESS THAN ( TO_DAYS('20180801') ), PARTITION p201808 VALUES LESS THAN ( TO_DAYS('20180901') ), PARTITION p201809 VALUES LESS THAN ( TO_DAYS('20181001') ), PARTITION p201810 VALUES LESS THAN ( TO_DAYS('20181101') ), PARTITION p201811 VALUES LESS THAN ( TO_DAYS('20181201') ), PARTITION p201812 VALUES LESS THAN ( TO_DAYS('20190101') ) );在/var/lib/mysql/data/可以找到对应的数据文件,每个分区表都有一个使用#分隔命名的表文件: -rw-r----- 1 MySQL MySQL 65 Mar 14 21:47 db.opt -rw-r----- 1 MySQL MySQL 8598 Mar 14 21:50 test_range_partition.frm -rw-r----- 1 MySQL MySQL 98304 Mar 14 21:50 test_range_partition#P#p201801.ibd -rw-r----- 1 MySQL MySQL 98304 Mar 14 21:50 test_range_partition#P#p201802.ibd -rw-r----- 1 MySQL MySQL 98304 Mar 14 21:50 test_range_partition#P#p201803.ibd ...list分区list分区和range分区相似,主要区别在于list是枚举值列表的集合,range是连续的区间值的集合。对于list分区,分区字段必须是已知的,如果插入的字段不在分区时的枚举值中,将无法插入。create table test_list_partiotion ( id int auto_increment, data_type tinyint, primary key(id,data_type) )partition by list(data_type) ( partition p0 values in (0,1,2,3,4,5,6), partition p1 values in (7,8,9,10,11,12), partition p2 values in (13,14,15,16,17) );hash分区可以将数据均匀地分布到预先定义的分区中。create table test_hash_partiotion ( id int auto_increment, create_date datetime, primary key(id,create_date) )partition by hash(year(create_date)) partitions 10;分区的问题?打开和锁住所有底层表的成本可能很高。当查询访问分区表时,MySQL 需要打开并锁住所有的底层表,这个操作在分区过滤之前发生,所以无法通过分区过滤来降低此开销,会影响到查询速度。可以通过批量操作来降低此类开销,比如批量插入、LOAD DATA INFILE和一次删除多行数据。维护分区的成本可能很高。例如重组分区,会先创建一个临时分区,然后将数据复制到其中,最后再删除原分区。所有分区必须使用相同的存储引擎。查询语句执行流程?查询语句的执行流程如下:权限校验、查询缓存、分析器、优化器、权限校验、执行器、引擎。举个例子,查询语句如下:select * from user where id > 1 and name = '大彬';首先检查权限,没有权限则返回错误;MySQL8.0以前会查询缓存,缓存命中则直接返回,没有则执行下一步;词法分析和语法分析。提取表名、查询条件,检查语法是否有错误;两种执行方案,先查 id > 1 还是 name = '大彬',优化器根据自己的优化算法选择执行效率最好的方案;校验权限,有权限就调用数据库引擎接口,返回引擎的执行结果。更新语句执行过程?更新语句执行流程如下:分析器、权限校验、执行器、引擎、redo log(prepare状态)、binlog、redo log(commit状态)举个例子,更新语句如下:update user set name = '大彬' where id = 1;先查询到 id 为1的记录,有缓存会使用缓存。拿到查询结果,将 name 更新为大彬,然后调用引擎接口,写入更新数据,innodb 引擎将数据保存在内存中,同时记录redo log,此时redo log进入 prepare状态。执行器收到通知后记录binlog,然后调用引擎接口,提交redo log为commit状态。更新完成。为什么记录完redo log,不直接提交,而是先进入prepare状态?假设先写redo log直接提交,然后写binlog,写完redo log后,机器挂了,binlog日志没有被写入,那么机器重启后,这台机器会通过redo log恢复数据,但是这个时候binlog并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。exist和in的区别?exists用于对外表记录做筛选。exists会遍历外表,将外查询表的每一行,代入内查询进行判断。当exists里的条件语句能够返回记录行时,条件就为真,返回外表当前记录。反之如果exists里的条件语句不能返回记录行,条件为假,则外表当前记录被丢弃。select a.* from A awhere exists(select 1 from B b where a.id=b.id)in是先把后边的语句查出来放到临时表中,然后遍历临时表,将临时表的每一行,代入外查询去查找。select * from Awhere id in(select id from B)子查询的表比较大的时候,使用exists可以有效减少总的循环次数来提升速度;当外查询的表比较大的时候,使用in可以有效减少对外查询表循环遍历来提升速度。MySQL中int(10)和char(10)的区别? int(10)中的10表示的是显示数据的长度,而char(10)表示的是存储数据的长度。truncate、delete与drop区别?相同点:truncate和不带where子句的delete、以及drop都会删除表内的数据。drop、truncate都是DDL语句(数据定义语言),执行后会自动提交。不同点:truncate 和 delete 只删除数据不删除表的结构;drop 语句将删除表的结构被依赖的约束、触发器、索引;一般来说,执行速度: drop > truncate > delete。having和where区别?二者作用的对象不同,where子句作用于表和视图,having作用于组。where在数据分组前进行过滤,having在数据分组后进行过滤。什么是MySQL主从同步?主从同步使得数据可以从一个数据库服务器复制到其他服务器上,在复制数据时,一个服务器充当主服务器(master),其余的服务器充当从服务器(slave)。因为复制是异步进行的,所以从服务器不需要一直连接着主服务器,从服务器甚至可以通过拨号断断续续地连接主服务器。通过配置文件,可以指定复制所有的数据库,某个数据库,甚至是某个数据库上的某个表。为什么要做主从同步?读写分离,使数据库能支撑更大的并发。在主服务器上生成实时数据,而在从服务器上分析这些数据,从而提高主服务器的性能。数据备份,保证数据的安全。乐观锁和悲观锁是什么?数据库中的并发控制是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观锁和悲观锁是并发控制主要采用的技术手段。悲观锁:假定会发生并发冲突,会对操作的数据进行加锁,直到提交事务,才会释放锁,其他事务才能进行修改。实现方式:使用数据库中的锁机制。乐观锁:假设不会发生并发冲突,只在提交操作时检查是否数据是否被修改过。给表增加version字段,在修改提交之前检查version与原来取到的version值是否相等,若相等,表示数据没有被修改,可以更新,否则,数据为脏数据,不能更新。实现方式:乐观锁一般使用版本号机制或CAS算法实现。用过processlist吗?show processlist 或 show full processlist 可以查看当前 MySQL 是否有压力,正在运行的SQL,有没有慢SQL正在执行。返回参数如下:id:线程ID,可以用kill id杀死某个线程db:数据库名称user:数据库用户host:数据库实例的IPcommand:当前执行的命令,比如Sleep,Query,Connect 等time:消耗时间,单位秒state:执行状态,主要有以下状态:Sleep,线程正在等待客户端发送新的请求Locked,线程正在等待锁Sending data,正在处理SELECT查询的记录,同时把结果发送给客户端Kill,正在执行kill语句,杀死指定线程Connect,一个从节点连上了主节点Quit,线程正在退出Sorting for group,正在为GROUP BY做排序Sorting for order,正在为ORDER BY做排序info:正在执行的SQL语句MySQL查询 limit 1000,10 和limit 10 速度一样快吗?两种查询方式。对应 limit offset, size 和 limit size 两种方式。而其实 limit size ,相当于 limit 0, size。也就是从0开始取size条数据。也就是说,两种方式的区别在于offset是否为0。先来看下limit sql的内部执行逻辑。MySQL内部分为server层和存储引擎层。一般情况下存储引擎都用innodb。server层有很多模块,其中需要关注的是执行器是用于跟存储引擎打交道的组件。执行器可以通过调用存储引擎提供的接口,将一行行数据取出,当这些数据完全符合要求(比如满足其他where条件),则会放到结果集中,最后返回给调用mysql的客户端。以主键索引的limit执行过程为例:执行select * from xxx order by id limit 0, 10;,select后面带的是星号,也就是要求获得行数据的所有字段信息。server层会调用innodb的接口,在innodb里的主键索引中获取到第0到10条完整行数据,依次返回给server层,并放到server层的结果集中,返回给客户端。把offset搞大点,比如执行的是:select * from xxx order by id limit 500000, 10;server层会调用innodb的接口,由于这次的offset=500000,会在innodb里的主键索引中获取到第0到(500000 + 10)条完整行数据,返回给server层之后根据offset的值挨个抛弃,最后只留下最后面的size条,也就是10条数据,放到server层的结果集中,返回给客户端。可以看出,当offset非0时,server层会从引擎层获取到很多无用的数据,而获取的这些无用数据都是要耗时的。因此,mysql查询中 limit 1000,10 会比 limit 10 更慢。原因是 limit 1000,10 会取出1000+10条数据,并抛弃前1000条,这部分耗时更大。深分页怎么优化?还是以上面的SQL为空:select * from xxx order by id limit 500000, 10;方法一:从上面的分析可以看出,当offset非常大时,server层会从引擎层获取到很多无用的数据,而当select后面是*号时,就需要拷贝完整的行信息,拷贝完整数据相比只拷贝行数据里的其中一两个列字段更耗费时间。因为前面的offset条数据最后都是不要的,没有必要拷贝完整字段,所以可以将sql语句修改成:select * from xxx where id >=(select id from xxx order by id limit 500000, 1) order by id limit 10;先执行子查询 select id from xxx by id limit 500000, 1, 这个操作,其实也是将在innodb中的主键索引中获取到500000+1条数据,然后server层会抛弃前500000条,只保留最后一条数据的id。但不同的地方在于,在返回server层的过程中,只会拷贝数据行内的id这一列,而不会拷贝数据行的所有列,当数据量较大时,这部分的耗时还是比较明显的。在拿到了上面的id之后,假设这个id正好等于500000,那sql就变成了select * from xxx where id >=500000 order by id limit 10;这样innodb再走一次主键索引,通过B+树快速定位到id=500000的行数据,时间复杂度是lg(n),然后向后取10条数据。方法二:将所有的数据根据id主键进行排序,然后分批次取,将当前批次的最大id作为下次筛选的条件进行查询。select * from xxx where id > start_id order by id limit 10;mysql通过主键索引,每次定位到start_id的位置,然后往后遍历10个数据,这样不管数据多大,查询性能都较为稳定。高度为3的B+树,可以存放多少数据?InnoDB存储引擎有自己的最小储存单元——页(Page)。查询InnoDB页大小的命令如下:mysql> show global status like 'innodb_page_size'; +------------------+-------+ | Variable_name | Value | +------------------+-------+ | Innodb_page_size | 16384 | +------------------+-------+可以看出 innodb 默认的一页大小为 16384B = 16384/1024 = 16kb。在MySQL中,B+树一个节点的大小设为一页或页的倍数最为合适。因为如果一个节点的大小 < 1页,那么读取这个节点的时候其实读取的还是一页,这样就造成了资源的浪费。B+树中非叶子节点存的是key + 指针;叶子节点存的是数据行。对于叶子节点,如果一行数据大小为1k,那么一页就能存16条数据。对于非叶子节点,如果key使用的是bigint,则为8字节,指针在MySQL中为6字节,一共是14字节,则16k能存放 16 * 1024 / 14 = 1170 个索引指针。于是可以算出,对于一颗高度为2的B+树,根节点存储索引指针节点,那么它有1170个叶子节点存储数据,每个叶子节点可以存储16条数据,一共 1170 x 16 = 18720 条数据。而对于高度为3的B+树,就可以存放 1170 x 1170 x 16 = 21902400 条数据(两千多万条数据),也就是对于两千多万条的数据,我们只需要高度为3的B+树就可以完成,通过主键查询只需要3次IO操作就能查到对应数据。所以在 InnoDB 中B+树高度一般为3层时,就能满足千万级的数据存储。参考:https://www.cnblogs.com/leefreeman/p/8315844.htmlMySQL单表多大进行分库分表?目前主流的有两种说法:MySQL 单表数据量大于 2000 万行,性能会明显下降,考虑进行分库分表。阿里巴巴《Java 开发手册》提出单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。事实上,这个数值和实际记录的条数无关,而与 MySQL 的配置以及机器的硬件有关。因为MySQL为了提高性能,会将表的索引装载到内存中。在InnoDB buffer size 足够的情况下,其能完成全加载进内存,查询不会有问题。但是,当单表数据库到达某个量级的上限时,导致内存无法存储其索引,使得之后的 SQL 查询会产生磁盘 IO,从而导致性能下降。当然,这个还有具体的表结构的设计有关,最终导致的问题都是内存限制。因此,对于分库分表,需要结合实际需求,不宜过度设计,在项目一开始不采用分库与分表设计,而是随着业务的增长,在无法继续优化的情况下,再考虑分库与分表提高系统的性能。对此,阿里巴巴《Java 开发手册》补充到:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。至于MySQL单表多大进行分库分表,应当根据机器资源进行评估。大表查询慢怎么优化?某个表有近千万数据,查询比较慢,如何优化?当MySQL单表记录数过大时,数据库的性能会明显下降,一些常见的优化措施如下:合理建立索引。在合适的字段上建立索引,例如在WHERE和ORDER BY命令上涉及的列建立索引,可根据EXPLAIN来查看是否用了索引还是全表扫描建立分区。对关键字段建立水平分区,比如时间字段,若查询条件往往通过时间范围来进行查询,能提升不少性能利用缓存。利用Redis等缓存热点数据,提高查询效率限定数据的范围。比如:用户在查询历史信息的时候,可以控制在一个月的时间范围内读写分离。经典的数据库拆分方案,主库负责写,从库负责读通过分库分表的方式进行优化,主要有垂直拆分和水平拆分说说count(1)、count(*)和count(字段名)的区别嗯,先说说count(1) and count(字段名)的区别。两者的主要区别是count(1) 会统计表中的所有的记录数,包含字段为null 的记录。count(字段名) 会统计该字段在表中出现的次数,忽略字段为null 的情况。即不统计字段为null 的记录。接下来看看三者之间的区别。执行效果上:count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为NULLcount(1)包括了忽略所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULLcount(字段名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,即某个字段值为NULL时,不统计。执行效率上:列名为主键,count(字段名)会比count(1)快列名不为主键,count(1)会比count(列名)快如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*)如果有主键,则 select count(主键)的执行效率是最优的如果表只有一个字段,则 select count(*)最优。MySQL中DATETIME 和 TIMESTAMP有什么区别?嗯,TIMESTAMP和DATETIME都可以用来存储时间,它们主要有以下区别:1.表示范围DATETIME:1000-01-01 00:00:00.000000 到 9999-12-31 23:59:59.999999TIMESTAMP:'1970-01-01 00:00:01.000000' UTC 到 '2038-01-09 03:14:07.999999' UTCTIMESTAMP支持的时间范围比DATATIME要小,容易出现超出的情况。2.空间占用TIMESTAMP :占 4 个字节DATETIME:在 MySQL 5.6.4 之前,占 8 个字节 ,之后版本,占 5 个字节3.存入时间是否会自动转换TIMESTAMP类型在默认情况下,insert、update 数据时,TIMESTAMP列会自动以当前时间(CURRENT_TIMESTAMP)填充/更新。DATETIME则不会做任何转换,也不会检测时区,你给什么数据,它存什么数据。4.TIMESTAMP比较受时区timezone的影响以及MYSQL版本和服务器的SQL MODE的影响。因为TIMESTAMP存的是时间戳,在不同的时区得出的时间不一致。5.如果存进NULL,两者实际存储的值不同。TIMESTAMP:会自动存储当前时间 now() 。DATETIME:不会自动存储当前时间,会直接存入 NULL 值。说说为什么不建议用外键?外键是一种约束,这个约束的存在,会保证表间数据的关系始终完整。外键的存在,并非全然没有优点。外键可以保证数据的完整性和一致性,级联操作方便。而且使用外键可以将数据完整性判断托付给了数据库完成,减少了程序的代码量。虽然外键能够保证数据的完整性,但是会给系统带来很多缺陷。1、并发问题。在使用外键的情况下,每次修改数据都需要去另外一个表检查数据,需要获取额外的锁。若是在高并发大流量事务场景,使用外键更容易造成死锁。2、扩展性问题。比如从MySQL迁移到Oracle,外键依赖于数据库本身的特性,做迁移可能不方便。3、不利于分库分表。在水平拆分和分库的情况下,外键是无法生效的。将数据间关系的维护,放入应用程序中,为将来的分库分表省去很多的麻烦。使用自增主键有什么好处?自增主键可以让主键索引尽量地保持递增顺序插入,避免了页分裂,因此索引更紧凑,在查询的时候,效率也就更高。自增主键保存在什么地方?不同的引擎对于自增值的保存策略不同:MyISAM引擎的自增值保存在数据文件中。在MySQL8.0以前,InnoDB引擎的自增值是存在内存中。MySQL重启之后内存中的这个值就丢失了,每次重启后第一次打开表的时候,会找自增值的最大值max(id),然后将最大值加1作为这个表的自增值;MySQL8.0版本会将自增值的变更记录在redo log中,重启时依靠redo log恢复。自增主键一定是连续的吗?不一定,有几种情况会导致自增主键不连续。1、唯一键冲突导致自增主键不连续。当我们向一个自增主键的InnoDB表中插入数据的时候,如果违反表中定义的唯一索引的唯一约束,会导致插入数据失败。此时表的自增主键的键值是会向后加1滚动的。下次再次插入数据的时候,就不能再使用上次因插入数据失败而滚动生成的键值了,必须使用新滚动生成的键值。2、事务回滚导致自增主键不连续。当我们向一个自增主键的InnoDB表中插入数据的时候,如果显式开启了事务,然后因为某种原因最后回滚了事务,此时表的自增值也会发生滚动,而接下里新插入的数据,也将不能使用滚动过的自增值,而是需要重新申请一个新的自增值。3、批量插入导致自增值不连续。MySQL有一个批量申请自增id的策略:语句执行过程中,第一次申请自增id,分配1个自增id1个用完以后,第二次申请,会分配2个自增id2个用完以后,第三次申请,会分配4个自增id依次类推,每次申请都是上一次的两倍(最后一次申请不一定全部使用)如果下一个事务再次插入数据的时候,则会基于上一个事务申请后的自增值基础上再申请。此时就出现自增值不连续的情况出现。4、自增步长不是1,也会导致自增主键不连续。InnoDB的自增值为什么不能回收利用?主要为了提升插入数据的效率和并行度。假设有两个并行执行的事务,在申请自增值的时候,为了避免两个事务申请到相同的自增 id,肯定要加锁,然后顺序申请。假设事务 A 申请到了 id=2, 事务 B 申请到 id=3,那么这时候表 t 的自增值是 4,之后继续执行。事务 B 正确提交了,但事务 A 出现了唯一键冲突。如果允许事务 A 把自增 id 回退,也就是把表 t 的当前自增值改回 2,那么就会出现这样的情况:表里面已经有 id=3 的行,而当前的自增 id 值是 2。接下来,继续执行的其他事务就会申请到 id=2,然后再申请到 id=3。这时,就会出现插入语句报错“主键冲突”。而为了解决这个主键冲突,有两种方法:每次申请 id 之前,先判断表里面是否已经存在这个 id。如果存在,就跳过这个 id。但是,这个方法的成本很高。因为,本来申请 id 是一个很快的操作,现在还要再去主键索引树上判断 id 是否存在。把自增 id 的锁范围扩大,必须等到一个事务执行完成并提交,下一个事务才能再申请自增 id。这个方法的问题,就是锁的粒度太大,系统并发能力大大下降。可见,这两个方法都会导致性能问题。因此,InnoDB 放弃了“允许自增 id 回退”这个设计,语句执行失败也不回退自增 id。MySQL数据如何同步到Redis缓存?参考:https://cloud.tencent.com/developer/article/1805755有两种方案:1、通过MySQL自动同步刷新Redis,MySQL触发器+UDF函数实现。过程大致如下:在MySQL中对要操作的数据设置触发器Trigger,监听操作客户端向MySQL中写入数据时,触发器会被触发,触发之后调用MySQL的UDF函数UDF函数可以把数据写入到Redis中,从而达到同步的效果2、解析MySQL的binlog,实现将数据库中的数据同步到Redis。可以通过canal实现。canal是阿里巴巴旗下的一款开源项目,基于数据库增量日志解析,提供增量数据订阅&消费。canal的原理如下:canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议mysql master收到dump请求,开始推送binary log给canalcanal解析binary log对象(原始为byte流),将数据同步写入Redis。为什么阿里Java手册禁止使用存储过程?先看看什么是存储过程。存储过程是在大型数据库系统中,一组为了完成特定功能的SQL 语句集,它存储在数据库中,一次编译后永久有效,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。存储过程主要有以下几个缺点。存储过程难以调试。存储过程的开发一直缺少有效的 IDE 环境。SQL 本身经常很长,调试式要把句子拆开分别独立执行,非常麻烦。移植性差。存储过程的移植困难,一般业务系统总会不可避免地用到数据库独有的特性和语法,更换数据库时这部分代码就需要重写,成本较高。管理困难。存储过程的目录是扁平的,而不是文件系统那样的树形结构,脚本少的时候还好办,一旦多起来,目录就会陷入混乱。存储过程是只优化一次,有的时候随着数据量的增加或者数据结构的变化,原来存储过程选择的执行计划也许并不是最优的了,所以这个时候需要手动干预或者重新编译了。最后给大家分享一个Github仓库,上面有大彬整理的300多本经典的计算机书籍PDF,包括C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~Github地址:https://github.com/Tyson0314/java-books
文章
存储  ·  SQL  ·  缓存  ·  NoSQL  ·  关系型数据库  ·  MySQL  ·  Java  ·  数据库  ·  Redis  ·  索引
2023-01-20
OLAP联机分析处理介绍
作用   联机分析处理是共享多维信息的、针对特定问题的联机数据访问和分析的快速软件技术。它通过对信息的多种可能的观察形式进行快速、稳定一致和交互性的存取,允许管理决策人员对数据进行深入观察。决策数据是多维数据,多维数据就是决策的主要内容。OLAP专门设计用于支持复杂的分析操作,侧重对决策人员和高层管理人员的决策支持,可以根据分析人员的要求快速、灵活地进行大数据量的复杂查询处理,并且以一种直观而易懂的形式将查询结果提供给决策人员,以便他们准确掌握企业(公司)的经营状况,了解对象的需求,制定正确的方案。联机分析处理具有灵活的分析功能、直观的数据操作和分析结果可视化表示等突出优点,从而使用户对基于大量复杂数据的分析变得轻松而高效,以利于迅速做出正确判断。它可用于证实人们提出的复杂的假设,其结果是以图形或者表格的形式来表示的对信息的总结。它并不将异常信息标记出来,是一种知识证实的方法。 起源   联机分析处理 (OLAP) 的概念最早是由关系数据库之父E.F.Codd于1993年提出的,他同时提出了关于OLAP的12条准则。OLAP的提出引起了很大的反响,OLAP作为一类产品同联机事务处理 (OLTP) 明显区分开来。   Codd提出OLAP的12条准则来描述OLAP系统:   准则1 OLAP模型必须提供多维概念视图   准则2 透明性准则   准则3 存取能力准则   准则4 稳定的报表能力   准则5 客户/服务器体系结构   准则6 维的等同性准则   准则7 动态的稀疏矩阵处理准则   准则8 多用户支持能力准则   准则9 非受限的跨维操作   准则10 直观的数据操纵   准则11 灵活的报表生成   准则12 不受限的维与聚集层次 分类   当今的数据处理大致可以分成两大类:联机事务处理OLTP(on-line transaction processing)、联机分析处理OLAP(On-Line Analytical Processing)。OLTP是传统的关系型数据库的主要应用,主要是基本的、日常的事务处理,例如银行交易。OLAP是数据仓库系统的主要应用,支持复杂的分析操作,侧重决策支持,并且提供直观易懂的查询结果。下表列出了OLTP与OLAP之间的比较。   OLTP OLAP 用户 操作人员,低层管理人员 决策人员,高级管理人员 功能 日常操作处理 分析决策 DB 设计 面向应用 面向主题 数据 当前的, 最新的细节的, 二维的分立的 历史的, 聚集的, 多维的集成的, 统一的 存取 读/写数十条记录 读上百万条记录 工作单位 简单的事务 复杂的查询 DB 大小 100MB-GB 100GB-TB 发展背景   随着数据库技术的广泛应用,企业信息系统产生了大量的数据,如何从这些海量数据中提取对企业决策分析有用的信息成为企业决策管理人员所面临的重要难题。传统的企业数据库系统(管理信息系统)即联机事务处理系统(On-LineTransactionProcessing,简称OLTP)作为数据管理手段,主要用于事务处理,但它对分析处理的支持一直不能令人满意。因此,人们逐渐尝试对OLTP数据库中的数据进行再加工,形成一个综合的、面向分析的、更好的支持决策制定的决策支持系统(DecisionSupportSystem,简称DSS)。企业的信息系统的数据一般由DBMS管理,但决策数据库和运行操作数据库在数据来源、数据内容、数据模式、服务对象、访问方式、事务管理乃至物理存储等方面都有不同的特点和要求,因此直接在运行操作的数据库上建立DSS是不合适的。数据仓库(DataWarehouse)技术就是在这样的背景下发展起来的。数据仓库的概念提出于20世纪80年代中期,20世纪90年代,数据仓库已从早期的探索阶段走向实用阶段。业界公认的数据仓库概念创始人W.H.Inmon在《BuildingtheDataWarehouse》一书中对数据仓库的定义是:“数据仓库是支持管理决策过程的、面向主题的、集成的、随时间变化的持久的数据集合”。构建数据仓库的过程就是根据预先设计好的逻辑模式从分布在企业内部各处的OLTP数据库中提取数据并对经过必要的变换最终形成全企业统一模式数据的过程。当前数据仓库的核心仍是RDBMS管理下的一个数据库系统。数据仓库中数据量巨大,为了提高性能,RDBMS一般也采取一些提高效率的措施:采用并行处理结构、新的数据组织、查询策略、索引技术等等。   包括联机分析处理(On-LineAnalyticalProcessing,简称OLAP)在内的诸多应用牵引驱动了数据仓库技术的出现和发展;而数据仓库技术反过来又促进了OLAP技术的发展。联机分析处理的概念最早由关系数据库之父E.F.Codd于1993年提出的。Codd认为联机事务处理(OLTP)已不能满足终端用户对数据库查询分析的要求,SQL对大数据库的简单查询也不能满足用户分析的需求。用户的决策分析需要对关系数据库进行大量计算才能得到结果,而查询的结果并不能满足决策者提出的需求。因此,Codd提出了多维数据库和多维分析的概念,即OLAP。OLAP委员会对联机分析处理的定义为:从原始数据中转化出来的、能够真正为用户所理解的、并真实反映企业多维特性的数据称为信息数据,使分析人员、管理人员或执行人员能够从多种角度对信息数据进行快速、一致、交互地存取,从而获得对数据的更深入了解的一类软件技术。OLAP的目标是满足决策支持或多维环境特定的查询和报表需求,它的技术核心是“维”这个概念,因此OLAP也可以说是多维数据分析工具的集合。 特点   在过去的二十年中,大量的企业利用关系型数据库来存储和管理业务数据,并建立相应的应用系统来支持日常业务运作。这种应用以支持业务处理为主要目的,被称为联机事务处理(OLTP,On-line Transaction Processing)应用,它所存储的数据被称为操作数据或者业务数据。   随着市场竞争的日趋激烈,企业更加强调决策的及时性和准确性,这使得以支持决策管理分析为主要目的的应用迅速崛起,这类应用被称为联机分析处理,它所存储的数据被称为信息数据。   联机分析处理的用户是企业中的专业分析人员及管理决策人员,他们在分析业务经营的数据时,从不同的角度来审视业务的衡量指标是一种很自然的思考模式。例如分析销售数据,可能会综合时间周期、产品类别、分销渠道、地理分布、客户群类等多种因素来考量。这些分析角度虽然可以通过报表来反映,但每一个分析的角度可以生成一张报表,各个分析角度的不同组合又可以生成不同的报表,使得IT人员的工作量相当大,而且往往难以跟上管理决策人员思考的步伐。   联机分析处理的主要特点,是直接仿照用户的多角度思考模式,预先为用户组建多维的数据模型,在这里,维指的是用户的分析角度。例如对销售数据的分析,时间周期是一个维度,产品类别、分销渠道、地理分布、客户群类也分别是一个维度。一旦多维数据模型建立完成,用户可以快速地从各个分析角度获取数据,也能动态的在各个角度之间切换或者进行多角度综合分析,具有极大的分析灵活性。这也是联机分析处理被广泛关注的根本原因,它从设计理念和真正实现上都与旧有的管理信息系统有着本质的区别。   事实上,随着数据仓库理论的发展,数据仓库系统已逐步成为新型的决策管理信息系统的解决方案。数据仓库系统的核心是联机分析处理,但数据仓库包括更为广泛的内容。   -概括来说,数据仓库系统是指具有综合企业数据的能力,能够对大量企业数据进行快速和准确分析,辅助做出更好的商业决策的系统。它本身包括三部分内容:   1、数据层:实现对企业操作数据的抽取、转换、清洗和汇总,形成信息数据,并存储在企业级的中心信息数据库中。   2、应用层:通过联机分析处理,甚至是数据挖掘等应用处理,实现对信息数据的分析。   3、表现层:通过前台分析工具,将查询报表、统计分析、多维联机分析和数据发掘的结论展现在用户面前。   从应用角度来说,数据仓库系统除了联机分析处理外,还可以采用传统的报表,或者采用数理统计和人工智能等数据挖掘手段,涵盖的范围更广;就应用范围而言,联机分析处理往往根据用户分析的主题进行应用分割,例如:销售分析、市场推广分析、客户利润率分析等等,每一个分析的主题形成一个OLAP应用,而所有的OLAP应用实际上只是数据仓库系统的一部分。 逻辑概念   OLAP展现在用户面前的是一幅幅多维视图。维(Dimension):是人们观察数据的特定角度,是考虑问题时的一类属性,属性集合构成一个维(时间维、地理维等)。   维的层次(Level):人们观察数据的某个特定角度(即某个维)还可以存在细节程度不同的各个描述方面(时间维:日期、月份、季度、年)。   维的成员(Member):维的一个取值,是数据项在某维中位置的描述。(“某年某月某日”是在时间维上位置的描述)。   度量(Measure):多维数组的取值。(2000年1月,上海,笔记本电脑,0000)。   OLAP的基本多维分析操作有钻取(Drill-up和Drill-down)、切片(Slice)和切块(Dice)、以及旋转(Pivot)等。   钻取:是改变维的层次,变换分析的粒度。它包括向下钻取(Drill-down)和向上钻取(Drill-up)/上卷(Roll-up)。Drill-up是在某一维上将低层次的细节数据概括到高层次的汇总数据,或者减少维数;而Drill-down则相反,它从汇总数据深入到细节数据进行观察或增加新维。   切片和切块:是在一部分维上选定值后,关心度量数据在剩余维上的分布。如果剩余的维只有两个,则是切片;如果有三个或以上,则是切块。   旋转:是变换维的方向,即在表格中重新安排维的放置(例如行列互换)。 体系结构   数据仓库与OLAP的关系是互补的,现代OLAP系统一般以数据仓库作为基础,即从数据仓库中抽取详细数据的一个子集并经过必要的聚集存储到OLAP存储器中供前端分析工具读取。典型的OLAP系统体系结构如下图所示:   OLAP系统按照其存储器的数据存储格式可以分为关系OLAP(RelationalOLAP,简称ROLAP)、多维OLAP(MultidimensionalOLAP,简称MOLAP)和混合型OLAP(HybridOLAP,简称HOLAP)三种类型。 ROLAP   ROLAP将分析用的多维数据存储在关系数据库中并根据应用的需要有选择的定义一批实视图作为表也存储在关系数据库中。不必要将每一个SQL查询都作为实视图保存,只定义那些应用频率比较高、计算工作量比较大的查询作为实视图。对每个针对OLAP服务器的查询,优先利用已经计算好的实视图来生成查询结果以提高查询效率。同时用作ROLAP存储器的RDBMS也针对OLAP作相应的优化,比如并行存储、并行查询、并行数据管理、基于成本的查询优化、位图索引、SQL的OLAP扩展(cube,rollup)等等。 MOLAP   MOLAP将OLAP分析所用到的多维数据物理上存储为多维数组的形式,形成“立方体”的结构。维的属性值被映射成多维数组的下标值或下标的范围,而总结数据作为多维数组的值存储在数组的单元中。由于MOLAP采用了新的存储结构,从物理层实现起,因此又称为物理OLAP(PhysicalOLAP);而ROLAP主要通过一些软件工具或中间软件实现,物理层仍采用关系数据库的存储结构,因此称为虚拟OLAP(VirtualOLAP)。 HOLAP   由于MOLAP和ROLAP有着各自的优点和缺点(如下表所示),且它们的结构迥然不同,这给分析人员设计OLAP结构提出了难题。为此一个新的OLAP结构——混合型OLAP(HOLAP)被提出,它能把MOLAP和ROLAP两种结构的优点结合起来。迄今为止,对HOLAP还没有一个正式的定义。但很明显,HOLAP结构不应该是MOLAP与ROLAP结构的简单组合,而是这两种结构技术优点的有机结合,能满足用户各种复杂的分析请求。   rolap molap   沿用现有的关系数据库的技术   专为olap所设计   响应速度比molap慢;   现有关系型数据库已经对olap做了很多优化,包括并行存储、并行查询、并行数据管理、基于成本的查询优化、位图索引、sql 的olap扩展(cube,rollup)等,性能有所提高   性能好、响应速度快   数据装载速度快   数据装载速度慢   存储空间耗费小,维数没有限制   需要进行预计算,可能导致数据爆炸,维数有限;无法支持维的动态变化   借用rdbms存储数据,没有文件大小限制   受操作系统平台中文件大小的限制,难以达到tb 级(只能10~20g)   可以通过sql实现详细数据与概要数据的存储   缺乏数据模型和数据访问的标准   –不支持有关预计算的读写操作   –sql无法完成部分计算   –无法完成多行的计算   –无法完成维之间的计算   –支持高性能的决策支持计算   –复杂的跨维计算   –多用户的读写操作   –行级的计算   维护困难   管理简便 实现方式   同样是仿照用户的多角度思考模式,联机分析处理有三种不同的实现方法:   · 关系型联机分析处理(ROLAP,Relational OLAP)   · 多维联机分析处理(MOLAP,Multi-Dimensional OLAP)   · 前端展示联机分析处理(Desktop OLAP)   其中,前端展示联机分析需要将所有数据下载到客户机上,然后在客户机上进行数据结构/报表格式重组,使用户能在本机实现动态分析。该方式比较灵活,然而它能够支持的数据量非常有限,严重地影响了使用的范围和效率。因此,随着时间的推移,这种方式已退居次要地位,在此不作讨论。 实施方法   以下就ROLAP和MOLAP的具体实施方法进行讨论: 关系型联机   顾名思义,关系型联机分析处理是以关系型数据库为基础的。唯一特别之处在于联机分析处理中的数据结构组织的方式。   让我们考察一个例子,假设我们要进行产品销售的财务分析,分析的角度包括时间、产品类别、市场分布、实际发生与预算四方面内容,分析的财务指标包括:销售额、销售支出、毛利(=销售额-销售支出)、费用、纯利(=毛利-费用)等内容,则我们可以建立如下的数据结构:   该数据结构的中心是主表,里面包含了所有分析维度的外键,以及所有的财务指标,可计算推导的财务指标不计在内,我们称之为事实表(Fact Table)。周围的表分别是对应于各个分析角度的维表(Dimension Table),每个维表除了主键以外,还包含了描述和分类信息。无论原来的业务数据的数据结构为何,只要原业务数据能够整理成为以上模式,则无论业务人员据此提出任何问题,都可以用SQL语句进行表连接或汇总(table join and group by)实现数据查询和解答。(当然,有一些现成的ROLAP前端分析工具是可以自动根据以上模型生成SQL语句的)。这种模式被称为星型模式(Star-Schema),可应用于不同的联机分析处理应用中。   以下是另一个采用星型模式的例子,分析的角度和指标截然不同,但数据结构模式一样。我们看到的不是表的数据,而是表的结构。在联机分析处理的数据模型设计中,这种表达方式更为常见:   有时候,维表的定义会变得复杂,例如对产品维,既要按产品种类进行划分,对某些特殊商品,又要另外进行品牌划分,商品品牌和产品种类划分方法并不一样。因此,单张维表不是理想的解决方案,可以采用以下方式,这种数据模型实际上是星型结构的拓展,我们称之为雪花型模式(snow-flake schema).   无论采用星型模式还是雪花型模式,关系型联机分析处理都具有以下特点:   · 数据结构和组织模式需要预先设计和建立;   · 数据查询需要进行表连接,在查询性能测试中往往是影响速度的关键;   · 数据汇总查询(例如查询某个品牌的所有产品销售额),需要进行Group by 操作,虽然实际得出的数据量很少,但查询时间变得更长;   · 为了改善数据汇总查询的性能,可以建立汇总表,但汇总表的数量与用户分析的角度数目和每个角度的层次数目密切相关。例如,用户从8个角度进行分析,每个角度有3个汇总层次,则汇总表的数目高达3的8次方。   可以采取对常用汇总数据建立汇总表,对不常用的汇总数据进行Group by 操作,这样来取得性能和管理复杂度之间的均衡。 多维联机   多维联机分析处理实际上是用多维数组的方式对关系型数据表进行处理。下图是ROLAP与MOLAP的对比:   图中左边是ROLAP方式,右边是MOLAP方式,两者对应的是同一个三维模型。MOLAP首先对事实表中的所有外键进行排序,并将排序后的具体指标数值一一写进虚拟的多维立方体中。当然,虚拟的多维立方体只是为了便于理解而构想的,MOLAP实际的数据存储放在数据文件(Data File)中,其数据放置的顺序与虚拟的多维立方体按x,y,z坐标展开的顺序是一致的(如上图)。同时,为了数据查找的方便,MOLAP需要预先建立维度的索引,这个索引被放置在MOLAP的概要文件(Outline)中。   概要文件是MOLAP的核心,相当于ROLAP的数据模型设计。概要文件包括所有维的定义(包括复杂的维度结构)以及各个层次的数据汇总关系(例如在时间维,日汇总至月,月汇总至季,季汇总至年),这些定义往往从关系型维表中直接引入即可。概要文件也包括分析指标的定义,因此可以在概要文件中包含丰富的衍生指标,这些衍生指标由基础指标计算推导出来(例如ROLAP例子1中的纯利和毛利)。概要文件的结构如下图所示:   一旦概要文件定义好,MOLAP系统可以自动安排数据存储的方式和进行数据查询。从MOLAP的数据文件与ROLAP的事实表的对比可以看出,MOLAP的数据文件完全不需要纪录维度的外键,在维度比较多的情况下,这种数据存储方式大量地节省了空间。   但是,如果数据相当稀疏,虚拟的多维立方体中很多数值为空时,MOLAP的数据文件需要对相关的位置留空,而ROLAP的事实表却不会存储这些纪录。为了有效地解决这种情况,MOLAP采用了稀疏维和密集维相结合的处理方式,如下图。   上图的背景是某些客户只通过某些分销渠道才购买,但是只要该客户存在,他在各个月和各个地区内均有消费(例如,华南IBM只通过熊猫国旅定购南航机票,但在华南四省在每个月均有机票订购)。则时间和地区维是密集维,客户和分销渠道是稀疏维,MOLAP将稀疏维建成索引文件(Index File),密集维所对应的数值仍然保留在数据文件中,索引文件不存储空纪录。这样保持了对空间的合理利用。我们也可以看到,如果所有维都是稀疏维,则MOLAP的索引文件就退化成ROLAP的事实表, 两者没有区别了。   在实际应用中,不可能所有分析的维度都是密集的,也绝少存在所有分析的维度都是稀疏的,因此稀疏维和密集维并用的模式几乎主导了所有的MOLAP应用。而稀疏维和密集维的定义全部集中在概要文件中,因此,只要预先定义好概要文件,所有的数据分布就自动确定了。   在这种模式中,密集维的组合组成了的数据块(Data Block),每个数据块是I/O读写的基础单位(如上图),所有的数据块组成了数据文件。稀疏维的组合组成了索引文件,索引文件的每一个数据纪录的末尾都带有一个指针,指向要读写的数据块。因此,进行数据查询时,系统先搜索索引文件纪录,然后直接调用指针指向的数据块进行I/O读写(如果该数据块尚未驻留内存),将相应数据块调入内存后,根据密集维的数据放置顺序直接计算出要查询的数据距离数据块头的偏移量,直接提取数据下传到客户端。因此,MOLAP 方式基本上是索引搜索与直接寻址的查询方式相结合,比起ROLAP的表/索引搜索和表连接方式,速度要快得多。   特点   · 需要预先定义概要文件;   · 数据查询采用索引搜索与直接寻址的方式相结合,不需要进行表连接,在查询性能测试中比起ROLAP有相当大的优势;   · 在进行数据汇总查询之前,MOLAP需要预先按概要文件中定义的数据汇总关系进行计算,这个计算通常以批处理方式运行。计算结果回存在数据文件中,当用户查询时,直接调用计算结果,速度非常快。   · 无论是数据汇总还是计算衍生数据,预先计算的方式实际上是用空间来换时间。当然,用户也可以选择动态计算的方式,用查询时间来换取存储空间。MOLAP可以灵活调整时空的取舍平衡。   · 用户难以使用概要文件中没有定义的数据汇总关系和衍生指标。   · 在大数据量环境下,关系型数据库可以达到TB级的数据量,现有的MOLAP应用局限于基于文件系统的处理和查询方式,其性能会在100GB级别开始下降,需要进行数据分区处理,因此扩展性不如ROLAP。因此,MOLAP多数用于部门级的主题分析应用。 其它因素   联机分析处理其他要素包括假设分析(What-if),复杂计算,数据评估等等。这些因素对用户的分析效用至关重要,但是与ROLAP和MOLAP的核心工作原理的不一定有很紧密的关系,事实上,ROLAP和MOLAP都可以在以上三方面有所建树,只不过实现的方法迥异。因此,这些因素更取决于各个厂商为他们的产品提供的外延功能。对于像IBM的DB2 OLAP Server这样一个成熟的产品来说,这三方面均有独特的优势: 假设分析   假设分析提出了类似于以下的问题:"如果产品降价5%,而运费增加8%,对不同地区的分销商的进货成本会有什么影响?"这些问题常用于销售预测、费用预算分配、奖金制度确定等等。据此,用户可以分析出哪些角度、哪些因素的变化将对企业产生重要影响;并且,用户可以灵活调节自己手中掌握的资源(例如费用预算等),将它用到最有效的地方中去。   假设分析要求OLAP系统能够随用户的思路调整数据,并动态反映出在调整后对其他数据的影响结果。事实上,进入OLAP的数据分两大类:事实数据和预算数据。事实数据一般情况下不容修改,而预算数据则应常常进行调整。DB2 OLAP Server通过详细的权限定义区分了数据的读写权限,允许用户对预算数据进行更改,系统可以对其他受影响的数据进行计算,以反映出"假如发生如上情况,将会引起以下结果"的结论。 复杂计算   分析人员往往需要分析复杂的衍生数据,诸如:同期对比、期初/期末余额、百分比份额计算、资源分配(按从顶向下的结构图逐级分配)、移动平均、均方差等等。对这些要求,DB2 OLAP Server提供丰富的功能函数以便用户使用。因为只有在无需编程的环境下,商业用户才能更好地灵活利用这些功能进行复杂的真实世界模拟。 数据评估   数据评估包括两方面内容,有效性评估和商业意义评估。在有效性评估方面,数据抽取、清洗和转换的规则的定义是至关重要的。而合理的数据模型设计能有效防止无效数据的进入。例如在ROLAP中,如果维表没有采用范式设计(normalise design),可能会接受如下的维表:   机构代码 机构名称 所属区县 所属城市 所属省份   001 越秀支行 越秀区 广州 广东   002 祖庙支行 佛山 广州 广东   003 翠屏支行 佛山 南海 广东   004 。。。 。。。 。。。 。。。   显然,002中显示的佛山属于广州市,与003中显示的佛山属于南海市是矛盾的。这显示出数据源有问题,但是如果采用星型模式设计,ROLAP无法自动发现数据源的问题!   在类似情况下,MOLAP的表现稍占优势。因为MOLAP需要预先定义概要文件,而概要文件会详细分析维度的层次关系,因此生成概要文件时会反映数据源的错误。因此,在DB2 OLAP Server中,记录003会被拒收,并纪录在出错日志中,供IT人员更正。   但是,OLAP对数据源有效性的验证能力毕竟是有限的,因此,数据有效性必须从源数据一级和数据抽取/清洗/转换处理一级来进行保障。   对用户而言,数据的商业含义评估更有意义。在商业活动中,指标数值的取值范围是比较稳定的,如果指标数值突然发生变化,或者在同期比较、同类比较中有特殊表现,意味着该指标代表的方方面面具有特别的分析意义。普通的OLAP往往需要用户自己去观察发现异常指标,而DB2 OLAP Server的OLAP Minor(多维数据挖掘功能)能为用户特别地指出哪些条件下的哪些指标偏离常值,从而引起用户的注意和思考。例如:12月份南部的圣诞礼品销售额不到同期类似区域(东部、中部、西部)的50%。   综上所述,无论ROLAP还是MOLAP,都能够实现联机分析处理的基本功能,两者在查询效率,存储空间和扩展性方面各有千秋。IT人员在选择OLAP系统时,既要考虑产品内部的实现机制,同时也应考虑假设分析,复杂计算,数据评估方面的功能,为实现决策管理信息系统打下坚实的基础。
文章
存储  ·  OLAP  ·  OLTP  ·  数据库  ·  索引
2013-12-19
【面试-八股文】mysql 万字总结,助你吊打面试官
大家好,我是温大大前段时间大家在面试过程中,经常被问到数据库相关的问题。像:sql怎么优化,解释下数据库常见锁的,having 和 where区别等等。所以温大大爆肝1天2夜。肝了「万字」从数据库基础知识、到数据索引、索、事务 以及 面试高频面试题。包你从sql入门到入土,其他面试汇总:Linux 万字总结网络 100道高频面试题欢迎加入温大大面试群,找到温大大,让我帮你规划下学习线路 & 职业规划线路,帮你升职加薪。建议可以先收藏,然后遇到有不会了查看目录,直接跳到该目录进行查阅。目录:基础0.0 数据准备0.1 关联 inner/left/right/full join0.2 数据库的三大范式索引1.1 什么是索引1.2 索引的优缺点?1.3 索引的作用?1.4 索引的数据结构1.5 索引的分类1.6 索引的设计原则1.7 索引的失效原则1.8 哪些场景 能 建立索引1.9 哪些场景 不能 建立索引1.10 什么是最左匹配原则?1.11 什么是聚集索引?1.12 什么是覆盖索引?1.13 什么是前缀索引?1.14 什么是分库分表?1.15 什么是分区表?锁2.1 共享锁和排他锁是什么2.2 乐观锁和悲观锁是什么事务3.1 事务四大特性3.2 事务隔离级别有哪些关键词4.1 having 和 where区别?4.2 exist 和 in的区别?4.3 truncate、delete与drop区别?4.4 bin log/redo log/undo log 有什么区别?4.5 int(10)和char(10)的区别?4.6 preparedStatement和statement的区别?4.7 union 和union all的区别?4.8 数据库查询语言 DQL/DML/DCL 区别?MySQL 底层原理5.1 查询执行流程5.2 更新执行过程5.3 MySQL架构引擎6.1 MyISAM6.2 InnoDB6.3 MEMORY6.4 MERGE6.5 Archive6.6 引擎选择0 关联0.0 数据准备表创建drop table if exists test_a;CREATE TABLE `test_a` ( `id` varchar(10) NOT NULL, `username` varchar(10) NOT NULL, `password` varchar(10) NOT NULL, PRIMARY KEY  (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;drop table if exists test_a_description;CREATE TABLE `test_a_description` ( `id` varchar(10) NOT NULL, `age` varchar(10) , `address` varchar(50) , `parent_id` varchar(10) NOT NULL, PRIMARY KEY  (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;数据创建insert into test_a values('1','小明','11');insert into test_a values('2','宁宁','22');insert into test_a values('3','敏敏','33');insert into test_a values('6','生生','66');insert into test_a_description values('1','10','aaa','1');insert into test_a_description values('2','20','bbb','2');insert into test_a_description values('3','30','ccc','3');insert into test_a_description values('4','40','ddd','4');0.1 关联内连接(inner join)典型的联接运算,使用像 = 或 <> 之类的比较运算符)。包括相等联接和自然联接。内联接使用比较运算符根据每个表共有的列的值匹配两个表中的行。例如,检索 students和courses表中学生标识号相同的所有行。select * from 表A inner join 表B on 判断条件;外连接左外连接(left join)以左表为主表(查询全部), 右表为辅表(没有的显示null)SQL:select * from 表A left join 表B on 判断条件;右外连接(right join)以右表为主表(查询全部), 左表为辅表(没有的显示null)SQL:select * from 表A right join 表B on 判断条件;全连接(full join)两个表的所有数据都展示出来SQL:select * from 表A full join 表B on 判断条件;联合(union / union all)union 操作符合并的结果集,不会允许重复值,如果允许有重复值的话,使用UNION ALL.SQL:select * from A   unionselect * from B0.2 数据库的三大范式第1范式确保数据库表字段的原子性。比如字段 userInfo: 广东省 10086' ,依照第一范式必须拆分成 userInfo: 广东省 userTel:10086两个字段。第2范式首先要满足第一范式,另外包含两部分内容,一是表必须有一个主键;二是非主键列必须完全依赖于主键,而不能只依赖于主键的一部分。举个例子。假定选课关系表为student_course(student_no, student_name, age, course_name, grade, credit),主键为(student_no, course_name)。其中学分完全依赖于课程名称,姓名年龄完全依赖学号,不符合第二范式,会导致数据冗余(学生选n门课,姓名年龄有n条记录)、插入异常(插入一门新课,因为没有学号,无法保存新课记录)等问题。可以拆分成三个表:学生:student(stuent_no, student_name, 年龄);课程:course(course_name, credit);选课关系:student_course_relation(student_no, course_name, grade)。第3范式首先要满足第二范式,另外非主键列必须直接依赖于主键,不能存在传递依赖。即不能存在:非主键列 A 依赖于非主键列 B,非主键列 B 依赖于主键的情况。假定学生关系表为Student(student_no, student_name, age, academy_id, academy_telephone),主键为"学号",其中学院id依赖于学号,而学院地点和学院电话依赖于学院id,存在传递依赖,不符合第三范式。可以把学生关系表分为如下两个表:学生:(student_no, student_name, age, academy_id);学院:(academy_id, academy_telephone)。2NF和3NF的区别2NF依据是非主键列是否完全依赖于主键,还是依赖于主键的一部分。3NF依据是非主键列是直接依赖于主键,还是直接依赖于非主键。1 索引1.1 什么是索引索引是存储引擎用于提高数据库表的访问速度的一种「数据结构」。1.2 索引的优缺点优点:加快数据查找的速度为用来排序或者是分组的字段添加索引,可以加快分组和排序的速度加快表与表之间的连接缺点:建立索引需要占用物理空间会降低表的增删改的效率,因为每次对表记录进行增删改,需要进行动态维护索引,导致增删改时间变长1.3 索引的作用?数据是存储在磁盘上的,查询数据时。如果没有索引,会加载所有的数据到内存,依次进行检索,读取磁盘次数较多。有了索引,就不需要加载所有数据,因为B+树的高度一般在2-4层,最多只需要读取2-4次磁盘,查询速度大大提升。1.4 索引的数据结构索引的数据结构主要有「B+树」和「哈希表」InnoDB引擎的索引类型有「B+树索引」和「哈希索引」默认的索引类型为「B+树索引」B+树索引B+ 树是基于「B 树」和「叶子节点」顺序访问指针进行实现,它具有B树的平衡性,并且通过「顺序访问指针」来提高区间查询的性能。在 B+ 树中,节点中的 key 从「左到右递」增排列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。进行查找操作时,首先在根节点进行「二分查找」,找到key所在的指针,然后「递归」地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出key所对应的数据项。MySQL 数据库使用最多的索引类型是「BTREE索引」,底层基于「B+树|数据结构来实现。哈希索引哈希索引是基于「哈希表」实现的对于每一行数据,存储引擎会对索引列进行哈希计算得到「哈希码」并且哈希算法要尽量保证不同的列值计算出的「哈希码值」是不同的,将哈希码的值作为哈希表的key值将指向数据行的「指针」作为哈希表的value值。这样查找一个数据的时间复杂度就是「O(1)」,一般多用于精确查找。Hash索引和B+树索引的区别?哈希索引「不支持排序」,因为哈希表是无序的。哈希索引「不支持范围查找」。哈希索引「不支持模糊查询」及「多列索引的最左前缀匹配」。因为哈希表中会存在哈希冲突,所以哈希索引的「性能是不稳定的」,而B+树索引的性能是「相对稳定的」,每次查询都是从根节点到叶子节点。为什么B+树比B树更适合实现数据库索引?由于B+树的数据都存储在「叶子结点」中,叶子结点均为索引,方便扫库,只需要扫一遍叶子结点即可但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次「中序遍历」按序来扫所以B+树更加适合在「区间查询」的情况,而在数据库中基于范围的查询是「非常频繁」的,所以通常B+树用于数据库索引。B+树的节点只存储「索引key」值,具体信息的地址存在于「叶子节点」的地址中。这就使以页为单位的索引中可以存放更多的节点,减少更多的「I/O支出」。B+树的查询「效率更加稳定」,任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。1.5 索引的分类1、主键索引:名为primary的唯一非空索引,不允许有空值。2、唯一索引:索引列中的值必须是唯一的,但是允许为空值。唯一索引和主键索引的区别是:唯一约束的列可以为null且可以存在多个null值。唯一索引的用途:唯一标识数据库表中的每条记录,主要是用来防止数据重复插入。3、组合索引:在表中的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用,使用组合索引时需遵循最左前缀原则。4、全文索引:只有在MyISAM引擎上才能使用,只能在CHAR、VARCHAR和TEXT类型字段上使用全文索引。1.6 索引的设计原则设计原则尽量使用「短索引」,对于较长的字符串进行索引时应该指定一个较短的前缀长度,因为较小的索引涉及到的磁盘I/O较少,查询速度更快。索引「不是越多越好」,每个索引都需要额外的物理空间,维护也需要花费时间。利用「最左前缀原则」。1.7 索引的失效原则导致索引失效的情况对于组合索引,不是使用组合索引最左边的字段,则不会使用索引以%开头的like查询如%abc,无法使用索引;非%开头的like查询如abc%,相当于范围查询,会使用索引查询条件中列类型是字符串,没有使用引号,可能会因为类型不同发生隐式转换,使索引失效,例:where col=a判断索引列是否不等于某个值时,例:where col!=123对索引列进行运算,查询条件使用or连接,也会导致索引失效,例:where col_a=123 or col_b=4561.8 哪些场景 能 建立索引经常用于查询的字段经常用于连接的字段建立索引,可以加快连接的速度经常需要排序的字段建立索引,因为索引已经排好序,可以加快排序查询速度1.9 哪些场景 不能 建立索引where条件中用不到的字段不适合建立索引表记录较少需要经常增删改参与列计算的列不适合建索引区分度不高的字段不适合建立索引,如性别等1.10 什么是最左匹配原则?最左匹配原则如果 SQL 语句中用到了组合索引中的最左边的索引,那么这条 SQL 语句就可以利用这个组合索引去进行匹配。当遇到范围查询(>、<、between、like)就会停止匹配,后面的字段不会用到索引。对(a,b,c)建立索引,查询条件使用 a/ab/abc 会走索引,使用 bc 不会走索引。对(a,b,c,d)建立索引,查询条件为a = 1 and b = 2 and c > 3 and d = 4,那么a、b和c三个字段能用到索引,而d无法使用索引。因为遇到了范围查询。1.11 什么是聚集索引?InnoDB使用表的主键构造主键索引树,同时叶子节点中存放的即为整张表的记录数据。聚集索引叶子节点的存储是逻辑上连续的,使用双向链表连接,叶子节点按照主键的顺序排序,因此对于主键的排序查找和范围查找速度比较快。聚集索引的叶子节点就是整张表的行记录。InnoDB 主键使用的是聚簇索引。聚集索引要比非聚集索引查询效率高很多。对于InnoDB来说,聚集索引一般是表中的主键索引,如果表中没有显示指定主键,则会选择表中的第一个不允许为NULL的唯一索引。如果没有主键也没有合适的唯一索引,那么InnoDB内部会生成一个隐藏的主键作为聚集索引,这个隐藏的主键长度为6个字节,它的值会随着数据的插入自增。1.12 什么是覆盖索引?select的数据列只用从索引中就能够取得,不需要回表进行二次查询,也就是说查询列要被所使用的索引覆盖。对于innodb表的二级索引,如果索引能覆盖到查询的列,那么就可以避免对主键索引的二次查询。不是所有类型的索引都可以成为覆盖索引。覆盖索引要存储索引列的值,而哈希索引、全文索引不存储索引列的值,所以MySQL使用b+树索引做覆盖索引。对于使用了覆盖索引的查询,在查询前面使用explain,输出的extra列会显示为using index。比如user_like 用户点赞表,组合索引为(user_id, blog_id),user_id和blog_id都不为null。explain select blog_id from user_like where user_id = 13;explain结果的Extra列为Using index,查询的列被索引覆盖,并且where筛选条件符合最左前缀原则,通过索引查找就能直接找到符合条件的数据,不需要回表查询数据。explain select user_id from user_like where blog_id = 1;explain结果的Extra列为Using where; Using index, 查询的列被索引覆盖,where筛选条件不符合最左前缀原则,无法通过索引查找找到符合条件的数据,但可以通过索引扫描找到符合条件的数据,也不需要回表查询数据。1.13 什么是前缀索引?有时需要在很长的字符列上创建索引,这会造成索引特别大且慢。使用前缀索引可以避免这个问题。前缀索引是指对文本或者字符串的前几个字符建立索引,这样索引的长度更短,查询速度更快。创建前缀索引的关键在于选择足够长的前缀以保证较高的索引选择性。索引选择性越高查询效率就越高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的数据行。建立前缀索引的方式:ALTER TABLE table_name ADD KEY(column_name(prefix_length));1.14 什么是分库分表?原因:索引不能提升性能时,引入分库分表当单表的数据量达到1000W或100G以后,优化索引、添加从库等可能对数据库性能提升效果不明显,此时就要考虑对其进行切分了。切分的目的就在于减少数据库的负担,缩短查询的时间。数据切分可以分为两种方式:垂直划分和水平划分。垂直划分垂直划分数据库是根据业务进行划分,例如购物场景,可以将库中涉及商品、订单、用户的表分别划分出成一个库,通过降低单库的大小来提高性能。同样的,分表的情况就是将一个大表根据业务功能拆分成一个个子表,例如商品基本信息和商品描述,商品基本信息一般会展示在商品列表,商品描述在商品详情页,可以将商品基本信息和商品描述拆分成两张表。优点:行记录变小,数据页可以存放更多记录,在查询时减少I/O次数。缺点:主键出现冗余,需要管理冗余列;会引起表连接JOIN操作,可以通过在业务服务器上进行join来减少数据库压力;依然存在单表数据量过大的问题。水平划分水平划分是根据一定规则,例如时间或id序列值等进行数据的拆分。比如根据年份来拆分不同的数据库。每个数据库结构一致,但是数据得以拆分,从而提升性能。优点:单库(表)的数据量得以减少,提高性能;切分出的表结构相同,程序改动较少。缺点:分片事务一致性难以解决,跨节点join性能差,逻辑复杂数据分片在扩容时需要迁移1.15 什么是分区表?分区表是一个独立的逻辑表,但是底层由多个物理子表组成。当查询条件的数据分布在某一个分区的时候,查询引擎只会去某一个分区查询,而不是遍历整个表。在管理层面,如果需要删除某一个分区的数据,只需要删除对应的分区即可。分区表类型按照范围分区。CREATE TABLE test_range_partition(       id INT auto_increment,       createdate DATETIME,       primary key (id,createdate)   )   PARTITION BY RANGE (TO_DAYS(createdate) ) (      PARTITION p201801 VALUES LESS THAN ( TO_DAYS('20180201') ),      PARTITION p201802 VALUES LESS THAN ( TO_DAYS('20180301') ),      PARTITION p201803 VALUES LESS THAN ( TO_DAYS('20180401') ),      PARTITION p201804 VALUES LESS THAN ( TO_DAYS('20180501') ),      PARTITION p201805 VALUES LESS THAN ( TO_DAYS('20180601') ),      PARTITION p201806 VALUES LESS THAN ( TO_DAYS('20180701') ),      PARTITION p201807 VALUES LESS THAN ( TO_DAYS('20180801') ),      PARTITION p201808 VALUES LESS THAN ( TO_DAYS('20180901') ),      PARTITION p201809 VALUES LESS THAN ( TO_DAYS('20181001') ),      PARTITION p201810 VALUES LESS THAN ( TO_DAYS('20181101') ),      PARTITION p201811 VALUES LESS THAN ( TO_DAYS('20181201') ),      PARTITION p201812 VALUES LESS THAN ( TO_DAYS('20190101') )   );list分区create table test_list_partiotion   (       id int auto_increment,       data_type tinyint,       primary key(id,data_type)   )partition by list(data_type)   (       partition p0 values in (0,1,2,3,4,5,6),       partition p1 values in (7,8,9,10,11,12),       partition p2 values in (13,14,15,16,17)   );hash分区create table test_hash_partiotion   (       id int auto_increment,       create_date datetime,       primary key(id,create_date)   )partition by hash(year(create_date)) partitions 10;分区的问题打开和锁住所有底层表的成本可能很高。例子:当查询访问分区表时,MySQL 需要打开并锁住所有的底层表,这个操作在分区过滤之前发生,所以无法通过分区过滤来降低此开销,会影响到查询速度。可以通过批量操作来降低此类开销,比如批量插入、LOAD DATA INFILE和一次删除多行数据维护分区的成本可能很高。例子:重组分区,会先创建一个临时分区,然后将数据复制到其中,最后再删除原分区。所有分区必须使用相同的存储引擎。2 锁2.1 共享锁和排他锁是什么共享锁例子:我们进入洗手间只是想洗手的话,我们一般不会锁门。而其他人也可以进来洗手、化妆等。但是,其他人是不可以进来上厕所的。这就是共享锁,也叫读锁。就是只读不写。用法select * from table where id<6 lock in share mode;--共享锁select ... lock in share mode;当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。其他线程也可以读取使用了共享锁的表,而且这些线程读取的是同一个版本的数据。排他锁例子:如果我们进入洗手间是为了上厕所,那么任何人不能再进来做任何事。这就是排他锁,也叫写锁。用法select * from table where id<6 for update;--排他锁select ... for update;在查询语句后面增加FOR UPDATE,Mysql会对查询结果中的每行都加排他锁加锁原则拿MySql的InnoDB引擎来说,对于insert、update、delete等操作。会自动给涉及的数据加排他锁;对于一般的select语句,InnoDB不会加任何锁,事务可以通过以下语句给显示加共享锁或排他锁。共享锁:SELECT ... LOCK IN SHARE MODE;排他锁:SELECT ... FOR UPDATE;事务3.1 事务四大特性事务特性ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。原子性:指事务包含的所有操作要么全部成功,要么全部失败回滚。一致性:指一个事务执行之前和执行之后都必须处于一致性状态。 比如a与b账户共有1000块,两人之间转账之后无论成功还是失败, 它们的账户总和还是1000。隔离性:跟隔离级别相关,如read committed,一个事务只能读到已经提交的修改。持久性:指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的。3.2 事务隔离级别有哪些问题脏读:是指在一个事务处理过程里读取了另一个未提交的事务中的数据。幻读:是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行,就像产生幻觉一样,这就是发生了幻读。不可重复读:是指在对于数据库中的某行记录,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,另一个事务修改了数据并提交了。区别不可重复读 和 脏读 的区别是:脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。不可重复读 和 幻读 都是读取了另一条已经提交的事务,不同的是不可重复读的重点是修改,幻读的重点在于新增或者删除。事务隔离就是为了解决上面的问题Serializable (串行化):通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。Repeatable read (可重复读):MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行,解决了不可重复读的问题。Read committed (读已提交):一个事务只能看见已经提交事务所做的改变。可避免脏读的发生。Read uncommitted (读未提交):所有事务都可以看到其他未提交事务的执行结果。4 关键词4.1 having 和 where区别?二者作用的对象不同,where子句作用于「表和视图」,having作用于「组」。where在数据「分组前」进行过滤,having在数据「分组后」进行过滤。4.2 exist 和 in的区别?exists用于对外表记录做筛选。exists会遍历外表,将外查询表的每一行,代入内查询进行判断。当exists里的条件语句能够返回记录行时,条件就为真,返回外表当前记录。反之如果exists里的条件语句不能返回记录行,条件为假,则外表当前记录被丢弃。select a.* from A awhere exists(select 1 from B b where a.id=b.id)in是先把后边的语句查出来放到临时表中,然后遍历临时表,将临时表的每一行,代入外查询去查找。select * from Awhere id in(select id from B)子查询的表比较大的时候,使用exists可以有效减少总的循环次数来提升速度;当外查询的表比较大的时候,使用in可以有效减少对外查询表循环遍历来提升速度。4.3 truncate、delete与drop区别?相同truncate和不带where子句的delete、以及drop都会删除表内的数据。drop、truncate都是DDL语句(数据定义语言),执行后会自动提交。不同truncate 和 delete 只删除数据不删除表的结构;drop 语句将删除表的结构被依赖的约束、触发器、索引;一般来说,执行速度: drop > truncate > delete。4.4 bin log/redo log/undo log 有什么区别?MySQL日志主要包括查询日志、慢查询日志、事务日志、错误日志、二进制日志等。 其中比较重要的是bin log(二进制日志)redo log(重做日志)undo log(回滚日志)bin logbin log是MySQL数据库级别的文件,记录对MySQL数据库执行修改的所有操作,不会记录select和show语句,主要用于恢复数据库和同步数据库。redo logredo log是innodb引擎级别,用来记录innodb存储引擎的事务日志,不管事务是否提交都会记录下来,用于数据恢复。当数据库发生故障,innoDB存储引擎会使用redo log恢复到发生故障前的时刻,以此来保证数据的完整性。将参数innodb_flush_log_at_tx_commit设置为1,那么在执行commit时会将redo log同步写到磁盘。undo log除了记录redo log外,当进行数据修改时还会记录undo log,undo log用于数据的撤回操作,它保留了记录修改前的内容。通过undo log可以实现事务回滚,并且可以根据undo log回溯到某个特定的版本的数据,实现MVCC4.5 int(10)和char(10)的区别?int(10) 表示「显示」数据的长度,char(10)表示「存储」数据的长度。4.6 preparedStatement和statement的区别?任何时候使用preparedStatement而不是statementPreparedStatement预编译,防止SQL注入PreparedStatement多次使用可提高效率4.7 union 和union all的区别?union会对结果集进行处理排除掉相同的结果union all 不会对结果集进行处理,不会处理掉相同的结果4.8 数据库查询语言 DQL/DML/DCL 区别?DQL(Data Query Language)数据查询语言DQL由SELECT子句,FROM子句,WHERE子句组成DML(Data Manipulation Language)数据操纵语言DML包含INSERT,UPDATE,DELETEDDL(Data Definition Language)数据定义语言DDL用来创建数据库中的各种对象-----表、视图、索引、同义词、聚簇等如:CREATE TABLE/VIEW/INDEX/SYN/CLUSTER DDL操作是隐性提交的!不能rollbackDCL(Data Control Language)数据控制语言(DCL)是用来设置或者更改数据库用户或角色权限的语句,这些语句包括GRANT、DENY、REVOKE等语句,在默认状态下,只有sysadmin、dbcreator、db_owner或db_securityadmin等角色的成员才有权利执行数据控制语言。5 mysql执行原理5.1 查询执行流程查询语句的执行流程如下: 权限校验、查询缓存、分析器、优化器、权限校验、执行器、引擎。举个例子,查询语句如下:select * from user where id > 1 and name = '温大大';首先检查权限,没有权限则返回错误;MySQL8.0以前会查询缓存,缓存命中则直接返回,没有则执行下一步;词法分析和语法分析。提取表名、查询条件,检查语法是否有错误;两种执行方案,先查 id > 1 还是 name = '大彬',优化器根据自己的优化算法选择执行效率最好的方案;校验权限,有权限就调用数据库引擎接口,返回引擎的执行结果。5.2 更新执行流程更新语句执行流程如下:分析器、权限校验、执行器、引擎、redo log(prepare状态)、binlog、redo log(commit状态)举个例子,更新语句如下:update user set name = '温大大' where id = 1;先查询到 id 为1的记录,有缓存会使用缓存。拿到查询结果,将 name 更新为大彬,然后调用引擎接口,写入更新数据,innodb 引擎将数据保存在内存中,同时记录redo log,此时redo log进入 prepare状态。执行器收到通知后记录binlog,然后调用引擎接口,提交redo log为commit状态。更新完成。问:为什么记录完redo log,不直接提交,而是先进入prepare状态?答:假设先写redo log直接提交,然后写binlog,写完redo log后,机器挂了,binlog日志没有被写入,那么机器重启后,这台机器会通过redo log恢复数据,但是这个时候binlog并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。5.3 MySQL架构MySQL主要分为Server 层存储引擎层:Server 层:主要包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块 binglog 日志模块。存储引擎:主要负责数据的存储和读取。server 层通过api与存储引擎进行通信。 Server 层基本组件连接器: 当客户端连接 MySQL 时,server层会对其进行身份认证和权限校验。查询缓存: 执行查询语句的时候,会先查询缓存,先校验这个 sql 是否执行过,如果有缓存这个 sql,就会直接返回给客户端,如果没有命中,就会执行后续的操作。分析器: 没有命中缓存的话,SQL 语句就会经过分析器,主要分为两步,词法分析和语法分析,先看 SQL 语句要做什么,再检查 SQL 语句语法是否正确。优化器: 优化器对查询进行优化,包括重写查询、决定表的读写顺序以及选择合适的索引等,生成执行计划。执行器: 首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会根据执行计划去调用引擎的接口,返回结果。6 引擎6.1 MyISAM简介MyISAM基于ISAM存储引擎,并对其进行扩展。它是在Web、数据仓储和其他应用环境下最常使用的存储引擎之一。MyISAM拥有较高的插入、查询速度,但不支持事务。MyISAM表格可以被压缩,而且它们支持全文搜索。不支持事务,而且也不支持外键。如果事物回滚将造成不完全回滚,不具有原子性。在进行updata时进行表锁,并发量相对较小。如果执行大量的SELECT,MyISAM是更好的选择。MyISAM的索引和数据是分开的,并且索引是有压缩的,内存使用率就对应提高了不少。能加载更多索引,而Innodb是索引和数据是紧密捆绑的,没有使用压缩从而会造成Innodb比MyISAM体积庞大。MyISAM缓存在内存的是索引,不是数据。而InnoDB缓存在内存的是数据,相对来说,服务器内存越大,InnoDB发挥的优势越大。特性大文件(达到63位文件长度)在支持大文件的文件系统和操作系统上被支持当把删除和更新及插入操作混合使用的时候,动态尺寸的行产生更少碎片。这要通过合并相邻被删除的块,以及若下一个块被删除,就扩展到下一块自动完成每个MyISAM表最大索引数是64,这可以通过重新编译来改变。每个索引最大的列数是16最大的键长度是1000字节,这也可以通过编译来改变,对于键长度超过250字节的情况,一个超过1024字节的键将被用上BLOB和TEXT列可以被索引NULL被允许在索引的列中,这个值占每个键的0~1个字节所有数字键值以高字节优先被存储以允许一个更高的索引压缩每个MyISAM类型的表都有一个AUTO_INCREMENT的内部列,当INSERT和UPDATE操作的时候该列被更新,同时AUTO_INCREMENT列将被刷新。所以说,MyISAM类型表的AUTO_INCREMENT列更新比InnoDB类型的AUTO_INCREMENT更快可以把数据文件和索引文件放在不同目录每个字符列可以有不同的字符集有VARCHAR的表可以固定或动态记录长度VARCHAR和CHAR列可以多达64KB使用MyISAM引擎创建数据库,将产生3个文件。文件的名字以表名字开始,扩展名之处文件类型:frm文件存储表定义、数据文件的扩展名为.MYD(MYData)、索引文件的扩展名时.MYI(MYIndex)6.2 InnoDB简介InnoDB是事务型数据库的首选引擎,支持事务安全表(ACID),支持行锁定和外键,上图也看到了,InnoDB是默认的MySQL引擎。InnoDB 采用MVCC(多版本并发控制)来支持高并发,并实现了四个标准的隔离级别。其默认级别是REPEATABLE READ(可重复读),并且通过间隙锁(next-key locking)策略防止幻读的出现。间隙锁是的 InnoDB 不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。InnoDB 表是基于聚簇索引建立的。InnoDB 的索引结构和 MySQL 的其他存储引擎有很大不同,聚簇索引对主键查询有很高的性能。不过它的二级索引(secondary index,非主键索引)中必须包含主键列,所以如果主键很大的话,其他的所有索引都会很大。因此,若表上的索引较多的话,主键应当尽可能的小。InnoDB不创建目录,使用InnoDB时,MySQL将在MySQL数据目录下创建一个名为ibdata1的10MB大小的自动扩展数据文件,以及两个名为ib_logfile0和ib_logfile1的5MB大小的日志文件。特性InnoDB给MySQL提供了具有提交、回滚和崩溃恢复能力的事物安全(ACID兼容)存储引擎。InnoDB锁定在行级并且也在SELECT语句中提供一个类似Oracle的非锁定读。这些功能增加了多用户部署和性能。在SQL查询中,可以自由地将InnoDB类型的表和其他MySQL的表类型混合起来,甚至在同一个查询中也可以混合InnoDB是为处理巨大数据量的最大性能设计。它的CPU效率可能是任何其他基于磁盘的关系型数据库引擎锁不能匹敌的InnoDB存储引擎完全与MySQL服务器整合,InnoDB存储引擎为在主内存中缓存数据和索引而维持它自己的缓冲池。InnoDB将它的表和索引在一个逻辑表空间中,表空间可以包含数个文件(或原始磁盘文件)。这与MyISAM表不同,比如在MyISAM表中每个表被存放在分离的文件中。InnoDB表可以是任何尺寸,即使在文件尺寸被限制为2GB的操作系统上InnoDB支持外键完整性约束,存储表中的数据时,每张表的存储都按主键顺序存放,如果没有显示在表定义时指定主键,InnoDB会为每一行生成一个6字节的ROWID,并以此作为主键InnoDB被用在众多需要高性能的大型数据库站点上6.3 MEMORY简介使用MySQL Memory存储引擎的出发点是速度。为得到最快的响应时间,采用的逻辑存储介质是系统内存。虽然在内存中存储表数据确实会提供很高的性能,但当mysqld守护进程崩溃时,所有的Memory数据都会丢失。获得速度的同时也带来了一些缺陷。它要求存储在Memory数据表里的数据使用的是长度不变的格式,这意味着不能使用BLOB和TEXT这样的长度可变的数据类型,VARCHAR是一种长度可变的类型,但因为它在MySQL内部当做长度固定不变的CHAR类型,所以可以使用。MEMORY主要特性有:特性MEMORY表的每个表可以有多达32个索引,每个索引16列,以及500字节的最大键长度MEMORY存储引擎执行HASH和BTREE缩影可以在一个MEMORY表中有非唯一键值MEMORY表使用一个固定的记录长度格式MEMORY不支持BLOB或TEXT列MEMORY支持AUTO_INCREMENT列和对可包含NULL值的列的索引MEMORY表在所由客户端之间共享(就像其他任何非TEMPORARY表)MEMORY表内存被存储在内存中,内存是MEMORY表和服务器在查询处理时的空闲中,创建的内部表共享当不再需要MEMORY表的内容时,要释放被MEMORY表使用的内存,应该执行DELETE FROM或TRUNCATE TABLE,或者删除整个表(使用DROP TABLE)6.4 MERGE简介MERGE存储引擎是一组MyISAM表的组合,这些MyISAM表结构必须完全相同,尽管其使用不如其它引擎突出,但是在某些情况下非常有用。说白了,Merge表就是几个相同MyISAM表的聚合器;Merge表中并没有数据,对Merge类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的MyISAM表进行操作。主要应用于服务器日志这种信息,一般常用的存储策略是将数据分成很多表,每个名称与特定的时间端相关。例如:可以用12个相同的表来存储服务器日志数据,每个表用对应各个月份的名字来命名。当有必要基于所有12个日志表的数据来生成报表,这意味着需要编写并更新多表查询,以反映这些表中的信息。与其编写这些可能出现错误的查询,不如将这些表合并起来使用一条查询,之后再删除Merge表,而不影响原来的数据,删除Merge表只是删除Merge表的定义,对内部的表没有任何影响。特性MERGE数据表可以用来创建一个尺寸超过各个MyISAM数据表所允许的最大长度逻辑单元你看一把经过压缩的数据表包括到MERGE数据表里。比如说,在某一年结束之后,你应该不会再往相应的日志文件里添加记录,所以你可以用myisampack工具压缩它以节省空间,而MERGE数据表仍可以像往常那样工作MERGE数据表也支持DELETE 和UPDATE操作。INSERT操作比较麻烦,因为MySQL需要知道应该把新数据行插入到哪一个成员表里去。在MERGE数据表的定义里可以包括一个INSERT_METHOD选项,这个选项的可取值是NO、FIRST、LAST,他们的含义依次是INSERT操作是被禁止的、新数据行将被插入到现在UNION选项里列出的第一个数据表或最后一个数据表。6.5 Archive简介Archive是归档的意思,在归档之后很多的高级功能就不再支持了,仅仅支持最基本的插入和查询两种功能。在MySQL 5.5版以前,Archive是不支持索引,但是在MySQL 5.5以后的版本中就开始支持索引了。Archive拥有很好的压缩机制,它使用zlib压缩库,在记录被请求时会实时压缩,所以它经常被用来当做仓库使用。6.6 引擎选择不同的存储引擎都有各自的特点,以适应不同的需求,如下表所示:InnoDB:如果要提供提交、回滚、崩溃恢复能力的事物安全(ACID兼容)能力,并要求实现并发控制,InnoDB是一个好的选择。MyISAM:如果数据表主要用来插入和查询记录,则MyISAM引擎能提供较高的处理效率。并且,如果你的应用程序对查询性能要求较高,就要使用MYISAM了。MYISAM索引和数据是分开的,而且其索引是压缩的,可以更好地利用内存。所以它的查询性能明显优于INNODB。压缩后的索引也能节约一些磁盘空间。MYISAM拥有全文索引的功能,这可以极大地优化LIKE查询的效率。Archive:如果只有INSERT和SELECT操作,可以选择Archive,Archive支持高并发的插入操作,但是本身不是事务安全的。Archive非常适合存储归档数据,如记录日志信息可以使用Archive。MERGE:对日志的一些综合操作,通常使用的是MERGE存储引擎。Memory:目标数据较小,而且被非常频繁地访问。1)在内存中存放数据,所以会造成内存的使用,可以通过参数max_heap_table_size控制Memory表的大小,设置此参数,就可以限制Memory表的最大大小。2)如果数据是临时的,而且要求必须立即可用,那么就可以存放在内存表中。3)存储在Memory表中的数据如果突然丢失,不会对应用服务产生实质的负面影响。4)如果只是临时存放数据,数据量不大,并且不需要较高的数据安全性,可以选择将数据保存在内存中的Memory引擎,MySQL中使用该引擎作为临时表,存放查询的中间结果。欢迎加入温大大面试群,找到温大大,让我帮你规划下学习线路 & 职业规划线路,帮你升职加薪。关注公众号:测试猿温大大
文章
存储  ·  SQL  ·  缓存  ·  自然语言处理  ·  安全  ·  算法  ·  关系型数据库  ·  MySQL  ·  数据库  ·  索引
2022-03-18
Code First开发系列之管理数据库创建,填充种子数据以及LINQ操作详解
本篇目录 管理数据库创建 管理数据库连接 管理数据库初始化 填充种子数据 LINQ to Entities详解 什么是LINQ to Entities 使用LINQ to Entities操作实体 LINQ操作 懒加载和预加载 插入数据 更新数据 删除数据 本章小结 自我测试 本篇的源码下载:点击下载 先附上codeplex上EF的源码:entityframework.codeplex.com,此外,本人的实验环境是VS 2013 Update 5,windows 10,MSSQL Server 2008。 上一篇《Code First开发系列之领域建模和管理实体关系》,我们主要介绍了EF中“约定大于配置”的概念,如何创建数据库的表结构,以及如何管理实体间的三种关系和三种继承模式。这一篇我们主要说三方面的问题,数据库创建的管理,种子数据的填充以及CRUD的操作详细用法。 管理数据库创建 管理数据库连接 使用配置文件管理连接 在数据库上下文类中,如果我们只继承了无参数的DbContext,并且在配置文件中创建了和数据库上下文类同名的连接字符串,那么EF会使用该连接字符串自动计算出该数据库的位置和数据库名。比如,我们的上下文定义如下: public class SampleDbEntities : DbContext { // Code here } 如果我们在配置文件中定义的连接字符串如下: <connectionStrings> <add name="SampleDbEntities" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=myTestDb;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\myTestDb.mdf" providerName="System.Data.SqlClient" /> </connectionStrings> 这样,EF会使用该连接字符串执行数据库操作。究竟发生了什么呢? 当运行应用程序时,EF会寻找我们的上下文类名,即“SampleDbEntities”,并在配置文件中寻找和它同名的连接字符串,然后它会使用该连接字符串计算出应该使用哪个数据库provider,之后检查数据库位置(例子中是当前的数据目录),之后会在指定的位置创建一个名为myTestDb.mdf的数据库文件,同时根据连接字符串的Initial Catalog属性创建了一个名为myTestDb的数据库。 使用配置文件指定数据库位置和名字对于控制上下文类的连接参数也许是最简单和最有效的方式,另一个好处是如果我们想为开发,生产和临时环境创建各自的连接字符串,那么在配置文件中更改连接字符串并在开发时将它指向确定的数据库也是一种方法。 这里要注意的重要的事情是在配置文件中定义的连接字符串具有最高优先权,它会覆盖所有在其它地方指定的连接参数。 从最佳实践的角度,也许不推荐使用配置文件。注入连接字符串是一种更好的方式,因为它给开发者更多的控制权和监管权。 使用已存在的ConnectionString 如果我们已经有了一个定义数据库位置和名称的ConnectionString,并且我们想在数据库上下文类中使用这个连接字符串,如下: <connectionStrings> <add name="AppConnection" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=testDb;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\testDb.mdf" providerName="System.Data.SqlClient" /> </connectionStrings> 那么我们可以将该连接字符串的名字传入数据库上下文DbContext的构造函数中,如下所示: public class SampleDbEntities : DbContext { public SampleDbEntities() :base("name=AppConnection") { } } 上面的代码将连接字符串的名字传给了DbContext类的构造函数,这样一来,我们的数据库上下文就会开始使用连接字符串了。 如果在配置文件中还有一个和数据库上下文类名同名的connectionString,那么就会使用这个同名的连接字符串。无论我们对传入的连接字符串名称如何改变,都是无济于事的,也就是说和数据库上下文类名同名的连接字符串优先权更大。 使用已存在的连接 通常在一些老项目中,我们只会在项目中的某个部分使用EF Code First,同时,我们想对数据上下文类使用已经存在的数据库连接,如果要实现这个,可将连接对象传给DbContext类的构造函数,如下: public class SampleDbEntities : DbContext { public SampleDbEntities( DbConnection con ) : base( con, contextOwnsConnection : false ) { } } 这里要注意一下contextOwnsConnection参数,之所以将它作为false传入到上下文,是因为它是从外部传入的,当上下文出了范围时,可能会有人想要使用该连接。如果传入true的话,那么一旦上下文出了范围,数据库连接就会立即关闭。 管理数据库初始化 首次运行EF Code First应用时,EF会做下面的这些事情: 检查正在使用的DbContext类。 找到该上下文类使用的connectionString。 找到领域实体并提取模式相关的信息。 创建数据库。 将数据插入系统。 一旦模式信息提取出来,EF会使用数据库初始化器将该模式信息推送给数据库。数据库初始化器有很多可能的策略,EF默认的策略是如果数据库不存在,那么就重新创建;如果存在的话就使用当前存在的数据库。当然,我们有时也可能需要覆盖默认的策略,可能用到的数据库初始化策略如下: CreateDatabaseIfNotExists:顾名思义,如果数据库不存在,那么就重新创建,否则就使用现有的数据库。如果从领域模型中提取到的模式信息和实际的数据库模式不匹配,那么就会抛出异常。 DropCreateDatabaseAlways:如果使用了该策略,那么每次运行程序时,数据库都会被销毁。这在开发周期的早期阶段通常很有用(比如设计领域实体时),从单元测试的角度也很有用。 DropCreateDatabaseIfModelChanges:这个策略的意思就是说,如果领域模型发生了变化(具体而言,从领域实体提取出来的模式信息和实际的数据库模式信息失配时),就会销毁以前的数据库(如果存在的话),并创建新的数据库。 MigrateDatabaseToLatestVersion:如果使用了该初始化器,那么无论什么时候更新实体模型,EF都会自动地更新数据库模式。这里很重要的一点是,这种策略更新数据库模式不会丢失数据,或者是在已有的数据库中更新已存在的数据库对象。 MigrateDatabaseToLatestVersion初始化器只有从EF 4.3才可用。 设置初始化策略 EF默认使用CreateDatabaseIfNotExists作为默认初始化器,如果要覆盖这个策略,那么需要在DbContext类中的构造函数中使用Database.SetInitializer方法,下面的例子使用DropCreateDatabaseIfModelChanges策略覆盖默认的策略: public class SampleDbEntities : DbContext { public SampleDbEntities() : base( "name=AppConnection" ) { Database.SetInitializer<SampleDbEntities>( new DropCreateDatabaseIfModelChanges<SampleDbEntities>() ); } } 这样一来,无论什么时候创建上下文类,Database.SetInitializer方法都会被调用,并且将数据库初始化策略设置为DropCreateDatabaseIfModelChanges。 如果处于生产环境,那么我们肯定不想丢失已存在的数据。这时我们就需要关闭该初始化器,只需要将null传给Database.SetInitializer方法,如下所示: public class SampleDbEntities : DbContext { public SampleDbEntities() : base( "name=AppConnection" ) { Database.SetInitializer<SampleDbEntities>(null); } } 填充种子数据 到目前为止,无论我们选择哪种策略初始化数据库,生成的数据库都是一个空的数据库。但是许多情况下我们总想在数据库创建之后、首次使用之前就插入一些数据,此外,开发阶段可能想以admin的资格为其填充一些数据,或者为了测试应用在特定的场景中表现如何,想要伪造一些数据。 当我们使用DropCreateDatabaseAlways和DropCreateDatabaseIfModelChanges初始化策略时,插入种子数据非常重要,因为每次运行应用时,数据库都要重新创建,每次数据库创建之后再手动插入数据非常乏味。接下来我们看一下当数据库创建之后如何使用EF来插入种子数据。 为了向数据库插入一些初始化数据,我们需要创建满足下列条件的数据库初始化器类: 从已存在的数据库初始化器类中派生数据 在数据库创建期间种子化 1.0 定义领域实体 假设我们的数据模型Employer定义如下: public class Employer { public int Id { get; set; } public string EmployerName { get; set; } } 2.0 创建数据库上下文 使用EF的Code First方法对上面的模型创建数据库上下文: public class SeedingDataContext:DbContext { public virtual DbSet<Employer> Employers { get; set; } } 3.0 创建数据库初始化器类 假设我们使用的是DropCreateDatabaseAlways数据库初始化策略,那么初始化器类就要从该泛型类继承,并传入数据库上下文作为类型参数。接下来,要种子化数据库就要重写DropCreateDatabaseAlways类的Seed方法,而Seed方法拿到了数据库上下文,因此我们可以使用它来将数据插入数据库: public class SeedingDataInitializer:DropCreateDatabaseAlways<SeedingDataContext> { protected override void Seed(SeedingDataContext context) { for (int i = 0; i < 6; i++) { var employer = new Employer { EmployerName = "Employer"+(i+1) }; context.Employers.Add(employer); } base.Seed(context); } } 前面的代码通过for循环创建了6个Employer对象,并将它们添加给数据库上下文类的Employers集合属性。这里值得注意的是我们并没有调用DbContext.SaveChanges(),因为它会在基类中自动调用。 4.0 将数据库初始化器类用于数据库上下文类 public class SeedingDataContext:DbContext { public virtual DbSet<Employer> Employers { get; set; } protected SeedingDataContext() { Database.SetInitializer<SeedingDataContext>(new SeedingDataInitializer()); } } 5.0 Main方法中访问数据库 class Program { static void Main(string[] args) { using (var db = new SeedingDataContext()) { var employers = db.Employers; foreach (var employer in employers) { Console.WriteLine("Id={0}\tName={1}",employer.Id,employer.EmployerName); } } Console.WriteLine("DB创建成功,并完成种子化!"); Console.Read(); } } 6.0 运行程序,查看效果 Main方法中只是简单的创建了数据库上下文对象,然后将数据读取出来: 此外,我们可以从数据库初始化的Seed方法中,通过数据库上下文类给数据库传入原生SQL来影响数据库模式。 楼主在实践的过程中,遇到了以下问题,以后可能还会遇到,你也很可能遇到:程序运行时没有错误(有时不会生成数据库),但在调试时就报下面的错误: The context cannot be used while the model is being created. This exception may be thrown if the context is used inside the OnModelCreating method or if the same context instance is accessed by multiple threads concurrently. Note that instance members of DbContext and related classes are not guaranteed to be thread safe. 提示的错误信息很清楚,但是依然没有找到问题的根本所在,网上也没找到满意的答案(有说更改连接字符串的,有说更新到最新版的EF等等),如果有解决了这个问题园友,欢迎留言! LINQ to Entities详解 到目前为止,我们已经学会了如何使用Code First方式来创建实体数据模型,也学会了使用EF进行领域建模,执行模型验证以及控制数据库连接参数。一旦数据建模完成,接下来就是要对这些模型进行各种操作了,通常有以下两种方式: LINQ to Entities Entity SQL 本系列教程只讲LINQ to Entities,Entity SQL就是通过在EF中执行SQL,大家可以自行研究。 什么是LINQ to Entities LINQ,全称是Language-INtegrated Query(集成语言查询),是.NET语言中查询数据的一种技术。LINQ to Entities 是一种机制,它促进了使用LINQ对概念模型的查询。 因为LINQ是声明式语言,它让我们聚焦于我们需要什么数据而不是应该如何检索数据。LINQ to Entities在实体数据模型之上提供了一个很好的抽象,所以我们可以使用LINQ来指定检索什么数据,然后LINQ to Entities provider会处理访问数据库事宜,并为我们取到必要的数据。 当我们使用LINQ to Entities对实体数据模型执行LINQ查询时,这些LINQ查询会首先被编译以决定我们需要获取什么数据,然后执行编译后的语句,从应用程序的角度看,最终会返回.NET理解的CLR对象。 上图展示了LINQ to Entities依赖EntityClient才能够使用EF的概念数据模型,接下来我们看下LINQ to SQL如何执行该查询并给应用程序返回结果: 应用程序创建一个LINQ查询。 LINQ to Entities会将该LINQ查询转换成EntityClient命令。 EntityClient命令然后使用EF和实体数据模型将这些命令转换成SQL查询。 然后会使用底层的ADO.NET provider将该SQL查询传入数据库。 该查询然后在数据库中执行。 执行结果返回给EF。 EF然后将返回的结果转成CLR类型,比如领域实体。 EntityClient使用项目,并返回必要的结果给应用程序。 EntityClient对象寄居在System.Data.EntityClient命名空间中,我们不必显式创建该对象,我们只需要使用命名空间,然后LINQ to Entities会处理剩下的事情。 如果我们对多种类型的数据库使用LINQ to Entities,那么我们只需要为该数据库使用正确的ADO.NET provider,然后EntityClient就会使用这个provider对任何数据库的LINQ查询无缝执行。 使用LINQ to Entities操作实体 编写LINQ查询的方式有两种: 查询语法 方法语法 选择哪种语法完全取决你的习惯,两种语法的性能是一样的。查询语法相对更容易理解,但是灵活性稍差;相反,方法语法理解起来有点困难,但是提供了更强大的灵活性。使用方法语法可以进行链接多个查询,因此在单个语句中可以实现最大的结果。 下面以一个简单的例子来理解一下这两种方法的区别。创建一个控制台应用,名称为“Donators_CRUD_Demo”,该demo也用于下面的CRUD一节。 领域实体模型定义如下: public class Donator { public int Id { get; set; } public string Name { get; set; } public decimal Amount { get; set; } public DateTime DonateDate { get; set; } } 数据库上下文定义如下: public class DonatorsContext : DbContext { public DonatorsContext() : base("name=DonatorsConn") { } public virtual DbSet<Donator> Donators { get; set; } } 定义好连接字符串之后,如果使用该实体数据模型通过执行LINQ查询来获取Donator数据,那么可以在数据库上下文类的Donators集合上操作。下面我们用两种方法来实现“找出打赏了50元的打赏者”。 查询语法 var donators = from donator in db.Donators where donator.Amount == 50m select donator; 方法语法 var donators = db.Donators.Where(d => d.Amount == 50m); 完整的Main方法如下: class Program { static void Main(string[] args) { using (var db=new DonatorsContext()) { //1.查询语法 //var donators = from donator in db.Donators // where donator.Amount == 50m // select donator; //2.方法语法 var donators = db.Donators.Where(d => d.Amount == 50m); Console.WriteLine("Id\t姓名\t金额\t打赏日期"); foreach (var donator in donators) { Console.WriteLine("{0}\t{1}\t{2}\t{3}",donator.Id,donator.Name,donator.Amount,donator.DonateDate.ToShortDateString()); } } Console.WriteLine("Operation completed!"); Console.Read(); } } 这两种方法的执行结果都是一样的,如下: 两种方法的LINQ查询我们都是使用了var隐式类型变量将LINQ查询的结果存储在了donators变量中。使用LINQ to Entities,我们可以使用隐式类型变量作为输出结果,编译器可以由该隐式变量基于LINQ查询推断出输出类型。一般而言,输出类型是IQueryable<T>类型,我们的例子中应该是IQueryable<Donator>。当然我们也可以明确指定返回的类型为IQueryable<Donator>或者IEnumerable<Donator>。 重点理解 当使用LINQ to Entities时,理解何时使用IEnumerable和IQueryable很重要。如果使用了IEnumerable,查询会立即执行,如果使用了IQueryable,直到应用程序请求查询结果的枚举时才会执行查询,也就是查询延迟执行了,延迟到的时间点是枚举查询结果时。 如何决定使用IEnumerable还是IQueryable呢?使用IQueryable会让你有机会创建一个使用多条语句的复杂LINQ查询,而不需要每条查询语句都对数据库执行查询。该查询只有在最终的LINQ查询要求枚举时才会执行。 LINQ操作 为了方便展示,我们需要再创建一张表,因此,我们需要再定义一个实体类,并且要修改之前的实体类,如下所示: public class Donator { public int Id { get; set; } public string Name { get; set; } public decimal Amount { get; set; } public DateTime DonateDate { get; set; } public virtual Province Province { get; set; }//新增字段 } //新增省份类 public class Province { public Province() { Donators=new HashSet<Donator>(); } public int Id { get; set; } [StringLength(225)] public string ProvinceName { get; set; } public virtual ICollection<Donator> Donators { get; set; } } 从上面定义的POCO类,我们不难发现,这两个实体之间是一对多的关系,一个省份可能会有多个打赏者,至于为何这么定义,上一篇已经提到了,这篇不再啰嗦。Main方法添加了一句代码Database.SetInitializer(new DropCreateDatabaseIfModelChanges<DonatorsContext>());,运行程序,会生成新的数据库,然后插入以下数据(数据纯粹是为了演示,不具真实性): INSERT dbo.Provinces VALUES( N'山东省') INSERT dbo.Provinces VALUES( N'河北省') INSERT dbo.Donators VALUES ( N'陈志康', 50, '2016-04-07',1) INSERT dbo.Donators VALUES ( N'海风', 5, '2016-04-08',1) INSERT dbo.Donators VALUES ( N'醉、千秋', 12, '2016-04-13',1) INSERT dbo.Donators VALUES ( N'雪茄', 18.8, '2016-04-15',2) INSERT dbo.Donators VALUES ( N'王小乙', 10, '2016-04-09',2) 数据如下图: 执行简单的查询 平时我们会经常需要从某张表中查询所有数据的集合,如这里查询所有打赏者的集合: //1.查询语法 var donators = from donator in db.Donators select donator; //2.方法语法 var donators = db.Donators; 下面是该LINQ查询生成的SQL: SELECT [t0].[Id], [t0].[Name], [t0].[Amount], [t0].[DonateDate], [t0].[Province_Id] FROM [Donators] AS [t0] LINQPad是一款练习LINQ to Entities出色的工具。在LINQPad中,我们已经在DbContext或ObjectContext内部了,不需要再实例化数据库上下文了,我们可以使用LINQ to Entities查询数据库。我们也可以使用LINQPad查看生成的SQL查询了。 LINQPad多余的就不介绍了,看下图,点击图片下载并学习。 下图为LINQPad将linq语法转换成了SQL。 使用导航属性 如果实体间存在一种关系,那么这个关系是通过它们各自实体的导航属性进行暴露的。在上面的例子中,省份Province实体有一个Donators集合属性用于返回该省份的所有打赏者,而在打赏者Donator实体中,也有一个Province属性用于跟踪该打赏者属于哪个省份。导航属性简化了从一个实体到和它相关的实体,下面我们看一下如何使用导航属性获取与其相关的实体数据。 比如,我们想要获取“山东省的所有打赏者”: #region 2.0 使用导航属性 //1 查询语法 var donators = from province in db.Provinces where province.ProvinceName == "山东省" from donator in province.Donators select donator; //2 方法语法 var donators = db.Provinces.Where(p => p.ProvinceName == "山东省").SelectMany(p => p.Donators); #endregion 最终的查询结果都是一样的: 反过来,如果我们想要获取打赏者“雪茄”的省份: //1 查询语法 //var province = from donator in db.Donators // where donator.Name == "雪茄" // select donator.Province; //2 方法语法 var province = db.Donators.Where(d => d.Name == "雪茄").Select(d => d.Province); 下面是“找出山东省的所有打赏者”的LINQ语句生成的SQL,可见,EF可以使用LINQ查询中的导航属性帮我们创建适当的SQL来获取数据。 SELECT [Extent1].[Id] AS [Id], [Extent2].[Id] AS [Id1], [Extent2].[Name] AS [Name], [Extent2].[Amount] AS [Amount], [Extent2].[DonateDate] AS [DonateDate], [Extent2].[Province_Id] AS [Province_Id] FROM [dbo].[Provinces] AS [Extent1] INNER JOIN [dbo].[Donators] AS [Extent2] ON [Extent1].[Id] = [Extent2].[Province_Id] WHERE N'山东省' = [Extent1].[ProvinceName] 过滤数据 实际上之前已经介绍了,根据某些条件过滤数据,可以在LINQ查询中使用Where。比如上面我们查询了山东省的所有打赏者,这里我们过滤出打赏金额在10~20元之间的打赏者: #region 3.0 过滤数据 //1 查询语法 var donators = from donator in db.Donators where donator.Amount > 10 && donator.Amount < 20 select donator; //2 方法语法 var donators = db.Donators.Where(d => d.Amount > 10 && d.Amount < 20); #endregion 最终查询的结果如下: 生成的SQL语句在这里不在贴出来了,大家自己通过LINQPad或者其他工具自己去看吧!只要知道EF会帮助我们自动将LINQ查询转换成合适的SQL语句就可以了。 LINQ投影 如果不指定投影的话,那么默认就是选择该实体或与之相关实体的所有字段,LINQ投影就是返回这些实体属性的子集或者返回一个包含了多个实体的某些属性的对象。 投影一般用在应用程序中的VIewModel(视图模型),我们可以从LINQ查询中直接返回一个视图模型。比如,我们想要查出“所有省的所有打赏者”: #region 4.0 LINQ投影 //1 查询语法 var donators = from province in db.Provinces select new { Province=province.ProvinceName, DonatorList=province.Donators }; //2 方法语法 var donators =db.Provinces.Select(p=>new { Province = p.ProvinceName, DonatorList = p.Donators }); Console.WriteLine("省份\t打赏者"); foreach (var donator in donators) { foreach (var donator1 in donator.DonatorList) { Console.WriteLine("{0}\t{1}", donator.Province,donator1.Name); } } #endregion 执行结果如下: 当然,如果我们已经定义了一个包含了Province和DonatorList属性的类型(比如视图模型),那么也可以直接返回该类型,下面只给出方法语法(查询语法大家可自行写出)的写法: public class DonatorsWithProvinceViewModel { public string Province { get; set; } public ICollection<Donator> DonatorList { get; set; } } //3 返回一个对象的方法语法 var donators = db.Provinces.Select(p => new DonatorsWithProvinceViewModel { Province = p.ProvinceName, DonatorList = p.Donators }); 在IQueryable<T>中处理结果也会提升性能,因为直到要查询的结果进行枚举时才会执行生成的SQL。 分组Group 分组的重要性相必大家都知道,这个肯定是要掌握的!下面就看看两种方法的写法。 #region 5.0 分组Group //1 查询语法 var donatorsWithProvince = from donator in db.Donators group donator by donator.Province.ProvinceName into donatorGroup select new { ProvinceName=donatorGroup.Key, Donators=donatorGroup }; //2 方法语法 var donatorsWithProvince = db.Donators.GroupBy(d => d.Province.ProvinceName) .Select(donatorGroup => new { ProvinceName = donatorGroup.Key, Donators = donatorGroup }); foreach (var dwp in donatorsWithProvince) { Console.WriteLine("{0}的打赏者如下:",dwp.ProvinceName); foreach (var d in dwp.Donators) { Console.WriteLine("{0}\t\t{1}", d.Name, d.Amount); } } #endregion 稍微解释一下吧,上面的代码会根据省份名称进行分组,最终以匿名对象的投影返回。结果中的ProvinceName就是分组时用到的字段,Donators属性包含了通过ProvinceName找到的Donator集合。 执行结果如下: 排序Ordering 对特定的列进行升序或降序排列也是经常使用的操作。比如我们按照打赏金额进行排序。 按照打赏金额升序排序(LINQ sql查询中ascending关键字可省略): #region 6.0 排序Ordering //1 升序查询语法 var donators = from donator in db.Donators orderby donator.Amount ascending //ascending可省略 select donator; //2 升序方法语法 var donators = db.Donators.OrderBy(d => d.Amount); //#endregion //1 降序查询语法 var donators = from donator in db.Donators orderby donator.Amount descending select donator; //2 降序方法语法 var donators = db.Donators.OrderByDescending(d => d.Amount); #endregion 升序查询执行结果: 降序查询执行结果: 聚合操作 使用LINQ to Entities可以执行下面的聚合操作: Count-数量 Sum-求和 Min-最小值 Max-最大值 Average-平均值 下面我找出山东省打赏者的数量: #region 7.0 聚合操作 var count = (from donator in db.Donators where donator.Province.ProvinceName == "山东省" select donator).Count(); var count2 = db.Donators.Count(d => d.Province.ProvinceName == "山东省"); Console.WriteLine("查询语法Count={0},方法语法Count={1}",count,count2); #endregion 执行结果见下图,可见,方法语法更加简洁,而且查询语法还要将前面的LINQ sql用括号括起来才能进行聚合(其实这是混合语法),没有方法语法简单灵活,所以下面的几个方法我们只用方法语法进行演示。 其他聚合函数的代码: var sum = db.Donators.Sum(d => d.Amount);//计算所有打赏者的金额总和 var min = db.Donators.Min(d => d.Amount);//最少的打赏金额 var max = db.Donators.Max(d => d.Amount);//最多的打赏金额 var average = db.Donators.Average(d => d.Amount);//打赏金额的平均值 Console.WriteLine("Sum={0},Min={1},Max={2},Average={3}",sum,min,max,average); 执行结果: 分页Paging 分页也是提升性能的一种方式,而不是将所有符合条件的数据一次性全部加载出来。在LINQ to Entities中,实现分页的两个主要方法是:Skip和Take,这两个方法在使用前都要先进行排序,切记。 Skip 该方法用于从查询结果中跳过前N条数据。假如我们根据Id排序后,跳过前2条数据: #region 8.0 分页Paging var donatorsBefore = db.Donators; var donatorsAfter = db.Donators.OrderBy(d => d.Id).Skip(2); Console.WriteLine("原始数据打印结果:"); PrintDonators(donatorsBefore); Console.WriteLine("Skip(2)之后的结果:"); PrintDonators(donatorsAfter); #endregion static void PrintDonators(IQueryable<Donator> donators) { Console.WriteLine("Id\t\t姓名\t\t金额\t\t打赏日期"); foreach (var donator in donators) { Console.WriteLine("{0,-10}\t{1,-10}\t{2,-10}\t{3,-10}", donator.Id, donator.Name, donator.Amount, donator.DonateDate.ToShortDateString()); } } 执行结果如下: Take Take方法用于从查询结果中限制元素的数量。比如我们只想取出前3条打赏者: var take = db.Donators.Take(3); Console.WriteLine("Take(3)之后的结果:"); PrintDonators(take); 执行结果: 分页实现 如果我们要实现分页功能,那么我们必须在相同的查询中同时使用Skip和Take方法。 由于现在我数据库只有5条打赏者的数据,所以我打算每页2条数据,这样就会有3页数据。 //分页实现 while (true) { Console.WriteLine("您要看第几页数据"); string pageStr = Console.ReadLine() ?? "1"; int page = int.Parse(pageStr); const int pageSize = 2; if (page>0&&page<4) { var donators = db.Donators.OrderBy(d => d.Id).Skip((page - 1)*pageSize).Take(pageSize); PrintDonators(donators); } else { break; } } 和聚合函数一样,分页操作只有方法语法。 实现多表连接join 如果两个实体之间是互相关联的,那么EF会在实体中创建一个导航属性来访问相关的实体。也可能存在一种情况,两个实体之间有公用的属性,但是没有在数据库中定义它们间的关系。如果我们要使用该隐式的关系,那么可以连接相关的实体。 但是之前我们创建实体类时已经给两个实体建立了一对多关系,所以这里我们使用导航属性模拟join连接: #region 9.0实现多表连接 var join1 = from province in db.Provinces join donator in db.Donators on province.Id equals donator.Province.Id into donatorList//注意,这里的donatorList是属于某个省份的所有打赏者,很多人会误解为这是两张表join之后的结果集 select new { ProvinceName = province.ProvinceName, DonatorList = donatorList }; var join2 = db.Provinces.GroupJoin(db.Donators,//Provinces集合要连接的Donators实体集合 province => province.Id,//左表要连接的键 donator => donator.Province.Id,//右表要连接的键 (province, donatorGroup) => new//返回的结果集 { ProvinceName = province.ProvinceName, DonatorList = donatorGroup } ); #endregion LINQ中的join和GroupJoin相当于SQL中的Left Outer Join。无论右边实体集合中是否包含任何实体,它总是会返回左边集合的所有元素。 懒加载和预加载 使用LINQ to Entities时,理解懒加载和预加载的概念很重要。因为理解了这些,就会很好地帮助你编写有效的LINQ查询。 懒加载 懒加载是这样一种过程,直到LINQ查询的结果被枚举时,该查询涉及到的相关实体才会从数据库加载。如果加载的实体包含了其他实体的导航属性,那么直到用户访问该导航属性时,这些相关的实体才会被加载。 在我们的领域模型中,Donator类的定义如下: public class Donator { public int Id { get; set; } public string Name { get; set; } public decimal Amount { get; set; } public DateTime DonateDate { get; set; } public virtual Province Province { get; set; } } 当我们使用下面的代码查询数据时,实际上并没有从数据库中加载数据: var donators=db.Donators; 要真正从数据库中加载数据,我们要枚举donators,通过ToList()方法或者在foreach循环中遍历都可以。 看下面的代码解释: #region 10.0 懒加载 var donators = db.Donators;//还没有查询数据库 var donatorList = donators.ToList();//已经查询了数据库,但由于懒加载的存在,还没有加载Provinces表的数据 var province = donatorList.ElementAt(0).Province;//因为用户访问了Province表的数据,因此这时才加载 #endregion 使用Code First时,懒加载依赖于导航属性的本质。如果导航属性是virtual修饰的,那么懒加载就开启了,如果要关闭懒加载,不要给导航属性加virtual关键字就可以了。 如果想要为所有的实体关闭懒加载,那么可以在数据库中的上下文中去掉实体集合属性的virtual关键字即可。 预加载 预加载是这样一种过程,当我们要加载查询中的主要实体时,同时也加载与之相关的实体。要实现预加载,我们要使用Include方法。下面我们看一下如何在加载Donator数据的时候,同时也预先加载所有的Provinces数据: //预加载,以下两种方式都可以 var donators2 = db.Donators.Include(d => d.Province).ToList(); var donators3 = db.Donators.Include("Provinces").ToList(); 这样,当我们从数据库中取到Donators集合时,也取到了Provinces集合。 插入数据 将新的数据插入数据库有多种方法,可以使用之前的Add方法,也可以给每个实体的状态设置为Added。如果你要添加的实体包含子实体,那么Added状态会扩散到该图的所有对象中。换言之,如果根实体是新的,那么EF会假定你附加了一个新的对象图。该对象图一般指的是许多相关的实体形成的一个复杂的树结构。比如,比如我们有一个Province对象,每个省份有很多打赏者Donators,包含在Province类的List属性中,那么我们就是在处理一个对象图,本质上,Donator实体是person对象的孩子。 首先,我们创建一个新的具有打赏者的Province实例,然后,我们把该实例添加到数据库上下文中,最后,调用SaveChanges将数据行提交到数据库: #region 11.0 插入数据 var province = new Province { ProvinceName = "浙江省" }; province.Donators.Add(new Donator { Name = "星空夜焰", Amount = 50m, DonateDate = DateTime.Parse("2016-5-30") }); province.Donators.Add(new Donator { Name = "伟涛", Amount = 25m, DonateDate = DateTime.Parse("2016-5-25") }); using (var db=new DonatorsContext()) { db.Provinces.Add(province); db.SaveChanges(); } #endregion 这和之前看到的代码还是有些不同的。我们在初始化上下文之前就创建了对象,这个表明了EF会追踪当时上下文中为attached或者added状态的实体。 另一种插入新数据的方法是使用DbContextAPI直接设置实体的状态,例如: //方法二:直接设置对象的状态 var province2 = new Province{ ProvinceName = "广东省"}; province2.Donators.Add(new Donator { Name = "邱宇", Amount = 30, DonateDate = DateTime.Parse("2016-04-25") }); using (var db=new DonatorsContext()) { db.Entry(province2).State=EntityState.Added; db.SaveChanges(); } DbContext上的Entry方法返回了一个DbEntityEntry类的实例。该类有许多有用的属性和方法用于EF的高级实现和场景。下面是EntityState的枚举值: 状态 描述 Added 添加了一个新的实体。该状态会导致一个插入操作。 Deleted 将一个实体标记为删除。设置该状态时,该实体会从DbSet中移除。该状态会导致删除操作。 Detached DbContext不再追踪该实体。 Modified 自从DbContext开始追踪该实体,该实体的一个或多个属性已经更改了。该状态会导致更新操作。 Unchanged 自从DbContext开始追踪该实体以来,它的任何属性都没有改变。 执行结果如下,可见刚才添加的数据都插入数据库了。 更新数据 当EF知道自从实体首次附加到DbContext之后发生了改变,那么就会触发一个更新查询。自从查询数据时 起,EF就会开始追踪每个属性的改变,当最终调用SaveChanges时,只有改变的属性会包括在更新SQL操作中。当想要在数据库中找到一个要更新的实体时,我们可以使用where方法来实现,也可以使用DbSet上的Find方法,该方法需要一个或多个参数,该参数对应于表中的主键。下面的例子中,我们使用拥有唯一ID的列作为主键,因此我们只需要传一个参数。如果你使用了复合主键(包含了不止一列,常见于连接表),就需要传入每列的值,并且主键列的顺序要准确。 #region 12.0 更新数据 using (var db=new DonatorsContext()) { var donator = db.Donators.Find(3); donator.Name = "醉千秋";//我想把“醉、千秋”中的顿号去掉 db.SaveChanges(); } #endregion 如果执行了SaveChanges之后,你跟踪发送到SQL Server数据库的SQL查询时,会发现执行了下面的sql语句: UPDATE [dbo].[Donators] SET [Name] = @0 WHERE ([Id] = @1) 这个sql查询确实证明了只有那些显式修改的更改才会发送给数据库。比如我们只更改了Donator的Name属性,其他都没动过,生成的sql也是只更新Name字段。如果在SQL Profiler中查看整个代码块,会发现Find方法会生成下面的SQL代码: SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Amount] AS [Amount], [Extent1].[DonateDate] AS [DonateDate], [Extent1].[Province_Id] AS [Province_Id] FROM [dbo].[Donators] AS [Extent1] WHERE [Extent1].[Id] = @p0 Find方法被翻译成了SingleOrDefault方法,所以是Select Top(2)。如果你在写桌面应用的话,可以使用Find方法先找到实体,再修改,最后提交,这是没问题的。但是在Web应用中就不行了,因为不能在两个web服务器调用之间保留原始的上下文。我们也没必要寻找一个实体两次,第一次用于展示给用户,第二次用于更新。相反,我们可以直接修改实体的状态达到目的。 因为我们的例子不是web应用,所以这里直接给出代码了: var province = new Province {Id = 1,ProvinceName = "山东省更新"}; province.Donators.Add(new Donator { Name = "醉、千秋",//再改回来 Id = 3, Amount = 12.00m, DonateDate = DateTime.Parse("2016/4/13 0:00:00"), }); using (var db=new DonatorsContext()) { db.Entry(province).State=EntityState.Modified; db.SaveChanges(); } 如果你也按照我这样做了,你会发现省份表更新了,但是Donators表根本没有修改成功,这是因为EF内部的插入和更新底层实现是不同的。当把状态设置为Modified时,EF不会将这个改变传播到整个对象图。因此,要使代码正常运行,需要再添加一点代码: using (var db=new DonatorsContext()) { db.Entry(province).State=EntityState.Modified; foreach (var donator in province.Donators) { db.Entry(donator).State=EntityState.Modified; } db.SaveChanges(); } 执行的结果就不贴了,有兴趣的可以练习一下。我们需要手动处理的是要为每个发生变化的实体设置状态。当然,如果要添加一个新的Donator,需要设置状态为Added而不是Modified。此外,还有更重要的一点,无论何时使用这种状态发生改变的方法时,我们都必须知道所有列的数据(例如上面的例子),包括每个实体的主键。这是因为当实体的状态发生变化时,EF会认为所有的属性都需要更新。 一旦实体被附加到上下文,EF就会追踪实体的状态,这么做是值得的。因此,如果你查询了数据,那么上下文就开始追踪你的实体。如果你在写一个web应用,那么该追踪就变成了一个查询操作的不必要开销,原因是只要web请求完成了获取数据,那么就会dispose上下文,并销毁追踪。EF有一种方法来减少这个开销: using (var db=new DonatorsContext()) { var provinceNormal = db.Provinces.Include(p => p.Donators); foreach (var p in provinceNormal) { Console.WriteLine("省份的追踪状态:{0}", db.Entry(p).State); foreach (var donator in p.Donators) { Console.WriteLine("打赏者的追踪状态:{0}", db.Entry(donator).State); } Console.WriteLine("**************"); } var province = db.Provinces.Include(p => p.Donators).AsNoTracking();//使用AsNoTracking()方法设置不再追踪该实体 Console.WriteLine("使用了AsNoTracking()方法之后"); foreach (var p in province) { Console.WriteLine("省份的追踪状态:{0}", db.Entry(p).State); foreach (var donator in p.Donators) { Console.WriteLine("打赏者的追踪状态:{0}",db.Entry(donator).State); } Console.WriteLine("**************"); } } 从以下执行结果可以看出,使用了AsNoTracking()方法之后,实体的状态都变成了Detached,而没有使用该方法时,状态是Unchanged。从之前的表中,我们可以知道,Unchanged至少数据库上下文还在追踪,只是追踪到现在还没发现它有变化,而Detached根本就没有追踪,这样就减少了开销。 如果在web应用中想更新用户修改的属性怎么做?假设你在web客户端必须跟踪发生的变化并且拿到了变化的东西,那么还可以使用另一种方法来完成更新操作,那就是使用DbSet的Attach方法。该方法本质上是将实体的状态设置为Unchanged,并开始跟踪该实体。附加一个实体后,一次只能设置一个更改的属性,你必须提前就知道哪个属性已经改变了。 var donator = new Donator { Id = 4, Name = "雪茄", Amount = 18.80m, DonateDate = DateTime.Parse("2016/4/15 0:00:00") }; using (var db = new DonatorsContext()) { db.Donators.Attach(donator); //db.Entry(donator).State=EntityState.Modified;//这句可以作为第二种方法替换上面一句代码 donator.Name = "秦皇岛-雪茄"; db.SaveChanges(); } 删除数据 删除和更新有很多相似之处,我们可以使用一个查询找到数据,然后通过DbSet的Remove方法将它标记为删除,这种方法也有和更新相同的缺点,会导致一个select查询和一个删除查询。 #region 13.0 删除数据 using (var db = new DonatorsContext()) { PrintAllDonators(db); Console.WriteLine("删除后的数据如下:"); var toDelete = db.Provinces.Find(2); toDelete.Donators.ToList().ForEach( d => db.Donators.Remove(d)); db.Provinces.Remove(toDelete); db.SaveChanges(); PrintAllDonators(db); } #endregion //输出所有的打赏者 private static void PrintAllDonators(DonatorsContext db) { var provinces = db.Provinces.ToList(); foreach (var province in provinces) { Console.WriteLine("{0}的打赏者如下:", province.ProvinceName); foreach (var donator in province.Donators) { Console.WriteLine("{0,-10}\t{1,-10}\t{2,-10}\t{3,-10}", donator.Id, donator.Name, donator.Amount, donator.DonateDate.ToShortDateString()); } } } 执行结果如下: 上面的代码会删除每个子实体,然后再删除根实体。删除一个实体时必须要知道它的主键值,上面的代码删除了省份Id=2的数据。另外,可以使用RemoveRange方法删除多个实体。 插入操作和删除操作有一个很大的不同:删除操作必须要手动删除每个子记录,而插入操作不需要手动插入每个子记录,只需要插入父记录即可。你也可以使用级联删除操作来代替,但是许多DBA都不屑于级联删除。 下面,我们通过为每个实体设置状态来删除实体,我们还是需要考虑每个独立的实体: //方法2:通过设置实体状态删除 var toDeleteProvince = new Province { Id = 1 };//id=1的省份是山东省,对应三个打赏者 toDeleteProvince.Donators.Add(new Donator { Id = 1 }); toDeleteProvince.Donators.Add(new Donator { Id = 2 }); toDeleteProvince.Donators.Add(new Donator { Id = 3 }); using (var db = new DonatorsContext()) { PrintAllDonators(db);//删除前先输出现有的数据,不能写在下面的using语句中,否则Attach方法会报错,原因我相信你已经可以思考出来了 } using (var db = new DonatorsContext()) { db.Provinces.Attach(toDeleteProvince); foreach (var donator in toDeleteProvince.Donators.ToList()) { db.Entry(donator).State=EntityState.Deleted; } db.Entry(toDeleteProvince).State=EntityState.Deleted;//删除完子实体再删除父实体 db.SaveChanges(); Console.WriteLine("删除之后的数据如下:\r\n"); PrintAllDonators(db);//删除后输出现有的数据 } 执行效果如下: 毫无疑问你会发现删除操作非常不同于其他操作,要删除一个省份,我们只需要传入它的主键即可,要删除这个省份下的所有打赏者,我们只需要在省份对象后追加要删除的打赏者对象,并给每个打赏者对象的Id属性赋值即可。在web应用中,我们需要提交所有的主键,或者需要查询子记录来找到对应的主键。 使用内存in-memory数据 有时,你需要在已存在的上下文中找到一个实体而不是每次都去数据库去找。当创建新的上下文时,EF默认总是对数据库进行查询。 应用情景:如果你的更新调用了很多方法,并且你想知道之前的某个方法添加了什么数据?这时,你可以使用DbSet的Local属性强制执行一个只针对内存数据的查询。 var query= db.Provinces.Local.Where(p => p.ProvinceName.Contains("东")).ToList(); Find方法在构建数据库查询之前,会先去本地的上下文中搜索。这个很好证明,只需要找到加载很多条实体数据,然后使用Find方法找到其中的一条即可。比如: #region 14.0 使用内存数据 using (var db=new DonatorsContext()) { var provinces = db.Provinces.ToList(); var query = db.Provinces.Find(3);//还剩Id=3和4的两条数据了 } #endregion 打开Sql Server Profiler,可以看到,只查询了一次数据库,而且还是第一句代码查询的,这就证明了Find方法首先去查询内存中的数据。 通过ChangeTracker对象,我们可以访问内存中所有实体的状态,也可以查看这些实体以及它们的DbChangeTracker。例如: using (var db=new DonatorsContext()) { //14.1 证明Find方法先去内存中寻找数据 var provinces = db.Provinces.ToList(); //var query = db.Provinces.Find(3);//还剩Id=3和4的两条数据了 //14.2 ChangeTracker的使用 foreach (var dbEntityEntry in db.ChangeTracker.Entries<Province>()) { Console.WriteLine(dbEntityEntry.State); Console.WriteLine(dbEntityEntry.Entity.ProvinceName); } } #endregion 运行结果,可以看到追踪到的状态等: 本章小结 首先,我们看到了如何控制多个数据库连接参数,如数据库位置,数据库名称,模式等等,我们也看到了如何使用数据库初始化器创建数据库初始化策略以满足应用程序的需求,最后,我们看到了如何在EF Code First中使用数据库初始化器来插入种子数据。 接下来,我们看到了如何在EF中使用LINQ to Entities来查询数据。我们看到了使用EF的LINQ to Entities无缝地执行各种数据检索任务。最后,我们深入介绍了EF Code First中的插入、更新和删除! 自我测试 LINQ不支持下列拿哪种语法? 方法 SQL 查询 通过LINQ从数据库中检索到一个实体,然后更改部分属性的值,再调用SaveChanges,所有的属性都会更新到数据库而不仅仅是修改后的属性,对吗? 要给多个属性排序,你只需要在LINQ的方法语法中多次调用OrderBy就行了吗? 如何在LINQ的查询语法中添加两个过滤条件? 使用多个where调用 使用and逻辑操作符 执行两个查询 如何给DbSet添加多个新的实体? 调用Add方法,然后传入数据库上下文属性识别的类的实例 调用AddRange方法,并传入目标实体类型的枚举 在每个新实体上使用数据库上下文API,将状态设置为Added 以上所有 如果想创建一个具有多个子实体的新实体,也叫做对象图,那么为了持久化该对象图,必须要在父亲和每个孩子实体上调用Add吗? 如果将一个实体的状态设置为Modified,那么当调用SaveChanges之后,相应表的所有列都会更新吗? 为了触发一个删除查询执行,你需要在SaveChanges时调用Add,然后调用Remove,对吗? 调用SaveChanges调用时,哪个实体状态不会导致数据库查询? Added Detached Deleted Modified DbSet的哪个属性让你可以访问已经从数据库加载到上下文中的实体? Memory Local Loaded
文章
SQL  ·  .NET  ·  数据库连接  ·  数据库
2017-12-11
MySQL索引之全文索引(FULLTEXT)
MySQL索引之全文索引(FULLTEXT) MySQL创建全文索引 使用索引时数据库性能优化的必备技能之一。在MySql数据库中,有四种索引:聚焦索引(主键索引)、普通索引、唯一索引以及我们这里将要介绍的全文索引(FUNLLTEXT INDEX)。 全文索引(也称全文检索)是目前搜索引擎使用的一种关键技术。它能够利用【分词技术】等多种算法智能分析出文本文字中关键词的频率和重要性,然后按照一定的算法规则智能地筛选出我们想要的搜索结果。 在MySql中,创建全文索引相对比较简单。例如:我们有一个文章表(article),其中有主键ID(id)、文章标题(title)、文章内容(content)三个字段。现在我们希望能够在title和content两个列上创建全文索引,article表及全文索引的创建SQL语句如下: CREATE TABLE `article` (   `id` int(10) unsigned NOT NULL AUTO_INCREMENT,   `title` varchar(200) DEFAULT NULL,   `content` text,   PRIMARY KEY (`id`),   FULLTEXT KEY `title` (`title`,`content`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; 上面就是在创建表的同时创建全文索引的SQL示例。此外,如果我们要给已经存在的表的指定字段创建全文索引,同样以article表为例,我们可以使用如下SQL语句进行创建: ALTER TABLE article ADD FULLTEXT INDEX fulltext_article(title,content); 在MySql中创建全文索引之后,现在就该了解如何使用了。众所周知,在数据库中进行模糊查询是使用like关键字进行查询的,例如: SELECT * FROM article WHERE content LIKE ‘%查询字符串%’; 那么,我们在使用全文索引也这样使用吗?当然不是,我们必须使用特有的语法才能使用全文索引进行查询,例如,我们想要在article表的title和content列中全文检索指定的查询字符串,我们可以如下编写SQL语句: SELECT * FROM article WHERE MATCH(title,content) AGAINST (‘查询字符串’); 强烈注意:MySql自带的全文索引只能用于数据库引擎为MYISAM的数据表,如果是其他数据引擎,则全文索引不会生效。此外,MySql自带的全文索引只能对英文进行全文检索,目前无法对中文进行全文检索。如果需要对包含中文在内的文本数据进行全文检索,我们需要采用Sphinx(斯芬克斯)/Coreseek技术来处理中文。 注:目前,使用MySql自带的全文索引时,如果查询字符串的长度过短将无法得到期望的搜索结果。MySql全文索引所能找到的词默认最小长度为4个字符。另外,如果查询的字符串包含停止词,那么该停止词将会被忽略。 注:如果可能,请尽量先创建表并插入所有数据后再创建全文索引,而不要在创建表时就直接创建全文索引,因为前者比后者的全文索引效率要高。 一种特殊的索引,它会把某个数据表的某个数据列出现过的所有单词生成一份清单。 alter table tablename add fulltext(column1,column2) 只能在MyISAM数据表中创建 少于3个字符的单词不会被包含在全文索引里,可以通过修改my.cnf修改选项 ft_min_word_len=3 重新启动MySQL服务器,用 repair table tablename quick 为有关数据表重新生成全文索引 select * from tablename where match(column1,column2) against('word1 word2 word3')>0.001 match ... against 把column1,column2数据列中至少包含word1,word2,word3三个单词之一的数据记录查找到,在关键字match后的数据列必须 跟创建全文索引的数据列相同,检索词不区分大小写和先后顺序,少于3个字符的单词通常被忽略。match... against ...表达式返回一个浮点数作为它本身的求值结果,这个数字反映了结果记录与被检索单词的匹配程度。如果没有匹配到任何记录,或者匹配到的结果记录太多反 而被忽略,表达式将返回0,表达式>0.001的作用是排除match的返回值太小的结果记录。 select *,match(column1,column2) against ('word1 word2 word3') as mtch from tablename having mtch>0.01 order by mtch desc limit 5 找出最匹配的5条记录,在where字句中不能使用假名,所以用having   布尔全文搜索的性能支持以下操作符:  +word:一个前导的加号表示该单词必须 出现在返回的每一行的开头位置。   -word: 一个前导的减号表示该单词一定不能出现在任何返回的行中。   (无操作符):在默认状态下(当没有指定 + 或–的情况下),该单词可有可无,但含有该单词的行等级较高。这和MATCH() ... AGAINST()不使用IN BOOLEAN MODE修改程序时的运作很类似。   > <这两个操作符用来改变一个单词对赋予某一行的相关值的影响。 > 操作符增强其影响,而 <操作符则减弱其影响。请参见下面的例子。   ( )括号用来将单词分成子表达式。括入括号的部分可以被嵌套。   ~word:一个前导的代字号用作否定符, 用来否定单词对该行相关性的影响。 这对于标记“noise(无用信息)”的单词很有用。包含这类单词的行较其它行等级低。   word* :搜索以word开头的单词,只允许出现在单词的末尾   "word1 word" :给定单词必须出现在数据记录中,先后顺序也必须匹配,区分字母大小写 select * from tablename where match(column1,column2) against ('+word1 +word2 -word3' in boolean mode') 布尔检索只能返回1或者0,不再返回表示匹配程度的浮点数 全文索引的缺陷:  数据表越大,全文索引效果好,比较小的数据表会返回一些难以理解的结果。   全文检索以整个单词作为匹配对象,单词变形(加上后缀,复数形式),就被认为另一个单词。   只有由字母,数字,单引号,下划线构成的字符串被认为是单词,带注音符号的字母仍是字母,像C++不再认为是单词   不区分大小写   只能在MyISAM上使用   全文索引创建速度慢,而且对有全文索引的各种数据修改操作也慢 前言:本文简单讲述全文索引的应用实例,MYSQL演示版本5.5.24。 Q:全文索引适用于什么场合? A:全文索引是目前实现大数据搜索的关键技术。 至于更详细的介绍请自行百度,本文不再阐述。 --------------------------------------------------------------------------------一、如何设置? 如图点击结尾处的{全文搜索}即可设置全文索引,不同MYSQL版本名字可能不同。二、设置条件1.表的存储引擎是MyISAM,默认存储引擎InnoDB不支持全文索引(新版本MYSQL5.6的InnoDB支持全文索引) 2.字段类型:char、varchar和text  三、配置my.ini配置文件中添加 # MySQL全文索引查询关键词最小长度限制 [mysqld] ft_min_word_len = 1 保存后重启MYSQL,执行SQL语句 复制代码代码如下: SHOW VARIABLES 查看ft_min_word_len是否设置成功,如果没设置成功请确保1.确认my.ini正确配置,注意不要搞错my.ini的位置2.确认mysql已经重启,实在不行重启电脑其他相关配置请自行百度。注:重新设置配置后,已经设置的索引需要重新设置生成索引 四、SQL语法首先生成temp表 CREATE TABLE IF NOT EXISTS `temp` (  `id` int(11) NOT NULL AUTO_INCREMENT,  `char` char(50) NOT NULL,  `varchar` varchar(50) NOT NULL,  `text` text NOT NULL,  PRIMARY KEY (`id`),  FULLTEXT KEY `char` (`char`),  FULLTEXT KEY `varchar` (`varchar`),  FULLTEXT KEY `text` (`text`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ; INSERT INTO `temp` (`id`, `char`, `varchar`, `text`) VALUES (1, 'a bc 我 知道 1 23', 'a bc 我 知道 1 23', 'a bc 我 知道 1 23'); 搜索`char`字段 'a' 值 ? SELECT * FROM `temp` WHERE MATCH(`char`) AGAINST ('a')但是你会发现查询无结果?!这时你也许会想:哎呀怎么回事,我明明按照步骤来做的啊,是不是那里漏了或者错了? 你不要着急,做程序是这样的,出错总是有的,静下心来,着急是不能解决问题的。   如果一个关键词在50%的数据出现,那么这个词会被当做无效词。 如果你想去除50%的现在请使用IN BOOLEAN MODE搜索SELECT * FROM `temp` WHERE MATCH(`char`) AGAINST ('a' IN BOOLEAN MODE) ? 这样就可以查询出结果了,但是我们不推荐使用。 全文索引的搜索模式的介绍自行百度。   我们先加入几条无用数据已解除50%限制 INSERT INTO `temp` ( `id` , `char` , `varchar` , `text` ) VALUES ( NULL , '7', '7', '7' ), ( NULL , '7', '7', '7' ), ( NULL , 'a,bc,我,知道,1,23', 'a,bc,我,知道,1,23', 'a,bc,我,知道,1,23' ), ( NULL , 'x', 'x', 'x' ); ? 这时你执行以下SQL语句都可以查询到数据 SELECT * FROM `temp` WHERE MATCH(`char`) AGAINST ('a'); SELECT * FROM `temp` WHERE MATCH(`char`) AGAINST ('bc'); SELECT * FROM `temp` WHERE MATCH(`char`) AGAINST ('我'); SELECT * FROM `temp` WHERE MATCH(`char`) AGAINST ('知道'); SELECT * FROM `temp` WHERE MATCH(`char`) AGAINST ('1'); SELECT * FROM `temp` WHERE MATCH(`char`) AGAINST ('23'); ? 以下SQL搜索不到数据 SELECT * FROM `temp` WHERE MATCH(`char`) AGAINST ('b'); SELECT * FROM `temp` WHERE MATCH(`char`) AGAINST ('c'); SELECT * FROM `temp` WHERE MATCH(`char`) AGAINST ('知'); SELECT * FROM `temp` WHERE MATCH(`char`) AGAINST ('道'); SELECT * FROM `temp` WHERE MATCH(`char`) AGAINST ('2'); SELECT * FROM `temp` WHERE MATCH(`char`) AGAINST ('3'); ? 如果搜索多个词,请用空格或者逗号隔开 SELECT * FROM `temp` WHERE MATCH(`char`) AGAINST ('a x'); SELECT * FROM `temp` WHERE MATCH(`char`) AGAINST ('a,x'); ? 上面的SQL都可以查询到三条数据  五、分词看到这里你应该发现我们字段里的值也是分词,不能直接插入原始数据。全文索引应用流程:1.接收数据-数据分词-入库 2.接收数据-数据分词-查询现在有个重要的问题:怎么对数据分词? 数据分词一般我们会使用一些成熟免费的分词系统,当然如果你有能力也可以自己做分词系统,这里我们推荐使用SCWS分词插件。首先下载1.php_scws.dll  注意对应版本 2.XDB词典文件 3.规则集文件下载地址 安装scws1.先建一个文件夹,位置不限,但是最好不要中文路径。 2.解压{规则集文件},把xdb、三个INI文件全部扔到 D:\scws 3.把php_scws.dll复制到你的PHP目录下的EXT文件夹里面 4.在 php.ini 的末尾加入以下几行: [scws] ; ; 注意请检查 php.ini 中的 extension_dir 的设定值是否正确, 否则请将 extension_dir 设为空, ; 再把 php_scws.dll 指定为绝对路径。 ; extension = php_scws.dll scws.default.charset = utf8 scws.default.fpath = "D:\scws"5.重启你的服务器测试 $str="测试中文分词"; $so = scws_new(); $so->send_text($str); $temp=$so->get_result(); $so->close(); var_dump($temp); ? 如果安装未成功,请参照官方说明文档 -------------------------------------------------------------------------------- 这样我们就可以使用全文索引技术了。 1.创建全文索引(FullText index)        旧版的MySQL的全文索引只能用在MyISAM表格的char、varchar和text的字段上。         不过新版的MySQL5.6.24上InnoDB引擎也加入了全文索引,所以具体信息要随时关注官网,      1.1. 创建表的同时创建全文索引              CREATE TABLE article (                   id INT AUTO_INCREMENT NOT NULL PRIMARY KEY,                   title VARCHAR(200),                   body TEXT,                   FULLTEXT(title, body)               ) TYPE=MYISAM;      1.2.通过 alter table 的方式来添加                 ALTER TABLE `student` ADD FULLTEXT INDEX ft_stu_name  (`name`) #ft_stu_name是索引名,可以随便起        或者:ALTER TABLE `student` ADD FULLTEXT ft_stu_name  (`name`)     1.3. 直接通过create index的方式                 CREATE FULLTEXT INDEX ft_email_name ON `student` (`name`)            也可以在创建索引的时候指定索引的长度:                 CREATE FULLTEXT INDEX ft_email_name ON `student` (`name`(20)) 2. 删除全文索引     2.1. 直接使用 drop index(注意:没有 drop fulltext index 这种用法)                  DROP INDEX full_idx_name ON tommy.girl ;     2.2. 使用 alter table的方式                  ALTER TABLE tommy.girl DROP INDEX ft_email_abcd; 3.使用全文索引      跟普通索引稍有不同      使用全文索引的格式:  MATCH (columnName) AGAINST ('string')      eg:            SELECT * FROM `student` WHERE MATCH(`name`) AGAINST('聪')            当查询多列数据时:                 建议在此多列数据上创建一个联合的全文索引,否则使用不了索引的。           SELECT * FROM `student` WHERE MATCH(`name`,`address`) AGAINST('聪 广东')      3.1. 使用全文索引需要注意的是:(基本单位是词)             分词,全文索引以词为基础的,MySQL默认的分词是所有非字母和数字的特殊符号都是分词符(外国人嘛)             这里推荐一篇文章:利用mysql的全文索引实现模糊查询         3.2. MySQL中与全文索引相关的几个变量:              使用命令:mysql> SHOW VARIABLES LIKE 'ft%'; #ft就是FullText的简写              ft_boolean_syntax    + -><()~*:""&|         #改变IN BOOLEAN MODE的查询字符,不用重新启动MySQL也不用重建索引              ft_min_word_len    4                                   #最短的索引字符串,默认值为4,(通常改为1)修改后必须重建索引文件                                                                                重新建立索引命令:repair table tablename quick               ft_max_word_len    84                                #最长的索引字符串,默认值为84,修改后必须重建索引文件              ft_query_expansion_limit   20                      #查询括展时取最相关的几个值用作二次查询              ft_stopword_file    (built-in)                      #全文索引的过滤词文件,具体可以参考:MySQL全文检索中不进行全文索引默认过滤词                       特别注意:50%的门坎限制(当查询结果很多,几乎所有记录都有,或者极少的数据,都有可能会返回非所期望的结果)                        -->可用IN BOOLEAN MODE即可以避开50%的限制。                       此时使用全文索引的格式就变成了: SELECT * FROM `student` WHERE MATCH(`name`) AGAINST('聪' IN BOOLEAN MODE)                         更多内容请参考:MySQL中的全文检索(1) 4. ft_boolean_syntax (+ -><()~*:""&|)使用的例子:         4.1  + : 用在词的前面,表示一定要包含该词,并且必须在开始位置。                             eg: +Apple 匹配:Apple123,     "tommy, Apple"         4.2  - : 不包含该词,所以不能只用「-yoursql」这样是查不到任何row的,必须搭配其他语法使用。                             eg: MATCH (girl_name) AGAINST ('-林志玲 +张筱雨')                               匹配到: 所有不包含林志玲,但包含张筱雨的记录          4.3. 空(也就是默认情况),表示可选的,包含该词的顺序较高。                         例子:                  apple banana           找至少包含上面词中的一个的记录行                  +apple +juice               两个词均在被包含                  +apple macintosh     包含词 “apple”,但是如果同时包含 “macintosh”,它的排列将更高一些                  +apple -macintosh   包含 “apple” 但不包含 “macintosh”         4.4. > :提高该字的相关性,查询的结果会排在比较靠前的位置。          4.5.< :降低相关性,查询的结果会排在比较靠后的位置。                       例子:4.5.1.先不使用 ><                                  select * from tommy.girl where match(girl_name) against('张欣婷' in boolean mode);                                    可以看到完全匹配的排的比较靠前                               4.5.2. 单独使用 >                                 select * from tommy.girl where match(girl_name) against('张欣婷 >李秀琴' in boolean mode);                                   使用了>的李秀琴马上就排到最前面了                              4.5.3. 单独使用 <                                 select * from tommy.girl where match(girl_name) against('张欣婷 <不是人' in boolean mode);                                  看到没,不是人也排到最前面了,这里使用的可是 < 哦,说好的降低相关性呢,往下看吧。                            4.5.4.同时使用><                               select * from tommy.girl where match(girl_name) against('张欣婷 >李秀琴 <练习册 <不是人>是个鬼' in boolean mode);                                到这里终于有答案了,只要使用了 ><的都会往前排,而且>的总是排在<的前面                         小结一下:1. 只要使用 ><的总比没用的 靠前;                                        2. 使用  >的一定比 <的排的靠前 (这就符合相关性提高和降低);                                        3. 使用同一类的,使用的越早,排的越前。         4.6. ( ):可以通过括号来使用字条件。                          eg: +aaa +(>bbb <ccc) 找到有aaa和bbb和ccc,aaa和bbb,或者aaa和ccc(因为bbb,ccc前面没有+,所以表示可有可无),                                          然后 aaa&bbb > aaa&bbb&ccc > aaa&ccc            4.7. ~ :将其相关性由正转负,表示拥有该字会降低相关性,但不像「-」将之排除,只是排在较后面。                             eg:   +apple ~macintosh   先匹配apple,但如果同时包含macintosh,就排名会靠后。            4.8. * :通配符,这个只能接在字符串后面。                                   MATCH (girl_name) AGAINST ('+*ABC*')   #错误,不能放前面                                  MATCH (girl_name) AGAINST ('+张筱雨*')  #正确            4.9. " " :整体匹配,用双引号将一段句子包起来表示要完全相符,不可拆字。                                   eg:  "tommy huang" 可以匹配  tommy huang xxxxx   但是不能匹配  tommy is huang。 5.补充:Windows下无法修改 ft_min_word_len的情况,           5. 1. 使用cmd打开 services.msc,                 找到你的 MySQL服务,右键Properties,找到你的my.ini所在的路径                            5.2. 停止MySQL,在my.ini中增加 ft_min_word_len = 1,重启MySQL,                     然后使用命令 show variables like 'ft_min_word_len'; 查看是否生效了 MySQL全文检索笔记 1. MySQL 4.x版本及以上版本提供了全文检索支持,但是表的存储引擎类型必须为MyISAM,以下是建表SQL,注意其中显式设置了存储引擎类型 CREATE TABLE articles ( id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, title VARCHAR(200), body TEXT, FULLTEXT (title,body) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;    其中FULLTEXT(title, body) 给title和body这两列建立全文索引,之后检索的时候注意必须同时指定这两列。   2. 插入测试数据 INSERT INTO articles (title,body) VALUES ('MySQL Tutorial','DBMS stands for DataBase ...'), ('How To Use MySQL Well','After you went through a ...'), ('Optimizing MySQL','In this tutorial we will show ...'), ('1001 MySQL Tricks','1. Never run mysqld as root. 2. ...'), ('MySQL vs. YourSQL','In the following database comparison ...'), ('MySQL Security','When configured properly, MySQL ...');   3. 全文检索测试 SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('database');    检索结果如下: 5 MySQL vs. YourSQL In the following database comparison ... 1 MySQL Tutorial DBMS stands for DataBase ...    说明全文匹配时忽略大小写。   4. 可能遇到的困扰    到目前为止都很顺利,但是如果检索SQL改为下面会怎样呢? SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('well');    结果让人大跌眼镜,开始我也困惑了许久,后来去网上查了下才知道原来是这么回事: mysql指定了最小字符长度,默认是4,必须要匹配大于4的才会有返回结果,可以用SHOW VARIABLES LIKE 'ft_min_word_len' 来查看指定的字符长度,也可以在mysql配置文件my.ini 更改最小字符长度,方法是在my.ini 增加一行 比如:ft_min_word_len = 2,改完后重启mysql即可。    所以上面不能返回结果。但是我用上面的方法改配置文件并重启MySQL服务器后,再用show命令查看,并没有改变。    另外,MySQL还会计算一个词的权值,以决定是否出现在结果集中,具体如下: mysql在集和查询中的对每个合适的词都会先计算它们的权重,一个出现在多个文档中的词将有较低的权重(可能甚至有一个零权重),因为在这个特定的集中,它有较低的语义值。否则,如果词是较少的,它将得到一个较高的权重,mysql默认的阀值是50%,上面‘you’在每个文档都出现,因此是100%,只有低于50%的才会出现在结果集中。    但是如果不考虑权重,那么该怎么办呢?MySQL提供了布尔全文检索(BOOLEAN FULLTEXT SEARCH)    假设well在所有记录中都出现,并且ft_min_word_len已经改为2,那么下面的SQL检索语句得到的结果集将包含所有记录:   SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('well' IN BOOLEAN MODE );     5. 布尔全文检索语法    上面通过IN BOOLEAN MODE指定全文检索模式为布尔全文检索。MySQL还提供了一些类似我们平时使用搜索引擎时用到的的语法:逻辑与、逻辑或、逻辑非等。具体通过几个SQL语句例子来说明 SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('+apple -banana' IN BOOLEAN MODE);    + 表示AND,即必须包含。- 表示NOT,即不包含。 SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('apple banana' IN BOOLEAN MODE);    apple和banana之间是空格,空格表示OR,即至少包含apple、banana中的一个。 SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('+apple banana' IN BOOLEAN MODE);    必须包含apple,但是如果同时也包含banana则会获得更高的权重。 SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('+apple ~banana' IN BOOLEAN MODE);    ~ 是我们熟悉的异或运算符。返回的记录必须包含apple,但是如果同时也包含banana会降低权重。但是它没有 +apple -banana 严格,因为后者如果包含banana压根就不返回。 SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('+apple +(>banana <orange)' IN BOOLEAN MODE);    返回同时包含apple和banana或者同时包含apple和orange的记录。但是同时包含apple和banana的记录的权重高于同时包含apple和orange的记录。   6. MySQL不支持中文的全文检索    默认MySQL不支持中文全文检索,怎么办?大致方法有下面几个:    A. 扩展MySQL,添加中文全文检索支持,难度较大    B. 为中文内容表提供一个对应的英文索引表(即将FULLTEXT索引列按照一定的规则转化成英文索引表中的每一条记录,比如全部进行base64编码,内容表和英文索引表的id相同),检索时先将检索词也用相同规则转换成英文,然后再使用。如果还要支持按拼音全文检索,那么还需要在索引表中增加对应的拼音内容(就需要中文转拼音算法了)。当然如果还需要支持中英文交互搜索,比如搜索William时也需要返回威廉,反之亦然,那么还需要将威廉对应的英文翻译也存到索引表中去。    参考网上的链接,具体做法包括先对中文内容进行分词,然后中文转换为四位区位码存到索引表中。检索时,包含中文的检索词也要先分词,再转换为四位区位码,然后在索引表中进行全文检索。   7. 核对条目    A. 只有存储引擎类型为MyISAM类型的表,并且MySQL版本为4.X或者以上才能使用MySQL内置的全文检索支持    B. MySQL全文检索默认不支持中文,且对英文检索时忽略大小写    C. MySQL全文检索时,默认检索长度为4,即关键词的长度必须大于5才能被捕获    D. MySQL全文检索时,所有FULLTEXT索引列必须使用相同的字符集    E. MySQL全文检索返回结果集时还会考虑权重    F. MySQL全文检索还支持灵活的布尔全文检索模式    G. 更多内容参考MySQL5官方手册   参考链接:    http://viralpatel.net/blogs/2009/04/full-text-search-using-mysql-full-text-search-capabilities.html    http://hi.baidu.com/gogogo/blog/item/28b16c81b3bc87d6bc3e1eb7.html    http://dev.mysql.com/doc/refman/5.1/zh/functions.html#fulltext-query-expansion    http://www.chinahtml.com/0702/mysql-117187149111362.html    http://www.phpx.com/happy/viewthread.php?tid=124691 一、设置全文索引: 添加:ALTER TABLE table_name ADD FULLTEXT ( column); 删除:DROP INDEX index_name ON table_name; 注:mysql5.6版本以下只有myisam存储引擎支持全文索引,mysql5.6以上版本myisam和innodb都支持全文索引,两者性能有兴趣了可以比较一下。 二、搜索语句: SELECT * FROM table_name WHERE MATCH(index_name) AGAINST(‘搜索值’); 多词请用逗号或空格分开:SELECT * FROM table_name WHERE MATCH(index_name) AGAINST(‘a,b’); 到这里基本已经可以使用了,但是有时候在搜索单个字符时候没有结果,这时候需要修改一下全文索引关键词长度设置了。 注:当个别词的出现频率超过50%时,被认作无效词,可以改为AGAINST (‘高频词’ IN BOOLEAN MODE)。 三、修改配置: 找到mysql.ini,在在 [mysqld] 位置添加:ft_min_word_len=1 重启mysql服务。 查看mysql环境变量:show variables;就可以看到设置的结果了  注: 1.全文索引的字段类型必须为:char,varchar,text 。 2.对于中文全文索引,必须先把字段值做好中文分词,每个关键词之间用“ ,”“ ”分开,不然即使全文索引还是无效,谁让这些都是老外开发的呢(英文单词之间都是空格,妥妥的),但是中文分词可以借助其他一些开源程序来做,比如:coreseek,附上下载地址:http://www.coreseek.cn/news/7/52/ 3.有人说将中文转成拼音,然后进行搜索,或许是一个好的方法,可以试一下。 在MYSQL中使用全文索引(FULLTEXT index) MYSQL的一个很有用的特性是使用全文索引(FULLTEXT index)查找文本的能力.目前只有使用MyISAM类型表的时候有效(MyISAM是默认的表类型,如果你不知道使用的是什么类型的表,那很可能就是 MyISAM).全文索引可以建立在TEXT,CHAR或者VARCHAR类型的字段,或者字段组合上.我们将建立一个简单的表用来解释各种特性.简单用法(MATCH()函数)对3.23.23以后的版本有效,复杂的用法(IN BOOLEAN MODE修饰语)对4以后的版本有效,本文的第一部分着重简单用法,第二部分讲复杂用法.一个简单的表 我们将在整个过程中使用下面的表.CREATE TABLE fulltext_sample(copy TEXT,FULLTEXT(copy)) TYPE=MyISAM; 如果你没有把默认的表类型设置成MyISAM以外的类型那么TYPE=MyISAM可以省略.建表之后,向其中填充一些数据,例如:INSERT INTO fulltext_sample VALUES('It appears good from here'),('The here and the past'),('Why are we hear'),('An all-out alert'),('All you need is love'),('A good alert'); 如果你已经建立好了一个表,你可以使用ALTER TABLE(就像CREATE INDEX语句一样)语句添加一个全文索引,例如:ALTER TABLE fulltext_sample ADD FULLTEXT(copy) 查找文本 全文索引搜索的语法很简单,你只要MATCH字段,AGAINST你要查找的文本,例如:mysql> SELECT * FROM fulltext_sample WHERE MATCH(copy) AGAINST('love');+----------------------+| copy |+----------------------+| All you need is love |+----------------------+ 在全文索引上进行搜索是不区分大小写的,因此下面的语句也可以正常运行:mysql> SELECT * FROM fulltext_sample WHERE MATCH(copy) AGAINST('LOVE');+----------------------+| copy |+----------------------+| All you need is love |+----------------------+ 全文索引通常用来搜索自然语言文本,例如报纸文章,网页内容等等.因此MySQL为这类搜索添加了很多特性.MySQL不索引任何长度小于等于3的文本, 也不索引有50%机会出现的单词.这意味着如果你的表少于2条记录,基于全文索引的搜索不会返回任何东西.将来,MySQL会使这项功能更灵活,但是现在 它应该可以适合大部分自然语言的使用.如果你的数据库中的大部分记录都包含”music”,你很可能不希望返回这些记录,你可以使用IN BOOLEAN MODE修饰符来获得50%左右的阀值,见本文第二部分.结果将按照关联性从高到底的顺序返回.主要特性 下面是标准的全文索引搜索的主要特性:1.排除重复词语2.排除长度小于4的词语3.排除在多于一半记录中出现的词语(就是说只要要有3条记录)4.带连字符的词语被认为两个词语5.结果按照关联度降序返回6.忽略列表中的词语也被从搜索结果中排除.忽略列表基于普通的英文单词,因此如果你的数据用作不同的目的,你可能希望改变忽略列表.不幸的是,这样作并 不容易.你需要编辑文件myisam/ft_static.c,重新编辑MySQL,并重建索引!这里有一个忽略列表.注意,这些在不同的版本里有所更 改.忽略列表"a", "a's", "able", "about", "above", "according", "accordingly", "across", "actually", "after", "afterwards", "again", "against", "ain't", "all", "allow", "allows", "almost", "alone", "along", "already", "also", "although", "always", "am", "among", "amongst", "an", "and", "another", "any", "anybody", "anyhow", "anyone", "anything", "anyway", "anyways", "anywhere", "apart", "appear", "appreciate", "appropriate", "are", "aren't", "around", "as", "aside", "ask", "asking", "associated", "at", "available", "away", "awfully", "b", "be", "became", "because", "become", "becomes", "becoming", "been", "before", "beforehand", "behind", "being", "believe", "below", "beside", "besides", "best", "better", "between", "beyond", "both", "brief", "but", "by", "c", "c'mon", "c's", "came", "can", "can't", "cannot", "cant", "cause", "causes", "certain", "certainly", "changes", "clearly", "co", "com", "come", "comes", "concerning", "consequently", "consider", "considering", "contain", "containing", "contains", "corresponding", "could", "couldn't", "course", "currently", "d", "definitely", "described", "despite", "did", "didn't", "different", "do", "does", "doesn't", "doing", "don't", "done", "down", "downwards", "during", "e", "each", "edu", "eg", "eight", "either", "else", "elsewhere", "enough", "entirely", "especially", "et", "etc", "even", "ever", "every", "everybody", "everyone", "everything", "everywhere", "ex", "exactly", "example", "except", "f", "far", "few", "fifth", "first", "five", "followed", "following", "follows", "for", "former", "formerly", "forth", "four", "from", "further", "furthermore", "g", "get", "gets", "getting", "given", "gives", "go", "goes", "going", "gone", "got", "gotten", "greetings", "h", "had", "hadn't", "happens", "hardly", "has", "hasn't", "have", "haven't", "having", "he", "he's", "hello", "help", "hence", "her", "here", "here's", "hereafter", "hereby", "herein", "hereupon", "hers", "herself", "hi", "him", "himself", "his", "hither", "hopefully", "how", "howbeit", "however", "i", "i'd", "i'll", "i'm", "i've", "ie", "if", "ignored", "immediate", "in", "inasmuch", "inc", "indeed", "indicate", "indicated", "indicates", "inner", "insofar", "instead", "into", "inward", "is", "isn't", "it", "it'd", "it'll", "it's", "its", "itself", "j", "just", "k", "keep", "keeps", "kept", "know", "knows", "known", "l", "last", "lately", "later", "latter", "latterly", "least", "less", "lest", "let", "let's", "like", "liked", "likely", "little", "look", "looking", "looks", "ltd", "m", "mainly", "many", "may", "maybe", "me", "mean", "meanwhile", "merely", "might", "more", "moreover", "most", "mostly", "much", "must", "my", "myself", "n", "name", "namely", "nd", "near", "nearly", "necessary", "need", "needs", "neither", "never", "nevertheless", "new", "next", "nine", "no", "nobody", "non", "none", "noone", "nor", "normally", "not", "nothing", "novel", "now", "nowhere", "o", "obviously", "of", "off", "often", "oh", "ok", "okay", "old", "on", "once", "one", "ones", "only", "onto", "or", "other", "others", "otherwise", "ought", "our", "ours", "ourselves", "out", "outside", "over", "overall", "own", "p", "particular", "particularly", "per", "perhaps", "placed", "please", "plus", "possible", "presumably", "probably", "provides", "q", "que", "quite", "qv", "r", "rather", "rd", "re", "really", "reasonably", "regarding", "regardless", "regards", "relatively", "respectively", "right", "s", "said", "same", "saw", "say", "saying", "says", "second", "secondly", "see", "seeing", "seem", "seemed", "seeming", "seems", "seen", "self", "selves", "sensible", "sent", "serious", "seriously", "seven", "several", "shall", "she", "should", "shouldn't", "since", "six", "so", "some", "somebody", "somehow", "someone", "something", "sometime", "sometimes", "somewhat", "somewhere", "soon", "sorry", "specified", "specify", "specifying", "still", "sub", "such", "sup", "sure", "t", "t's", "take", "taken", "tell", "tends", "th", "than", "thank", "thanks", "thanx", "that", "that's", "thats", "the", "their", "theirs", "them", "themselves", "then", "thence", "there", "there's", "thereafter", "thereby", "therefore", "therein", "theres", "thereupon", "these", "they", "they'd", "they'll", "they're", "they've", "think", "third", "this", "thorough", "thoroughly", "those", "though", "three", "through", "throughout", "thru", "thus", "to", "together", "too", "took", "toward", "towards", "tried", "tries", "truly", "try", "trying", "twice", "two", "u", "un", "under", "unfortunately", "unless", "unlikely", "until", "unto", "up", "upon", "us", "use", "used", "useful", "uses", "using", "usually", "v", "value", "various", "very", "via", "viz", "vs", "w", "want", "wants", "was", "wasn't", "way", "we", "we'd", "we'll", "we're", "we've", "welcome", "well", "went", "were", "weren't", "what", "what's", "whatever", "when", "whence", "whenever", "where", "where's", "whereafter", "whereas", "whereby", "wherein", "whereupon", "wherever", "whether", "which", "while", "whither", "who", "who's", "whoever", "whole", "whom", "whose", "why", "will", "willing", "wish", "with", "within", "without", "won't", "wonder", "would", "would", "wouldn't", "x", "y", "yes", "yet", "you", "you'd", "you'll", "you're", "you've", "your", "yours", "yourself", "yourselves", "z", "zero", 让我们看一下其中的一些词.如果你懒的输入,但是想查找”love”这个词,象下面这样:mysql> SELECT * FROM fulltext_sample WHERE MATCH(copy) AGAINST('lov');Empty set (0.00 sec) 什么都没返回,因为全文索引只包含完整的单词,不是部分单词.如果想得到返回,你必须把单词写完整,就像第一个例子里一样.就像我们提过的,连字符单词在全文索引中被排除(它们被作为单独的单词索引),因此下面的语句什么都不返回:mysql> SELECT * FROM fulltext_sample WHERE MATCH(copy) AGAINST('all-out');Empty set (0.00 sec) 很不幸,两个单词都小于4个字符,因此单独搜索时也不会出现,而且通常的搜索中也不会出现.本文的第二部分中使用BOOLEAN MODE搜索可以搜索部分的或者包含连字符的单词.你也可以一次搜索多个单词,用逗号分隔.下面的例子查找包含”here”和”appears”的记录:mysql> SELECT * FROM fulltext_sample WHERE MATCH(copy) AGAINST('here','appears');Empty set (0.01 sec) 出乎意料这个语句没有返回.但是仔细看看忽略列表,这个词被列在其中,因此被从索引中排除了.忽略列表可能是人们解释MySQL全文索引没有生效的通常原因.如果你的查询返回了一个结果,那么你的版本的MySQL的忽略列表不包含”here”这个词.关联度 下面的例子说明记录返回的优先级mysql> SELECT * FROM fulltext_sample WHERE MATCH(copy) AGAINST('good,alert');+---------------------------+| copy |+---------------------------+| A good alert || It appears good from here || An all-out alert |+---------------------------+ 记录”A good alert”首先出现,因为它同时包含要搜索的两个词.你不必相信我-只需要看看MySQL在结果中显示的优先级.简单的在字段列表中重复MATCH()函数,例如:mysql> SELECT copy,MATCH(copy) AGAINST('good,alert') AS relevanceFROM fulltext_sample WHERE MATCH(copy) AGAINST('good,alert');+---------------------------+------------------+| copy | relevance |+---------------------------+------------------+| A good alert | 1.3551264824316 || An all-out alert | 0.68526663197496 || It appears good from hear | 0.67003110026735 |+---------------------------+------------------+ 关联度的计算非常复杂,它基于索引中单词的数量,记录中不同单词的个数,索引和返回结果中单词的总数,以及单词的重要程度.这个数字可能在你的MySQL版本中有所不同,MySQL偶尔会强化计算逻辑.对大多数应用来说标准的全文索引搜索非常有用而充分,MySQL 4让它更加强大. [原文地址:http://blog.csdn.net/zyz511919766/article/details/12780173] 1.概要 InnoDB引擎对FULLTEXT索引的支持是MySQL5.6新引入的特性,之前只有MyISAM引擎支持FULLTEXT索引。对于FULLTEXT索引的内容可以使用MATCH()…AGAINST语法进行查询。 为了在InnoDB驱动的表中使用FULLTEXT索引MySQL5.6引入了一些新的配置选项和INFORMATION_SCHEMA表。比如,为了监视一个FULLTEXT索引中文本处理过程的某一方面可以查询INNODB_FT_CONFIG,INNODB_FT_INDEX_TABLE,INNODB_FT_INDEX_CACHE,INNODB_FT_DEFAULT_STOPWORD,INNODB_FT_DELETED和INNODB_FT_BEING_DELETED这些表。可以通过innodb_ft_num_word_optimize和innodb_optimize_fulltext_only选项控制OPTIMIZETABLE命令对InnoDB FULLTEXT索引的更新。 2.相关库表 INFORMATION_SCHEMA库中与InnoDB全文索引相关的表如下: INNODB_SYS_INDEXES INNODB_SYS_TABLES  INNODB_FT_CONFIG INNODB_FT_INDEX_TABLE INNODB_FT_INDEX_CACHE INNODB_FT_DEFAULT_STOPWORD INNODB_FT_DELETED INNODB_FT_BEING_DELETED ? INNODB_SYS_INDEXES:提供了InnoDB索引的状态信息。 ? INNODB_SYS_TABLES:提供了InnoDB表的状态信息。 ? INNODB_FT_CONFIG:显示一个InnoDB表的FULLTEXT索引及其相关处理的元数据。 ? INNODB_FT_INDEX_TABLE:转化后的索引信息用于处理基于InnoDB表FULLTEXT索引的文本搜索。一般用于调试诊断目的。使用该表前需先配置innodb_ft_aux_table配置选项,将其指定为想要查看的含FULLTEXT索引的InnoDB表,选项值的格式为database_name/table_name。配置了该选项后INNODB_FT_INDEX_TABLE,INNODB_FT_INDEX_CACHE, INNODB_FT_CONFIG, INNODB_FT_DELETED和INNODB_FT_BEING_DELETED表将被填充与innodb_ft_aux_table配置选项指定的表关联的搜索索引相关信息。 ? INNODB_FT_INDEX_CACHE:向含FULLTEXT索引的InnoDB表插入数据后新插入数据转后的索引信息。表结构与INNODB_FT_INDEX_TABLE一致。为含FULLTEXT索引的InnoDB表执行DML操作期间重组索引开销很大,因此将新插入的被索引的词单独存储于该表中,当且仅当为InnoDB表执行OPTIMIZE TABLE语句后才将新的转换后的索引信息与原有的主索引信息合并。使用该表前需先配置innodb_ft_aux_table配置选项。 ? INNODB_FT_DEFAULT_STOPWORD:在InnoDB表上创建FULLTEXT索引所使用的默认停止字表。 ? INNODB_FT_DELETED:记录了从InnoDB表FULLTEXT索引中删除的行。为了避免为InnoDB的FULLTEXT索引执行DML操作期间重组索引的高开销,新删除的词的信息单独存储于此表。当且仅当为此InnoDB表执行了OPTIMIZE TABLE操作后才会从主搜索索引中移除已删除的词信息。使用该表前需先配置innodb_ft_aux_table选项。 ? INNODB_FT_BEING_DELETED:为含FULLTEXT索引的InnoDB表执行OPTIMIZE TABLE操作时会根据INNODB_FT_DELETED表中记录的文档ID从InnoDB表的FULLTEXT索引中删除相应的索引信息。而INNOFB_FT_BEING_DELETED表用于记录正在被删除的信息,用于监控和调试目的。 3.相关配置选项 Name Cmd-Line Option file System Var Status Var Scope Dynamic innodb_ft_aux_table Yes Yes Yes   Global Yes innodb_ft_cache_size Yes Yes Yes   Global No innodb_ft_enable_diag_print Yes Yes Yes   Global Yes innodb_ft_enable_stopword Yes Yes Yes   Global Yes innodb_ft_max_token_size Yes Yes Yes   Global No innodb_ft_min_token_size Yes Yes Yes   Global No innodb_ft_num_word_optimize Yes Yes Yes   Global Yes innodb_ft_server_stopword_table Yes Yes Yes   Global Yes innodb_ft_sort_pll_degree Yes Yes Yes   Global No innodb_ft_user_stopword_table Yes Yes Yes   Both Yes innodb_optimize_fulltext_only Yes Yes Yes   Global Yes ? innodb_ft_aux_table:指定包含FULLTEXT索引的InnoDB表的的名称。该变量在运行时设置用于诊断目的。设置该值后INNODB_FT_INDEX_TABLE, INNODB_FT_INDEX_CACHE, INNODB_FT_CONFIG,INNODB_FT_DELETED和INNODB_FT_BEING_DELETED表将被填充与innodb_ft_aux_table指定的表关联的搜索索引相关信息。 ? innodb_ft_cache_size:当创建一个InnoDB FULLTEXT索引时在内存中存储已解析文档的缓存大小。 ? innodb_ft_enable_diag_print:是否开启额外的全文搜索诊断输出。 ? innodb_ft_enable_stopword:是否开启停止字。InnoDB FUllTEXT索引被创建时为其指定一个关联的停止字集。(若设置了innodb_ft_user_stopword_table则停止字由该选项指定的表获取,若没有设置innodb_ft_user_stopword_table而设置了innodb_ft_server_stopword_table则停止字由该选项指定的表获取,否则使用内置的停止字。) ? innodb_ft_max_token_size:存储在InnoDB的FULLTEXT索引中的最大词长。设置这样一个限制后可通过忽略过长的关键字等有效降低索引大小从而加速查询。 ? innodb_ft_min_token_size:存储在InnoDB的FULLTEXT索引中的最小词长。增加该值后会忽略掉一些通用的没有显著意义的词汇从而降低索引大小继而加速查询。 ? innodb_ft_num_word_optimize:为InnoDB FULLTEXT索引执行OPTIMIZE操作每次所处理的词数。因为在含有全文搜索索引的表中执行批量的插入或更新操作需要大量的索引维护操作来合并所有的变化。因此,一般会运行一系列OPTIMIZE TABLE语句,每次从上一次的位置开始,处理指定数目的词,知道搜索索引被完全更新。 ? innodb_ft_server_stopword_table:含有停止字的表,在创建InnoDB FULLTEXT索引时或忽略表中的停止字。停止字表需为InnoDB表,且在指定前应当已存在。 ? innodb_ft_sort_pll_degree:为较大的表构建搜索索引时用于索引和记号化文本的并行线程数。 ? innodb_ft_user_stopword_table:含有停止字的表,在创建InnoDB FULLTEXT索引时或忽略表中的停止字。停止字表需为InnoDB表,且在指定前应当已存在。 ? innodb_optimize_fulltext_only:改变OPTIMIZE TABLE语句对InnoDB表操作的方式。对含FULLTEXT 索引的InnoDB表进行维护操作期间,一般临时的开启该选项。默认情况下,OPTIMIZE TABLE语句会重组表的聚集索引中的数据。若开启了该选项则该语句会跳过表数据的重组,而是只处理FULLTEXT索引中新插入的、删除的、更新的标记数据。(在对作为FULLTEXT索引的一部分的InnoDB表列进行了大量的插入、更新或删除操作后,先将innodb_optimize_fulltext_only设置为on以改变OPTIMIZE TABLE的默认行为,然后设置innodb_ft_num_word_optimize为合适的值以将索引维护时间控制在一个合理的可接受范围内,最后执行一系列的OPTIMIZE语句知道搜索索引被完全更新。) 4.全文搜索功能 全文搜索的语法:MATCH(col1,col2,…) AGAINST (expr[search_modifier])。其中MATCH中的内容为已建立FULLTEXT索引并要从中查找数据的列,AGAINST中的expr为要查找的文本内容,search_modifier为可选搜索类型。search_modifier的可能取值有:IN NATURAL LANGUAGEMODE、IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION、IN BOOLEAN MODE、WITH QUERY EXPANSION。search_modifier的每个取值代表一种类型的全文搜索,分别为自然语言全文搜索、带查询扩展的自然语言全文搜索、布尔全文搜索、查询扩展全文搜索(默认使用IN NATURAL LANGUAGE MODE)。 MySQL中全文索引的关键字为FULLTEXT,目前可对MyISAM表和InnoDB表的CHAR、VARCHAR、TEXT类型的列创建全文索引。全文索引同其他索引一样,可在创建表是由CREATE TABLE语句创建也可以在表创建之后用ALTER TABLE或者CREATE INDEX命令创建(对于要导入大量数据的表先导入数据再创建FULLTEXT索引比先创建索引后导入数据会更快)。 4.1自然语言全文搜索 自然语言全文搜索是MySQL全文搜索的默认搜索方式,实现从一个文本集合中搜索给定的字符串。这里,文本集合指的是指由FULLTEXT索引的一个或者多个列。 建表,并给title,body字段加FULLTEXT索引 CREATE TABLE articles (      id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,      title VARCHAR(200),      body TEXT,      FULLTEXT (title,body) ) ENGINE=InnoDB; 导入数据 INSERT INTO articles (title,body) VALUES    ('MySQL Tutorial','DBMS stands for DataBase ...'),    ('How To Use MySQL Well','After you went through a ...'),    ('Optimizing MySQL','In this tutorial we will show ...'),    ('1001 MySQL Tricks','1. Never run mysqld as root. 2. ...'),    ('MySQL vs. YourSQL','In the following database comparison ...'),    ('MySQL Security','When configured properly, MySQL ...'); 例1: SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('database' IN NATURAL LANGUAGE MODE); 可以看到,语句查找到了包含指定内容的行。实际上,返回的行是按与所查找内容的相关度由高到低的顺序排列的。这个相关度的值由WHERE语句中的MATCH (…) AGAINST (…)计算所得,是一个非负浮点数。该值越大表明相应的行与所查找的内容越相关,0值表明不相关。该值基于行中的单词数、行中不重复的单词数、文本集合中总单词数以及含特定单词的行数计算得出。 例2: 由上例可知MATCH (…) AGAINST (…)实际上会计算一个相关值,可通过下例来验证。 SELECT id, MATCH (title,body) AGAINST ('Tutorial' IN NATURAL LANGUAGE MODE) AS score FROM articles; 可以看到,所得结果的第二列即为改行与查找内容的相关度。上例1中所得结果的顺序就是按此相关度排列的。 例3: 若想既看到查找到的结果又需要了解具体的相关度,可用下述方法达成。 SELECT id, body, MATCH (title,body) AGAINST    ('Security implications of running MySQL as root'    IN NATURAL LANGUAGE MODE) AS score    FROM articles WHERE MATCH (title,body) AGAINST    ('Security implications of running MySQL as root' IN NATURAL LANGUAGE MODE); 可以看到,通过在查找部分和条件部分分别使用相同的MATCH(…) AGAINST(…)可以同时获取两方面的内容(不会增加额外开销,优化器知道两个MATCH(…) AGAINST(..)是相同的,只会执行一次该语句) 注意事项 默认情况下全文搜索大小写不敏感,如上例1,查找的内容为‘database’但含有‘DataBase’的行也会返回。可以通过为FULLTEXT索引列所使用的字符集指定一个特定的校对集来改变这种行为。 考虑下述两个SELECT语句: 1.  SELECTCOUNT(*) FROM articles             WHEREMATCH (title,body)             AGAINST('database' IN NATURAL LANGUAGE MODE); 2.  SELECTCOUNT(IF(MATCH (title,body) AGAINST('database' IN NATURAL LANGUAGE MODE), 1, NULL)) AS count             FROMarticles; 这两条查询语句均可返回匹配的行数。但第一条语句可以利用基于WHERE从句的索引查找,因此在匹配的行数较少时速度较第二句更快。第二句执行了全表扫描,因此在匹配的行数较多时较第一句更快。 MATCH()函数中的列必须与FULLTEXT索引中的列相同。如MATCH(title,body)与FULLTEXT(title,body)。若要单独搜索某列,如body列,则需另外单独为该列建全文索引FULLTEXT(body),然后用MATCH(body)搜索。 对于InnoDB表MATCH()中的列仅能来自于同一个表,因为索引不能快多张表(MyISAM表的的布尔搜索因为可以不使用索引所以可以跨多张表中的列,但速度很慢)。 全文搜索不仅可以搜索类似例1中‘database’这样的单个的单词,还可以搜索句子(这才是其被称为‘全文搜索‘的关键),如例3。全文搜索把任何数字、字母、下划线序列看作是单词,还可以包含“’”如aaa’bbb备解析为一个单词,但aaa’’bbb备解析为两个单词,FULLTEXT解析器自动移除首尾的“’”,如’aaa’bbb’被解析为aaa’bbb。FULLTEXT解析器用“ ”(空格)、“,”(逗号)“.”(点号)作为默认的单词分隔符,因此对于不使用这些分隔符的语言如汉语来说FULLTEXT解析器不能正确的识别单词,对于这种情况需做额外处理。 全文搜索中一些单词会被忽略。首先是过短的单词,InnoDB全文搜索中默认为3个字符,MyISAM默认4个字符,可通过在创建FULLTEXT索引前改变配置参数来改变默认行为,对于InnoDB该参数为:innodb_ft_min_token_size,对于MyISAM为ft_min_word_len;另外stopword列表中的单词会被忽略。stopword列表包含诸如“the”、“or”、“and”等常用单词,这些词通常被认为没有什么语义价值。MySQL由内建的停止字列表,但是可以所使用自定义的停止字列表来覆盖默认列表。对于InnoDB控制停止字的配置参数为innodb_ft_enable_stopword,innodb_ft_server_stopword_table,  innodb_ft_user_stopword_table对于MyISAM参数为ft_stopword_file。 文本集合和查询语句中的单词的权重由该单词在集合或语句中的重要性确定。单词在越多的行中出现则该单词的权重越低,因为这表明其在文本集合中的语义价值较小。反之权重越高。例1中提到的相关度计算也与此值有关。 4.2布尔全文搜索 如果在AAGAINST()函数中指定了INBOOLEN MODE模式,则MySQL会执行布尔全文搜索。在该搜索模式下,待搜索单词前或后的一些特定字符会有特殊的含义。 例1: SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('+MySQL-YourSQL' IN BOOLEAN MODE); 该查询语句中“MySQL”前的“+”表明结果中必须包含“MySQL”而“YourSQL”前的“-”表明所得结果中不能含有“YourSQL”。 除了“+”和“-”外还有其他一些特定的字符。如空字符表明后跟的单词是可选的,但出现的话会增加该行的相关性;“@distance”用于指定两个或多个单词相互之间的距离(以单词度量)需在指定的范围内;“>”用于增加后跟单词对其所在行的相关性的贡献“<”用于降低该贡献;“()”用于将单词分组为子表达式且可以嵌套;“~”是后跟单词对其所在行的相关性的贡献值为负;“*”为普通的通配符,若为单词指定了通配符,那么即使该单词过短或者出现在了停止字列表中它也不会被移除;“””,括在双引号中的短语指明行必须在字面上包含指定的短语,全文搜索将短语分割为词后在FULLTEXT索引中搜索。非字字符无需完全匹配,如”test phrase”可以匹配含”test phrase”和”test phrase”的行,但匹配含”phrase test”的行。 例2: SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('MySQL YourSQL' IN BOOLEAN MODE); 找到包含MySQL或者YourSQL的行 例3: SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('+MySQL+YourSQL' IN BOOLEAN MODE); 找到包含同时MySQL和YourSQL的行 例4: SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('+MySQL YourSQL' IN BOOLEAN MODE); 找到必须包含MySQl的行,YourSQL可有可无,但有YourSQL会增加相关性。 例5: SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('+MySQL ~YourSQL' INBOOLEAN MODE); 找到包含必须包含MySQL的行,YourSQL可有可无,若出现了YourSQL则会降低其所在行的相关性。 例6: SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('+MySQL +(>Security <Optimizing)' IN BOOLEANMODE); 找到必须同时包含MySQL以及Security或Optimizing的行Security会增加所在行的相关性,而Optimizing会降低所在行的相关性。 例7: SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('da*' IN BOOLEAN MODE); 找到包含da*的行。如包含DataBase、database等。 例8: SELECT * FROM articles WHERE MATCH (title,body) AGAINST('"MySQL,Tutorial"' IN BOOLEAN MODE); 找到包含“MySQL Tutorial”短语的行。 布尔全文搜索的一些特点 ? MyISAM全文搜索会忽略至少在一半以上数据行中出现的单词(也即所谓的50%阈值),InnoDB无此限制。而在布尔全文搜索中MyISAM的50%阈值不生效。 ? 停止字列表也适用于布尔全文搜索。 ? 最小和最大词长全文搜索参数也适用于布尔全文搜索 ? MyISAM中的布尔搜索在FULLTEXT索引不存在的时候仍可工作,但速度很慢。而InnoDB表的各类全文搜索必须有FULLTEXT索引,否则会出现找不到与指定列相匹配的FULLTEXT索引的错误 ? InnoDB中的全文搜索不支持在单一搜索单词前使用多个操作符如“++MySQL”。MyISAM中全文搜索可以处理这种情况,但是会忽略除了紧邻单词之外的其他操作符。 4.3查询扩展全文搜索 某些时候我们通过全文搜索来查找包含某方面内容的行,比如我们搜索“database”,实际上我们期望返回结果不仅仅是仅包含“database”单词的行,一些包含“MySQL”、“SQLServer”、“Oracle”、“DB2”、“RDBMS”等的行也期望被返回。这个时候查询扩展全文搜索就能大显身手。 通过在AGAINST()函数中指定WITHQUERY EXPANSION 或者IN NATURAL MODE WITH QUERY EXPANSION可以开启查询扩展全文搜索模式。其工作原理是执行两次搜索,第一次用给定的短语搜索,第二次使用给定的短语结合第一次搜索返回结果中相关性非常高的一些行进行搜索。 例1: SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('database' IN NATURAL LANGUAGE MODE); 使用自然语言搜索返回了包含“database”的行。 例2: SELECT * FROM articles  WHERE MATCH (title,body) AGAINST ('database' WITH QUERY EXPANSION); 使用查询扩展全文搜索,不进返回了包含“database”的行,也返回了与例1中返回的行的内容相关的行。 注意事项 因为查询扩展会返回一些不相关的内容,因此会显著的引入噪声。索引仅当要查询的短语较短时才在考虑使用查询扩展全文搜索。 4.4全文搜索的停止字 上文已经简单介绍过了停止字列表,这里做详细介绍。停止字列表用MySQL Server所使用的字符集和校对集(分别由character_set_server和collation_server两个参数控制)载入并执行搜索。若用于全文索引和搜索的停止字文件或者停止字表使用了与MySQL Server不同的字符集和校对集会则导致查找停止字时错误的命中或未命中。 停止字查找的大小写敏感性也依赖于MySQL Server所使用的校对集,例如校对集为latin1_swedish_ci则查找是大小写不敏感的,若校对集为latin1_geberal_cs或者latin1_bin则查找是大小写敏感的。 InnoDB默认的停止字列表相对较短(因为技术上的或者文学等方面的文档常使用较短的词作为关键字或者有其他显著意义)。InnoDB默认的停止字列表存储在information_schema.innodb_ft_default_stopword表中。当然也可以通过自定义与innodb_ft_default_stopword表结构相同的表,填充期望的停止字,然后通过innodb_ft_server_stopword_table选项指定自定义的停止字表db_name/table_name,来改变默认的行为。另外还可以为innodb_ft_user_stopword_table选项指定含停止字的表,若同时指定了innodb_ft_default_stopword和innodb_ft_user_stopword_table则将使用后者指定的停止字表。上述操作改变所使用停止字表的操作需在创建全文索引前完成。且在指定所使用的停止字表时,表必须已经存在。 对于MyISAM可通过 ft_stopword_file选项指定所使用的停止字列表。MyISAM默认的停止字列表可在MySQL源码的 storage/myisam/ft_static.c文件中找到。 4.5全文搜索的限制 ? 目前只有InnoDB和MyISAM引擎支持全文搜索。其中InnodB表对FULLTEXT索引的支持从MySQL5.6.4开始。 ? 分区表不支持全文搜索。 ? 全文索引适用于多数多字节字符集。例外情况是:对于Unicode,utf8字符集可用但ucs2字符集不适用。尽管不能在ucs2列建立FULLTEXT索引,但可以在MyISAM表IN BOOLEAN MODE模式的搜索中搜索没有建立FULLTEXT索引的列。utf8的特性适用于utf8mb4,ucs2的特性适用于utf16、utf16e和utf32。 ? 表意型语言如汉语、日语没有诸如空格之类的单词定界符。因此FULLTEXT解析器不能确定此类语言中词的起止。对于此种情况要特殊处理(比如将中文转换成一种单字节类似英文习惯的存储方式)。 ? 允许在同一表中使用多种字符集,但FULLTEXT索引中的列必须使用同一字符集和校对集。 ? MATCH()函数中的列必须与FULLTEXT索引中定义的列完全一致,除非是在MyISAM表中使用IN BOOLEAN MODE模式的全文搜索(可在没有建立索引的列执行搜索,但速度很慢)。 ? AGAINST()函数中的参数需为在查询评估期间保持不变的字符串常量。 ? FULLTEXT搜索的索引提示比non-FULLTEXT搜索的索引提示要多一些限定:对于自然语言模式的全文搜索,索引提示会被忽略而不给出任何提示,比如虽明确在查询语句中给出了IGNORE INDEX(i)指明不使用i索引,但是该索引提示会被忽略掉,最终的查询中仍会使用索引i;对于布尔模式的全文搜索,FOR ORDER BY和FOR GROUP BY的索引提示会被忽略,FOR JOIN和不带FOR修饰符的索引提示不被忽略。 4.6全文搜索参数调整 仅有少量的用户可调参数用于调整MySQL的全文搜索能力。可以通过修改源码来获取更多对MySQL全文搜索行为的控制。但一般情况下不推荐这么做,除非很清楚自己在做什么,因为这些参数已经针对效率做过调整,修改默认的行为多数情况下反而会带来性能下降。 多数全文搜索相关的变量不能在Server运行的时候修改。需在Server启动时指定这些参数,或者修改完参数之后重新启动Server。另外,某些变量修改后需要重建FULLTEXT索引。 控制最小、最大字长的配置选项对于InnoDB为:innodb_ft_min_token_size和innodb_ft_max_token_size,对于MyISAM为:ft_min_word_len 和 ft_max_word_len。改变这些选项中任意一个的值都需重建FULLTEXT索引并重启Server。 用于停止字列表的配置选项对于InnoDB为:innodb_ft_enable_stopword、innodb_ft_server_stopword_table和innodb_ft_user_stopword_table,对于MyISAM为:ft_stopword_file。可以通过改变这些选项的值来开启/关闭停止字过滤并指定停止字列表。修改了这些选项后需重建索引并在必要的时候重启Server。 ft_stopword_file指定了包含停止字列表的文件,Server默认在数据目录搜索该文件除非用绝对路径指定了文件位置,若文件内容为空,则会关闭MyISAM的停止字过滤功能。停止字文件格式很灵活,可以使用任何非字母或数字的字符来界定停止字,但“_”和“’”例外,它们会被当作字的一部分处理。停止字列表使用Server默认的字符集。 MyISAM全文搜索的50%阈值特性可通过修改源码来关闭,将源码storage/myisam/ftdefs.h中的宏#define GWS_IN_USEGWS_PROB替换为#define GWS_IN_USE GWS_FREQ后重新编译MySQL即可。同样,不推荐上述方式,如果确实需要搜索一些通用的词,可以用布尔模式的全文搜获,此种情况下50%阈值特性不生效。 可以通过修改ft_boolean_syntax选项的值来更改MyISAM布尔全文搜做中默认使用的操作符(InnoDB无此选项)。该选项可动态改变但须超级用户权限,另外,改变了改制后无需重建FULLTEXT索引。 可以通过多种方式更改期望被认作是单词字符成分的字符集合。默认情况下“_”和“’”以及字母和数字被认为是组成单词的字符,其他的被默认为定界符。例如,我们现在想把连字符“-”也作为组成单词的字符处理,那么可以通过如下方式完成: ? 修改MySQL源码,在storage/myisam/ftdefs.h文件中找到true_word_char()和misc_word_char()两个宏,在任一个宏定义里添加“-”,重新编译MySQL。 ? 修改字符集文件,true_word_char()宏实际上利用“character type”表来从其他字符中区分出字母和数字。可以通过编辑字符集对应的XML文件中<ctype><map>节点中的内容来将“-”指定为“字母“,然后将该字符集用于FULLTEXT索引。此种方式无需重新编译MySQL。对于编辑字符集XML文件,可参阅MySQL参考手册CharacterDefinition Arrays部分。http://dev.mysql.com/doc/refman/5.6/en/character-arrays.html ? 对FULLTEXT索引列使用的字符集添加新的校对集,然后更新该列以使用新添加的校对集。具体参阅MySQL手册Adding a Collation to a Character Set以及Adding a Collation for Full-Text Indexing部分。http://dev.mysql.com/doc/refman/5.6/en/full-text-adding-collation.htmlhttp://dev.mysql.com/doc/refman/5.6/en/adding-collation.html 为InnoDB表重建FULLTEXT索引可以通过带DROP INDEX和ADD INDEX从句的ALTER TABLE语句完成,先删除旧的再创建新的。为MyISAM表重建FULLTEXT索引同样可通过上述语句完成,也可以通过QUICK repair操作来重建(但通常第一种方式会更快),如: mysql> REPAIR TABLE tbl_name QUICK; 需要特别说明的是,若通过repair表的方式来为MyISAM表重建FULLTEXT索引,则通过上述语句进行即可。用myisamchk工具也可以为MyISAM表重建索引,但是容易导致查询产生错误的结果,对表的修改可能使Server认为该表被损坏了。究其原因是因为通过myisamchk工具执行修改MyISAM表的索引的操作时,除非明确指定了要使用的参数值否则使用默认的全文索引参数值(如最小最大词长等)重建FULLTEXT索引。导致这种情况是因为只有Server才知道这些全文索引参数值,MyISAM索引文件中不存储这些值。若更改过了这些值,如设置了ft_min_word_len=2,则在通过myisamchk工具修复表时要明确指定该修改过的参数值如: shell> myisamchk --recover--ft_min_word_len=3 tbl_name.MYI 当然也可以通过在MySQL配置文件[myisamchk]节中加入同[mysqld]节中与全文搜索相关参数一致的参数来确保myisamchk使用最新的参数值来重建表的FULLTEXT索引。 用myisamchk为MyISAM表修改索引的替代方式是使用REPAIR TABLE、ANALYZE TABLE、 OPTIMIZE TABLE、ALTER TABLE,这些语句是由Server执行的因此可以读取到正确的全文索引参数值,不会引起问题。 4.7为全文搜索添加校对字符集 参考 10.4. Adding a Collation to a Character Set http://dev.mysql.com/doc/refman/5.6/en/adding-collation.html 12.9.7. Adding a Collation for Full-Text Indexing http://dev.mysql.com/doc/refman/5.6/en/full-text-adding-collation.html 5.性能对比测试 5.1测试环境 测试机:SVR644HP380 内存容量:8G MySQL Server版本:5.6.12 5.2测试设计 词汇量:6个等级,分别用vocab01k、vocab05k、vocab10k、vocab15k,vocab25k、vocab35k标记,每个等级的词汇数如下,1000、5000、10000、15000、25000、35000。(取牛津词典单词部分,去重复后随机打乱顺序,分别截取前1000、5000、10000……作为对应的词汇量) 记录数:20个等级,分别用rec005k、rec010k、rec015k、rec020k、……rec095k、rec100k标记,每个等级的记录数如下,5000、10000、15000、20000、25000、30000、……、95000、100000。 根据词汇量等级和记录数等级分别生成含不同记录数且表中文本列是由对应的词汇量生成的随机文本的表,共6*20=120个。表的存储引擎使用InnoDB。表由id和body两个字段组成,分别为整型和文本型,且在body列创建了FULLTEXT索引。表名的命名规则为vocab01k_rec005k,表示该表中共含有5千条记录,每条记录中的body列由vocab01k对应的词汇量生成的随机单词组成,以此类推。每行记录中的body列定为由50个随机单词组成。 比较两类查询:LIKE从句查询以及使用FULLTEXT索引的MATCH()AGAINST()查询。在每个表上分别执行LIKE查询和MATCH() AGAINST()全文查询,每个表上的每个查询分别执行50次,记录每次所耗费的时间。对于每50个消耗的时间,删除其最大两个值和最小两个值,取剩余值的均值作为查询耗时的最终结果。这样一共可获得120*2 = 240个时间数据,根据这些数据绘图。在每个表上执行的查询如下(其中random_word1、random_word2、random_word3是根据查询时表对应的词汇量生成的随机单词。): LIKE搜索:SELECT body FROM table_name WHERE body LIKE "%random_word1%" AND bodyLIKE "% random_word2%" AND body LIKE "% random_word3%"; FULLTEXT搜索:SELECT body FROM table_name WHERE MATCH(body) AGAINST("+random_word3 + random_word3+ random_word3" IN BOOLEAN MODE) 5.3测试结果 图示 LIKE搜索: FULLTEXT搜索: FULLTEXT搜索与LIKE搜索对比: 结果讨论 LIKE搜索的耗时随着记录数的增加而线性增长,但对于10万行记录以下的表(这里共100000*50个单词)搜索时间基本上能保持在1秒以内,所以like搜索的性能也不是特别差。由不同词汇量生成的文本对LIKE搜索的性能影响不大,不同词汇量对应的搜索时间基本上在一个很小的时间范围内变化。 FULLTEXT搜索耗时也随表中记录数的增长而线性增加。对于10万行记录以下的表(这里共100000*50个单词)搜索时间基本上能保持在0.01秒以内。由不同词汇量生成的随机文本对FULLTEXT搜索性能有相对来说比较显著的影响。每行记录中含同样的单词数,这样,较大的词汇量倾向于生成冗余度更低的文本,相应的搜索耗时倾向于更少。这可能与FULLTEXT索引建立单词索引的机制有关,较大的词汇量倾向于生成范围广但相对较浅的索引,因而能快速确定文本是否匹配。 与LIKE搜索相比,FULLTEXT全文搜索的性能要强很多,对于10万行记录的表,搜索时间都在0.02秒以下。因此可以将基于FULLTEXT索引的文本搜索部署于网站项目中的文本搜索功能中。但是,正如上述提到的,无论是LIKE搜索还是FULLTEXT搜索,其性能都会随着记录数的增长而下降,因此,若网站项目中的文本搜索数据库记录数庞大的一定规模后,可能需要考虑使用MySQL数据库全文搜索以外的文本搜索解决方案了。 About Me ............................................................................................................................................. ● 本文作者:小麦苗,部分内容整理自网络,若有侵权请联系小麦苗删除 ● 本文在itpub(http://blog.itpub.net/26736162/abstract/1/)、博客园(http://www.cnblogs.com/lhrbest)和个人微信公众号(xiaomaimiaolhr)上有同步更新 ● 本文itpub地址:http://blog.itpub.net/26736162/abstract/1/ ● 本文博客园地址:http://www.cnblogs.com/lhrbest ● 本文pdf版、个人简介及小麦苗云盘地址:http://blog.itpub.net/26736162/viewspace-1624453/ ● 数据库笔试面试题库及解答:http://blog.itpub.net/26736162/viewspace-2134706/ ● DBA宝典今日头条号地址:http://www.toutiao.com/c/user/6401772890/#mid=1564638659405826 ............................................................................................................................................. ● QQ群号:230161599(满)、618766405 ● 微信群:可加我微信,我拉大家进群,非诚勿扰 ● 联系我请加QQ好友(646634621),注明添加缘由 ● 于 2017-09-01 09:00 ~ 2017-09-30 22:00 在魔都完成 ● 文章内容来源于小麦苗的学习笔记,部分整理自网络,若有侵权或不当之处还请谅解 ● 版权所有,欢迎分享本文,转载请保留出处 ............................................................................................................................................. ● 小麦苗的微店:https://weidian.com/s/793741433?wfr=c&ifr=shopdetail ● 小麦苗出版的数据库类丛书:http://blog.itpub.net/26736162/viewspace-2142121/ ............................................................................................................................................. 使用微信客户端扫描下面的二维码来关注小麦苗的微信公众号(xiaomaimiaolhr)及QQ群(DBA宝典),学习最实用的数据库技术。    小麦苗的微信公众号      小麦苗的DBA宝典QQ群1     小麦苗的DBA宝典QQ群2        小麦苗的微店 .............................................................................................................................................
文章
SQL  ·  自然语言处理  ·  关系型数据库  ·  MySQL  ·  索引
2017-09-08
阿里云实时计算Flink
198486 人关注 | 6532 讨论 | 765 内容
+ 订阅
  • 中原银行对金融行业实时数仓的现状与发展趋势思考
  • Disney 流媒体广告 Flink 的应用实践
  • Apache Flink 社区 2022 年度报告:Evolution, Diversity, Connection
查看更多 >
数据库
252223 人关注 | 50596 讨论 | 93852 内容
+ 订阅
  • 测试开发知识总结(一)
  • 《MYSQL必知必会》读书笔记(三)
  • 《MYSQL必知必会》读书笔记(二)
查看更多 >
开发与运维
5594 人关注 | 131318 讨论 | 299029 内容
+ 订阅
  • 阿里云云原生助理考试笔记(2.3)
  • 实战案例!Python批量识别银行卡号码并且写入Excel,小白也可以轻松使用~
  • Verilog HDL仿真常用命令
查看更多 >
人工智能
2778 人关注 | 11221 讨论 | 95974 内容
+ 订阅
  • 实战案例!Python批量识别银行卡号码并且写入Excel,小白也可以轻松使用~
  • Verilog HDL仿真常用命令
  • 测试开发知识总结(一)
查看更多 >
安全
1179 人关注 | 23939 讨论 | 80482 内容
+ 订阅
  • 阿里云云原生助理考试笔记(2.3)
  • 测试开发知识总结(一)
  • ubuntu kvm winpe 使用 引导修复 重置windows 密码 windows qocw2 镜像
查看更多 >