01 面试题

1、请讲一下常见的SQL优化方法(至少10条)

  1. 应尽量避免在 where 子句中使用 !=<> 操作符,否则将引擎放弃使用索引而进行全表扫描

  2. 对查询进行优化,应尽量避免全表扫描,首先应考虑在 whereorder by 涉及的列上建立索引

  3. 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,
    如:select id from t where num is null
    可以在num列上设置默认值 0 ,确保表中num列没有null值,然后这样查询: select id from t where num=0

  4. 尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,
    如:select id from t where num=10 or num=20
    可以这样查询:select id from t where num=10 union all select id from t where num=20

  5. 下面的模糊查询也将导致全表扫描:select id from t where name like ‘%c%’
    若要提高效率,可以使用向左匹配 select id from t where name like c% (不能前置百分号),或者考虑全文检索。

  6. innot in 也要慎用,否则会导致全表扫描,如:select id from t where num in(1,2,3)
    对于连续的数值,能用 between 就不要用 in 了:select id from t where num between 1 and 3

  7. 如果在 where 子句中使用参数,也会导致全表扫描。因为 SQL 只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。
    如下面语句将进行全表扫描:select id from t where num=@num
    可以改为强制查询使用索引:select id from t with(index(索引名)) where num=@num

  8. 应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。
    如:select id from t where num/2=100
    应改为:select id from t where num=100*2

  9. 应尽量避免在 where 子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。
    如:select id from t where substring(name,1,3)=’abc'
    生成的id应改为:select id from t where name like ‘abc%’
    如:select id from t where substring(name,1,3)=’abc'
    生成的id应改为:select id from t where name like ‘abc%’

  10. 不要在 where 子句中的 “=” 左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。

  11. 在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使 用,并且应尽可能的让字段顺序与索引顺序相一致。

  12. 不要写一些没有意义的查询,如需要生成一个空表结构:select col1,col2 into t from t where 1=0 ,这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:create table t(…)

  13. 很多时候用 exists 代替 in 是一个好的选择:select num from a where num in(select num from b)
    用下面的语句替换:select num from a where exists(select 1 from b where num=a.num)

  14. 并不是所有索引对查询都有效,SQL 是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL 查询可能不会去利用索引,如一表中有字段 sex、male、female几乎各一半,那么即使在 sex 上建了索引也对查询效率起不了作用。

  15. 索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insertupdate 的效率,因为 insertupdate 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过 6 个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。

  16. 应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。

  17. 尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会 逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。

  18. 尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

  19. 任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。 

  20. 尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。

  21. 避免频繁创建和删除临时表,以减少系统表资源的消耗。

  22. 临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使用导出表。

  23. 在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert

  24. 如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。

  25. 尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过 1 万行,那么就应该考虑改写。

  26. 使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。

  27. 与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。

  28. 在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。无需在执行存储过程和触发器的每个语句后向客户端发送 DONE_IN_PROC 消息。

  29. 尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。

  30. 尽量避免大事务操作,提高系统并发能力。

2、请讲一下MySQL数据库innodb引擎索引底层实现的原理及查找过程

   innodb索引中主要包含主键索引和辅助键索引,它们都是通过B+Tree实现的。在使用主键进行数据查询时,通过主键索引找到对应的叶子节点,然后得到叶子节点上的数据;而通过辅助键索引查找数据时,首先找到叶子节点,得到对应的主键,然后再通过主键值作为查询条件到主键索引上找到对应的叶子节点得到数据。

 

3、请讲一下MySQL数据库innodb引擎与myisam引擎索引的区别

  InnoDB使用的是聚簇索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上,若使用"where id = 14"这样的条件查找主键,则按照B+树的检索算法即可查找到对应的叶节点,之后获得行数据。若对Name列进行条件搜索,则需要两个步骤:第一步在辅助索引B+树中检索Name,到达其叶子节点获取对应的主键。第二步使用主键在主索引B+树种再执行一次B+树检索操作,最终到达叶子节点即可获取整行数据。

   MyISM使用的是非聚簇索引,非聚簇索引的两棵B+树看上去没什么不同,节点的结构完全一致只是存储的内容不同而已,主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键。表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别。由于索引树是独立的,通过辅助键检索无需访问主键的索引树。

 

