MySQL初步笔记——

[TOC]

本文将详细介绍 MySQL 及其常用的 SQL 基础操作,从数据库、数据表的创建与管理,到增删改查的基本语句,以及常见的约束与函数使用。适合对数据库基础尚不熟悉的初学者阅读。

什么是 MySQL

MySQL 是一种关系型数据库管理系统(RDBMS),广泛应用于各类应用程序和网站。其特点包括:

  • 开源、免费:社区版本可免费使用,且有丰富的插件与扩展支持。
  • 跨平台:支持多种操作系统(如 Linux、Windows、macOS)。
  • 性能优异:读写速度快,支持多线程。
  • 广泛的社区支持:学习资源丰富,遇到问题易于寻找解决方案。

基础概念

在开始实际操作之前,需要理解一些基础概念:

  • 数据库(Database):数据的集合。可以将其类比为一个文件夹,里面装着各种表格(表)。
  • 数据表(Table):数据存放的结构化表格,每张表由行(记录)和列(字段)组成。
  • 记录(Row):表中的一行数据。
  • 字段(Column):表中的一列,用来描述数据的属性。
  • 主键(Primary Key):唯一标识表中每条记录的列或列的组合。
  • 外键(Foreign Key):在一张表中引用另一张表主键的字段,用于建立表之间的关系。

连接与选择数据库

连接到 MySQL 服务器

安装并启动 MySQL 服务后,可使用命令行工具连接:

1
2
# 连接到本地 MySQL
mysql -u 用户名 -p

根据提示输入密码后即可进入 MySQL 命令行环境。

查看数据库列表

1
SHOW DATABASES;

选择数据库

1
USE 数据库名;

比如要选择名为 test_db 的数据库:

1
USE test_db;

查看当前使用的数据库

1
SELECT DATABASE();

数据库的创建与删除

创建数据库

1
CREATE DATABASE 数据库名;

如:

1
CREATE DATABASE test_db;

可选地,指定字符集与校对规则:

1
2
3
CREATE DATABASE test_db 
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

删除数据库

警告:此操作不可逆!

1
DROP DATABASE 数据库名;

如:

1
DROP DATABASE test_db;

数据表的创建与管理

创建数据表

基本语法:

1
2
3
4
5
6
CREATE TABLE 表名 (
列名1 数据类型 [约束],
列名2 数据类型 [约束],
...
列名N 数据类型 [约束]
);

示例:创建一个用户表 users,包含用户 ID(主键,自增),用户名,电子邮箱,创建时间列:

1
2
3
4
5
6
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

此表中:

  • id 列为整数类型,自增并作为主键。
  • usernameemail 为字符串列,其中 email 设置为唯一值。
  • created_at 为日期时间列,默认值为当前时间戳。

查看数据表结构

1
DESC 表名;

1
SHOW COLUMNS FROM 表名;

如:

1
DESC users;

修改数据表结构(DDL 操作)

添加列

1
ALTER TABLE 表名 ADD COLUMN 列名 数据类型 [约束];

如在 users 表中添加 age 列:

1
ALTER TABLE users ADD COLUMN age INT DEFAULT 0;

修改列类型或名称

1
ALTER TABLE 表名 MODIFY COLUMN 列名 新数据类型 [约束];

如将 age 改为 TINYINT 类型:

1
ALTER TABLE users MODIFY COLUMN age TINYINT;

若要重命名列可使用 CHANGE

1
ALTER TABLE 表名 CHANGE 旧列名 新列名 新数据类型 [约束];

如将 username 改为 name

1
ALTER TABLE users CHANGE username name VARCHAR(50) NOT NULL;

删除列

1
ALTER TABLE 表名 DROP COLUMN 列名;

如删除 age 列:

1
ALTER TABLE users DROP COLUMN age;

删除表

1
DROP TABLE 表名;

如删除 users 表:

1
DROP TABLE users;

清空表数据(但保留结构)

1
TRUNCATE TABLE 表名;

如清空 users 表数据:

1
TRUNCATE TABLE users;

数据操作 (DML 操作)

插入数据

1
INSERT INTO 表名 (列1, 列2, ...) VALUES (值1, 值2, ...);

如向 users 表插入一条记录:

1
2
INSERT INTO users (name, email)
VALUES ('Alice', 'alice@example.com');

注意:id 列为自增且 created_at 有默认值,因此无需指定。

一次插入多条记录:

1
2
3
INSERT INTO users (name, email)
VALUES ('Bob', 'bob@example.com'),
('Charlie', 'charlie@example.com');

查询数据 (SELECT)

基本语法:

1
SELECT1, 列2, ... FROM 表名 [WHERE 条件] [ORDER BY ...] [LIMIT ...];

查询所有列

1
SELECT * FROM users;

查询指定列

1
SELECT name, email FROM users;

使用 WHERE 条件过滤

1
SELECT * FROM users WHERE email = 'alice@example.com';

使用比较、逻辑运算符:

  • =<><=>=<>!=
  • ANDORNOT
  • LIKE(模糊匹配),IN(在集合中),BETWEEN ... AND ...(区间)

如查询邮箱以 @example.com 结尾的用户:

1
SELECT * FROM users WHERE email LIKE '%@example.com';

排序 (ORDER BY)

1
SELECT * FROM users ORDER BY created_at DESC;

限制返回行数 (LIMIT)

1
SELECT * FROM users ORDER BY created_at DESC LIMIT 5;

返回最新的 5 条记录。

使用 DISTINCT 去除重复值

DISTINCT 是 SQL 中的一个关键字,用于从查询结果中去除重复的记录或字段值。它能帮助我们获取唯一的结果集,避免重复数据的干扰。

1
SELECT DISTINCT1, 列2, ... FROM 表名;

分页查询

1
SELECT * FROM users ORDER BY created_at DESC LIMIT 10 OFFSET 20;

从第 21 条(偏移 20 条)开始,取 10 条记录。

更新数据 (UPDATE)

1
UPDATE 表名 SET1 =1, 列2 =2, ... [WHERE 条件];

如更新 Alice 的邮箱:

1
UPDATE users SET email = 'alice_new@example.com' WHERE name = 'Alice';

警告:如果不加 WHERE 条件,会更新表中所有行!

删除数据 (DELETE)

1
DELETE FROM 表名 [WHERE 条件];

如删除 Bob 的记录:

1
DELETE FROM users WHERE name = 'Bob';

警告:如果不加 WHERE 条件,会删除整张表的所有记录!

常用约束 (Constraints)

NOT NULL

列值不可为空:

1
2
3
4
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(100) NOT NULL
);

UNIQUE

列值必须唯一:

1
2
3
4
CREATE TABLE products (
id INT PRIMARY KEY,
code VARCHAR(50) UNIQUE
);

PRIMARY KEY

主键列:

1
2
3
4
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL
);

FOREIGN KEY

外键引用其他表的主键:

1
2
3
4
5
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
FOREIGN KEY (user_id) REFERENCES users(id)
);

AUTO_INCREMENT

AUTO_INCREMENT 是 MySQL 提供的自动递增功能,通常用于主键列,以确保每条记录都有唯一的标识符。AUTO_INCREMENT 的值会从 1 开始,并在插入新记录时自动递增。

以下是创建一个带 AUTO_INCREMENT 主键的表的示例:

1
2
3
4
5
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL
);
  • id 列会自动从 1 开始递增。
  • 如果插入记录时未指定 id 的值,系统会自动生成一个递增的值。

常用函数与操作

聚合函数

  • COUNT:计数
  • SUM:求和
  • AVG:求平均值
  • MAX:求最大值
  • MIN:求最小值

示例:统计 users 表中有多少行数据:

1
SELECT COUNT(*) FROM users;

查询订单中价格总和:

1
SELECT SUM(total_price) FROM orders;

分组与分组过滤 (GROUP BY, HAVING)

1
2
3
4
SELECT user_id, COUNT(*) as order_count 
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 5;

上例统计每个用户的订单数,并筛选出订单数大于 5 的用户。

字符串函数示例

  • CONCAT(str1, str2, ...):拼接字符串
  • LENGTH(str):返回字符串长度
  • UPPER(str) / LOWER(str):大小写转换
1
SELECT CONCAT(name, ' <', email, '>') AS contact_info FROM users;

日期与时间函数示例

  • NOW():当前时间
  • CURDATE():当前日期
  • DATE_FORMAT(date, format):格式化日期
1
2
SELECT name, DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s') AS formatted_created
FROM users;

下面是一篇详细的博客文章草稿,介绍 MySQL 中与时间相关的函数及其用法。


深入了解 MySQL 时间与日期函数

在日常数据处理中,日期与时间是非常重要的数据类型。从简单的数据统计到复杂的商业分析,你常常需要对日期和时间进行操作,如格式化输出、日期计算、提取特定部分(年、月、日、时、分、秒)等。MySQL 为开发者提供了一系列内置的日期和时间函数,大大简化了相关操作。

本文将详细介绍 MySQL 中的日期、时间类型以及常用的日期/时间函数的用法和技巧。

一、MySQL 中的日期与时间数据类型

在介绍函数之前,我们先了解 MySQL 中可用的日期和时间类型:

  • DATE:只包含日期部分,如 YYYY-MM-DD
  • TIME:只包含时间部分,如 HH:MM:SS
  • DATETIME:包含日期和时间,如 YYYY-MM-DD HH:MM:SS。范围为 1000-01-01 00:00:009999-12-31 23:59:59
  • TIMESTAMP:与 DATETIME 类似,但使用 Unix 时间戳存储,范围较小:1970-01-01 00:00:012038-01-19 03:14:07。在一些情况下会随服务器时区改变显示。
  • YEAR:只包含年份信息,范围 19012155

对于大多数应用,DATETIMETIMESTAMP 是最常用的。

二、获取当前日期和时间

  1. NOW()CURRENT_TIMESTAMP()
  • ```
    NOW()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    :返回当前的日期和时间,精确到秒。

    示例



    ```sql
    SELECT NOW();
    -- 返回: 2024-12-15 12:34:56 (示例)
  • CURRENT_TIMESTAMP():与 NOW() 等价。

  1. CURDATE()CURRENT_DATE()
  • ```
    CURDATE()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    :返回当前日期(不包含时间)。

    示例



    ```sql
    SELECT CURDATE();
    -- 返回: 2024-12-15
  • CURRENT_DATE():与 CURDATE() 等价。

  1. CURTIME()CURRENT_TIME()
  • ```
    CURTIME()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    :返回当前时间(不包含日期)。

    示例



    ```sql
    SELECT CURTIME();
    -- 返回: 12:34:56 (示例)
  • CURRENT_TIME():与 CURTIME() 等价。

    三、日期和时间的提取与格式化

  1. YEAR(), MONTH(), DAY(), HOUR(), MINUTE(), SECOND()

这些函数用于从日期/时间中提取特定部分:

1
2
3
4
5
SELECT YEAR('2024-12-15'), MONTH('2024-12-15'), DAY('2024-12-15'); 
-- 返回: 2024, 12, 15

SELECT HOUR('2024-12-15 12:34:56'), MINUTE('2024-12-15 12:34:56'), SECOND('2024-12-15 12:34:56');
-- 返回: 12, 34, 56
  1. DATE_FORMAT()

用于按指定格式输出日期和时间。常用的格式符包括:

  • %Y:四位年份
  • %m:两位月份
  • %d:两位日期
  • %H:两位小时(00-23)
  • %i:两位分钟
  • %s:两位秒

示例

1
2
3
4
5
SELECT DATE_FORMAT('2024-12-15 12:34:56', '%Y-%m-%d %H:%i:%s');
-- 返回: '2024-12-15 12:34:56'

SELECT DATE_FORMAT('2024-12-15', '%W, %M %d, %Y');
-- 返回: 'Sunday, December 15, 2024'(注:取决于日期实际对应的星期几)
  1. DAYNAME(), MONTHNAME()
  • DAYNAME(date):返回该日期为星期几的英文名称,如 Sunday
  • MONTHNAME(date):返回月份的英文名称,如 December
1
2
SELECT DAYNAME('2024-12-15'), MONTHNAME('2024-12-15');
-- 返回: 'Sunday', 'December'

四、日期和时间的计算

  1. DATE_ADD()DATE_SUB()
  • DATE_ADD(date, INTERVAL expr unit):在指定日期上加上一个时间间隔。
  • DATE_SUB(date, INTERVAL expr unit):在指定日期上减去一个时间间隔。

unit 可以是 DAY, MONTH, YEAR, HOUR, MINUTE, SECOND 等。

示例

1
2
3
4
5
SELECT DATE_ADD('2024-12-15', INTERVAL 10 DAY);
-- 返回: '2024-12-25'

SELECT DATE_SUB('2024-12-15', INTERVAL 1 MONTH);
-- 返回: '2024-11-15'
  1. ADDDATE()SUBDATE()

ADDDATE()SUBDATE()DATE_ADD()DATE_SUB() 的别名,语法略有区别,但功能相似:

1
2
SELECT ADDDATE('2024-12-15', INTERVAL 10 DAY);
-- 返回:2024-12-25
  1. DATEDIFF()

用于计算两个日期之间的天数差值(不计时分秒):

1
2
SELECT DATEDIFF('2024-12-25', '2024-12-15');
-- 返回: 10 (天)
  1. TIMEDIFF()

计算两个时间值的差值(精确到时分秒):

1
2
SELECT TIMEDIFF('12:34:56', '11:30:00');
-- 返回: '01:04:56'

若比较日期时间混合类型:

1
2
SELECT TIMEDIFF('2024-12-15 12:34:56', '2024-12-15 11:30:00');
-- 返回: '01:04:56'

五、其他常用日期与时间函数

  1. LAST_DAY(date)

返回指定日期所在月份的最后一天的日期。

1
2
SELECT LAST_DAY('2024-12-15');
-- 返回: '2024-12-31'
  1. WEEK(date), WEEKOFYEAR(date), YEARWEEK(date)
  • WEEK(date):返回该日期的周数(基于不同的周起始配置)。
  • WEEKOFYEAR(date):返回日期所在年的第几周(1~53)。
  • YEARWEEK(date):返回年与周的组合,如 202452 表示2024年的第52周。
1
SELECT WEEK('2024-12-15'), WEEKOFYEAR('2024-12-15'), YEARWEEK('2024-12-15');
  1. TO_DAYS(date)FROM_DAYS(days)
  • TO_DAYS(date):返回从公元0年到该日期的总天数,可用于日期间的计算(不常用在现代实际应用中,但有时可用于快速差值计算)。
  • FROM_DAYS(days):将天数转换成日期。
1
2
SELECT TO_DAYS('2024-12-15');
SELECT FROM_DAYS(TO_DAYS('2024-12-15'));

六、实战案例

  1. 统计过去30天每天的活跃用户数(使用日期函数构造日期范围):
1
2
3
4
5
6
7
SELECT 
activity_date,
COUNT(DISTINCT user_id) AS daily_active_users
FROM Activity
WHERE activity_date BETWEEN DATE_SUB(CURDATE(), INTERVAL 29 DAY) AND CURDATE()
GROUP BY activity_date
ORDER BY activity_date;
  1. 将日期格式化为特定字符串
1
SELECT DATE_FORMAT(NOW(), '%Y/%m/%d %H:%i:%s') AS formatted_now;
  1. 计算用户注册后经过的天数
1
2
3
SELECT user_id, 
DATEDIFF(CURDATE(), registration_date) AS days_since_registration
FROM Users;

七、性能与注意事项

  • 索引和查询优化:当使用日期列作为过滤条件(如 WHERE activity_date BETWEEN ...)时,确保在该列上有合适的索引以加快查询速度。

  • 函数与索引:在 WHERE 条件中直接对日期列使用函数(如 DATE(activity_datetime) = '2024-12-15')可能导致索引失效。尽量在查询条件中使用原始列,并在应用层或SELECT列表中使用函数处理结果。

  • 时区

    1
    TIMESTAMP

    类型会受到服务器时区影响,如果多时区用户共享同一数据库,需要谨慎处理时区转换。可使用

    1
    CONVERT_TZ()

    函数进行时区转换:

    1
    SELECT CONVERT_TZ('2024-12-15 12:34:56','UTC','Asia/Shanghai');

多表查询

下面将对多表查询(也称为连接查询)进行详细的介绍和说明。多表查询是关系型数据库使用的核心功能之一,通过关联多个表的数据来获得更丰富、复杂的结果集。

多表连接基础概念

当数据分散在多张相关联的表中时,通过 JOIN(连接)操作,可将数据组合起来从而提取出有用的信息。例如,一个 users 表存放用户信息,一个 orders 表存放订单信息,二者通过 user_id 字段建立关联。此时,如果需要查询用户姓名及其订单编号,就需要进行多表连接查询。

连接的基本语法

1
2
3
4
5
6
SELECT 列名或表达式
FROM1
[INNER|LEFT|RIGHT|CROSS] JOIN2 ON1.关联列 =2.关联列
[WHERE 条件]
[GROUP BY ...]
[ORDER BY ...]

其中:

  • FROM 表1 指定主表(左表)。
  • JOIN 表2 指定要连接的第二张表(右表)。
  • ON 表1.关联列 = 表2.关联列 指定两表之间的连接条件。

常见连接类型

INNER JOIN(内连接)

内连接返回两张表中相互匹配的行,即只有满足连接条件的记录才会出现在结果中。

示例:
假设有两张表:

users 表:

id name
1 Alice
2 Bob
3 John

orders 表:

id user_id order_no
1 1 ORD001
2 1 ORD002
3 2 ORD003

查询每个订单对应的用户名和订单号:

1
2
3
SELECT u.name, o.order_no
FROM users u
INNER JOIN orders o ON u.id = o.user_id;

结果:

name order_no
Alice ORD001
Alice ORD002
Bob ORD003

John 没有订单记录,因此不被显示。

LEFT JOIN(左连接)

左连接返回左表中的所有记录,以及匹配右表的记录。如果右表中没有匹配项,则对应的右表列返回 NULL。

示例:
使用刚才相同的表,改用 LEFT JOIN:

1
2
3
SELECT u.name, o.order_no
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

结果:

name order_no
Alice ORD001
Alice ORD002
Bob ORD003
John NULL

可以看到,尽管 John 没有订单数据,但在 LEFT JOIN 下依然显示 John 的记录,order_no 列为 NULL。

RIGHT JOIN(右连接)

右连接与左连接相反,返回右表所有记录,以及匹配左表的记录。若左表无匹配项,则左表列返回 NULL。

示例:

1
2
3
SELECT u.name, o.order_no
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;

结果:

name order_no
Alice ORD001
Alice ORD002
Bob ORD003

由于右表有 3 条订单记录,全部被显示出来。如果右表有不与用户匹配的记录,也会显示出来(此例中无此情况)。

FULL JOIN(全连接)

MySQL 本身不直接支持 FULL JOIN(全外连接),但可通过 UNION 合并 LEFT JOINRIGHT JOIN 的结果来模拟全连接。全连接会返回左右表的全部记录,如果有匹配则合并显示,没有匹配则对应列显示 NULL。

