Skip to main content
今日目标:Shell 脚本进入”模块化”阶段,SQL 能够处理多表关联与复杂统计。今天不只是写代码,而是要彻底理解 “如何通过管道组合多个命令”“如何编写可复用的函数” 以及 “如何通过联表查询获取关联数据”。这是数据分析的基础。

学习内容 (30 mins)

在开始写代码前,先搞懂这些核心概念,否则后面的代码你会看得云里雾里。
什么是管道?管道(|)是 Linux 最强大的特性之一,它可以将一个命令的输出作为另一个命令的输入,实现命令的组合和串联。为什么管道如此重要?
  • 命令组合:将简单命令组合成复杂操作
  • 数据流处理:像流水线一样处理数据
  • 提高效率:一行命令完成多步操作
  • Unix 哲学:每个命令做好一件事,通过管道组合
管道 (|) 详解
  • 基本语法
    command1 | command2 | command3
    
    • command1 的输出作为 command2 的输入
    • command2 的输出作为 command3 的输入
    • 数据从左到右流动
  • 实际应用
    • ps aux | grep mysql:查找 MySQL 进程
    • cat file.log | grep ERROR | wc -l:统计错误日志行数
    • ls -la | grep "^-" | awk '{print $9}':列出所有文件名
  • 管道 vs 重定向
    • 管道:命令之间的数据传递
    • 重定向:命令与文件之间的数据传递
重定向详解
  • 输出重定向
    • command > file:覆盖写入文件(如果文件不存在会创建)
    • command >> file:追加写入文件
    • command 2> file:将错误输出重定向到文件
    • command > file 2>&1:将标准输出和错误输出都重定向到文件
  • 输入重定向
    • command < file:从文件读取输入
    • command << EOF:Here Document,多行输入
  • 实际应用
    • echo "text" > file.txt:创建文件并写入内容
    • echo "text" >> file.txt:追加内容到文件
    • grep "error" log.txt > errors.txt:将错误日志保存到文件
函数详解
  • 为什么需要函数?
    • 代码复用:避免重复编写相同代码
    • 模块化:将复杂脚本拆分成小函数
    • 可维护性:修改一处,所有调用处都生效
    • 可读性:函数名就是注释,代码更清晰
  • 函数定义
    function function_name() {
        # 函数体
        echo "Hello from function"
    }
    
    # 或者(更简洁)
    function_name() {
        echo "Hello from function"
    }
    
  • 函数调用
    function_name  # 直接调用函数名
    
  • 函数参数
    function greet() {
        echo "Hello, $1"  # $1 是第一个参数
        echo "Age: $2"    # $2 是第二个参数
    }
    
    greet "Alice" 25  # 调用函数并传递参数
    
  • 返回值
    • Shell 函数通过 return 返回退出码(0-255)
    • 通过 echo 输出返回值,调用者用 $(function_name) 捕获
什么是聚合函数?聚合函数对一组值执行计算并返回单个值。它们是数据分析的基础工具。为什么需要聚合函数?
  • 统计分析:计算总数、平均值、最大值、最小值
  • 数据汇总:将多行数据汇总成一行
  • 报表生成:生成各种统计报表
  • 业务分析:分析业务指标和趋势
聚合函数详解
  • COUNT():统计行数
    SELECT COUNT(*) FROM users;  -- 统计所有用户数
    SELECT COUNT(email) FROM users;  -- 统计有邮箱的用户数(NULL 不计数)
    
  • SUM():求和
    SELECT SUM(amount) FROM orders;  -- 计算订单总金额
    
  • AVG():平均值
    SELECT AVG(amount) FROM orders;  -- 计算平均订单金额
    
  • MAX() / MIN():最大值/最小值
    SELECT MAX(amount) FROM orders;  -- 最大订单金额
    SELECT MIN(created_at) FROM users;  -- 最早注册时间
    
GROUP BY 详解
  • 什么是 GROUP BY?
    • 将数据按照指定字段分组
    • 对每个分组应用聚合函数
    • 每个分组返回一行结果
  • 基本语法
    SELECT column1, aggregate_function(column2)
    FROM table
    GROUP BY column1;
    
  • 实际应用
    -- 统计每个用户的订单数
    SELECT user_id, COUNT(*) as order_count
    FROM orders
    GROUP BY user_id;
    
  • 注意事项
    • SELECT 中的非聚合字段必须出现在 GROUP BY 中
    • GROUP BY 通常与聚合函数一起使用
