本篇文章分享一些自己遇到的或者网上看到的一些面试题。
SQL基础部分的面试题可以参阅这篇文章:http://smilecoc.vip/2020/04/04/SQL%E9%9D%A2%E8%AF%9544%E9%A2%98/

本篇文章代码无特殊说明的话均基于SQL Server.

第一题:连续三天登陆

有如下表格:

+──────────+─────────────+
| user_id  | login_date  |
+──────────+─────────────+
| A        | 2019-09-02  |
| A        | 2019-09-03  |
| A        | 2019-09-04  |
| B        | 2018-11-25  |
| B        | 2018-12-31  |
| C        | 2019-01-01  |
| C        | 2019-04-04  |
| C        | 2019-09-03  |
| C        | 2019-09-04  |
| C        | 2019-09-05  |
+──────────+─────────────+

现在需要利用SQL找出这张表中所有的连续3天登录用户.

答案:

select user_id
from (
  select user_id,login_date,
    row_number() over(partition by user_id order by login_date) as
rn
  from last_3_day_test_table
) t
group by user_id,DATEADD(D,-t.rn,login_date)
having count(1)>=3;

本题需要使用窗口函数,具体可参阅:
http://smilecoc.vip/2019/12/25/SQL%E5%AE%9E%E7%8E%B0%E7%AD%9B%E9%80%89%E5%87%BA%E8%BF%9E%E7%BB%AD3%E5%A4%A9%E7%99%BB%E5%BD%95%E7%94%A8%E6%88%B7/

第二题:活动运营数据分析

表1——订单表orders,要用到的字段有(user_id‘用户编号’, order_pay‘订单金额’ , order_time‘下单时间’)。
表2——活动报名表act_apply,要用到的字段有(act_id‘活动编号’, user_id‘报名用户’,act_time‘报名时间’)
注意这里的表1和表2都还有其他的字段

需求:

  1. 统计每个活动对应所有用户在报名后产生的总订单金额,总订单数。(每个用户限报一个活动,题干默认用户报名后产生的订单均为参加活动的订单)。

  2. 统计每个活动从开始后到今天平均每天产生的订单数,活动开始时间定义为最早有用户报名的时间。(涉及到时间的数据类型均为:datetime)。

答案:
1.

select t2.act_id,sum(order_pay) as total_cost,count(order_time) as order_num
from 
(select user_id,order_pay,order_time from orders) as t1
inner join
(select act_id,user_id,act_time) as t2
on t1.user_id=t2.user_id
where t1.order_time>=t2.act_time
group by t2.act_id

2.

select t1.act_id,count(order_time)/dateiff(now(),t1.begin_time) as avg_ordercount
from
(select act_id,user_id,act_time,min(act_time) over (partition by act_id) as begin_time from act_apply) as t1
inner join
(select user_id,order_time from orders) as t2
on t1.user_id=t2.user_id
where t1.act_time between t1.begin_time and now()
and t2.order_time >= t1.act_time
group by t1.act_id

第三题:用户行为分析

表1——用户行为表tracking_log,大概字段有(user_id‘用户编号’,opr_id‘操作编号’,log_time‘操作时间’)

需求:

1、计算每天的访客数和他们的平均操作次数。

2、统计每天符合以下条件的用户数:A操作之后是B操作,AB操作必须相邻。

答案:
1.

select date(log_time),count(distinct user_id) as user_num,avg(num_ci) as avg_operqationcount
from
(select date(log_time),user_id,count(opr_id) as num_ci from tracking_log group by date(log_time),user_id)
group by date(log_time)

2.

select date(log_time),count(distinct user_id) as user_num
from
(select user_id,date(log_time),opr_id,lead(opr_id,1) over(partition by user_id order by lod_time) as opr_id_2 from tracking_log)
where opr_id='A' and opr_id_2='B'
group by date(log_time)

解析:
使用lead()函数实现。LEAD()是一个窗口函数,它提供对当前行之后的指定物理偏移量的行的访问。
例如,通过使用LEAD()函数,可以从当前行访问下一行的数据或下一行之后的行,依此类推。
LEAD()函数对于将当前行的值与后续行的值进行比较非常有用。
以下是LEAD()函数的语法:

LEAD(return_value ,offset [,default]) 
OVER ([PARTITION BY partition_expression, ... ])