4、请大概讲一下MySQL索引在哪些情况下会失效(至少5条)

  1. 索引无法存储null值
    单列索引无法储null值,复合索引无法储全为null的值。
    查询时,采用is null条件时,不能利用到索引,只能全表扫描。
    创建索引的列,该列不允许有null的出现,因为null无法进行大小的比较,也就无法进行排序。

  2. 不适合键值较少的列(重复数据较多的列)
    假如索引列TYPE有5个键值,如果有1万条数据,那么 WHERE TYPE = 1将访问表中的2000个数据块。再加上访问索引块,一共要访问大于200个的数据块。如果全表扫描,假设10条数据一个数据块,那么只需访问1000个数据块,既然全表扫描访问的数据块少一些,肯定就不会利用索引了。

  3. 如果条件中有or,即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因)要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引
  4. 对于多列索引,不是使用的第一部分,则不会使用索引

  5. like查询以%开头
  6. 如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引

  7. 如果mysql估计使用全表扫描要比使用索引快,则不使用索引

  8. 使用 is null 或 is not null
    使用 is null 或is not null也会限制索引的使用,因为数据库并没有定义null值。如果被索引的列中有很多null,就不会使用这个索引(除非索引是一个位图索引,关于位图索引)。在sql语句中使用null会造成很多麻烦。
    解决这个问题的办法就是:建表时把需要索引的列定义为非空(not null)

 

5、请你讲一下ArrayList和LinkedList的区别

 

  1.  ArrayList和LinkList的内部实现的数据结构不同
    ①ArrayList内部是由数组是实现的,而LinkList内部是由循环双向链表实现的。
    ②由于ArrayList是由数组实现的,所以ArrayList在进行查找操作时,速度要优于由链表实现的LinkList;
    ③但是在进行删除添加操作时,LinkList速度要优于ArrayList;
    ④所以当进行查找操作更多时,使用ArrayList,而如果进行插入和删除操作更多时,使用LinkList。
  2. LinkList实现了堆栈和队列的功能,通过push和pop方法可以使用堆栈功能,而使用offer和poll方法可以使用队列功能,ArrayList并没有实现堆栈、队列的功能

 

6、请讲一下你知道的设计模式

  1.  工厂模式
    现在要创建一些对象,他们都实现了某个接口或者继承了某个类。我们不再需要的时候使用new操作符,而是把创建的操作让一个“工厂类”完成,我们在需要新对象时只需要把需要的东西的名字以参数形式传递给工厂类就行了,而不用去管怎么创建的。
  2. 抽象工厂模式
    在工厂模式中,一个工厂类只生产实现某个接口或者继承了某个类的对象,也就是具体工厂生产具体对象,如果建立一个抽象工厂类和若干个具体工厂,每个具体工厂负责产生一类对象,就成了抽象工厂模式。就是一个抽象工厂类可以生产多种类型的对象,具体每种类型的对象怎么生成,要用一个专门的工厂类来决定。

  3. 单例模式
    一个类,它虽然有构造方法,但是把它设定为private,不能被外界使用。这个类只存在一个实例,保存在这个类自己的一个静态字段里,如果要用这个类的实例属性、实例方法,都通过这个静态字段访问这个唯一的实例来实现。
  4. 建造者模式
    现在要创建一个很复杂的对象,我们把这个工作分开来做,先定义并实现一个建造者类,在这个类中实现构建这个对象所需要的全部方法。再定义并实现一个导演类,把一个建造者类传给它,让它负责这些方法调用的逻辑次序和对象的组合,然后统一给客户端返回一个生成好的复杂对象。

  5. 适配器模式
    一个类和另一个类有一些相似的操作,但是它们的形式是不一致的,需要有一个东西把它包裹起来,变成另一种比较合适的形式。
  6. 装饰器模式
    一个类有一些方法,但是我们想让这些方法调用的时候多执行一些东西,于是可以定义一个装饰器的类,它和被修饰的类同时一个类的子类或是一个接口的实现,然后将这个被修饰的类委托给这个装饰器类,如果想使用这个修饰后的方法,只需要调用这个修饰器的方法就可以了。

  7. 代理模式
    我想访问一个对象,但是这个对象出于多种考虑,比如细节复杂、需要控制访问、隐藏细节等,不能让别人直接使用,必须要使用一个中间层性质的代理类,对这个对象的所有访问都由这个代理类来完成。

 