联表查询详解
  • 为什么需要联表?
    • 数据分散在多个表中
    • 需要同时获取关联数据
    • 避免数据冗余
  • JOIN 类型
    • INNER JOIN:只返回两表都有匹配的记录
    • LEFT JOIN:返回左表所有记录,右表没有匹配的用 NULL 填充
    • RIGHT JOIN:返回右表所有记录,左表没有匹配的用 NULL 填充
    • FULL JOIN:返回两表所有记录(MySQL 不支持)
  • LEFT JOIN(最常用)
    SELECT u.username, o.amount
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id;
    
    • 返回所有用户,即使没有订单
    • 没有订单的用户,订单相关字段为 NULL
  • INNER JOIN
    SELECT u.username, o.amount
    FROM users u
    INNER JOIN orders o ON u.id = o.user_id;
    
    • 只返回有订单的用户
    • 没有订单的用户不会出现在结果中

代码任务 (90 mins)

1

Shell 函数与日志处理

编写 03_log_cleaner.sh任务分解
  1. 定义带时间戳的日志函数
  2. 使用函数记录操作日志
  3. 查找日志文件并记录到文件
  4. 使用管道和重定向处理数据
#!/bin/bash
# 03_log_cleaner.sh - 日志清理脚本

# ============================================
# 函数定义:带时间戳的日志输出
# ============================================
# function 关键字可以省略,但保留可以提高可读性
# $1 是函数的第一个参数,表示日志消息
function log_msg() {
    # date '+%Y-%m-%d %H:%M:%S':格式化当前时间
    # %Y:4位年份,%m:月份,%d:日期
    # %H:小时,%M:分钟,%S:秒
    # $1:函数的第一个参数(日志消息)
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}

# ============================================
# 主程序开始
# ============================================

# 调用函数,输出开始信息
log_msg "Starting cleanup task..."

# 查找日志文件
# find ./logs:在当前目录的 logs 文件夹中查找
# -name "*.log":查找所有 .log 结尾的文件
# > found_logs.txt:将结果重定向到文件(覆盖写入)
find ./logs -name "*.log" > found_logs.txt

# 统计找到的日志文件数量
# wc -l:统计行数(每个文件一行)
# $(...):命令替换,将命令的输出作为变量值
log_count=$(wc -l < found_logs.txt)

# 调用函数,输出统计信息
log_msg "Found $log_count log files"

# 如果找到日志文件,显示前 5 个
if [ $log_count -gt 0 ]; then
    log_msg "First 5 log files:"
    # head -5:显示前 5 行
    # 使用管道将 find 的结果传递给 head
    find ./logs -name "*.log" | head -5 | while read file; do
        echo "  - $file"
    done
fi

# 调用函数,输出完成信息
log_msg "Cleanup finished. Check found_logs.txt"
常见错误
  • log_msg "message" - 函数未定义就调用,会报错
  • function log_msg { ... } - 语法错误,应该是 function log_msg() { ... }
  • find ./logs -name *.log - 通配符未加引号,会被 Shell 展开
  • function log_msg() { ... } - 正确写法
  • find ./logs -name "*.log" - 正确写法
验证步骤
  1. 确保 logs 目录存在(Day 02 已创建):
    ls -la logs/
    
  2. 运行脚本:
    chmod +x 03_log_cleaner.sh
    ./03_log_cleaner.sh
    
  3. 检查输出:
    • 应该看到带时间戳的日志信息
    • 应该看到找到的日志文件数量
    • 应该看到前 5 个日志文件列表
  4. 检查生成的文件:
    cat found_logs.txt
    
  5. 测试边界情况:
    • 删除 logs 目录后运行脚本
    • 创建空的 logs 目录后运行脚本
2

SQL 联表分析

新建 orders 表并练习联表查询 03_joins.sql任务分解
  1. 创建订单表
  2. 插入测试数据
  3. 使用聚合函数统计订单
  4. 使用 GROUP BY 分组统计
  5. 使用 LEFT JOIN 联表查询
-- ============================================
-- Day 03: SQL 联表查询实战
-- ============================================

USE infra_db;

-- ============================================
-- 1. 创建订单表
-- ============================================