在上面语法中,

  • return_value - 基于指定偏移量的后续行的返回值。返回值必须求值为单个值,不能是另一个Window函数。offset是从当前行转发的行数,用于访问数据。
  • offset可以是表达式,子查询或列,其值为正整数。如果未明确指定,则offset的默认值为1。如果offset超出分区范围,则该函数返回default。 如果未指定,则默认为NULL。
  • PARTITION BY子句将结果集的行分配到应用了LEAD()函数的分区。如果未指定PARTITION BY子句,则该函数将整个结果集视为单个分区。

关于lead函数可参阅:https://docs.microsoft.com/zh-cn/sql/t-sql/functions/lead-transact-sql?view=sql-server-ver15

第三题:用户新增留存分析

表1——用户登陆表user_log,大概字段有(user_id‘用户编号’,log_time‘登陆时间’)

要求:

每天新增用户数,以及他们第2天、30天的回访比例

如何定义新增用户:用户登陆表中最早的登陆时间所在的用户数为当天新增用户数;

第2天回访用户数:第一天登陆的用户中,第二天依旧登陆的用户;—次日留存率

第30天的回访用户数:第一天登陆用户中,第30天依旧登陆的用户;

select date(t1.user_begin),count(distinct t1.user_id) as new_user,count(distinct t2.user_id) as twodays_retained_users,count(distinct t3.user_id) as thrityday_retained_users
from
(select user_id,min(log_time) as user_begin from user_log group by user_id) t1
left join
(select user_id,log_time from user_log) t2
on t1.user_id=t2.user_id
and date(t2.log_time)=date(t1.user_begin)+1
left join
(select user_id,log_time from user_log) t3
on t1.user_id=t3.user_id
and date(t3.log_time)=date(t1.user_begin)+29
group by date(t1.user_begin)

第四题:从不订购的客户

某网站包含两个表,Customers 表和 Orders 表。编写一个 SQL 查询,找出所有从不订购任何东西的客户。
Customers表:

| ID | Name  |
|----|-------|
| 1  | Joe   |
| 2  | Henry |
| 3  | Sam   |
| 4  | Max   |

Orders表:

| ID | CustomerID |
|----|------------|
| 1  | 3          |
| 2  | 1          |

例如给定上述表格,你的查询应返回:

|Customers|
|---------|
|  Henry  |
|   Max   |

解答:

select c.name 
from customers c
left join (select distinct customerid from orders) o 
on c.id=o.customerid
where o.customerid is null

第五题:删除重复的电子邮箱

编写一个 SQL 查询,来删除 Person 表中所有重复的电子邮箱,重复的邮箱里只保留 Id 最小的那个。
注意:Person 表中数据量很大,ID为主键

| ID | Email             |
|----|-------------------|
| 1  | john@example\.com |
| 2  | bob@example\.com  |
| 3  | john@example\.com |

例如,在运行你的查询语句之后,上面的 Person 表应返回以下几行:

| ID | Email             |
|----|-------------------|
| 1  | john@example\.com |
| 2  | bob@example\.com  |
delete from email 
where id not in(
select a.id from (select min(id) id from email group by email) a
)

第六题:按周汇总数据

现有原始数据表如下所示,包含一个时间段内每天的预算金额与所属分类信息:

表名:databasetable

+───────────+──────────+───────────+
| Date      | buget    | category  |
+───────────+──────────+───────────+
| 2020-8-4  | 2574.2   | A         |
| 2020-8-5  | 260.38   | A         |
| 2020-8-6  | 9045.16  | A         |
| 2020-8-12 | 17.431   | A         |
| 2020-8-8  | 658.308  | B         |
| 2020-8-12 | 496.263  | A         |
| 2020-9-15 | 85.05    | B         |
......

现在需要编写一个存储过程,在给定category与日期的情况下,得到在给定category与给定日期之前的数据分周的汇总以及全部汇总.。分周依据为周一为一周开始,周日为一周结束。

例如给定结束日期为2020/9/13.给定category为A,则输出结果的参考格式为:

|    period     |       buget         |
|----------------|---------------------|
| 0803-0809 | 2142574.2332 |
| 0810-0816 | 2608220.3855 |
| 0817-0823 | 1549045.1096 |
| 0824-0830 | 1004057.4311 |
| 0831-0906 | 654538.3078  |
| 0907-0913 | 495186.2633  |
| Total     | 8453621.7305 |