7、单例模式中饱汉模式与饿汉模式的区别,为什么饱汉模式不是线程安全的

   区别是饿汉模式首先在创建私有静态属性时就已经通过调用构造方法的方式将对象创建好了,在通过静态方法获取对象时直接返回该对象。而饱汉模式最开始是没有创建对象,只有等到第一次调用静态方法获取对象时才会去创建。因为在多线程情况下存在多个线程同时判断到静态私有属性的值为null,那么就会造成创建多个对象的情况,解决方案是通过加锁的方式,避免多个线程同时进入到判断语句

 

8、JDK代理与CGLIB代理的区别

  1. JDK动态代理
    利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
  2. CGLiB动态代理
    利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。实现CGLIB动态代理必须实现MethodInterceptor(方法拦截器)接口

  3. JDK只能针对接口不能针对类实现代理,要求被代理的类必须实现至少一个接口。通过JDK代理生成的代理类是接口的子类,与被代理之间是兄弟关系。(UserService是接口,UserServiceImpl是被代理类(目标对象),它实现了UserService接口,代理类与被代理类之间是兄弟关系)
  4. CGLIB通过继承方式实现代理。所以类或方法最好不要声明成final,对于final类或方法,是无法继承的。生成的代理类是被代理类的子类,所以CGLIB代理也称作子类代理。(UserService是一个类,代理类)

 

9、请讲一下你对IOC/DI的理解

  1.  IOC   invocation of control 控制反转
    IOC不是什么技术,而是一种设计思想。在Java开发中,IOC意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。如何理解好IOC呢?理解好IOC的关键是要明确“谁控制谁,控制什么,为何是反转(有反转就应该有正转了),哪些方面反转了”,那我们来深入分析一下:
    谁控制谁,控制什么:传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IOC是有专门一个容器来创建这些对象,即由IOC容器来控制对象的创建而不再显式地使用new;
    谁控制谁:当然是IOC容器控制了对象;
    控制什么:那就是主要控制了外部资源获取和生命周期(不只是对象也包括文件等)。
    为何是反转,哪些方面反转了:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;
    为何是反转:因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;
    哪些方面反转了:依赖对象的获取被反转了
  2. DI  Dependency Injection 依赖注入
    依赖注入是组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
    理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:
    谁依赖于谁:当然是应用程序依赖于IOC容器;
    为什么需要依赖:应用程序需要IOC容器来提供对象需要的外部资源;
    谁注入谁:很明显是IOC容器注入应用程序某个对象,应用程序依赖的对象;
    注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

  3. IOC和DI有什么关系呢?
    其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IOC 而言,“依赖注入”明确描述了“被注入对象依赖IOC容器配置依赖对象”。

 

10、请讲一下spring bean的生命周期

   spring bean生命周期主要包含四个阶段:

  • 实例化 Instantiation
  • 属性赋值 Populate
  • 初始化 Initialization
  • 销毁 Destruction

  实例化 -> 属性赋值 -> 初始化 -> 销毁

 

  具体执行流程如下:

  • Bean容器找到配置文件中Spring Bean的定义。
  • Bean容器利用Java Reflection API创建一个Bean的实例。
  • 如果涉及到一些属性值 利用set方法设置一些属性值。
  • 如果Bean实现了BeanNameAware接口,调用setBeanName()方法,传入Bean的名字。
  • 如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。
  • 如果Bean实现了BeanFactoryAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。
  • 与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。
  • 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执行postProcessBeforeInitialization()方法
  • 如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法。
  • 如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。
  • 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执行postProcessAfterInitialization()方法
  • 当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy()方法。
  • 当要销毁Bean的时候,如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。
posted @ 2022-08-13 15:00  suzbuing  阅读(30)  评论(0)    收藏  举报