-- 创建订单表,用于存储用户的订单信息
CREATE TABLE IF NOT EXISTS orders (
    -- 订单ID:主键,自增
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '订单ID',
    
    -- 用户ID:外键(关联 users 表)
    -- NOT NULL:每个订单必须关联一个用户
    user_id INT UNSIGNED NOT NULL COMMENT '用户ID',
    
    -- 订单金额:十进制数,总共10位,小数点后2位
    -- DECIMAL(10, 2):可以存储最大 99999999.99
    amount DECIMAL(10, 2) COMMENT '订单金额',
    
    -- 创建时间:自动记录订单创建时间
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

-- ============================================
-- 2. 插入测试数据
-- ============================================

-- 确保 users 表中有数据(如果 Day 02 已插入,这里会报错,可以忽略)
-- 如果没有数据,先执行 Day 02 的 INSERT 语句

-- 插入订单数据
-- 注意:user_id 必须存在于 users 表中(外键约束)
INSERT INTO orders (user_id, amount) VALUES 
(1, 99.50),   -- 用户 1 的第一个订单
(1, 19.00),   -- 用户 1 的第二个订单
(2, 500.00);  -- 用户 2 的订单

-- 验证插入结果
SELECT * FROM orders;

-- ============================================
-- 3. 聚合函数统计
-- ============================================

-- 统计订单总数
SELECT COUNT(*) as total_orders FROM orders;

-- 统计订单总金额
SELECT SUM(amount) as total_amount FROM orders;

-- 计算平均订单金额
SELECT AVG(amount) as avg_amount FROM orders;

-- 找出最大和最小订单金额
SELECT MAX(amount) as max_amount, MIN(amount) as min_amount FROM orders;

-- ============================================
-- 4. GROUP BY 分组统计
-- ============================================

-- 统计每个用户的订单数和总消费
-- GROUP BY user_id:按用户ID分组
-- COUNT(id):统计每个用户的订单数
-- SUM(amount):计算每个用户的总消费
-- as:给列起别名,便于理解
SELECT 
    user_id,
    COUNT(id) as order_count,      -- 订单数量
    SUM(amount) as total_spend,    -- 总消费
    AVG(amount) as avg_order       -- 平均订单金额
FROM orders 
GROUP BY user_id;

-- ============================================
-- 5. LEFT JOIN 联表查询
-- ============================================

-- 查询订单详情,包括用户名
-- LEFT JOIN:返回所有订单,即使没有对应的用户(虽然这里不会发生)
-- u.username:users 表的用户名
-- o.amount:orders 表的订单金额
-- ON u.id = o.user_id:关联条件,用户ID匹配
SELECT 
    u.username AS '用户名',
    o.amount AS '订单金额',
    o.created_at AS '订单时间'
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
ORDER BY o.created_at DESC;  -- 按创建时间倒序排列

-- ============================================
-- 6. 综合查询:统计每个用户的订单情况
-- ============================================

-- 使用 LEFT JOIN 确保所有用户都出现在结果中
-- 即使某个用户没有订单,也会显示(订单相关字段为 NULL)
SELECT 
    u.username AS '用户名',
    COALESCE(COUNT(o.id), 0) AS '订单数',           -- 如果没有订单,显示 0
    COALESCE(SUM(o.amount), 0) AS '总消费',        -- 如果没有订单,显示 0
    COALESCE(AVG(o.amount), 0) AS '平均订单金额'   -- 如果没有订单,显示 0
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.username
ORDER BY SUM(o.amount) DESC;  -- 按总消费倒序排列
字段类型选择详解
  • DECIMAL(10, 2):精确小数,适合金额
    • 存储:固定精度,不会丢失精度
    • 范围:取决于定义的长度
    • 适用:金额、价格等需要精确计算的场景
  • FLOAT / DOUBLE:浮点数,有精度损失
    • 存储:近似值,可能有精度损失
    • 范围:更大
    • 适用:科学计算、不需要精确值的场景
  • 选择依据:金额必须用 DECIMAL,保证精确性
LEFT JOIN
  • 返回左表所有记录
  • 右表没有匹配的用 NULL 填充
  • 适用:需要显示所有用户,即使没有订单
INNER JOIN
  • 只返回两表都有匹配的记录
  • 没有匹配的记录不显示
  • 适用:只需要有订单的用户
示例对比
-- LEFT JOIN:显示所有用户
SELECT u.username, o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
-- 结果:所有用户,没有订单的显示 NULL

-- INNER JOIN:只显示有订单的用户
SELECT u.username, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id;
-- 结果:只有有订单的用户
作用:返回第一个非 NULL 值语法COALESCE(value1, value2, ...)应用场景
  • 处理 LEFT JOIN 产生的 NULL 值
  • 提供默认值
示例
-- 如果没有订单,显示 0 而不是 NULL
SELECT 
    u.username,
    COALESCE(SUM(o.amount), 0) AS total_spend
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id;
验证步骤
  1. 连接 MySQL:
    docker exec -it mysql-learn mysql -uroot -proot
    
  2. 执行 SQL 脚本:
    USE infra_db;
    source /path/to/03_joins.sql;
    
    或者直接复制粘贴 SQL 语句执行
  3. 验证表创建:
    DESCRIBE orders;
    
  4. 验证数据插入:
    SELECT * FROM orders;
    -- 应该看到 3 条订单记录
    
  5. 验证聚合函数:
    SELECT COUNT(*) as total_orders, SUM(amount) as total_amount FROM orders;
    -- 应该看到 total_orders = 3, total_amount = 618.50
    
  6. 验证 GROUP BY:
    SELECT user_id, COUNT(*) as order_count, SUM(amount) as total_spend
    FROM orders
    GROUP BY user_id;
    -- 应该看到每个用户的订单统计
    
  7. 验证 LEFT JOIN:
    SELECT u.username, o.amount
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id;
    -- 应该看到用户名和对应的订单金额
    
常见错误
  • SELECT user_id, amount FROM orders GROUP BY user_id - amount 未聚合,会报错
  • SELECT * FROM users JOIN orders - 缺少 ON 条件,会产生笛卡尔积
  • INSERT INTO orders (user_id, amount) VALUES (999, 100) - user_id 不存在,外键约束失败
  • SELECT user_id, SUM(amount) FROM orders GROUP BY user_id - 正确
  • SELECT * FROM users u JOIN orders o ON u.id = o.user_id - 正确
  • INSERT INTO orders (user_id, amount) VALUES (1, 100) - 正确(user_id 存在)

拓展任务 (30 mins)

Shell 挑战

任务:学习 grepawksed 的简单组合用法,编写一个脚本统计日志文件中的错误数量。提示
  • 使用 grep "ERROR" log.txt 过滤错误行
  • 使用 awk '{print $2}' 提取第二列
  • 使用 sed 's/old/new/g' 替换文本
  • 思考:如何用管道组合这些命令?

SQL 挑战

任务:学习 HAVING 子句,过滤分组后的数据(如:找出消费总额 > 100 的用户)。提示
  • 使用 HAVING SUM(amount) > 100 过滤分组结果
  • 思考:HAVING 和 WHERE 有什么区别?
  • 什么时候用 WHERE,什么时候用 HAVING?

今日产出物

  • day03/03_log_cleaner.sh - Shell 日志处理脚本
  • day03/03_joins.sql - SQL 联表查询脚本

参考代码

查看参考代码

在 GitHub 查看完整的示例代码

在线运行

使用在线编辑器测试代码

实际应用场景

Shell 管道和函数在运维中的应用

  • 日志分析:使用管道组合 grep、awk、sed 分析日志
  • 数据提取:从命令输出中提取特定信息
  • 批量处理:通过函数封装重复操作,提高代码复用
  • 脚本模块化:将复杂脚本拆分成多个函数,便于维护
  • 错误处理:使用函数统一处理错误和日志输出

SQL 聚合和联表在生产环境的应用

  • 数据统计:使用聚合函数生成各种统计报表
  • 业务分析:通过 GROUP BY 分析业务指标
  • 关联查询:使用 JOIN 获取分散在多个表中的数据
  • 报表生成:结合聚合和联表生成复杂的业务报表
  • 数据分析:为数据分析和决策提供支持
与 Day 02 和 Day 04 的关联
  • Day 02 学习了基础的流程控制和 CRUD,今天学习了如何通过函数和管道组合这些操作
  • Day 04 将学习更高级的文本处理工具(grep/awk/sed),今天的管道知识是基础
  • 掌握今天的函数和联表查询,明天的学习会更轻松

上一天: 流程控制

Day 02 | Shell 流程控制与 SQL CRUD

下一天: 三剑客优化

Day 04 | Shell 三剑客与 SQL 优化