答案:

create proc summaruy_by_week_total
( @category VARCHAR(250),
 @enddate date)
as
With all_data
as
(
select *,
format(DATEADD(wk, DATEDIFF(wk,0,DATEADD(dd,-1,[date])), 0),'MMdd')+'-'+format(DATEADD(dd,-1,DATEADD(wk, DATEDIFF(wk,0,DATEADD(dd,6,[date])), 0)),'MMdd') as period
from databasetable
where category=@category 
and [date]<= @enddate
)
select
[period],
sum(buget) as buget,
union all
Select
'Total' as [period],
sum(buget) as buget,
from all_data
order by [period]
--执行存储过程
exec summaruy_by_week_total 'A','2020/9/13'

解析
1.存储过程
关于存储过程的基础知识可以看http://smilecoc.vip/2020/11/12/SQL%E5%AD%98%E5%82%A8%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3%E4%B8%8E%E5%AE%9E%E6%88%98/

首先介绍一下存储过程。简单来说数据库中的存储过程就是一系列代码的集合,类似于其他编程中的函数。在创建完成后我们就可以直接通过调用存储过程来实现一系列操作从而提升代码可读性与执行效率。

题目中要求我们使用存储过程并且需要在存储过程中筛选category和日期。所以直接创建带参数的存储过程。创建语句为

create proc summaruy_by_week_total
( @category VARCHAR(250),
 @enddate date)
as

2.日期函数
在分周汇总中,我们需要进行日期的计算与日期的格式化。
如果你是小白请先查看对应日期函数的基础语法与详细解析:http://smilecoc.vip/2020/09/20/SQL%20Server%E6%97%A5%E6%9C%9F%E5%87%BD%E6%95%B0%E8%AF%A6%E8%A7%A3%E4%B8%8E%E5%AE%9E%E6%88%98/

日期的计算与日期的格式化的代码为:

format(DATEADD(wk, DATEDIFF(wk,0,DATEADD(dd,-1,[date])), 0),'MMdd')+'-'+format(DATEADD(dd,-1,DATEADD(wk, DATEDIFF(wk,0,DATEADD(dd,6,[date])), 0)),'MMdd') as period

首先利用datediif(wk,0,date)计算从1900-01-01 到指定日期的周数(0代表日期对应的数字形式,即1900-01-01)。之后利用DateAdd函数在1900-01-01 的基础上加上刚刚我们计算出的周数,从而得到每周的开始时间。最后通过forma函数对日期格式化即可得出结果

3.CTE
关于CTE的详细介绍与基础可以查看文章
http://smilecoc.vip/2020/09/20/SQL%20%20Server%20%E5%85%AC%E7%94%A8%E8%A1%A8%E8%A1%A8%E8%BE%BE%E5%BC%8F(CTE)//)

在上述日期格式化过程中,我们产生了一个中间表。由于后面会调用两次,所以我们使用CTE使代码更加简洁易读并最终得出代码。对应答案中的:

With all_data
as
(
select *,
format(DATEADD(wk, DATEDIFF(wk,0,DATEADD(dd,-1,[date])), 0),'MMdd')+'-'+format(DATEADD(dd,-1,DATEADD(wk, DATEDIFF(wk,0,DATEADD(dd,6,[date])), 0)),'MMdd') as period
from databasetable
where category=@category 
and [date]<= @enddate
)

4.数据汇总
分别求出数据分周汇总与全部数据汇总并使用union all连接.同时使用到上述CTE中定义的表:all_data。对应答案的代码为:

select
[period],
sum(buget) as buget,
union all
Select
'Total' as [period],
sum(buget) as buget,
from all_data
order by [period]

第七题:查找地理层级

有如下原始表#hierarchy,表结构为:

| ID | ParentID | name |
|----|----------|------|
| 1  | 0        | 河南省  |
| 19 | 0        | 北京市  |
| 20 | 0        | 江苏省  |
| 21 | 1        | 南京市  |
| 2  | 1        | 信阳市  |
| 5  | 1        | 安阳市  |
| 15 | 1        | 南阳市  |
| 17 | 1        | 驻马店市|
| 10 | 2        |  息县   |
| 8  | 2        | 固始县  |
| 3  | 2        | 淮滨县  |
| 22 | 2        | 正阳县  |
| 4  | 3        | 芦集乡  |
| 12 | 3        | 邓湾乡  |
| 13 | 3        | 台头乡  |
| 14 | 3        | 谷堆乡  |
| 6  | 5        | 滑县   |
| 7  | 6        | 老庙乡  |
| 9  | 8        | 李店乡  |
| 11 | 10       | 关店乡  |
| 16 | 15       | 方城县  |
| 18 | 17       | 正阳县  |