模拟方式:

1
2
3
4
5
6
7
SELECT u.name, o.order_no
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
UNION
SELECT u.name, o.order_no
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;

虽然有些其他数据库(如 PostgreSQL)直接支持 FULL JOIN,在 MySQL 中则需要这样变通。

CROSS JOIN(交叉连接)

交叉连接不使用 ON 条件,它返回两表的笛卡尔积,即所有可能的组合。通常不直接用于实际业务查询,因为返回的记录数可能非常庞大。

1
2
3
SELECT u.name, o.order_no
FROM users u
CROSS JOIN orders o;

如果 users 有 3 条记录,orders 有 3 条记录,则结果会有 3 * 3 = 9 条记录。

多表连接示例与技巧

三表连接示例

有时查询不止涉及两张表。例如:

  • users:用户表
  • orders:订单表
  • order_items:订单详情表,每条记录对应订单中的一项商品
1
2
3
4
5
SELECT u.name, o.order_no, oi.product_name, oi.qty
FROM users u
INNER JOIN orders o ON u.id = o.user_id
INNER JOIN order_items oi ON o.id = oi.order_id
WHERE u.name = 'Alice';

这个查询中:

  1. 先通过 users uorders o 使用 INNER JOIN 获取特定用户的订单数据。
  2. 再将 ordersorder_items 内连接以获取订单中各个商品详情。
  3. 最终从三个表中联查出 Alice 的订单及订单中商品的信息。

自连接 (Self JOIN)

自连接是指一张表通过不同的别名自己与自己进行连接,典型应用场景是存储层次结构的数据表(如员工表中有上级和下级的信息)。

示例:

假设 employees 表:

id name manager_id
1 Alice NULL
2 Bob 1
3 Charlie 1

要查询每个员工的上级名字:

1
2
3
SELECT e.name AS employee_name, m.name AS manager_name
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

此处 employees 表被使用了两次:

  • e 表示当前员工行
  • m 表示作为上级的员工行

使用别名提高可读性

在连接查询中,通常对表使用别名,以简化代码与提高可读性。
例如:

1
2
3
SELECT u.name, o.order_no
FROM users AS u
JOIN orders AS o ON u.id = o.user_id;

比起:

1
2
3
SELECT users.name, orders.order_no
FROM users
JOIN orders ON users.id = orders.user_id;

更为简洁易懂。

优化连接查询

  • 索引:在连接条件涉及的列(如 users.id, orders.user_id)上建立索引,可显著提高查询性能。
  • 避免返回无关列:只查询需要的列,减少数据传输与处理成本。
  • 合理使用 JOIN 类型:大部分场景用 INNER JOINLEFT JOIN 即可。

多表连接是 MySQL 数据查询的重要手段,它能将分散在多个表的数据整合在一起,满足更复杂的业务需求。通过学习 INNER JOINLEFT JOINRIGHT JOINCROSS JOIN,以及使用子查询或 UNION 等手段模拟 FULL JOIN,你可以轻松应对各种数据查询场景。

熟练掌握连接查询后,你可以:

  • 从多张相关表中一次性获取所需数据。
  • 使用自连接来处理层次结构数据。
  • 通过索引与优化技巧提升查询性能。

这些技巧将在日常开发和数据分析中极大地提高你的工作效率和数据库操作能力。

索引与优化

创建索引

索引可以提高查询速度:

1
CREATE INDEX idx_name ON users(name);

创建唯一索引:

1
CREATE UNIQUE INDEX idx_email ON users(email);

查看索引

1
SHOW INDEX FROM 表名;

删除索引

1
DROP INDEX 索引名 ON 表名;

如:

1
DROP INDEX idx_name ON users;

用户与权限管理

创建用户

1
CREATE USER 'username'@'host' IDENTIFIED BY 'password';

如:

1
CREATE USER 'bob'@'localhost' IDENTIFIED BY '123456';

授权

1
2
GRANT ALL PRIVILEGES ON 数据库名.* TO 'username'@'host';
FLUSH PRIVILEGES;

如给用户 bob 完全访问 test_db

1
2
GRANT ALL PRIVILEGES ON test_db.* TO 'bob'@'localhost';
FLUSH PRIVILEGES;

撤销权限

1
2
REVOKE ALL PRIVILEGES ON test_db.* FROM 'bob'@'localhost';
FLUSH PRIVILEGES;

删除用户

1
DROP USER 'bob'@'localhost';

备份与恢复

导出数据库

使用命令行 mysqldump

1
mysqldump -u 用户名 -p 数据库名 > backup.sql

导入数据库

1
mysql -u 用户名 -p 数据库名 < backup.sql

常用 SQL 关键字与语法扩展

DESC / DESCRIBE

用法1:DESCRIBE 或 DESC
DESCRIBE(或简写为 DESC)用于查看数据表的结构,包括列名、数据类型、是否允许 NULL、键类型、默认值及其他信息。

1
DESC 表名;

示例:

1
DESC users;

返回结果示例:

Field Type Null Key Default Extra
id int NO PRI NULL auto_increment
name varchar(50) NO NULL
email varchar(100) NO UNI NULL
created_at datetime YES CURRENT_TIMESTAMP

用法2:DESC 作为 ORDER BY 的修饰符
DESC 可以在 ORDER BY 子句中与 ASC 相对,表示降序排序。
示例:

1
SELECT * FROM users ORDER BY id DESC;

上例会根据 id 列从大到小排列查询结果。

ASC(Ascending)

ASCORDER BY 子句中用来指定升序排序的关键字,默认情况下,如不指定 ASCDESC,排序即为升序。

示例:

1
SELECT * FROM users ORDER BY name ASC;

此例将根据 name 列从 A 到 Z 排序。

WHERE

WHERE 用于指定过滤条件,从而检索满足特定条件的记录。常用的比较和逻辑运算符包括 =<>!=><>=<=ANDORNOT 等。

示例:

1
SELECT * FROM users WHERE email = 'alice@example.com';

AND、OR、NOT

  • AND:多个条件均需满足
  • OR:多个条件任意一个满足即可
  • NOT:对条件求反

示例:

1
2
3
4
5
6
SELECT * FROM users 
WHERE name = 'Alice' AND email LIKE '%@example.com';
SELECT * FROM users
WHERE city = 'New York' OR city = 'Los Angeles';
SELECT * FROM users
WHERE NOT (name = 'Bob');

LIKE

LIKE 用于字符串的模糊匹配,可结合 %(匹配任意长度的字符)和 _(匹配单个字符)。

示例:

1
SELECT * FROM users WHERE email LIKE '%@example.com';

IN

IN 用于指定多值匹配条件,可以避免使用过多的 OR

1
2
SELECT * FROM users 
WHERE city IN ('New York', 'Los Angeles');

BETWEEN … AND …

BETWEEN 用于范围匹配,适用于数字、日期等可比较的范围值。

示例:

1
2
SELECT * FROM orders 
WHERE total_price BETWEEN 100 AND 500;

该语句会返回 total_price 在 100 至 500(含边界)之间的所有记录。

ALIAS (AS)

AS 用于对表名或列名设置别名,查询结果中显示别名更易读。

示例:

1
2
SELECT name AS username, email AS user_email
FROM users;

在联表查询时常用表别名简化书写:

1
2
3
SELECT u.name, o.id AS order_id
FROM users AS u
JOIN orders AS o ON u.id = o.user_id;

SHOW

SHOW 是一系列用于查看数据库和服务器相关信息的命令:

  • SHOW DATABASES; 显示所有数据库
  • SHOW TABLES; 显示当前数据库中的所有数据表
  • SHOW COLUMNS FROM 表名; 显示表的列结构(与 DESC 表名; 效果类似)
  • SHOW INDEX FROM 表名; 显示指定表的索引信息
  • SHOW STATUS; 显示服务器状态信息
  • SHOW VARIABLES; 显示服务器系统变量信息

UNION

UNION 将两个或多个 SELECT 查询的结果合并成一个结果集,并去掉重复行。

语法:

1
2
3
SELECT 列名 FROM1
UNION
SELECT 列名 FROM2;

如需保留重复结果,可使用 UNION ALL

JOIN(补充说明)

前文已介绍过 JOIN,这里强调常见类型:

  • INNER JOIN:只返回两表中匹配的记录
  • LEFT JOIN:返回左表所有记录及匹配的右表记录
  • RIGHT JOIN:返回右表所有记录及匹配的左表记录
  • CROSS JOIN:笛卡尔积,不常用,返回组合结果

CASE

CASE 表达式用于条件逻辑判断,在 SELECT 查询中根据条件返回不同的值。

示例:

1
2
3
4
5
6
7
8
SELECT 
name,
CASE
WHEN city = 'New York' THEN 'East Coast'
WHEN city = 'Los Angeles' THEN 'West Coast'
ELSE 'Other'
END AS region
FROM users;

IS NULL / IS NOT NULL

用于判断字段值是否为 NULL 或非 NULL。

1
2
sql复制代码SELECT * FROM users WHERE email IS NULL;
SELECT * FROM users WHERE email IS NOT NULL;

EXISTS

EXISTS 用于判断子查询是否返回结果,通常和子查询结合使用。

1
2
3
4
sql复制代码SELECT * FROM users 
WHERE EXISTS (
SELECT 1 FROM orders WHERE orders.user_id = users.id
);

如果 orders 表中存在与 users.id 对应的记录,则返回该用户。

LIMIT、OFFSET(再次强调)

LIMIT 用于限制返回的行数,OFFSET 指定起始返回行的偏移量。

1
SELECT * FROM users ORDER BY id ASC LIMIT 10 OFFSET 20;

从第 21 行开始,返回 10 条记录。


基础题目练习:力扣MySQL,基础50道

SQL计划

1.查询

1757. 可回收且低脂的产品

表:Products

1
2
3
4
5
6
7
8
9
10
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| product_id | int |
| low_fats | enum |
| recyclable | enum |
+-------------+---------+
product_id 是该表的主键(具有唯一值的列)。
low_fats 是枚举类型,取值为以下两种 ('Y', 'N'),其中 'Y' 表示该产品是低脂产品,'N' 表示不是低脂产品。
recyclable 是枚举类型,取值为以下两种 ('Y', 'N'),其中 'Y' 表示该产品可回收,而 'N' 表示不可回收。

编写解决方案找出既是低脂又是可回收的产品编号。

返回结果 无顺序要求

1
select product_id from Products where low_fats='Y' and recyclable='Y';

584. 寻找用户推荐人

表: Customer

1
2
3
4
5
6
7
8
9
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| id | int |
| name | varchar |
| referee_id | int |
+-------------+---------+
在 SQL 中,id 是该表的主键列。
该表的每一行表示一个客户的 id、姓名以及推荐他们的客户的 id。

找出那些 没有被 id = 2 的客户 推荐 的客户的姓名。

任意顺序 返回结果表。

结果格式如下所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
输入: 
Customer 表:
+----+------+------------+
| id | name | referee_id |
+----+------+------------+
| 1 | Will | null |
| 2 | Jane | null |
| 3 | Alex | 2 |
| 4 | Bill | null |
| 5 | Zack | 1 |
| 6 | Mark | 2 |
+----+------+------------+
输出:
+------+
| name |
+------+
| Will |
| Jane |
| Bill |
| Zack |
+------+
1
select name from Customer where referee_id!=2 or referee_id is NULL;

error: referee_id = NULL;❌


595. 大的国家

World 表:

1
2
3
4
5
6
7
8
9
10
11
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| name | varchar |
| continent | varchar |
| area | int |
| population | int |
| gdp | bigint |
+-------------+---------+
name 是该表的主键(具有唯一值的列)。
这张表的每一行提供:国家名称、所属大陆、面积、人口和 GDP 值。