现要编写SQL语句查找查找芦集乡所属省,市,县并输出。期望输出结果为:

| ID | ParentID | name |
|----|----------|------|
| 1  | 0        | 河南省  |
| 2  | 1        | 信阳市  |
| 3  | 2        | 淮滨县  |
| 4  | 3        | 芦集乡  |

答案:
芦集乡的ID是4,根据芦集乡的ID进行递归查找
关于CTE的详细介绍与基础可以查看文章
http://smilecoc.vip/2020/09/20/SQL%20%20Server%20%E5%85%AC%E7%94%A8%E8%A1%A8%E8%A1%A8%E8%BE%BE%E5%BC%8F(CTE)//)

with cte as
(
select ID,ParentID,name
from #hierarchy
where id=4 --芦集乡的ID

union all
select h.ID,h.ParentID,h.name
from #hierarchy h
inner join cte c on h.id=c.ParentID
)
select ID,ParentID,name
from cte
order by ParentID

第八题:查询众数与中位数

现有如下原始表#grade:

| id | name   | score |
|----|--------|-------|
| 1  | Kevin  | 85    |
| 2  | Mary   | 59    |
| 3  | Andy   | 60    |
| 4  | Tony   | 79    |
| 5  | Lucky  | 90    |
| 6  | Bob    | 60    |
| 7  | Tom    | 100   |
| 8  | Lucy   | 90    |
| 9  | Cherry | 92    |
| 10 | Ada    | 99    |
| 11 | Alice  | 83    |
| 12 | Amy    | 82    |

求成绩的众数与中位数

求众数:

select  top(1) WITH TIES
score,
count(score) as sc_count 
from #grade 
group by score 
order by sc_count desc

求中位数:

select avg(score)
from
(
  select score,
  count(*) over() total,
  row_number() over(order by score) rn
  from #grade
) g
where g.rn in (
floor(cast((total+1) as decimal)/2),ceiling(cast((total+1) as decimal)/2)

详细解释参阅:
http://smilecoc.vip/2020/09/20/SQL%E6%9F%A5%E8%AF%A2%E4%BC%97%E6%95%B0%E4%B8%8E%E4%B8%AD%E4%BD%8D%E6%95%B0/

第九题:连接条件查询

有如下两张表,执行语句:select count(s_id) from students left join information on s_id=id where province in ('山东','湖南'),后的查询结果有几行结果?
students表:

|  s_id  |  name  |
|--------| ------ |
| 123585 | 张小飞 |
| 123586 | 程小英 |
| 123587 | 吴天   |
| 123588 | 张仙   |
| 123589 | 刘成   |
| 123590 | 何梅   |
| 123591 | 王程杰 |
| 123592 | 李成浩 |
| 123593 | 李静   |
| 123594 | 刘敏敏 |

information表:

| id     | name | 省份 | 年龄 | 性别 |
|--------|------|----|----|----|
| 123585 | 张小飞  | 湖南 | 22 | 男  |
| 123586 | 程小英  | 河北 | 20 | 女  |
| 123587 | 吴天   | 河南 | 22 | 男  |
| 123589 | 刘成   | 河南 | 21 | 男  |
| 123590 | 何梅   | 河南 | 22 | 女  |
| 123591 | 王程杰  | 苏州 | 20 | 男  |
| 123593 | 李静   | 湖南 | 23 | 女  |
| 123594 | 刘敏敏  | 湖南 | 20 | 女  |
| 123595 | 王志峰  | 山东 | 21 | 男  |
| 123596 | 赵罗生  | 江西 | 21 | 男  |
| 123597 | 李静   | 山东 | 20 | 女  |
| 123598 | 程丽   | 湖南 | 21 | 女  |

答案:
执行的结果为3.

具体解析可以查看:http://smilecoc.vip/2020/08/17/left%20join%20on%20and%20%E4%B8%8E%20left%20join%20on%20where%E7%9A%84%E5%8C%BA%E5%88%AB/

第十题:数据库增删改

存在如下订单表order:

| 订单号    | name | 货币类型 | 金额 | 性别 |
|--------|------|------|----|----|
| 123585 | 张小飞  | USD  | 22 | 男  |
| 123586 | 程小英  | RMB  |    | 女  |
| 123587 | 吴天   | RMB  | 140 | 男  |
| 123588 | 刘成   | RMB  | 106 | 男  |
| 123589 | 何梅   | USD  |    | 女  |
| 123590 | 王程杰  | USD  | 20 | 男  |

其中1USD=7RMB
请根据需求写出对应SQL语句:

  1. 将年龄中的空全部修改为0
  2. 增加一条记录,id为123598,name为张三,货币类型为RMB,金额150,性别男
  3. 计算当前总订单额
  4. 删除name为程小英的记录

答案:第一题

UPDATE order SET 年龄= 0 WHERE 年龄 is null

第二题

INSERT INTO order VALUES (值1, 值2,....)

第三题:

select sum((case when 货币类型='USD' then 金额 * 7 else 金额)) as 总订单额
 from order

第四题:

DELETE FROM order WHERE name = '程小英'

第十一题:数据库优化

在一些面试题中会提到数据库优化的部分 ,具体可参阅:http://smilecoc.vip/2020/11/11/SQL%E8%AF%AD%E5%8F%A5%E4%BC%98%E5%8C%96/

其中比较常见的优化考点有:
1、 在表中建立索引,优先考虑where、group by使用到的字段。

2、 尽量避免使用select *,返回无用的字段会降低查询效率。如下:

SELECT * FROM t

优化方式:使用具体的字段代替*,只返回使用到的字段。

3、尽量避免使用in 和not in,会导致数据库引擎放弃索引进行全表扫描。如下:

SELECT * FROM t WHERE id IN (2,3)

SELECT * FROM t1 WHERE username IN (SELECT username FROM t2)

优化方式:如果是连续数值,可以用between代替。如下:

SELECT * FROM t WHERE id BETWEEN 2 AND 3

如果是子查询,可以用exists代替。如下:

SELECT * FROM t1 WHERE EXISTS (SELECT * FROM t2 WHERE t1.username = t2.username)

4、尽量避免使用or,会导致数据库引擎放弃索引进行全表扫描。如下:

SELECT * FROM t WHERE id = 1 OR id = 3

优化方式:可以用union代替or。如下:

SELECT * FROM t WHERE id = 1
UNION
SELECT * FROM t WHERE id = 3

(PS:如果or两边的字段是同一个,如例子中这样。貌似两种方式效率差不多,即使union扫描的是索引,or扫描的是全表)

5、尽量避免在字段开头模糊查询,会导致数据库引擎放弃索引进行全表扫描。如下:

SELECT * FROM t WHERE username LIKE '%li%'

优化方式:尽量在字段后面使用模糊查询。如下:

SELECT * FROM t WHERE username LIKE 'li%'

6、尽量避免进行null值的判断,会导致数据库引擎放弃索引进行全表扫描。如下:

SELECT * FROM t WHERE score IS NULL

优化方式:可以给字段添加默认值0,对0值进行判断。如下:

SELECT * FROM t WHERE score = 0

7、尽量避免在where条件中等号的左侧进行表达式、函数操作,会导致数据库引擎放弃索引进行全表扫描。如下:

SELECT * FROM t2 WHERE score/10 = 9

SELECT * FROM t2 WHERE SUBSTR(username,1,2) = 'li'

优化方式:可以将表达式、函数操作移动到等号右侧。如下:

SELECT * FROM t2 WHERE score = 10*9

SELECT * FROM t2 WHERE username LIKE 'li%'

第十二题:SQL数据类型

在SQL中执行如下两段语句的结果:

select 256*1.000/100
select 256/100*1.000

答案:
结果分别为:

2.5600000
2.000

解析:
在第一段代码中,256为整数类型,在乘以数值型的1.000后变为了数值型的256.000,从而除以100后得到2.56.
而第二段代码中整数类型的256除以整数类型的100后仍为整数类型,因此只保留整数部分2,再乘以数值型的1.000后变为2.000

**持续更新中...**