如果一个国家满足下述两个条件之一,则认为该国是 大国

  • 面积至少为 300 万平方公里(即,3000000 km2),或者
  • 人口至少为 2500 万(即 25000000

编写解决方案找出 大国 的国家名称、人口和面积。

任意顺序 返回结果表。

返回结果格式如下例所示。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
输入:
World 表:
+-------------+-----------+---------+------------+--------------+
| name | continent | area | population | gdp |
+-------------+-----------+---------+------------+--------------+
| Afghanistan | Asia | 652230 | 25500100 | 20343000000 |
| Albania | Europe | 28748 | 2831741 | 12960000000 |
| Algeria | Africa | 2381741 | 37100000 | 188681000000 |
| Andorra | Europe | 468 | 78115 | 3712000000 |
| Angola | Africa | 1246700 | 20609294 | 100990000000 |
+-------------+-----------+---------+------------+--------------+
输出:
+-------------+------------+---------+
| name | population | area |
+-------------+------------+---------+
| Afghanistan | 25500100 | 652230 |
| Algeria | 37100000 | 2381741 |
+-------------+------------+---------+
1
select name, population,area from World where area>=3000000 or population>=25000000;

1148. 文章浏览 I

Views 表:

1
2
3
4
5
6
7
8
9
10
11
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| article_id | int |
| author_id | int |
| viewer_id | int |
| view_date | date |
+---------------+---------+
此表可能会存在重复行。(换句话说,在 SQL 中这个表没有主键)
此表的每一行都表示某人在某天浏览了某位作者的某篇文章。
请注意,同一人的 author_id 和 viewer_id 是相同的。

请查询出所有浏览过自己文章的作者

结果按照 id 升序排列。

查询结果的格式如下所示:

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
输入:
Views 表:
+------------+-----------+-----------+------------+
| article_id | author_id | viewer_id | view_date |
+------------+-----------+-----------+------------+
| 1 | 3 | 5 | 2019-08-01 |
| 1 | 3 | 6 | 2019-08-02 |
| 2 | 7 | 7 | 2019-08-01 |
| 2 | 7 | 6 | 2019-08-02 |
| 4 | 7 | 1 | 2019-07-22 |
| 3 | 4 | 4 | 2019-07-21 |
| 3 | 4 | 4 | 2019-07-21 |
+------------+-----------+-----------+------------+

输出:
+------+
| id |
+------+
| 4 |
| 7 |
+------+
1
select DISTINCT author_id as id from Views where author_id=viewer_id order by author_id ASC;

关键: distinct去重, 别名应用,排序使用


1683. 无效的推文

表:Tweets

1
2
3
4
5
6
7
8
9
+----------------+---------+
| Column Name | Type |
+----------------+---------+
| tweet_id | int |
| content | varchar |
+----------------+---------+
在 SQL 中,tweet_id 是这个表的主键。
content 只包含美式键盘上的字符,不包含其它特殊字符。
这个表包含某社交媒体 App 中所有的推文。

查询所有无效推文的编号(ID)。当推文内容中的字符数严格大于 15 时,该推文是无效的。

任意顺序返回结果表。

查询结果格式如下所示:

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
输入:
Tweets 表:
+----------+----------------------------------+
| tweet_id | content |
+----------+----------------------------------+
| 1 | Vote for Biden |
| 2 | Let us make America great again! |
+----------+----------------------------------+

输出:
+----------+
| tweet_id |
+----------+
| 2 |
+----------+
解释:
推文 1 的长度 length = 14。该推文是有效的。
推文 2 的长度 length = 32。该推文是无效的。
1
select tweet_id from Tweets where length(content)>15;

length()字符串函数的使用


2.连接

1378. 使用唯一标识码替换员工ID

Employees 表:

1
2
3
4
5
6
7
8
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| id | int |
| name | varchar |
+---------------+---------+
在 SQL 中,id 是这张表的主键。
这张表的每一行分别代表了某公司其中一位员工的名字和 ID 。

EmployeeUNI 表:

1
2
3
4
5
6
7
8
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| id | int |
| unique_id | int |
+---------------+---------+
在 SQL 中,(id, unique_id) 是这张表的主键。
这张表的每一行包含了该公司某位员工的 ID 和他的唯一标识码(unique ID)。

展示每位用户的 唯一标识码(unique ID );如果某位员工没有唯一标识码,使用 null 填充即可。

你可以以 任意 顺序返回结果表。

返回结果的格式如下例所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
输入:
Employees 表:
+----+----------+
| id | name |
+----+----------+
| 1 | Alice |
| 7 | Bob |
| 11 | Meir |
| 90 | Winston |
| 3 | Jonathan |
+----+----------+
EmployeeUNI 表:
+----+-----------+
| id | unique_id |
+----+-----------+
| 3 | 1 |
| 11 | 2 |
| 90 | 3 |
+----+-----------+
输出:
+-----------+----------+
| unique_id | name |
+-----------+----------+
| null | Alice |
| null | Bob |
| 2 | Meir |
| 3 | Winston |
| 1 | Jonathan |
+-----------+----------+
解释:
Alice and Bob 没有唯一标识码, 因此我们使用 null 替代。
Meir 的唯一标识码是 2 。
Winston 的唯一标识码是 3 。
Jonathan 唯一标识码是 1 。
1
select EmployeeUNI.unique_id, Employees.name from Employees LEFT JOIN EmployeeUNI on Employees.id=EmployeeUNI.id;

error1: select EmployeeUNI.unique_id, Employees.name from Employees INNER JOIN EmployeeUNI on Employees.id=EmployeeUNI.id; 不会语法

error2: select EmployeeUNI.unique_id, Employees.name from Employees INNER JOIN EmployeeUNI on Employees.id=EmployeeUNI.id; INNER只会返回匹配的。LEFT,把左表记录覆盖右边,不匹配的用NULL


1068. 产品销售分析 I

销售表 Sales

1
2
3
4
5
6
7
8
9
10
11
12
13
+-------------+-------+
| Column Name | Type |
+-------------+-------+
| sale_id | int |
| product_id | int |
| year | int |
| quantity | int |
| price | int |
+-------------+-------+
(sale_id, year) 是销售表 Sales 的主键(具有唯一值的列的组合)。
product_id 是关联到产品表 Product 的外键(reference 列)。
该表的每一行显示 product_id 在某一年的销售情况。
注意: price 表示每单位价格。

产品表 Product

1
2
3
4
5
6
7
8
+--------------+---------+
| Column Name | Type |
+--------------+---------+
| product_id | int |
| product_name | varchar |
+--------------+---------+
product_id 是表的主键(具有唯一值的列)。
该表的每一行表示每种产品的产品名称。

编写解决方案,以获取 Sales 表中所有 sale_id 对应的 product_name 以及该产品的所有 yearprice

返回结果表 无顺序要求

结果格式示例如下。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
输入:
Sales 表:
+---------+------------+------+----------+-------+
| sale_id | product_id | year | quantity | price |
+---------+------------+------+----------+-------+
| 1 | 100 | 2008 | 10 | 5000 |
| 2 | 100 | 2009 | 12 | 5000 |
| 7 | 200 | 2011 | 15 | 9000 |
+---------+------------+------+----------+-------+
Product 表:
+------------+--------------+
| product_id | product_name |
+------------+--------------+
| 100 | Nokia |
| 200 | Apple |
| 300 | Samsung |
+------------+--------------+
输出:
+--------------+-------+-------+
| product_name | year | price |
+--------------+-------+-------+
| Nokia | 2008 | 5000 |
| Nokia | 2009 | 5000 |
| Apple | 2011 | 9000 |
+--------------+-------+-------+
1
select Product.product_name,Sales.year,Sales.price from Sales left join Product on  Sales.product_id=Product.product_id;

1581. 进店却未进行过交易的顾客

表:Visits

1
2
3
4
5
6
7
8
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| visit_id | int |
| customer_id | int |
+-------------+---------+
visit_id 是该表中具有唯一值的列。
该表包含有关光临过购物中心的顾客的信息。

表:Transactions

1
2
3
4
5
6
7
8
9
+----------------+---------+
| Column Name | Type |
+----------------+---------+
| transaction_id | int |
| visit_id | int |
| amount | int |
+----------------+---------+
transaction_id 是该表中具有唯一值的列。
此表包含 visit_id 期间进行的交易的信息。

有一些顾客可能光顾了购物中心但没有进行交易。请你编写一个解决方案,来查找这些顾客的 ID ,以及他们只光顾不交易的次数。

返回以 任何顺序 排序的结果表。

返回结果格式如下例所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
输入:
Visits
+----------+-------------+
| visit_id | customer_id |
+----------+-------------+
| 1 | 23 |
| 2 | 9 |
| 4 | 30 |
| 5 | 54 |
| 6 | 96 |
| 7 | 54 |
| 8 | 54 |
+----------+-------------+
Transactions
+----------------+----------+--------+
| transaction_id | visit_id | amount |
+----------------+----------+--------+
| 2 | 5 | 310 |
| 3 | 5 | 300 |
| 9 | 5 | 200 |
| 12 | 1 | 910 |
| 13 | 2 | 970 |
+----------------+----------+--------+
输出:
+-------------+----------------+
| customer_id | count_no_trans |
+-------------+----------------+
| 54 | 2 |
| 30 | 1 |
| 96 | 1 |
+-------------+----------------+
解释:
ID = 23 的顾客曾经逛过一次购物中心,并在 ID = 12 的访问期间进行了一笔交易。
ID = 9 的顾客曾经逛过一次购物中心,并在 ID = 13 的访问期间进行了一笔交易。
ID = 30 的顾客曾经去过购物中心,并且没有进行任何交易。
ID = 54 的顾客三度造访了购物中心。在 2 次访问中,他们没有进行任何交易,在 1 次访问中,他们进行了 3 次交易。
ID = 96 的顾客曾经去过购物中心,并且没有进行任何交易。
如我们所见,ID 为 30 和 96 的顾客一次没有进行任何交易就去了购物中心。顾客 54 也两次访问了购物中心并且没有进行任何交易。
1
2
3
4
select Visits.customer_id, COUNT(*) as count_no_trans 
from Visits LEFT JOIN Transactions on Visits.visit_id=Transactions.visit_id
where Transactions.transaction_id is NULL
GROUP BY Visits.customer_id;

select Visits.customer_id, COUNT(*) as count_no_trans from Visits LEFT JOIN Transactions on Visits.visit_id=Transactions.visit_id GROUP BY Visits.visit_id where Visits.transaction_id is NULL;
以上代码错误:

WHERE 子句位置错误
在 SQL 中,WHERE 子句应放在 GROUP BY 子句之前,而你将它放在了 GROUP BY 子句之后。

错误引用列
Visits 表并没有 transaction_id 列,而 transaction_id 是在 Transactions 表中的。

聚合逻辑不正确
你想统计每个 customer_id 没有交易的次数,但是你的 GROUP BY 是按 visit_id 分组,而不是 customer_id


197. 上升的温度

表: Weather

1
2
3
4
5
6
7
8
9
10
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| id | int |
| recordDate | date |
| temperature | int |
+---------------+---------+
id 是该表具有唯一值的列。
没有具有相同 recordDate 的不同行。
该表包含特定日期的温度信息

编写解决方案,找出与之前(昨天的)日期相比温度更高的所有日期的 id

返回结果 无顺序要求

结果格式如下例子所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
输入:
Weather 表:
+----+------------+-------------+
| id | recordDate | Temperature |
+----+------------+-------------+
| 1 | 2015-01-01 | 10 |
| 2 | 2015-01-02 | 25 |
| 3 | 2015-01-03 | 20 |
| 4 | 2015-01-04 | 30 |
+----+------------+-------------+
输出:
+----+
| id |
+----+
| 2 |
| 4 |
+----+
解释:
2015-01-02 的温度比前一天高(10 -> 25)
2015-01-04 的温度比前一天高(20 -> 30)
1
2
3
4
5
6
7
8
9
10
SELECT 
W1.id
FROM
Weather W1
JOIN
Weather W2
ON
DATE(W1.recordDate) = DATE(W2.recordDate + INTERVAL 1 DAY)
WHERE
W1.temperature > W2.temperature;

要找出与前一天相比温度更高的日期的 id,可以使用 自连接 (self-join),即将 Weather 表连接到自身,并比较每条记录的日期和温度

表别名

  • W1W2 是对 Weather 表的两个别名,用于进行自连接。

日期条件

  • W1.recordDate = W2.recordDate + INTERVAL 1 DAY:表示 W1 的日期是 W2 的后一天,即 W1W2 的“昨天”。

温度比较

  • W1.temperature > W2.temperature:筛选出当天温度比前一天更高的记录。

1661. 每台机器的进程平均运行时间

表: Activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+----------------+---------+
| Column Name | Type |
+----------------+---------+
| machine_id | int |
| process_id | int |
| activity_type | enum |
| timestamp | float |
+----------------+---------+
该表展示了一家工厂网站的用户活动。
(machine_id, process_id, activity_type) 是当前表的主键(具有唯一值的列的组合)。
machine_id 是一台机器的ID号。
process_id 是运行在各机器上的进程ID号。
activity_type 是枚举类型 ('start', 'end')。
timestamp 是浮点类型,代表当前时间(以秒为单位)。
'start' 代表该进程在这台机器上的开始运行时间戳 , 'end' 代表该进程在这台机器上的终止运行时间戳。
同一台机器,同一个进程都有一对开始时间戳和结束时间戳,而且开始时间戳永远在结束时间戳前面。

现在有一个工厂网站由几台机器运行,每台机器上运行着 相同数量的进程 。编写解决方案,计算每台机器各自完成一个进程任务的平均耗时。

完成一个进程任务的时间指进程的'end' 时间戳 减去 'start' 时间戳。平均耗时通过计算每台机器上所有进程任务的总耗费时间除以机器上的总进程数量获得。

结果表必须包含machine_id(机器ID) 和对应的 average time(平均耗时) 别名 processing_time,且四舍五入保留3位小数。

任意顺序 返回表。

具体参考例子如下。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
输入:
Activity table:
+------------+------------+---------------+-----------+
| machine_id | process_id | activity_type | timestamp |
+------------+------------+---------------+-----------+
| 0 | 0 | start | 0.712 |
| 0 | 0 | end | 1.520 |
| 0 | 1 | start | 3.140 |
| 0 | 1 | end | 4.120 |
| 1 | 0 | start | 0.550 |
| 1 | 0 | end | 1.550 |
| 1 | 1 | start | 0.430 |
| 1 | 1 | end | 1.420 |
| 2 | 0 | start | 4.100 |
| 2 | 0 | end | 4.512 |
| 2 | 1 | start | 2.500 |
| 2 | 1 | end | 5.000 |
+------------+------------+---------------+-----------+
输出:
+------------+-----------------+
| machine_id | processing_time |
+------------+-----------------+
| 0 | 0.894 |
| 1 | 0.995 |
| 2 | 1.456 |
+------------+-----------------+
解释:
一共有3台机器,每台机器运行着两个进程.
机器 0 的平均耗时: ((1.520 - 0.712) + (4.120 - 3.140)) / 2 = 0.894
机器 1 的平均耗时: ((1.550 - 0.550) + (1.420 - 0.430)) / 2 = 0.995
机器 2 的平均耗时: ((4.512 - 4.100) + (5.000 - 2.500)) / 2 = 1.456
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SELECT 
machine_id,
ROUND(AVG(end_time - start_time), 3) AS processing_time
FROM (
SELECT
machine_id,
process_id,
MAX(CASE WHEN activity_type = 'start' THEN timestamp END) AS start_time,
MAX(CASE WHEN activity_type = 'end' THEN timestamp END) AS end_time
FROM
Activity
GROUP BY
machine_id, process_id
) AS ProcessDurations
GROUP BY
machine_id;

解析

  1. 内部查询 (ProcessDurations)

    • 使用

      1
      CASE WHEN

      1
      start

      1
      end

      的时间戳分别提取出来:

      1
      2
      MAX(CASE WHEN activity_type = 'start' THEN timestamp END) AS start_time,
      MAX(CASE WHEN activity_type = 'end' THEN timestamp END) AS end_time
    • 通过 GROUP BY machine_id, process_id,确保同一机器的同一进程聚合在一起。

  2. 外层查询

    • 计算每台机器的平均耗时:

      1
      ROUND(AVG(end_time - start_time), 3) AS processing_time
    • 按机器分组计算平均值,并用 ROUND 保留三位小数。

示例数据验证

以示例数据为例:

ProcessDurations 子查询输出:

machine_id process_id start_time end_time
0 0 0.712 1.520
0 1 3.140 4.120
1 0 0.550 1.550
1 1 0.430 1.420
2 0 4.100 4.512
2 1 2.500 5.000

外层查询输出:

machine_id processing_time
0 0.894
1 0.995
2 1.456

或自连接,扫描表两次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
WITH ProcessTime AS (
SELECT
a.machine_id,
a.process_id,
b.timestamp - a.timestamp AS time_spent
FROM
Activity a
JOIN
Activity b
ON
a.machine_id = b.machine_id
AND a.process_id = b.process_id
AND a.activity_type = 'start'
AND b.activity_type = 'end'
)
SELECT
machine_id,
ROUND(AVG(time_spent), 3) AS processing_time
FROM
ProcessTime
GROUP BY
machine_id;

自连接

  • Activity 表被分成两份,分别用别名 ab 表示。
  • 使用 ON 条件确保连接条件是同一台机器 (machine_id) 和同一个进程 (process_id)。
  • 进一步限制 a.activity_type = 'start'b.activity_type = 'end',确保连接的是开始时间和结束时间。

计算时间差

  • 计算每个进程任务的耗时,公式是 b.timestamp - a.timestamp

计算平均耗时

  • 使用 AVG 函数对每台机器上的耗时取平均值。
  • 使用 ROUND 函数保留结果的三位小数。

分组

  • 按照 machine_id 分组,每台机器分别计算平均耗时。

577. 员工奖金

表:Employee

1
2
3
4
5
6
7
8
9
10
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| empId | int |
| name | varchar |
| supervisor | int |
| salary | int |
+-------------+---------+
empId 是该表中具有唯一值的列。
该表的每一行都表示员工的姓名和 id,以及他们的工资和经理的 id。

表:Bonus

1
2
3
4
5
6
7
8
9
+-------------+------+
| Column Name | Type |
+-------------+------+
| empId | int |
| bonus | int |
+-------------+------+
empId 是该表具有唯一值的列。
empId 是 Employee 表中 empId 的外键(reference 列)。
该表的每一行都包含一个员工的 id 和他们各自的奖金。

编写解决方案,报告每个奖金 少于 1000 的员工的姓名和奖金数额。

任意顺序 返回结果表。

结果格式如下所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
输入:
Employee table:
+-------+--------+------------+--------+
| empId | name | supervisor | salary |
+-------+--------+------------+--------+
| 3 | Brad | null | 4000 |
| 1 | John | 3 | 1000 |
| 2 | Dan | 3 | 2000 |
| 4 | Thomas | 3 | 4000 |
+-------+--------+------------+--------+
Bonus table:
+-------+-------+
| empId | bonus |
+-------+-------+
| 2 | 500 |
| 4 | 2000 |
+-------+-------+
输出:
+------+-------+
| name | bonus |
+------+-------+
| Brad | null |
| John | null |
| Dan | 500 |
+------+-------+

相当基础,注意处理奖金为零的条目

1
2
3
4
select Employee.name, Bonus.bonus 
from Employee LEFT JOIN Bonus
on Employee.empID=Bonus.empID
where Bonus.bonus<1000 or Bonus.bonus is NULL;

1280. 学生们参加各科测试的次数

学生表: Students

1
2
3
4
5
6
7
8
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| student_id | int |
| student_name | varchar |
+---------------+---------+
在 SQL 中,主键为 student_id(学生ID)。
该表内的每一行都记录有学校一名学生的信息。

科目表: Subjects

1
2
3
4
5
6
7
+--------------+---------+
| Column Name | Type |
+--------------+---------+
| subject_name | varchar |
+--------------+---------+
在 SQL 中,主键为 subject_name(科目名称)。
每一行记录学校的一门科目名称。

考试表: Examinations

1
2
3
4
5
6
7
8
9
+--------------+---------+
| Column Name | Type |
+--------------+---------+
| student_id | int |
| subject_name | varchar |
+--------------+---------+
这个表可能包含重复数据(换句话说,在 SQL 中,这个表没有主键)。
学生表里的一个学生修读科目表里的每一门科目。
这张考试表的每一行记录就表示学生表里的某个学生参加了一次科目表里某门科目的测试。

查询出每个学生参加每一门科目测试的次数,结果按 student_idsubject_name 排序。

查询结构格式如下所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
输入:
Students table:
+------------+--------------+
| student_id | student_name |
+------------+--------------+
| 1 | Alice |
| 2 | Bob |
| 13 | John |
| 6 | Alex |
+------------+--------------+
Subjects table:
+--------------+
| subject_name |
+--------------+
| Math |
| Physics |
| Programming |
+--------------+
Examinations table:
+------------+--------------+
| student_id | subject_name |
+------------+--------------+
| 1 | Math |
| 1 | Physics |
| 1 | Programming |
| 2 | Programming |
| 1 | Physics |
| 1 | Math |
| 13 | Math |
| 13 | Programming |
| 13 | Physics |
| 2 | Math |
| 1 | Math |
+------------+--------------+
输出:
+------------+--------------+--------------+----------------+
| student_id | student_name | subject_name | attended_exams |
+------------+--------------+--------------+----------------+
| 1 | Alice | Math | 3 |
| 1 | Alice | Physics | 2 |
| 1 | Alice | Programming | 1 |
| 2 | Bob | Math | 1 |
| 2 | Bob | Physics | 0 |
| 2 | Bob | Programming | 1 |
| 6 | Alex | Math | 0 |
| 6 | Alex | Physics | 0 |
| 6 | Alex | Programming | 0 |
| 13 | John | Math | 1 |
| 13 | John | Physics | 1 |
| 13 | John | Programming | 1 |
+------------+--------------+--------------+----------------+
解释:
结果表需包含所有学生和所有科目(即便测试次数为0):
Alice 参加了 3 次数学测试, 2 次物理测试,以及 1 次编程测试;
Bob 参加了 1 次数学测试, 1 次编程测试,没有参加物理测试;
Alex 啥测试都没参加;
John 参加了数学、物理、编程测试各 1 次。
1
2
3
4
5
6
7
8
9
10
11
12
WITH StudentSubjects AS(
select
s.student_id,
s.student_name,
sub.subject_name
from Students s CROSS JOIN Subjects sub
)
select ss.student_id,ss.student_name,ss.subject_name,COUNT(e.subject_name) as attended_exams
from StudentSubjects ss LEFT JOIN Examinations e
on ss.student_id=e.student_id AND ss.subject_name=e.subject_name
group by ss.student_id, ss.subject_name
order by ss.student_id, ss.subject_name;

生成所有学生和科目组合

  • 使用 CROSS JOINStudents 表与 Subjects 表组合,生成每个学生和每个科目的可能组合,命名为 StudentSubjects

统计每个组合的考试次数

  • 使用 LEFT JOINStudentSubjectsExaminations 表连接起来。
  • 按学生 ID 和科目名称分组,同时统计匹配的 Examinations.subject_name 出现次数。

确保分组和排序

  • GROUP BY 包括 student_id, student_name, 和 subject_name
  • student_idsubject_name 排序,确保结果顺序符合要求

注: CROSS JOIN 用来生成笛卡尔集,即学生和各个科目的全部组合


570. 至少有5名直接下属的经理

表: Employee

1
2
3
4
5
6
7
8
9
10
11
12
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| id | int |
| name | varchar |
| department | varchar |
| managerId | int |
+-------------+---------+
id 是此表的主键(具有唯一值的列)。
该表的每一行表示雇员的名字、他们的部门和他们的经理的id。
如果managerId为空,则该员工没有经理。
没有员工会成为自己的管理者。

编写一个解决方案,找出至少有五个直接下属的经理。

任意顺序 返回结果表。

查询结果格式如下所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
输入: 
Employee 表:
+-----+-------+------------+-----------+
| id | name | department | managerId |
+-----+-------+------------+-----------+
| 101 | John | A | Null |
| 102 | Dan | A | 101 |
| 103 | James | A | 101 |
| 104 | Amy | A | 101 |
| 105 | Anne | A | 101 |
| 106 | Ron | B | 101 |
+-----+-------+------------+-----------+
输出:
+------+
| name |
+------+
| John |
+------+
1
2
3
4
5
6
7
8
9
10
11
12
SELECT 
e.name
FROM
Employee m
JOIN
Employee e
ON
m.managerId = e.id
GROUP BY
e.id, e.name
HAVING
COUNT(m.id) >= 5;

JOIN 语句

  • 通过

    1
    JOIN

    ,将

    1
    Employee

    表分成两部分:

    • m 表:代表员工(managerId 是他们的经理)。
    • e 表:代表经理(id 是其自身的 ID)。
  • 连接条件是 m.managerId = e.id

分组统计

  • e.id(经理 ID)和 e.name(经理姓名)分组。
  • 使用 COUNT(m.id) 统计每个经理的直接下属数量。

筛选条件

  • 使用 HAVING COUNT(m.id) >= 5 筛选至少有 5 个直接下属的经理。

选择列

  • 最终查询仅返回满足条件的经理的姓名。

注:


HAVING 子句是 SQL 中用于筛选分组后的数据的一种工具。它与 GROUP BY 一起使用,用于对分组后的数据应用筛选条件。

HAVING 的作用

  • 用于分组后的筛选
    • GROUP BY 之后,SQL 会对数据进行分组并计算聚合函数(如 SUM, COUNT, AVG, 等)的值。
    • HAVING 子句允许根据这些聚合结果进行筛选。
    • WHERE 不同,HAVING 操作的是分组后的结果,而 WHERE 只能作用于原始数据。

HAVING 的语法

1
2
3
4
SELECT column1, column2, AGGREGATE_FUNCTION(column3)
FROM table_name
GROUP BY column1, column2
HAVING condition;
  • AGGREGATE_FUNCTION:聚合函数,例如 SUM, AVG, COUNT
  • condition:对聚合结果的过滤条件。

HAVING VS WHERE

区别

特性 WHERE HAVING
作用时机 在数据分组之前过滤记录。 在数据分组之后过滤分组结果。
作用范围 作用于单行数据。 作用于分组或聚合后的结果。
是否支持聚合函数 不支持。 支持。

示例对比

假设有一张表 Sales

Product Region Sales
A East 100
A West 200
B East 150
B West 300
  • 使用 WHERE

    1
    2
    3
    4
    5
    -- 筛选 Sales > 100 的行,按 Region 分组后计算总销售额
    SELECT Region, SUM(Sales)
    FROM Sales
    WHERE Sales > 100
    GROUP BY Region;

    结果

    1
    2
    3
    4
    5
    6
    +--------+-------------+
    | Region | SUM(Sales) |
    +--------+-------------+
    | West | 500 |
    | East | 150 |
    +--------+-------------+

    在原始数据中,过滤掉了 Sales <= 100 的记录。

  • 使用 HAVING

    1
    2
    3
    4
    5
    -- 按 Region 分组后,筛选总销售额 > 300 的分组
    SELECT Region, SUM(Sales)
    FROM Sales
    GROUP BY Region
    HAVING SUM(Sales) > 300;

    结果

    1
    2
    3
    4
    5
    +--------+-------------+
    | Region | SUM(Sales) |
    +--------+-------------+
    | West | 500 |
    +--------+-------------+

    原始数据未过滤,分组后筛选出总销售额大于 300 的区域。


HAVING 的应用场景

  1. 聚合后数据的筛选
    • 找出销售额超过一定数值的商品/区域/店铺。
    • 找出参与考试次数超过一定数量的学生。
  2. 筛选复杂分组条件
    • 比如,找出员工人数超过一定数量的部门。

示例 1:筛选总销量超过 500 的商品

1
2
3
4
SELECT Product, SUM(Sales) AS TotalSales
FROM Sales
GROUP BY Product
HAVING SUM(Sales) > 500;

示例 2:筛选至少有 3 名成员的团队

1
2
3
4
SELECT Team, COUNT(MemberId) AS MemberCount
FROM Members
GROUP BY Team
HAVING COUNT(MemberId) >= 3;

总结

  • WHERE 是在分组之前对原始数据进行过滤。
  • HAVING 是在分组之后对分组结果进行过滤。
  • HAVING 主要用于聚合操作的结果条件筛选。

1934. 确认率

表: Signups

1
2
3
4
5
6
7
8
+----------------+----------+
| Column Name | Type |
+----------------+----------+
| user_id | int |
| time_stamp | datetime |
+----------------+----------+
User_id是该表的主键。
每一行都包含ID为user_id的用户的注册时间信息。

表: Confirmations

1
2
3
4
5
6
7
8
9
10
11
+----------------+----------+
| Column Name | Type |
+----------------+----------+
| user_id | int |
| time_stamp | datetime |
| action | ENUM |
+----------------+----------+
(user_id, time_stamp)是该表的主键。
user_id是一个引用到注册表的外键。
action是类型为('confirmed', 'timeout')的ENUM
该表的每一行都表示ID为user_id的用户在time_stamp请求了一条确认消息,该确认消息要么被确认('confirmed'),要么被过期('timeout')。

用户的 确认率'confirmed' 消息的数量除以请求的确认消息的总数。没有请求任何确认消息的用户的确认率为 0 。确认率四舍五入到 小数点后两位

编写一个SQL查询来查找每个用户的 确认率 。

以 任意顺序 返回结果表。

查询结果格式如下所示。

示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
输入:
Signups 表:
+---------+---------------------+
| user_id | time_stamp |
+---------+---------------------+
| 3 | 2020-03-21 10:16:13 |
| 7 | 2020-01-04 13:57:59 |
| 2 | 2020-07-29 23:09:44 |
| 6 | 2020-12-09 10:39:37 |
+---------+---------------------+
Confirmations 表:
+---------+---------------------+-----------+
| user_id | time_stamp | action |
+---------+---------------------+-----------+
| 3 | 2021-01-06 03:30:46 | timeout |
| 3 | 2021-07-14 14:00:00 | timeout |
| 7 | 2021-06-12 11:57:29 | confirmed |
| 7 | 2021-06-13 12:58:28 | confirmed |
| 7 | 2021-06-14 13:59:27 | confirmed |
| 2 | 2021-01-22 00:00:00 | confirmed |
| 2 | 2021-02-28 23:59:59 | timeout |
+---------+---------------------+-----------+
输出:
+---------+-------------------+
| user_id | confirmation_rate |
+---------+-------------------+
| 6 | 0.00 |
| 3 | 0.00 |
| 7 | 1.00 |
| 2 | 0.50 |
+---------+-------------------+
解释:
用户 6 没有请求任何确认消息。确认率为 0。
用户 3 进行了 2 次请求,都超时了。确认率为 0。
用户 7 提出了 3 个请求,所有请求都得到了确认。确认率为 1。
用户 2 做了 2 个请求,其中一个被确认,另一个超时。确认率为 1 / 2 = 0.5。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
WITH ConfirmationStats AS (
SELECT
c.user_id,
COUNT(*) AS total_requests,
SUM(CASE WHEN c.action = 'confirmed' THEN 1 ELSE 0 END) AS confirmed_requests
FROM
Confirmations c
GROUP BY
c.user_id
)
SELECT
s.user_id,
ROUND(COALESCE(cs.confirmed_requests, 0) / COALESCE(cs.total_requests, 1), 2) AS confirmation_rate
FROM
Signups s
LEFT JOIN
ConfirmationStats cs
ON
s.user_id = cs.user_id;

计算统计信息

  • 1
    ConfirmationStats
    • COUNT(*) 统计总请求数。
    • 使用 CASE WHEN 计算 confirmed 请求的数量。
  • 按照 user_id 分组。

确保所有用户被包含

  • 使用 Signups 表作为主表。
  • 通过 LEFT JOIN 确保即使用户没有请求记录,也会包含在结果中。

处理没有请求的用户

  • 使用

    1
    COALESCE
    • cs.confirmed_requestscs.total_requestsNULL 时,分别替换为 01
  • 避免除以 0 的情况。

四舍五入确认率

  • 使用 ROUND 函数将计算结果四舍五入到小数点后两位。

3.聚合函数

620. 有趣的电影

表:cinema

1
2
3
4
5
6
7
8
9
10
11
+----------------+----------+
| Column Name | Type |
+----------------+----------+
| id | int |
| movie | varchar |
| description | varchar |
| rating | float |
+----------------+----------+
id 是该表的主键(具有唯一值的列)。
每行包含有关电影名称、类型和评级的信息。
评级为 [0,10] 范围内的小数点后 2 位浮点数。

编写解决方案,找出所有影片描述为 boring (不无聊) 的并且 id 为奇数 的影片。

返回结果按 rating 降序排列

结果格式如下示例:

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
输入:
+---------+-----------+--------------+-----------+
| id | movie | description | rating |
+---------+-----------+--------------+-----------+
| 1 | War | great 3D | 8.9 |
| 2 | Science | fiction | 8.5 |
| 3 | irish | boring | 6.2 |
| 4 | Ice song | Fantacy | 8.6 |
| 5 | House card| Interesting| 9.1 |
+---------+-----------+--------------+-----------+
输出:
+---------+-----------+--------------+-----------+
| id | movie | description | rating |
+---------+-----------+--------------+-----------+
| 5 | House card| Interesting| 9.1 |
| 1 | War | great 3D | 8.9 |
+---------+-----------+--------------+-----------+
解释:
我们有三部电影,它们的 id 是奇数:1、3 和 5。id = 3 的电影是 boring 的,所以我们不把它包括在答案中。
1
2
select * from cinema where description!='boring' and id%2=1
order by rating DESC;

1251. 平均售价

表:Prices

1
2
3
4
5
6
7
8
9
10
11
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| product_id | int |
| start_date | date |
| end_date | date |
| price | int |
+---------------+---------+
(product_id,start_date,end_date) 是 prices 表的主键(具有唯一值的列的组合)。
prices 表的每一行表示的是某个产品在一段时期内的价格。
每个产品的对应时间段是不会重叠的,这也意味着同一个产品的价格时段不会出现交叉。

表:UnitsSold

1
2
3
4
5
6
7
8
9
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| product_id | int |
| purchase_date | date |
| units | int |
+---------------+---------+
该表可能包含重复数据。
该表的每一行表示的是每种产品的出售日期,单位和产品 id。

编写解决方案以查找每种产品的平均售价。average_price 应该 四舍五入到小数点后两位。如果产品没有任何售出,则假设其平均售价为 0。

返回结果表 无顺序要求

结果格式如下例所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
输入:
Prices table:
+------------+------------+------------+--------+
| product_id | start_date | end_date | price |
+------------+------------+------------+--------+
| 1 | 2019-02-17 | 2019-02-28 | 5 |
| 1 | 2019-03-01 | 2019-03-22 | 20 |
| 2 | 2019-02-01 | 2019-02-20 | 15 |
| 2 | 2019-02-21 | 2019-03-31 | 30 |
+------------+------------+------------+--------+
UnitsSold table:
+------------+---------------+-------+
| product_id | purchase_date | units |
+------------+---------------+-------+
| 1 | 2019-02-25 | 100 |
| 1 | 2019-03-01 | 15 |
| 2 | 2019-02-10 | 200 |
| 2 | 2019-03-22 | 30 |
+------------+---------------+-------+
输出:
+------------+---------------+
| product_id | average_price |
+------------+---------------+
| 1 | 6.96 |
| 2 | 16.96 |
+------------+---------------+
解释:
平均售价 = 产品总价 / 销售的产品数量。
产品 1 的平均售价 = ((100 * 5)+(15 * 20) )/ 115 = 6.96
产品 2 的平均售价 = ((200 * 15)+(30 * 30) )/ 230 = 16.96
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
WITH Sales AS (
SELECT
u.product_id,
u.units,
p.price,
(u.units * p.price) AS total_revenue
FROM
UnitsSold u
JOIN
Prices p
ON
u.product_id = p.product_id
AND u.purchase_date BETWEEN p.start_date AND p.end_date
),
Aggregated AS (
SELECT
product_id,
SUM(total_revenue) AS total_revenue,
SUM(units) AS total_units
FROM
Sales
GROUP BY
product_id
)
SELECT
p.product_id,
ROUND(COALESCE(a.total_revenue / a.total_units, 0), 2) AS average_price
FROM
(SELECT DISTINCT product_id FROM Prices) p
LEFT JOIN
Aggregated a
ON
p.product_id = a.product_id;

要解决这个问题,需要以下步骤:

  1. 匹配出售日期和价格区间
    • UnitsSold 表中的 purchase_datePrices 表中的 start_dateend_date 关联起来,以找到每个销售记录对应的价格。
  2. 计算总收入
    • 对于每个产品,计算总收入 units * price
  3. 计算总销量
    • 对于每个产品,计算总销量(即 units 的总和)。
  4. 计算平均售价
    • 平均售价 = 总收入 / 总销量。
    • 如果某产品没有销量,平均售价为 0。
  5. 四舍五入到两位小数
    • 使用 ROUND 函数对结果进行格式化。

查询逻辑解析

  1. 匹配价格区间
    • 通过 JOIN 和条件 u.purchase_date BETWEEN p.start_date AND p.end_date,确保每次购买找到正确的价格。
  2. 计算总收入和总销量
    • Sales CTE 计算每次购买的收入 units * price
    • Aggregated CTE 对每个产品的总收入和总销量进行汇总。
  3. 包含所有产品
    • 通过从 Prices 表中获取所有 product_id 并使用 LEFT JOIN,确保即使产品没有销量也会出现在结果中。
  4. 计算平均售价
    • 如果某产品没有销量,使用 COALESCE 将其平均售价设置为 0
  5. 结果格式化
    • 使用 ROUND 四舍五入到两位小数。

1075. 项目员工 I

项目表 Project

1
2
3
4
5
6
7
8
9
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| project_id | int |
| employee_id | int |
+-------------+---------+
主键为 (project_id, employee_id)。
employee_id 是员工表 Employee 表的外键。
这张表的每一行表示 employee_id 的员工正在 project_id 的项目上工作。

员工表 Employee

1
2
3
4
5
6
7
8
9
+------------------+---------+
| Column Name | Type |
+------------------+---------+
| employee_id | int |
| name | varchar |
| experience_years | int |
+------------------+---------+
主键是 employee_id。数据保证 experience_years 非空。
这张表的每一行包含一个员工的信息。

请写一个 SQL 语句,查询每一个项目中员工的 平均 工作年限,精确到小数点后两位

任意 顺序返回结果表。

查询结果的格式如下。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
输入:
Project 表:
+-------------+-------------+
| project_id | employee_id |
+-------------+-------------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 2 | 1 |
| 2 | 4 |
+-------------+-------------+

Employee 表:
+-------------+--------+------------------+
| employee_id | name | experience_years |
+-------------+--------+------------------+
| 1 | Khaled | 3 |
| 2 | Ali | 2 |
| 3 | John | 1 |
| 4 | Doe | 2 |
+-------------+--------+------------------+

输出:
+-------------+---------------+
| project_id | average_years |
+-------------+---------------+
| 1 | 2.00 |
| 2 | 2.50 |
+-------------+---------------+
解释:第一个项目中,员工的平均工作年限是 (3 + 2 + 1) / 3 = 2.00;第二个项目中,员工的平均工作年限是 (3 + 2) / 2 = 2.50
1
2
3
4
5
6
7
8
9
10
11
SELECT 
p.project_id,
ROUND(AVG(e.experience_years), 2) AS average_years
FROM
Project p
JOIN
Employee e
ON
p.employee_id = e.employee_id
GROUP BY
p.project_id;

INNER JOIN(通常直接写为 JOIN

  • 作用: 返回两张表中满足连接条件的行。
  • 特点: 如果两张表中有匹配的记录,就会出现在结果中;没有匹配的记录会被过滤掉。

1633. 各赛事的用户注册率

用户表: Users

1
2
3
4
5
6
7
8
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| user_id | int |
| user_name | varchar |
+-------------+---------+
user_id 是该表的主键(具有唯一值的列)。
该表中的每行包括用户 ID 和用户名。

注册表: Register

1
2
3
4
5
6
7
8
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| contest_id | int |
| user_id | int |
+-------------+---------+
(contest_id, user_id) 是该表的主键(具有唯一值的列的组合)。
该表中的每行包含用户的 ID 和他们注册的赛事。

编写解决方案统计出各赛事的用户注册百分率,保留两位小数。

返回的结果表按 percentage降序 排序,若相同则按 contest_id升序 排序。

返回结果如下示例所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
输入:
Users 表:
+---------+-----------+
| user_id | user_name |
+---------+-----------+
| 6 | Alice |
| 2 | Bob |
| 7 | Alex |
+---------+-----------+

Register 表:
+------------+---------+
| contest_id | user_id |
+------------+---------+
| 215 | 6 |
| 209 | 2 |
| 208 | 2 |
| 210 | 6 |
| 208 | 6 |
| 209 | 7 |
| 209 | 6 |
| 215 | 7 |
| 208 | 7 |
| 210 | 2 |
| 207 | 2 |
| 210 | 7 |
+------------+---------+
输出:
+------------+------------+
| contest_id | percentage |
+------------+------------+
| 208 | 100.0 |
| 209 | 100.0 |
| 210 | 100.0 |
| 215 | 66.67 |
| 207 | 33.33 |
+------------+------------+
解释:
所有用户都注册了 208、209 和 210 赛事,因此这些赛事的注册率为 100% ,我们按 contest_id 的降序排序加入结果表中。
Alice 和 Alex 注册了 215 赛事,注册率为 ((2/3) * 100) = 66.67%
Bob 注册了 207 赛事,注册率为 ((1/3) * 100) = 33.33%
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
WITH TotalUsers AS (
SELECT COUNT(*) AS total_users
FROM Users
)
SELECT
r.contest_id,
ROUND(COUNT(DISTINCT r.user_id) * 100.0 / tu.total_users, 2) AS percentage
FROM
Register r
CROSS JOIN
TotalUsers tu
GROUP BY
r.contest_id, tu.total_users
ORDER BY
percentage DESC, contest_id ASC;

1211. 查询结果的质量和占比

Queries 表:

1
2
3
4
5
6
7
8
9
10
11
12
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| query_name | varchar |
| result | varchar |
| position | int |
| rating | int |
+-------------+---------+
此表可能有重复的行。
此表包含了一些从数据库中收集的查询信息。
“位置”(position)列的值为 1 到 500 。
“评分”(rating)列的值为 1 到 5 。评分小于 3 的查询被定义为质量很差的查询。

将查询结果的质量 quality 定义为:

各查询结果的评分与其位置之间比率的平均值。

将劣质查询百分比 poor_query_percentage 定义为:

评分小于 3 的查询结果占全部查询结果的百分比。

编写解决方案,找出每次的 query_namequalitypoor_query_percentage

qualitypoor_query_percentage 都应 四舍五入到小数点后两位

任意顺序 返回结果表。

结果格式如下所示:

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
输入:
Queries table:
+------------+-------------------+----------+--------+
| query_name | result | position | rating |
+------------+-------------------+----------+--------+
| Dog | Golden Retriever | 1 | 5 |
| Dog | German Shepherd | 2 | 5 |
| Dog | Mule | 200 | 1 |
| Cat | Shirazi | 5 | 2 |
| Cat | Siamese | 3 | 3 |
| Cat | Sphynx | 7 | 4 |
+------------+-------------------+----------+--------+
输出:
+------------+---------+-----------------------+
| query_name | quality | poor_query_percentage |
+------------+---------+-----------------------+
| Dog | 2.50 | 33.33 |
| Cat | 0.66 | 33.33 |
+------------+---------+-----------------------+
解释:
Dog 查询结果的质量为 ((5 / 1) + (5 / 2) + (1 / 200)) / 3 = 2.50
Dog 查询结果的劣质查询百分比为 (1 / 3) * 100 = 33.33

Cat 查询结果的质量为 ((2 / 5) + (3 / 3) + (4 / 7)) / 3 = 0.66
Cat 查询结果的劣质查询百分比为 (1 / 3) * 100 = 33.33
1
2
3
select DISTINCT q.query_name, ROUND(AVG(q.rating/q.position),2) as quality,ROUND((SUM(CASE WHEN q.rating < 3 THEN 1 ELSE 0 END) / COUNT(*)) * 100, 2) as poor_query_percentage
from Queries q
group by q.query_name;

了解group by操作(将查询用分组看,各组直接不影响);以及ROUND((SUM(CASE WHEN q.rating < 3 THEN 1 ELSE 0 END) / COUNT()) 100, 2)这个求百分比的写法


1193. 每月交易 I

表:Transactions

1
2
3
4
5
6
7
8
9
10
11
12
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| id | int |
| country | varchar |
| state | enum |
| amount | int |
| trans_date | date |
+---------------+---------+
id 是这个表的主键。
该表包含有关传入事务的信息。
state 列类型为 ["approved", "declined"] 之一。

编写一个 sql 查询来查找每个月和每个国家/地区的事务数及其总金额、已批准的事务数及其总金额。

任意顺序 返回结果表。

查询结果格式如下所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
输入:
Transactions table:
+------+---------+----------+--------+------------+
| id | country | state | amount | trans_date |
+------+---------+----------+--------+------------+
| 121 | US | approved | 1000 | 2018-12-18 |
| 122 | US | declined | 2000 | 2018-12-19 |
| 123 | US | approved | 2000 | 2019-01-01 |
| 124 | DE | approved | 2000 | 2019-01-07 |
+------+---------+----------+--------+------------+
输出:
+----------+---------+-------------+----------------+--------------------+-----------------------+
| month | country | trans_count | approved_count | trans_total_amount | approved_total_amount |
+----------+---------+-------------+----------------+--------------------+-----------------------+
| 2018-12 | US | 2 | 1 | 3000 | 1000 |
| 2019-01 | US | 1 | 1 | 2000 | 2000 |
| 2019-01 | DE | 1 | 1 | 2000 | 2000 |
+----------+---------+-------------+----------------+--------------------+-----------------------+
1
2
3
4
5
6
7
8
9
10
11
SELECT 
DATE_FORMAT(trans_date, '%Y-%m') AS month,
country,
COUNT(*) AS trans_count,
SUM(CASE WHEN state = 'approved' THEN 1 ELSE 0 END) AS approved_count,
SUM(amount) AS trans_total_amount,
SUM(CASE WHEN state = 'approved' THEN amount ELSE 0 END) AS approved_total_amount
FROM
Transactions
GROUP BY
DATE_FORMAT(trans_date, '%Y-%m'), country;

DATE_FORMAT,进行时间转换,按具体时间分组


1174. 即时食物配送 II

配送表: Delivery

1
2
3
4
5
6
7
8
9
10
+-----------------------------+---------+
| Column Name | Type |
+-----------------------------+---------+
| delivery_id | int |
| customer_id | int |
| order_date | date |
| customer_pref_delivery_date | date |
+-----------------------------+---------+
delivery_id 是该表中具有唯一值的列。
该表保存着顾客的食物配送信息,顾客在某个日期下了订单,并指定了一个期望的配送日期(和下单日期相同或者在那之后)。

如果顾客期望的配送日期和下单日期相同,则该订单称为 「即时订单」,否则称为「计划订单」。

首次订单」是顾客最早创建的订单。我们保证一个顾客只会有一个「首次订单」。

编写解决方案以获取即时订单在所有用户的首次订单中的比例。保留两位小数。

结果示例如下所示:

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
输入:
Delivery 表:
+-------------+-------------+------------+-----------------------------+
| delivery_id | customer_id | order_date | customer_pref_delivery_date |
+-------------+-------------+------------+-----------------------------+
| 1 | 1 | 2019-08-01 | 2019-08-02 |
| 2 | 2 | 2019-08-02 | 2019-08-02 |
| 3 | 1 | 2019-08-11 | 2019-08-12 |
| 4 | 3 | 2019-08-24 | 2019-08-24 |
| 5 | 3 | 2019-08-21 | 2019-08-22 |
| 6 | 2 | 2019-08-11 | 2019-08-13 |
| 7 | 4 | 2019-08-09 | 2019-08-09 |
+-------------+-------------+------------+-----------------------------+
输出:
+----------------------+
| immediate_percentage |
+----------------------+
| 50.00 |
+----------------------+
解释:
1 号顾客的 1 号订单是首次订单,并且是计划订单。
2 号顾客的 2 号订单是首次订单,并且是即时订单。
3 号顾客的 5 号订单是首次订单,并且是计划订单。
4 号顾客的 7 号订单是首次订单,并且是即时订单。
因此,一半顾客的首次订单是即时的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
WITH FirstOrders AS (
-- 找出每个顾客的首次订单
SELECT *
FROM Delivery d
WHERE d.order_date = (
SELECT MIN(order_date)
FROM Delivery
WHERE customer_id = d.customer_id
)
),
ImmediateOrders AS (
-- 标记首次订单是否为即时订单
SELECT
*,
CASE
WHEN order_date = customer_pref_delivery_date THEN 1
ELSE 0
END AS is_immediate
FROM FirstOrders
)
-- 计算即时订单的比例
SELECT
ROUND(SUM(is_immediate) * 100.0 / COUNT(*), 2) AS immediate_percentage
FROM ImmediateOrders;

用MIN(order_date)找到首次订单


550. 游戏玩法分析 IV

Table: Activity

1
2
3
4
5
6
7
8
9
10
11
+--------------+---------+
| Column Name | Type |
+--------------+---------+
| player_id | int |
| device_id | int |
| event_date | date |
| games_played | int |
+--------------+---------+
(player_id,event_date)是此表的主键(具有唯一值的列的组合)。
这张表显示了某些游戏的玩家的活动情况。
每一行是一个玩家的记录,他在某一天使用某个设备注销之前登录并玩了很多游戏(可能是 0)。

编写解决方案,报告在首次登录的第二天再次登录的玩家的 比率四舍五入到小数点后两位。换句话说,你需要计算从首次登录日期开始至少连续两天登录的玩家的数量,然后除以玩家总数。

结果格式如下所示:

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
输入:
Activity table:
+-----------+-----------+------------+--------------+
| player_id | device_id | event_date | games_played |
+-----------+-----------+------------+--------------+
| 1 | 2 | 2016-03-01 | 5 |
| 1 | 2 | 2016-03-02 | 6 |
| 2 | 3 | 2017-06-25 | 1 |
| 3 | 1 | 2016-03-02 | 0 |
| 3 | 4 | 2018-07-03 | 5 |
+-----------+-----------+------------+--------------+
输出:
+-----------+
| fraction |
+-----------+
| 0.33 |
+-----------+
解释:
只有 ID 为 1 的玩家在第一天登录后才重新登录,所以答案是 1/3 = 0.33
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
WITH FirstLogin AS (
-- 计算每个玩家的首次登录日期和首次登录的第二天日期
SELECT
player_id,
MIN(event_date) AS first_login_date,
DATE_ADD(MIN(event_date), INTERVAL 1 DAY) AS second_login_date
FROM Activity
GROUP BY player_id
)

SELECT
ROUND(SUM(CASE WHEN a.player_id IS NOT NULL THEN 1 ELSE 0 END) / COUNT(*), 2) AS fraction
FROM FirstLogin fl
LEFT JOIN Activity a
ON fl.player_id = a.player_id
AND fl.second_login_date = a.event_date;

4.排序和分组

2356. 每位教师所教授的科目种类的数量

表: Teacher

1
2
3
4
5
6
7
8
9
+-------------+------+
| Column Name | Type |
+-------------+------+
| teacher_id | int |
| subject_id | int |
| dept_id | int |
+-------------+------+
在 SQL 中,(subject_id, dept_id) 是该表的主键。
该表中的每一行都表示带有 teacher_id 的教师在系 dept_id 中教授科目 subject_id。

查询每位老师在大学里教授的科目种类的数量。

任意顺序 返回结果表。

查询结果格式示例如下。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
输入: 
Teacher 表:
+------------+------------+---------+
| teacher_id | subject_id | dept_id |
+------------+------------+---------+
| 1 | 2 | 3 |
| 1 | 2 | 4 |
| 1 | 3 | 3 |
| 2 | 1 | 1 |
| 2 | 2 | 1 |
| 2 | 3 | 1 |
| 2 | 4 | 1 |
+------------+------------+---------+
输出:
+------------+-----+
| teacher_id | cnt |
+------------+-----+
| 1 | 2 |
| 2 | 4 |
+------------+-----+
解释:
教师 1:
- 他在 3、4 系教科目 2。
- 他在 3 系教科目 3。
教师 2:
- 他在 1 系教科目 1。
- 他在 1 系教科目 2。
- 他在 1 系教科目 3。
- 他在 1 系教科目 4。
1
2
3
4
5
6
7
8
WITH Mylist as(
select DISTINCT t.teacher_id, t.subject_id
from Teacher t
group by t.teacher_id,t.subject_id
)
select m.teacher_id, COUNT(*) as cnt
from Mylist m
group by m.teacher_id;
1
2
3
4
5
SELECT 
teacher_id,
COUNT(DISTINCT subject_id) AS cnt
FROM Teacher
GROUP BY teacher_id;

DISTINCT 关键字用法


1141. 查询近30天活跃用户数

表:Activity

1
2
3
4
5
6
7
8
9
10
11
12
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| user_id | int |
| session_id | int |
| activity_date | date |
| activity_type | enum |
+---------------+---------+
该表没有包含重复数据。
activity_type 列是 ENUM(category) 类型, 从 ('open_session', 'end_session', 'scroll_down', 'send_message') 取值。
该表记录社交媒体网站的用户活动。
注意,每个会话只属于一个用户。

编写解决方案,统计截至 2019-07-27(包含2019-07-27),近 30 天的每日活跃用户数(当天只要有一条活动记录,即为活跃用户)。

任意顺序 返回结果表。

结果示例如下。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
输入:
Activity table:
+---------+------------+---------------+---------------+
| user_id | session_id | activity_date | activity_type |
+---------+------------+---------------+---------------+
| 1 | 1 | 2019-07-20 | open_session |
| 1 | 1 | 2019-07-20 | scroll_down |
| 1 | 1 | 2019-07-20 | end_session |
| 2 | 4 | 2019-07-20 | open_session |
| 2 | 4 | 2019-07-21 | send_message |
| 2 | 4 | 2019-07-21 | end_session |
| 3 | 2 | 2019-07-21 | open_session |
| 3 | 2 | 2019-07-21 | send_message |
| 3 | 2 | 2019-07-21 | end_session |
| 4 | 3 | 2019-06-25 | open_session |
| 4 | 3 | 2019-06-25 | end_session |
+---------+------------+---------------+---------------+
输出:
+------------+--------------+
| day | active_users |
+------------+--------------+
| 2019-07-20 | 2 |
| 2019-07-21 | 2 |
+------------+--------------+
解释:注意非活跃用户的记录不需要展示。
1
2
3
4
select a.activity_date as day, COUNT(DISTINCT user_id) as active_users
from Activity a
WHERE activity_date BETWEEN DATE_SUB('2019-07-27', INTERVAL 29 DAY) AND '2019-07-27'
group by a.activity_date;

1084. 销售分析 III

表: Product

1
2
3
4
5
6
7
8
9
+--------------+---------+
| Column Name | Type |
+--------------+---------+
| product_id | int |
| product_name | varchar |
| unit_price | int |
+--------------+---------+
product_id 是该表的主键(具有唯一值的列)。
该表的每一行显示每个产品的名称和价格。

表:Sales

1
2
3
4
5
6
7
8
9
10
11
12
13
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| seller_id | int |
| product_id | int |
| buyer_id | int |
| sale_date | date |
| quantity | int |
| price | int |
+------ ------+---------+
这个表可能有重复的行。
product_id 是 Product 表的外键(reference 列)。
该表的每一行包含关于一个销售的一些信息。

编写解决方案,报告 2019年春季 才售出的产品。即 **2019-01-01** (含)至 **2019-03-31** (含)之间出售的商品。

任意顺序 返回结果表。

结果格式如下所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
输入:
Product table:
+------------+--------------+------------+
| product_id | product_name | unit_price |
+------------+--------------+------------+
| 1 | S8 | 1000 |
| 2 | G4 | 800 |
| 3 | iPhone | 1400 |
+------------+--------------+------------+
Sales table:
+-----------+------------+----------+------------+----------+-------+
| seller_id | product_id | buyer_id | sale_date | quantity | price |
+-----------+------------+----------+------------+----------+-------+
| 1 | 1 | 1 | 2019-01-21 | 2 | 2000 |
| 1 | 2 | 2 | 2019-02-17 | 1 | 800 |
| 2 | 2 | 3 | 2019-06-02 | 1 | 800 |
| 3 | 3 | 4 | 2019-05-13 | 2 | 2800 |
+-----------+------------+----------+------------+----------+-------+
输出:
+-------------+--------------+
| product_id | product_name |
+-------------+--------------+
| 1 | S8 |
+-------------+--------------+
解释:
id 为 1 的产品仅在 2019 年春季销售。
id 为 2 的产品在 2019 年春季销售,但也在 2019 年春季之后销售。
id 为 3 的产品在 2019 年春季之后销售。
我们只返回 id 为 1 的产品,因为它是 2019 年春季才销售的产品。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
WITH SpringSales AS (
SELECT DISTINCT s.product_id
FROM Sales s
WHERE s.sale_date BETWEEN '2019-01-01' AND '2019-03-31'
),
NonSpringSales AS (
SELECT DISTINCT s.product_id
FROM Sales s
WHERE s.sale_date < '2019-01-01' OR s.sale_date > '2019-03-31'
)
SELECT p.product_id, p.product_name
FROM Product p
JOIN SpringSales ss ON p.product_id = ss.product_id
WHERE p.product_id NOT IN (SELECT product_id FROM NonSpringSales);

596. 超过 5 名学生的课

表: Courses

1
2
3
4
5
6
7
8
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| student | varchar |
| class | varchar |
+-------------+---------+
(student, class)是该表的主键(不同值的列的组合)。
该表的每一行表示学生的名字和他们注册的班级。

查询 至少有 5 个学生 的所有班级。

任意顺序 返回结果表。

结果格式如下所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
输入: 
Courses table:
+---------+----------+
| student | class |
+---------+----------+
| A | Math |
| B | English |
| C | Math |
| D | Biology |
| E | Math |
| F | Computer |
| G | Math |
| H | Math |
| I | Math |
+---------+----------+
输出:
+---------+
| class |
+---------+
| Math |
+---------+
解释:
-数学课有 6 个学生,所以我们包括它。
-英语课有 1 名学生,所以我们不包括它。
-生物课有 1 名学生,所以我们不包括它。
-计算机课有 1 个学生,所以我们不包括它。
1
2
3
4
SELECT class
FROM Courses
GROUP BY class
HAVING COUNT(student) >= 5;

GROUP BY: 按照 class 分组统计每个班级的学生数。

HAVING: 使用 HAVING 子句过滤出学生数大于或等于 5 的班级。

SELECT: 最终只返回符合条件的班级名称。


1729. 求关注者的数量

表: Followers

1
2
3
4
5
6
7
8
+-------------+------+
| Column Name | Type |
+-------------+------+
| user_id | int |
| follower_id | int |
+-------------+------+
(user_id, follower_id) 是这个表的主键(具有唯一值的列的组合)。
该表包含一个关注关系中关注者和用户的编号,其中关注者关注用户。

编写解决方案,对于每一个用户,返回该用户的关注者数量。

user_id 的顺序返回结果表。

查询结果的格式如下示例所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
输入:
Followers 表:
+---------+-------------+
| user_id | follower_id |
+---------+-------------+
| 0 | 1 |
| 1 | 0 |
| 2 | 0 |
| 2 | 1 |
+---------+-------------+
输出:
+---------+----------------+
| user_id | followers_count|
+---------+----------------+
| 0 | 1 |
| 1 | 1 |
| 2 | 2 |
+---------+----------------+
解释:
0 的关注者有 {1}
1 的关注者有 {0}
2 的关注者有 {0,1}
1
2
3
4
SELECT user_id, COUNT(follower_id) AS followers_count
FROM Followers
GROUP BY user_id
ORDER BY user_id;

COUNT(follower_id):统计每个用户被关注的数量,表示关注者的数量。

GROUP BY user_id:将结果按 user_id 分组,使每个用户的关注者数被单独统计。

ORDER BY user_id:确保结果按照 user_id 升序排列。


619. 只出现一次的最大数字

MyNumbers 表:

1
2
3
4
5
6
7
+-------------+------+
| Column Name | Type |
+-------------+------+
| num | int |
+-------------+------+
该表可能包含重复项(换句话说,在SQL中,该表没有主键)。
这张表的每一行都含有一个整数。

单一数字 是在 MyNumbers 表中只出现一次的数字。

找出最大的 单一数字 。如果不存在 单一数字 ,则返回 null

查询结果如下例所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
输入:
MyNumbers 表:
+-----+
| num |
+-----+
| 8 |
| 8 |
| 3 |
| 3 |
| 1 |
| 4 |
| 5 |
| 6 |
+-----+
输出:
+-----+
| num |
+-----+
| 6 |
+-----+
解释:单一数字有 1、4、5 和 6 。
6 是最大的单一数字,返回 6 。

示例 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
输入:
MyNumbers table:
+-----+
| num |
+-----+
| 8 |
| 8 |
| 7 |
| 7 |
| 3 |
| 3 |
| 3 |
+-----+
输出:
+------+
| num |
+------+
| null |
+------+
解释:输入的表中不存在单一数字,所以返回 null 。
1
2
3
4
5
6
7
SELECT MAX(num) AS num
FROM (
SELECT num
FROM MyNumbers
GROUP BY num
HAVING COUNT(*) = 1
) AS SingleNumbers;

1045. 买下所有产品的客户

Customer 表:

1
2
3
4
5
6
7
8
9
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| customer_id | int |
| product_key | int |
+-------------+---------+
该表可能包含重复的行。
customer_id 不为 NULL。
product_key 是 Product 表的外键(reference 列)。

Product 表:

1
2
3
4
5
6
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| product_key | int |
+-------------+---------+
product_key 是这张表的主键(具有唯一值的列)。

编写解决方案,报告 Customer 表中购买了 Product 表中所有产品的客户的 id。

返回结果表 无顺序要求

返回结果格式如下所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
输入:
Customer 表:
+-------------+-------------+
| customer_id | product_key |
+-------------+-------------+
| 1 | 5 |
| 2 | 6 |
| 3 | 5 |
| 3 | 6 |
| 1 | 6 |
+-------------+-------------+
Product 表:
+-------------+
| product_key |
+-------------+
| 5 |
| 6 |
+-------------+
输出:
+-------------+
| customer_id |
+-------------+
| 1 |
| 3 |
+-------------+
解释:
购买了所有产品(5 和 6)的客户的 id 是 1 和 3 。
1
2
3
4
SELECT customer_id
FROM Customer
GROUP BY customer_id
HAVING COUNT(DISTINCT product_key) = (SELECT COUNT(*) FROM Product);

5.高级查询和连接

1731. 每位经理的下属员工数量

表:Employees

1
2
3
4
5
6
7
8
9
10
+-------------+----------+
| Column Name | Type |
+-------------+----------+
| employee_id | int |
| name | varchar |
| reports_to | int |
| age | int |
+-------------+----------+
employee_id 是这个表中具有不同值的列。
该表包含员工以及需要听取他们汇报的上级经理的 ID 的信息。 有些员工不需要向任何人汇报(reports_to 为空)。

对于此问题,我们将至少有一个其他员工需要向他汇报的员工,视为一个经理。

编写一个解决方案来返回需要听取汇报的所有经理的 ID、名称、直接向该经理汇报的员工人数,以及这些员工的平均年龄,其中该平均年龄需要四舍五入到最接近的整数。

返回的结果集需要按照 employee_id 进行排序。

结果的格式如下:

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
输入:
Employees 表:
+-------------+---------+------------+-----+
| employee_id | name | reports_to | age |
+-------------+---------+------------+-----+
| 9 | Hercy | null | 43 |
| 6 | Alice | 9 | 41 |
| 4 | Bob | 9 | 36 |
| 2 | Winston | null | 37 |
+-------------+---------+------------+-----+
输出:
+-------------+-------+---------------+-------------+
| employee_id | name | reports_count | average_age |
+-------------+-------+---------------+-------------+
| 9 | Hercy | 2 | 39 |
+-------------+-------+---------------+-------------+
解释:
Hercy 有两个需要向他汇报的员工, 他们是 Alice and Bob. 他们的平均年龄是 (41+36)/2 = 38.5, 四舍五入的结果是 39.

示例 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
输入: 
Employees 表:
+-------------+---------+------------+-----+
| employee_id | name | reports_to | age |
|-------------|---------|------------|-----|
| 1 | Michael | null | 45 |
| 2 | Alice | 1 | 38 |
| 3 | Bob | 1 | 42 |
| 4 | Charlie | 2 | 34 |
| 5 | David | 2 | 40 |
| 6 | Eve | 3 | 37 |
| 7 | Frank | null | 50 |
| 8 | Grace | null | 48 |
+-------------+---------+------------+-----+
输出:
+-------------+---------+---------------+-------------+
| employee_id | name | reports_count | average_age |
| ----------- | ------- | ------------- | ----------- |
| 1 | Michael | 2 | 40 |
| 2 | Alice | 2 | 37 |
| 3 | Bob | 1 | 37 |
+-------------+---------+---------------+-------------+
1
2
3
4
5
6
7
SELECT e.employee_id, e.name, COUNT(r.employee_id) AS reports_count, 
ROUND(AVG(r.age)) AS average_age
FROM Employees e
LEFT JOIN Employees r ON e.employee_id = r.reports_to
WHERE r.employee_id IS NOT NULL
GROUP BY e.employee_id, e.name
ORDER BY e.employee_id;

LEFT JOIN:

  • Employees 表与自身连接,匹配出每个经理和直接向其汇报的员工。

COUNT(r.employee_id):

  • 统计每个经理的直接汇报员工人数。

AVG(r.age):

  • 计算直接向每个经理汇报的员工的平均年龄。

ROUND(AVG(r.age)):

  • 将平均年龄四舍五入到最接近的整数。

GROUP BY e.employee_id, e.name:

  • 按经理分组,确保统计正确。

ORDER BY e.employee_id:

  • employee_id 排序返回结果。

1789. 员工的直属部门

表:Employee

1
2
3
4
5
6
7
8
9
10
11
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| employee_id | int |
| department_id | int |
| primary_flag | varchar |
+---------------+---------+
这张表的主键为 employee_id, department_id (具有唯一值的列的组合)
employee_id 是员工的ID
department_id 是部门的ID,表示员工与该部门有关系
primary_flag 是一个枚举类型,值分别为('Y', 'N'). 如果值为'Y',表示该部门是员工的直属部门。 如果值是'N',则否

一个员工可以属于多个部门。当一个员工加入超过一个部门的时候,他需要决定哪个部门是他的直属部门。请注意,当员工只加入一个部门的时候,那这个部门将默认为他的直属部门,虽然表记录的值为'N'.

请编写解决方案,查出员工所属的直属部门。

返回结果 没有顺序要求

返回结果格式如下例子所示:

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
输入:
Employee table:
+-------------+---------------+--------------+
| employee_id | department_id | primary_flag |
+-------------+---------------+--------------+
| 1 | 1 | N |
| 2 | 1 | Y |
| 2 | 2 | N |
| 3 | 3 | N |
| 4 | 2 | N |
| 4 | 3 | Y |
| 4 | 4 | N |
+-------------+---------------+--------------+
输出:
+-------------+---------------+
| employee_id | department_id |
+-------------+---------------+
| 1 | 1 |
| 2 | 1 |
| 3 | 3 |
| 4 | 3 |
+-------------+---------------+
解释:
- 员工 1 的直属部门是 1
- 员工 2 的直属部门是 1
- 员工 3 的直属部门是 3
- 员工 4 的直属部门是 3
1
2
3
4
SELECT employee_id, department_id
FROM Employee
WHERE primary_flag = 'Y'
OR (employee_id NOT IN (SELECT employee_id FROM Employee WHERE primary_flag = 'Y') AND primary_flag = 'N');

primary_flag = 'Y':

  • 首先选择明确标记为直属部门的记录。

员工只有一个部门的情况:

  • 使用子查询 employee_id NOT IN (...) 来找出没有标记为 'Y' 的员工。
  • 对于这些员工,选取 primary_flag = 'N' 的部门。

610. 判断三角形

表: Triangle

1
2
3
4
5
6
7
8
9
+-------------+------+
| Column Name | Type |
+-------------+------+
| x | int |
| y | int |
| z | int |
+-------------+------+
在 SQL 中,(x, y, z)是该表的主键列。
该表的每一行包含三个线段的长度。

对每三个线段报告它们是否可以形成一个三角形。

任意顺序 返回结果表。

查询结果格式如下所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
输入: 
Triangle 表:
+----+----+----+
| x | y | z |
+----+----+----+
| 13 | 15 | 30 |
| 10 | 20 | 15 |
+----+----+----+
输出:
+----+----+----+----------+
| x | y | z | triangle |
+----+----+----+----------+
| 13 | 15 | 30 | No |
| 10 | 20 | 15 | Yes |
+----+----+----+----------+
1
2
3
4
5
6
SELECT x, y, z,
CASE
WHEN x + y > z AND x + z > y AND y + z > x THEN 'Yes'
ELSE 'No'
END AS triangle
FROM Triangle;

180. 连续出现的数字

表:Logs

1
2
3
4
5
6
7
8
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| id | int |
| num | varchar |
+-------------+---------+
在 SQL 中,id 是该表的主键。
id 是一个自增列。

找出所有至少连续出现三次的数字。

返回的结果表中的数据可以按 任意顺序 排列。

结果格式如下面的例子所示:

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
输入:
Logs 表:
+----+-----+
| id | num |
+----+-----+
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 2 |
| 5 | 1 |
| 6 | 2 |
| 7 | 2 |
+----+-----+
输出:
Result 表:
+-----------------+
| ConsecutiveNums |
+-----------------+
| 1 |
+-----------------+
解释:1 是唯一连续出现至少三次的数字。
1
2
3
4
5
6
7
8
9
WITH ConsecutiveLogs AS (
SELECT num,
LEAD(num) OVER (ORDER BY id) AS next_num1,
LEAD(num, 2) OVER (ORDER BY id) AS next_num2
FROM Logs
)
SELECT DISTINCT num AS ConsecutiveNums
FROM ConsecutiveLogs
WHERE num = next_num1 AND num = next_num2;

WITH ConsecutiveLogs:

  • 使用窗口函数 LEAD,为每条记录计算它的下一个和下两个数字。
  • 这样可以方便地检测连续的三个相同数字。

主查询:

  • ConsecutiveLogs 中筛选出 num 等于 next_num1 且等于 next_num2 的记录,表示该数字至少连续出现三次。
  • 使用 DISTINCT 确保结果中每个数字只出现一次。

LEAD 的语法

LEAD 是一种窗口函数,用于从当前行向后(即下一行)查找某列的值。它非常适合处理序列中后续的值,无需在表中进行多次自连接。

1
2
3
4
5
sql


复制代码
LEAD(column_name, offset, default_value) OVER (PARTITION BY column_name ORDER BY column_name)
  • column_name: 要获取的列值。
  • offset(可选,默认为1): 指定向后偏移的行数。
  • default_value(可选): 如果没有足够的行来计算结果,可以返回默认值。
  • OVER: 定义分区和排序规则。

    用途和例子

假设有一张 Logs 表:

1
2
3
4
5
6
7
8
9
plaintext复制代码+----+-----+
| id | num |
+----+-----+
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 2 |
| 5 | 1 |
+----+-----+

使用 LEAD 示例

1
2
3
sql复制代码SELECT id, num, 
LEAD(num) OVER (ORDER BY id) AS next_num
FROM Logs;

输出:

1
2
3
4
5
6
7
8
9
plaintext复制代码+----+-----+----------+
| id | num | next_num |
+----+-----+----------+
| 1 | 1 | 1 |
| 2 | 1 | 1 |
| 3 | 1 | 2 |
| 4 | 2 | 1 |
| 5 | 1 | NULL |
+----+-----+----------+
  • LEAD(num) 返回当前行后面的值。
  • 对于最后一行,由于没有后续值,返回 NULL

    在连续检测中的应用

在检测某个值是否连续多次出现时,LEAD 允许我们简单地查看当前值是否与下一行或下两行相同,而无需复杂的连接操作。例如:

1
2
3
4
5
6
7
8
9
sql复制代码WITH ConsecutiveLogs AS (
SELECT id, num,
LEAD(num) OVER (ORDER BY id) AS next_num1,
LEAD(num, 2) OVER (ORDER BY id) AS next_num2
FROM Logs
)
SELECT DISTINCT num
FROM ConsecutiveLogs
WHERE num = next_num1 AND num = next_num2;
  • LEAD(num)LEAD(num, 2) 分别用于检查当前值是否与下一行和下两行相同。

这种方式高效且易读,非常适合连续性检测等问题。


1164. 指定日期的产品价格

产品数据表: Products

1
2
3
4
5
6
7
8
9
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| product_id | int |
| new_price | int |
| change_date | date |
+---------------+---------+
(product_id, change_date) 是此表的主键(具有唯一值的列组合)。
这张表的每一行分别记录了 某产品 在某个日期 更改后 的新价格。

编写一个解决方案,找出在 2019-08-16 时全部产品的价格,假设所有产品在修改前的价格都是 10

任意顺序 返回结果表。

结果格式如下例所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
输入:
Products 表:
+------------+-----------+-------------+
| product_id | new_price | change_date |
+------------+-----------+-------------+
| 1 | 20 | 2019-08-14 |
| 2 | 50 | 2019-08-14 |
| 1 | 30 | 2019-08-15 |
| 1 | 35 | 2019-08-16 |
| 2 | 65 | 2019-08-17 |
| 3 | 20 | 2019-08-18 |
+------------+-----------+-------------+
输出:
+------------+-------+
| product_id | price |
+------------+-------+
| 2 | 50 |
| 1 | 35 |
| 3 | 10 |
+------------+-------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
WITH LatestPrices AS (
SELECT product_id, new_price, change_date
FROM Products
WHERE change_date <= '2019-08-16'
),
RankedPrices AS (
SELECT product_id, new_price,
ROW_NUMBER() OVER (PARTITION BY product_id ORDER BY change_date DESC) AS rn
FROM LatestPrices
)
SELECT p.product_id,
COALESCE(lp.new_price, 10) AS price
FROM (SELECT DISTINCT product_id FROM Products) p
LEFT JOIN RankedPrices lp
ON p.product_id = lp.product_id AND lp.rn = 1;

LatestPrices:

  • 筛选出所有 change_date 小于等于 '2019-08-16' 的记录,代表在该日期之前或当天的所有价格。

RankedPrices:

  • 使用 ROW_NUMBER()product_idchange_date 降序排列,获取每个产品最新的价格。

主查询:

  • 使用 COALESCE 将没有价格变更记录的产品的价格默认为 10
  • 使用 LEFT JOIN 确保所有产品都包含在结果中,即使它们没有在目标日期之前的价格记录。

1204. 最后一个能进入巴士的人

表: Queue

1
2
3
4
5
6
7
8
9
10
11
12
13
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| person_id | int |
| person_name | varchar |
| weight | int |
| turn | int |
+-------------+---------+
person_id 是这个表具有唯一值的列。
该表展示了所有候车乘客的信息。
表中 person_id 和 turn 列将包含从 1 到 n 的所有数字,其中 n 是表中的行数。
turn 决定了候车乘客上巴士的顺序,其中 turn=1 表示第一个上巴士,turn=n 表示最后一个上巴士。
weight 表示候车乘客的体重,以千克为单位。

有一队乘客在等着上巴士。然而,巴士有1000 千克 的重量限制,所以其中一部分乘客可能无法上巴士。

编写解决方案找出 最后一个 上巴士且不超过重量限制的乘客,并报告 person_name 。题目测试用例确保顺位第一的人可以上巴士且不会超重。

返回结果格式如下所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
输入:
Queue 表
+-----------+-------------+--------+------+
| person_id | person_name | weight | turn |
+-----------+-------------+--------+------+
| 5 | Alice | 250 | 1 |
| 4 | Bob | 175 | 5 |
| 3 | Alex | 350 | 2 |
| 6 | John Cena | 400 | 3 |
| 1 | Winston | 500 | 6 |
| 2 | Marie | 200 | 4 |
+-----------+-------------+--------+------+
输出:
+-------------+
| person_name |
+-------------+
| John Cena |
+-------------+
解释:
为了简化,Queue 表按 turn 列由小到大排序。
+------+----+-----------+--------+--------------+
| Turn | ID | Name | Weight | Total Weight |
+------+----+-----------+--------+--------------+
| 1 | 5 | Alice | 250 | 250 |
| 2 | 3 | Alex | 350 | 600 |
| 3 | 6 | John Cena | 400 | 1000 | (最后一个上巴士)
| 4 | 2 | Marie | 200 | 1200 | (无法上巴士)
| 5 | 4 | Bob | 175 | ___ |
| 6 | 1 | Winston | 500 | ___ |
+------+----+-----------+--------+--------------+
1
2
3
4
5
6
7
8
9
10
WITH CumulativeWeight AS (
SELECT person_id, person_name, weight, turn,
SUM(weight) OVER (ORDER BY turn) AS total_weight
FROM Queue
)
SELECT person_name
FROM CumulativeWeight
WHERE total_weight <= 1000
ORDER BY total_weight DESC
LIMIT 1;

CumulativeWeight 子查询:

  • 使用 SUM(weight) OVER (ORDER BY turn) 计算乘客按 turn 顺序的累积体重。

过滤条件:

  • 筛选 total_weight 小于或等于 1000 的乘客。

主查询:

  • 按累积体重降序排序 (ORDER BY total_weight DESC),并限制为最后一个符合条件的乘客 (LIMIT 1)。

返回值:

  • 输出字段为 person_name,即最后一个能上巴士的乘客的姓名。

1907. 按分类统计薪水

表: Accounts

1
2
3
4
5
6
7
8
+-------------+------+
| 列名 | 类型 |
+-------------+------+
| account_id | int |
| income | int |
+-------------+------+
在 SQL 中,account_id 是这个表的主键。
每一行都包含一个银行帐户的月收入的信息。

查询每个工资类别的银行账户数量。 工资类别如下:

  • "Low Salary":所有工资 严格低于 20000 美元。
  • "Average Salary"包含 范围内的所有工资 [$20000, $50000]
  • "High Salary":所有工资 严格大于 50000 美元。

结果表 必须 包含所有三个类别。 如果某个类别中没有帐户,则报告 0

任意顺序 返回结果表。

查询结果格式如下示例。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
输入:
Accounts 表:
+------------+--------+
| account_id | income |
+------------+--------+
| 3 | 108939 |
| 2 | 12747 |
| 8 | 87709 |
| 6 | 91796 |
+------------+--------+
输出:
+----------------+----------------+
| category | accounts_count |
+----------------+----------------+
| Low Salary | 1 |
| Average Salary | 0 |
| High Salary | 3 |
+----------------+----------------+
解释:
低薪: 有一个账户 2.
中等薪水: 没有.
高薪: 有三个账户,他们是 3, 6和 8.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
WITH SalaryCategories AS (
SELECT
CASE
WHEN income < 20000 THEN 'Low Salary'
WHEN income BETWEEN 20000 AND 50000 THEN 'Average Salary'
ELSE 'High Salary'
END AS category
FROM Accounts
)
SELECT
category,
COUNT(*) AS accounts_count
FROM SalaryCategories
GROUP BY category
UNION ALL
SELECT 'Low Salary', 0 WHERE NOT EXISTS (SELECT 1 FROM SalaryCategories WHERE category = 'Low Salary')
UNION ALL
SELECT 'Average Salary', 0 WHERE NOT EXISTS (SELECT 1 FROM SalaryCategories WHERE category = 'Average Salary')
UNION ALL
SELECT 'High Salary', 0 WHERE NOT EXISTS (SELECT 1 FROM SalaryCategories WHERE category = 'High Salary');

CASE 分类:

  • 根据 income 的范围,将工资划分为 "Low Salary", "Average Salary", 和 "High Salary" 三个类别。

GROUP BYCOUNT:

  • 对每个类别进行分组,计算其账户数量。

UNION ALLNOT EXISTS:

  • 确保每个类别都包含在结果中,即使该类别没有账户,则返回 0。

1978. 上级经理已离职的公司员工

表: Employees

1
2
3
4
5
6
7
8
9
10
11
+-------------+----------+
| Column Name | Type |
+-------------+----------+
| employee_id | int |
| name | varchar |
| manager_id | int |
| salary | int |
+-------------+----------+
在 SQL 中,employee_id 是这个表的主键。
这个表包含了员工,他们的薪水和上级经理的id。
有一些员工没有上级经理(其 manager_id 是空值)。

查找这些员工的id,他们的薪水严格少于$30000 并且他们的上级经理已离职。当一个经理离开公司时,他们的信息需要从员工表中删除掉,但是表中的员工的manager_id 这一列还是设置的离职经理的id 。

返回的结果按照employee_id从小到大排序。

查询结果如下所示:

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
输入:
Employees table:
+-------------+-----------+------------+--------+
| employee_id | name | manager_id | salary |
+-------------+-----------+------------+--------+
| 3 | Mila | 9 | 60301 |
| 12 | Antonella | null | 31000 |
| 13 | Emery | null | 67084 |
| 1 | Kalel | 11 | 21241 |
| 9 | Mikaela | null | 50937 |
| 11 | Joziah | 6 | 28485 |
+-------------+-----------+------------+--------+
输出:
+-------------+
| employee_id |
+-------------+
| 11 |
+-------------+

解释:
薪水少于 30000 美元的员工有 1 号(Kalel) 和 11号 (Joziah)。
Kalel 的上级经理是 11 号员工,他还在公司上班(他是 Joziah )。
Joziah 的上级经理是 6 号员工,他已经离职,因为员工表里面已经没有 6 号员工的信息了,它被删除了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
WITH MissingManagers AS (
SELECT e.employee_id
FROM Employees e
LEFT JOIN Employees m ON e.manager_id = m.employee_id
WHERE m.employee_id IS NULL AND e.manager_id IS NOT NULL
),
LowSalaryEmployees AS (
SELECT employee_id
FROM Employees
WHERE salary < 30000
)
SELECT e.employee_id
FROM LowSalaryEmployees e
JOIN MissingManagers mm ON e.employee_id = mm.employee_id
ORDER BY e.employee_id;

issingManagers CTE:

  • 找出 manager_id 不为空但其对应的 manager_idEmployees 表中不存在的员工,即其上级经理已经离职的员工。

LowSalaryEmployees CTE:

  • 找出薪水低于 30,000 美元的员工。

主查询:

  • LowSalaryEmployeesMissingManagers 进行连接,筛选出既满足薪水条件又有离职经理的员工。
  • 结果按 employee_id 升序排序。

626. 换座位

表: Seat

1
2
3
4
5
6
7
8
9
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| id | int |
| student | varchar |
+-------------+---------+
id 是该表的主键(唯一值)列。
该表的每一行都表示学生的姓名和 ID。
ID 序列始终从 1 开始并连续增加。

编写解决方案来交换每两个连续的学生的座位号。如果学生的数量是奇数,则最后一个学生的id不交换。

id 升序 返回结果表。

查询结果格式如下所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
输入: 
Seat 表:
+----+---------+
| id | student |
+----+---------+
| 1 | Abbot |
| 2 | Doris |
| 3 | Emerson |
| 4 | Green |
| 5 | Jeames |
+----+---------+
输出:
+----+---------+
| id | student |
+----+---------+
| 1 | Doris |
| 2 | Abbot |
| 3 | Green |
| 4 | Emerson |
| 5 | Jeames |
+----+---------+
解释:
请注意,如果学生人数为奇数,则不需要更换最后一名学生的座位。
1
2
3
4
5
6
7
8
9
10
11
12
13
WITH SeatSwap AS (
SELECT
CASE
WHEN MOD(id, 2) = 1 AND id + 1 <= (SELECT MAX(id) FROM Seat) THEN id + 1
WHEN MOD(id, 2) = 0 THEN id - 1
ELSE id
END AS new_id,
student
FROM Seat
)
SELECT new_id AS id, student
FROM SeatSwap
ORDER BY id;

SeatSwap CTE:

  • 通过

    1
    CASE

    语句计算每个学生的新

    1
    id

    • 如果 id 为奇数且其后有学生,则新 idid + 1
    • 如果 id 为偶数,则新 idid - 1
    • 如果是最后一个学生且为奇数(无对应交换学生),则保留原 id

主查询:

  • SeatSwap 的结果按新 id 升序排序并返回学生信息。

保证顺序和正确性:

  • 最后一个学生的座位不变,其他每两个连续学生的座位号完成交换。

1341. 电影评分

表:Movies

1
2
3
4
5
6
7
8
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| movie_id | int |
| title | varchar |
+---------------+---------+
movie_id 是这个表的主键(具有唯一值的列)。
title 是电影的名字。

表:Users

1
2
3
4
5
6
7
8
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| user_id | int |
| name | varchar |
+---------------+---------+
user_id 是表的主键(具有唯一值的列)。
'name' 列具有唯一值。

表:MovieRating

1
2
3
4
5
6
7
8
9
10
11
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| movie_id | int |
| user_id | int |
| rating | int |
| created_at | date |
+---------------+---------+
(movie_id, user_id) 是这个表的主键(具有唯一值的列的组合)。
这个表包含用户在其评论中对电影的评分 rating 。
created_at 是用户的点评日期。

请你编写一个解决方案:

  • 查找评论电影数量最多的用户名。如果出现平局,返回字典序较小的用户名。
  • 查找在 February 2020 平均评分最高 的电影名称。如果出现平局,返回字典序较小的电影名称。

字典序 ,即按字母在字典中出现顺序对字符串排序,字典序较小则意味着排序靠前。

返回结果格式如下例所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
输入:
Movies 表:
+-------------+--------------+
| movie_id | title |
+-------------+--------------+
| 1 | Avengers |
| 2 | Frozen 2 |
| 3 | Joker |
+-------------+--------------+
Users 表:
+-------------+--------------+
| user_id | name |
+-------------+--------------+
| 1 | Daniel |
| 2 | Monica |
| 3 | Maria |
| 4 | James |
+-------------+--------------+
MovieRating 表:
+-------------+--------------+--------------+-------------+
| movie_id | user_id | rating | created_at |
+-------------+--------------+--------------+-------------+
| 1 | 1 | 3 | 2020-01-12 |
| 1 | 2 | 4 | 2020-02-11 |
| 1 | 3 | 2 | 2020-02-12 |
| 1 | 4 | 1 | 2020-01-01 |
| 2 | 1 | 5 | 2020-02-17 |
| 2 | 2 | 2 | 2020-02-01 |
| 2 | 3 | 2 | 2020-03-01 |
| 3 | 1 | 3 | 2020-02-22 |
| 3 | 2 | 4 | 2020-02-25 |
+-------------+--------------+--------------+-------------+
输出:
Result 表:
+--------------+
| results |
+--------------+
| Daniel |
| Frozen 2 |
+--------------+
解释:
Daniel 和 Monica 都点评了 3 部电影("Avengers", "Frozen 2" 和 "Joker") 但是 Daniel 字典序比较小。
Frozen 2 和 Joker 在 2 月的评分都是 3.5,但是 Frozen 2 的字典序比较小。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
-- 查找评论电影数量最多的用户名
WITH UserMovieCounts AS (
SELECT
u.name,
COUNT(mr.movie_id) AS review_count
FROM
Users u
JOIN
MovieRating mr
ON u.user_id = mr.user_id
GROUP BY
u.name
),
MaxReviewCount AS (
SELECT
MAX(review_count) AS max_reviews
FROM
UserMovieCounts
),
TopUsers AS (
SELECT
umc.name
FROM
UserMovieCounts umc
JOIN
MaxReviewCount mrc
ON umc.review_count = mrc.max_reviews
ORDER BY
umc.name
LIMIT 1
)

-- 查找在 2020 年 2 月平均评分最高的电影名称
, FebruaryRatings AS (
SELECT
mr.movie_id,
AVG(mr.rating) AS avg_rating
FROM
MovieRating mr
WHERE
mr.created_at BETWEEN '2020-02-01' AND '2020-02-29'
GROUP BY
mr.movie_id
),
MaxAverageRating AS (
SELECT
MAX(avg_rating) AS max_avg_rating
FROM
FebruaryRatings
),
TopMovies AS (
SELECT
m.title
FROM
FebruaryRatings fr
JOIN
MaxAverageRating mar
ON fr.avg_rating = mar.max_avg_rating
JOIN
Movies m
ON fr.movie_id = m.movie_id
ORDER BY
m.title
LIMIT 1
)

-- 返回结果
SELECT
tu.name AS results
FROM
TopUsers tu
UNION ALL
SELECT
tm.title AS results
FROM
TopMovies tm;
  1. UserMovieCounts: 计算每个用户的点评电影数量。
  2. MaxReviewCount: 找到点评电影数量最多的用户。
  3. TopUsers: 获取点评数量最多且字典序最小的用户名。
  4. FebruaryRatings: 计算2020年2月每部电影的平均评分。
  5. MaxAverageRating: 找到2020年2月平均评分最高的电影。
  6. TopMovies: 获取评分最高且字典序最小的电影名称。

最后通过 UNION ALL 合并结果,并以正确的顺序返回答案。


UNION ALL 是 SQL 中用来将多个查询结果组合在一起的关键字。其作用是将两个或多个结果集合并为一个集合,并包括所有的行(即使存在重复)


UNION ALL

  1. 包含重复行
    • 不会对结果进行去重。
    • 如果多个查询返回相同的记录,它们会全部出现在最终的结果集中。
  2. 效率高
    • 因为 UNION ALL 不需要对结果集进行去重(不像 UNION),所以在处理大数据量时性能更高。
  3. 结果顺序
    • 合并的结果集不会自动排序。可以通过 ORDER BY 对整个结果进行排序。

使用场景

  1. 需要保留重复记录
    • 如果希望保留所有重复记录(如日志或交易数据的合并),UNION ALL 是合适的选择。
  2. 性能优先
    • 在大规模数据处理中,去重操作可能代价高昂。使用 UNION ALL 可以避免去重的开销,提升查询速度。

示例

假设有两张表 Table1Table2

数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Table1:
+----+------+
| ID | Name |
+----+------+
| 1 | A |
| 2 | B |
+----+------+

Table2:
+----+------+
| ID | Name |
+----+------+
| 2 | B |
| 3 | C |
+----+------+

查询:

1
2
3
SELECT * FROM Table1
UNION ALL
SELECT * FROM Table2;

结果:

1
2
3
4
5
6
7
8
+----+------+
| ID | Name |
+----+------+
| 1 | A |
| 2 | B |
| 2 | B | -- 包含重复行
| 3 | C |
+----+------+

对比 UNION

如果使用 UNION

1
2
3
SELECT * FROM Table1
UNION
SELECT * FROM Table2;

结果会去重:

1
2
3
4
5
6
7
+----+------+
| ID | Name |
+----+------+
| 1 | A |
| 2 | B |
| 3 | C |
+----+------+

总结

UNION ALLUNION 的高效版本,适用于无需去重的数据合并场景。在合并结果时需明确是否需要去重,合理选择 UNIONUNION ALL


1321. 餐馆营业额变化增长

表: Customer

1
2
3
4
5
6
7
8
9
10
11
12
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| customer_id | int |
| name | varchar |
| visited_on | date |
| amount | int |
+---------------+---------+
在 SQL 中,(customer_id, visited_on) 是该表的主键。
该表包含一家餐馆的顾客交易数据。
visited_on 表示 (customer_id) 的顾客在 visited_on 那天访问了餐馆。
amount 是一个顾客某一天的消费总额。

你是餐馆的老板,现在你想分析一下可能的营业额变化增长(每天至少有一位顾客)。

计算以 7 天(某日期 + 该日期前的 6 天)为一个时间段的顾客消费平均值。average_amount保留两位小数。

结果按 visited_on 升序排序

返回结果格式的例子如下。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
输入:
Customer 表:
+-------------+--------------+--------------+-------------+
| customer_id | name | visited_on | amount |
+-------------+--------------+--------------+-------------+
| 1 | Jhon | 2019-01-01 | 100 |
| 2 | Daniel | 2019-01-02 | 110 |
| 3 | Jade | 2019-01-03 | 120 |
| 4 | Khaled | 2019-01-04 | 130 |
| 5 | Winston | 2019-01-05 | 110 |
| 6 | Elvis | 2019-01-06 | 140 |
| 7 | Anna | 2019-01-07 | 150 |
| 8 | Maria | 2019-01-08 | 80 |
| 9 | Jaze | 2019-01-09 | 110 |
| 1 | Jhon | 2019-01-10 | 130 |
| 3 | Jade | 2019-01-10 | 150 |
+-------------+--------------+--------------+-------------+
输出:
+--------------+--------------+----------------+
| visited_on | amount | average_amount |
+--------------+--------------+----------------+
| 2019-01-07 | 860 | 122.86 |
| 2019-01-08 | 840 | 120 |
| 2019-01-09 | 840 | 120 |
| 2019-01-10 | 1000 | 142.86 |
+--------------+--------------+----------------+
解释:
第一个七天消费平均值从 2019-01-01 到 2019-01-07 是restaurant-growth/restaurant-growth/ (100 + 110 + 120 + 130 + 110 + 140 + 150)/7 = 122.86
第二个七天消费平均值从 2019-01-02 到 2019-01-08 是 (110 + 120 + 130 + 110 + 140 + 150 + 80)/7 = 120
第三个七天消费平均值从 2019-01-03 到 2019-01-09 是 (120 + 130 + 110 + 140 + 150 + 80 + 110)/7 = 120
第四个七天消费平均值从 2019-01-04 到 2019-01-10 是 (130 + 110 + 140 + 150 + 80 + 110 + 130 + 150)/7 = 142.86
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
WITH MyClist as(
SELECT
c.visited_on,
SUM(c.amount) as amount
FROM Customer c
GROUP BY
c.visited_on
),
SevenDayWindow AS (
SELECT
c1.visited_on,
SUM(c2.amount) AS total_amount,
COUNT(DISTINCT c2.visited_on) AS total_days
FROM
MyClist c1
JOIN
MyClist c2
ON
c2.visited_on BETWEEN DATE_SUB(c1.visited_on, INTERVAL 6 DAY) AND c1.visited_on
GROUP BY
c1.visited_on
HAVING
total_days = 7 -- 确保滚动窗口的天数为 7
)
SELECT
visited_on,
total_amount AS amount,
ROUND(total_amount / total_days, 2) AS average_amount
FROM
SevenDayWindow
ORDER BY
visited_on;

MyClist 子句:

  • 计算每天的总消费金额(SUM(c.amount)),按日期(visited_on)分组。
  • 结果是一个日期-金额对,每个日期只有一条记录。

SevenDayWindow 子句:

  • 对于每个日期,找到它之前 6 天内(包括当天)的所有日期。
  • 计算 7 天总消费金额和涉及的天数(COUNT(c2.visited_on))。
  • 使用 HAVING total_days = 7 确保只保留满足完整 7 天的记录。

最终查询:

  • 输出日期、总消费金额、平均消费金额(保留两位小数)。
  • 按日期升序排序。

602. 好友申请 II :谁有最多的好友

RequestAccepted 表:

1
2
3
4
5
6
7
8
9
+----------------+---------+
| Column Name | Type |
+----------------+---------+
| requester_id | int |
| accepter_id | int |
| accept_date | date |
+----------------+---------+
(requester_id, accepter_id) 是这张表的主键(具有唯一值的列的组合)。
这张表包含发送好友请求的人的 ID ,接收好友请求的人的 ID ,以及好友请求通过的日期。

编写解决方案,找出拥有最多的好友的人和他拥有的好友数目。

生成的测试用例保证拥有最多好友数目的只有 1 个人。

查询结果格式如下例所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
输入:
RequestAccepted 表:
+--------------+-------------+-------------+
| requester_id | accepter_id | accept_date |
+--------------+-------------+-------------+
| 1 | 2 | 2016/06/03 |
| 1 | 3 | 2016/06/08 |
| 2 | 3 | 2016/06/08 |
| 3 | 4 | 2016/06/09 |
+--------------+-------------+-------------+
输出:
+----+-----+
| id | num |
+----+-----+
| 3 | 3 |
+----+-----+
解释:
编号为 3 的人是编号为 1 ,2 和 4 的人的好友,所以他总共有 3 个好友,比其他人都多。

进阶:在真实世界里,可能会有多个人拥有好友数相同且最多,你能找到所有这些人吗?

1
2
3
4
5
6
7
8
9
10
11
SELECT id, COUNT(*) AS num
FROM (
SELECT requester_id AS id
FROM RequestAccepted
UNION ALL
SELECT accepter_id AS id
FROM RequestAccepted
) AS combined_ids
GROUP BY id
ORDER BY num DESC
LIMIT 1;
1
2
3
4
5
SELECT requester_id AS id
FROM RequestAccepted
UNION ALL
SELECT accepter_id AS id
FROM RequestAccepted

输出:

1
2
3
4
5
6
7
8
9
10
| id |
| -- |
| 1 |
| 1 |
| 2 |
| 3 |
| 2 |
| 3 |
| 3 |
| 4 |


585. 2016年的投资

Insurance 表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+-------------+-------+
| Column Name | Type |
+-------------+-------+
| pid | int |
| tiv_2015 | float |
| tiv_2016 | float |
| lat | float |
| lon | float |
+-------------+-------+
pid 是这张表的主键(具有唯一值的列)。
表中的每一行都包含一条保险信息,其中:
pid 是投保人的投保编号。
tiv_2015 是该投保人在 2015 年的总投保金额,tiv_2016 是该投保人在 2016 年的总投保金额。
lat 是投保人所在城市的纬度。题目数据确保 lat 不为空。
lon 是投保人所在城市的经度。题目数据确保 lon 不为空。

编写解决方案报告 2016 年 (tiv_2016) 所有满足下述条件的投保人的投保金额之和:

  • 他在 2015 年的投保额 (tiv_2015) 至少跟一个其他投保人在 2015 年的投保额相同。
  • 他所在的城市必须与其他投保人都不同(也就是说 (lat, lon) 不能跟其他任何一个投保人完全相同)。

tiv_2016 四舍五入的 两位小数

查询结果格式如下例所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
输入:
Insurance 表:
+-----+----------+----------+-----+-----+
| pid | tiv_2015 | tiv_2016 | lat | lon |
+-----+----------+----------+-----+-----+
| 1 | 10 | 5 | 10 | 10 |
| 2 | 20 | 20 | 20 | 20 |
| 3 | 10 | 30 | 20 | 20 |
| 4 | 10 | 40 | 40 | 40 |
+-----+----------+----------+-----+-----+
输出:
+----------+
| tiv_2016 |
+----------+
| 45.00 |
+----------+
解释:
表中的第一条记录和最后一条记录都满足两个条件。
tiv_2015 值为 10 与第三条和第四条记录相同,且其位置是唯一的。

第二条记录不符合任何一个条件。其 tiv_2015 与其他投保人不同,并且位置与第三条记录相同,这也导致了第三条记录不符合题目要求。
因此,结果是第一条记录和最后一条记录的 tiv_2016 之和,即 45 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 查询符合条件的投保人在2016年的投保金额之和
SELECT ROUND(SUM(tiv_2016), 2) AS tiv_2016
FROM Insurance
WHERE tiv_2015 IN (
-- 找出在2015年投保金额相同的投保人
SELECT tiv_2015
FROM Insurance
GROUP BY tiv_2015
HAVING COUNT(*) > 1
)
AND (lat, lon) NOT IN (
-- 找出位置重复的城市
SELECT lat, lon
FROM Insurance
GROUP BY lat, lon
HAVING COUNT(*) > 1
);

第一部分

  • 子查询 SELECT tiv_2015 FROM Insurance GROUP BY tiv_2015 HAVING COUNT(*) > 1 找出在 tiv_2015 中存在重复值的记录。

第二部分

  • 子查询 SELECT lat, lon FROM Insurance GROUP BY lat, lon HAVING COUNT(*) > 1 找出重复的 (lat, lon) 对。

过滤条件

  • 主查询中筛选 tiv_2015 存在重复值的记录,且 (lat, lon) 不在重复的位置对中。

求和与四舍五入

  • 使用 SUM(tiv_2016) 求和,并用 ROUND 将结果保留两位小数。

185. 部门工资前三高的所有员工

表: Employee

1
2
3
4
5
6
7
8
9
10
11
+--------------+---------+
| Column Name | Type |
+--------------+---------+
| id | int |
| name | varchar |
| salary | int |
| departmentId | int |
+--------------+---------+
id 是该表的主键列(具有唯一值的列)。
departmentId 是 Department 表中 ID 的外键(reference 列)。
该表的每一行都表示员工的ID、姓名和工资。它还包含了他们部门的ID。

表: Department

1
2
3
4
5
6
7
8
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| id | int |
| name | varchar |
+-------------+---------+
id 是该表的主键列(具有唯一值的列)。
该表的每一行表示部门ID和部门名。

公司的主管们感兴趣的是公司每个部门中谁赚的钱最多。一个部门的 高收入者 是指一个员工的工资在该部门的 不同 工资中 排名前三

编写解决方案,找出每个部门中 收入高的员工

任意顺序 返回结果表。

返回结果格式如下所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
输入: 
Employee 表:
+----+-------+--------+--------------+
| id | name | salary | departmentId |
+----+-------+--------+--------------+
| 1 | Joe | 85000 | 1 |
| 2 | Henry | 80000 | 2 |
| 3 | Sam | 60000 | 2 |
| 4 | Max | 90000 | 1 |
| 5 | Janet | 69000 | 1 |
| 6 | Randy | 85000 | 1 |
| 7 | Will | 70000 | 1 |
+----+-------+--------+--------------+
Department 表:
+----+-------+
| id | name |
+----+-------+
| 1 | IT |
| 2 | Sales |
+----+-------+
输出:
+------------+----------+--------+
| Department | Employee | Salary |
+------------+----------+--------+
| IT | Max | 90000 |
| IT | Joe | 85000 |
| IT | Randy | 85000 |
| IT | Will | 70000 |
| Sales | Henry | 80000 |
| Sales | Sam | 60000 |
+------------+----------+--------+
解释:
在IT部门:
- Max的工资最高
- 兰迪和乔都赚取第二高的独特的薪水
- 威尔的薪水是第三高的

在销售部:
- 亨利的工资最高
- 山姆的薪水第二高
- 没有第三高的工资,因为只有两名员工
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- 修改后的查询每个部门收入最高的员工的 SQL 语句
WITH RankedSalaries AS (
SELECT
e.id AS employee_id,
e.name AS employee_name,
e.salary,
e.departmentId,
d.name AS department_name,
DENSE_RANK() OVER (PARTITION BY e.departmentId ORDER BY e.salary DESC) AS rk
FROM
Employee e
JOIN
Department d
ON
e.departmentId = d.id
)
SELECT
department_name AS Department,
employee_name AS Employee,
salary AS Salary
FROM
RankedSalaries
WHERE
rk <= 3;

RANK()DENSE_RANK() 的介绍

两者是 SQL 中的窗口函数,用于对数据集进行排名。它们主要的区别在于 排名中是否存在跳跃


  1. RANK()
  • 功能:为每个分组的行按指定的排序条件分配一个排名。如果有多个行具有相同的值,它们会共享相同的排名,但是排名会跳过。

  • 特性

    • 相同值的记录共享相同排名。
    • 排名存在跳跃。例如:如果两条记录共享排名 1,下一条记录的排名将是 3,而不是 2。
  • 语法

    1
    RANK() OVER (PARTITION BY group_column ORDER BY sort_column DESC)
  • 示例: 表 Scores

    | id | name | score |
    | —— | ———- | ——- |
    | 1 | Alice | 90 |
    | 2 | Bob | 85 |
    | 3 | Charlie | 85 |
    | 4 | Dave | 80 |

    SQL 查询:

    1
    SELECT name, score, RANK() OVER (ORDER BY score DESC) AS rank FROM Scores;

    输出:

    | name | score | rank |
    | ———- | ——- | —— |
    | Alice | 90 | 1 |
    | Bob | 85 | 2 |
    | Charlie | 85 | 2 |
    | Dave | 80 | 4 |


  1. DENSE_RANK()
  • 功能:类似于 RANK(),但没有排名跳跃。即使有多个行共享相同的值,它们共享相同的排名,下一条记录的排名紧接其后。

  • 特性

    • 相同值的记录共享相同排名。
    • 排名连续,不会跳跃。
  • 语法

    1
    DENSE_RANK() OVER (PARTITION BY group_column ORDER BY sort_column DESC)
  • 示例: 表 Scores

    | id | name | score |
    | —— | ———- | ——- |
    | 1 | Alice | 90 |
    | 2 | Bob | 85 |
    | 3 | Charlie | 85 |
    | 4 | Dave | 80 |

    SQL 查询:

    1
    SELECT name, score, DENSE_RANK() OVER (ORDER BY score DESC) AS rk FROM Scores;

    输出:

    | name | score | rank |
    | ———- | ——- | —— |
    | Alice | 90 | 1 |
    | Bob | 85 | 2 |
    | Charlie | 85 | 2 |
    | Dave | 80 | 3 |


比较总结

特性 RANK() DENSE_RANK()
相同值排名 共享排名 共享排名
排名跳跃
适用场景 当需要保留跳跃行为 当需要连续排名时

选择使用场景*

  • 使用 RANK():当你需要保留跳跃行为,例如某些分析需要强调实际排名的间隙。
  • 使用 DENSE_RANK():当需要连续的排名且无跳跃,例如获取前 3 名时不会漏掉数据。

6.高级字符串函数/正则表达式/子句

1667. 修复表中的名字

表: Users

1
2
3
4
5
6
7
8
+----------------+---------+
| Column Name | Type |
+----------------+---------+
| user_id | int |
| name | varchar |
+----------------+---------+
user_id 是该表的主键(具有唯一值的列)。
该表包含用户的 ID 和名字。名字仅由小写和大写字符组成。

编写解决方案,修复名字,使得只有第一个字符是大写的,其余都是小写的。

返回按 user_id 排序的结果表。

返回结果格式示例如下。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
输入:
Users table:
+---------+-------+
| user_id | name |
+---------+-------+
| 1 | aLice |
| 2 | bOB |
+---------+-------+
输出:
+---------+-------+
| user_id | name |
+---------+-------+
| 1 | Alice |
| 2 | Bob |
+---------+-------+
1
2
3
select user_id, CONCAT(UPPER(SUBSTRING(name, 1, 1)), LOWER(SUBSTRING(name, 2))) as name
from Users
Order by user_id ASC;

SUBSTRING(name, 1, 1):提取字符串的第一个字符。

UPPER():将第一个字符转换为大写。

SUBSTRING(name, 2):提取从第二个字符到字符串结束的子字符串。

LOWER():将子字符串的其余部分转换为小写。

CONCAT():将大写的首字母与小写的剩余部分组合起来。


1527. 患某种疾病的患者

患者信息表: Patients

1
2
3
4
5
6
7
8
9
10
+--------------+---------+
| Column Name | Type |
+--------------+---------+
| patient_id | int |
| patient_name | varchar |
| conditions | varchar |
+--------------+---------+
在 SQL 中,patient_id (患者 ID)是该表的主键。
'conditions' (疾病)包含 0 个或以上的疾病代码,以空格分隔。
这个表包含医院中患者的信息。

查询患有 I 类糖尿病的患者 ID (patient_id)、患者姓名(patient_name)以及其患有的所有疾病代码(conditions)。I 类糖尿病的代码总是包含前缀 DIAB1

任意顺序 返回结果表。

查询结果格式如下示例所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
输入:
Patients表:
+------------+--------------+--------------+
| patient_id | patient_name | conditions |
+------------+--------------+--------------+
| 1 | Daniel | YFEV COUGH |
| 2 | Alice | |
| 3 | Bob | DIAB100 MYOP |
| 4 | George | ACNE DIAB100 |
| 5 | Alain | DIAB201 |
+------------+--------------+--------------+
输出:
+------------+--------------+--------------+
| patient_id | patient_name | conditions |
+------------+--------------+--------------+
| 3 | Bob | DIAB100 MYOP |
| 4 | George | ACNE DIAB100 |
+------------+--------------+--------------+
解释:Bob 和 George 都患有代码以 DIAB1 开头的疾病。
1
2
3
4
5
6
7
8
SELECT 
patient_id,
patient_name,
conditions
FROM
Patients
WHERE
conditions REGEXP '(^| )DIAB1[0-9]*( |$)';

问题的根源在于,之前使用的正则表达式 \\bDIAB1[0-9]*\\b 会尝试匹配“字边界”(word boundary),而在某些情况下(如前面带有“+”号),+并非字字符,从而导致匹配失败。我们需要确保匹配条件代码时,其前后是空格或字符串开头/结尾等明确边界,而不是依赖字边界 \b

考虑到 conditions 中的疾病代码是通过空格分隔的,我们可以使用以下策略:

  • 使用 (^| ) 来表示代码前面要么是字符串开头 ^ 要么是空格 ``。
  • 使用 ( |$) 来表示代码后面要么是空格 `要么是字符串结尾$`。
  • 中间匹配 DIAB1 开头并跟零个或多个数字 [0-9]*

这样就能确保 +DIAB100 无法匹配到,因为 + 后面并不是空格或字符串开头.通过这种正则表达式,只有以 DIAB1 开头并独立成为一个以空格或行首/行尾分割的单元的疾病代码才会被匹配到,从而排除诸如 +DIAB100 这类不符合条件的记录。


196. 删除重复的电子邮箱

表: Person

1
2
3
4
5
6
7
8
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| id | int |
| email | varchar |
+-------------+---------+
id 是该表的主键列(具有唯一值的列)。
该表的每一行包含一封电子邮件。电子邮件将不包含大写字母。

编写解决方案 删除 所有重复的电子邮件,只保留一个具有最小 id 的唯一电子邮件。

(对于 SQL 用户,请注意你应该编写一个 DELETE 语句而不是 SELECT 语句。)

(对于 Pandas 用户,请注意你应该直接修改 Person 表。)

运行脚本后,显示的答案是 Person 表。驱动程序将首先编译并运行您的代码片段,然后再显示 Person 表。Person 表的最终顺序 无关紧要

返回结果格式如下示例所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
输入: 
Person 表:
+----+------------------+
| id | email |
+----+------------------+
| 1 | john@example.com |
| 2 | bob@example.com |
| 3 | john@example.com |
+----+------------------+
输出:
+----+------------------+
| id | email |
+----+------------------+
| 1 | john@example.com |
| 2 | bob@example.com |
+----+------------------+
解释: john@example.com重复两次。我们保留最小的Id = 1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DELETE FROM Person
WHERE id IN (
SELECT id
FROM (
SELECT id
FROM Person
WHERE id NOT IN (
SELECT MIN(id)
FROM Person
GROUP BY email
)
) AS Temp
);
-- 解释:
-- 1. 内层子查询 `SELECT MIN(id) FROM Person GROUP BY email` 找到每个唯一 email 的最小 id。
-- 2. 中间子查询生成一个临时表 `Temp`,包含需要删除的 id。
-- 3. 主查询从 `Person` 表中删除这些 id 对应的记录。
-- 这种方式避免了直接在 `DELETE` 中引用目标表的 FROM 子查询问题。

176. 第二高的薪水

Employee 表:

1
2
3
4
5
6
7
8
+-------------+------+
| Column Name | Type |
+-------------+------+
| id | int |
| salary | int |
+-------------+------+
id 是这个表的主键。
表的每一行包含员工的工资信息。

查询并返回 Employee 表中第二高的 不同 薪水 。如果不存在第二高的薪水,查询应该返回 null(Pandas 则返回 None)

查询结果如下例所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
输入:
Employee 表:
+----+--------+
| id | salary |
+----+--------+
| 1 | 100 |
| 2 | 200 |
| 3 | 300 |
+----+--------+
输出:
+---------------------+
| SecondHighestSalary |
+---------------------+
| 200 |
+---------------------+

示例 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
输入:
Employee 表:
+----+--------+
| id | salary |
+----+--------+
| 1 | 100 |
+----+--------+
输出:
+---------------------+
| SecondHighestSalary |
+---------------------+
| null |
+---------------------+
1
2
3
4
5
SELECT 
(SELECT DISTINCT salary
FROM Employee
ORDER BY salary DESC
LIMIT 1 OFFSET 1) AS SecondHighestSalary;

使用 SELECT DISTINCT salary 获取去重的薪水列表。

使用 ORDER BY salary DESC 将薪水从高到低排序。

使用 LIMIT 1 OFFSET 1 获取排序后第二条记录。

如果不存在第二高的薪水,查询会返回 NULL


1484. 按日期分组销售产品

Activities

1
2
3
4
5
6
7
8
+-------------+---------+
| 列名 | 类型 |
+-------------+---------+
| sell_date | date |
| product | varchar |
+-------------+---------+
该表没有主键(具有唯一值的列)。它可能包含重复项。
此表的每一行都包含产品名称和在市场上销售的日期。

编写解决方案找出每个日期、销售的不同产品的数量及其名称。
每个日期的销售产品名称应按词典序排列。
返回按 sell_date 排序的结果表。
结果表结果格式如下例所示。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
输入:
Activities 表:
+------------+-------------+
| sell_date | product |
+------------+-------------+
| 2020-05-30 | Headphone |
| 2020-06-01 | Pencil |
| 2020-06-02 | Mask |
| 2020-05-30 | Basketball |
| 2020-06-01 | Bible |
| 2020-06-02 | Mask |
| 2020-05-30 | T-Shirt |
+------------+-------------+
输出:
+------------+----------+------------------------------+
| sell_date | num_sold | products |
+------------+----------+------------------------------+
| 2020-05-30 | 3 | Basketball,Headphone,T-shirt |
| 2020-06-01 | 2 | Bible,Pencil |
| 2020-06-02 | 1 | Mask |
+------------+----------+------------------------------+
解释:
对于2020-05-30,出售的物品是 (Headphone, Basketball, T-shirt),按词典序排列,并用逗号 ',' 分隔。
对于2020-06-01,出售的物品是 (Pencil, Bible),按词典序排列,并用逗号分隔。
对于2020-06-02,出售的物品是 (Mask),只需返回该物品名。
1
2
3
4
5
6
7
8
9
10
SELECT 
sell_date,
COUNT(DISTINCT product) AS num_sold,
GROUP_CONCAT(DISTINCT product ORDER BY product SEPARATOR ',') AS products
FROM
Activities
GROUP BY
sell_date
ORDER BY
sell_date;

COUNT(DISTINCT product):统计每个日期销售的不同产品的数量。

GROUP_CONCAT(DISTINCT product ORDER BY product SEPARATOR ','):将每个日期销售的不同产品按字典序排列,并用逗号连接为字符串。

GROUP BY sell_date:按销售日期分组,计算每组的数据。

ORDER BY sell_date:按销售日期排序返回结果。


1327. 列出指定时间段内所有的下单产品

表: Products

1
2
3
4
5
6
7
8
9
+------------------+---------+
| Column Name | Type |
+------------------+---------+
| product_id | int |
| product_name | varchar |
| product_category | varchar |
+------------------+---------+
product_id 是该表主键(具有唯一值的列)。
该表包含该公司产品的数据。

表: Orders

1
2
3
4
5
6
7
8
9
10
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| product_id | int |
| order_date | date |
| unit | int |
+---------------+---------+
该表可能包含重复行。
product_id 是表单 Products 的外键(reference 列)。
unit 是在日期 order_date 内下单产品的数目。

写一个解决方案,要求获取在 2020 年 2 月份下单的数量不少于 100 的产品的名字和数目。

返回结果表单的 顺序无要求

查询结果的格式如下。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
输入:
Products 表:
+-------------+-----------------------+------------------+
| product_id | product_name | product_category |
+-------------+-----------------------+------------------+
| 1 | Leetcode Solutions | Book |
| 2 | Jewels of Stringology | Book |
| 3 | HP | Laptop |
| 4 | Lenovo | Laptop |
| 5 | Leetcode Kit | T-shirt |
+-------------+-----------------------+------------------+
Orders 表:
+--------------+--------------+----------+
| product_id | order_date | unit |
+--------------+--------------+----------+
| 1 | 2020-02-05 | 60 |
| 1 | 2020-02-10 | 70 |
| 2 | 2020-01-18 | 30 |
| 2 | 2020-02-11 | 80 |
| 3 | 2020-02-17 | 2 |
| 3 | 2020-02-24 | 3 |
| 4 | 2020-03-01 | 20 |
| 4 | 2020-03-04 | 30 |
| 4 | 2020-03-04 | 60 |
| 5 | 2020-02-25 | 50 |
| 5 | 2020-02-27 | 50 |
| 5 | 2020-03-01 | 50 |
+--------------+--------------+----------+
输出:
+--------------------+---------+
| product_name | unit |
+--------------------+---------+
| Leetcode Solutions | 130 |
| Leetcode Kit | 100 |
+--------------------+---------+
解释:
2020 年 2 月份下单 product_id = 1 的产品的数目总和为 (60 + 70) = 130 。
2020 年 2 月份下单 product_id = 2 的产品的数目总和为 80 。
2020 年 2 月份下单 product_id = 3 的产品的数目总和为 (2 + 3) = 5 。
2020 年 2 月份 product_id = 4 的产品并没有下单。
2020 年 2 月份下单 product_id = 5 的产品的数目总和为 (50 + 50) = 100 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SELECT 
p.product_name,
SUM(o.unit) AS unit
FROM
Products p
JOIN
Orders o
ON
p.product_id = o.product_id
WHERE
DATE_FORMAT(o.order_date, '%Y-%m') = '2020-02'
GROUP BY
p.product_id, p.product_name
HAVING
SUM(o.unit) >= 100;

JOIN: 将 Products 表与 Orders 表基于 product_id 连接,获取产品信息及订单详情。

DATE_FORMAT(o.order_date, '%Y-%m') = '2020-02': 筛选订单日期在 2020 年 2 月的记录。

SUM(o.unit): 计算每种产品在 2020 年 2 月的总订单数。

HAVING SUM(o.unit) >= 100: 筛选总订单数不少于 100 的产品。

GROUP BY: 按产品分组,确保总数计算正确。


1517. 查找拥有有效邮箱的用户

表: Users

1
2
3
4
5
6
7
8
9
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| user_id | int |
| name | varchar |
| mail | varchar |
+---------------+---------+
user_id 是该表的主键(具有唯一值的列)。
该表包含了网站已注册用户的信息。有一些电子邮件是无效的。

编写一个解决方案,以查找具有有效电子邮件的用户。

一个有效的电子邮件具有前缀名称和域,其中:

  1. 前缀 名称是一个字符串,可以包含字母(大写或小写),数字,下划线 '_' ,点 '.' 和/或破折号 '-' 。前缀名称 必须 以字母开头。
  2. '@leetcode.com'

以任何顺序返回结果表。

结果的格式如以下示例所示:

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
输入:
Users 表:
+---------+-----------+-------------------------+
| user_id | name | mail |
+---------+-----------+-------------------------+
| 1 | Winston | winston@leetcode.com |
| 2 | Jonathan | jonathanisgreat |
| 3 | Annabelle | bella-@leetcode.com |
| 4 | Sally | sally.come@leetcode.com |
| 5 | Marwan | quarz#2020@leetcode.com |
| 6 | David | david69@gmail.com |
| 7 | Shapiro | .shapo@leetcode.com |
+---------+-----------+-------------------------+
输出:
+---------+-----------+-------------------------+
| user_id | name | mail |
+---------+-----------+-------------------------+
| 1 | Winston | winston@leetcode.com |
| 3 | Annabelle | bella-@leetcode.com |
| 4 | Sally | sally.come@leetcode.com |
+---------+-----------+-------------------------+
解释:
用户 2 的电子邮件没有域。
用户 5 的电子邮件带有不允许的 '#' 符号。
用户 6 的电子邮件没有 leetcode 域。
用户 7 的电子邮件以点开头。
1
2
3
4
5
6
7
8
SELECT 
user_id,
name,
mail
FROM
Users
WHERE
mail REGEXP '^[a-zA-Z][a-zA-Z0-9_\./\-]*\@leetcode[\.]com$';