> ## Documentation Index
> Fetch the complete documentation index at: https://docs.akria.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Day 03 | 管道与联表查询

> Shell 管道/重定向/函数与 SQL 聚合统计/联表查询

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

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="book" size={26} color="#059669" /> 学习内容 (30 mins)</span>

在开始写代码前，先搞懂这些核心概念，否则后面的代码你会看得云里雾里。

<AccordionGroup>
  <Accordion title="Shell 进阶 (15 mins)" icon="terminal">
    **什么是管道？**

    管道（`|`）是 Linux 最强大的特性之一，它可以将一个命令的输出作为另一个命令的输入，实现命令的组合和串联。

    **为什么管道如此重要？**

    * **命令组合**：将简单命令组合成复杂操作
    * **数据流处理**：像流水线一样处理数据
    * **提高效率**：一行命令完成多步操作
    * **Unix 哲学**：每个命令做好一件事，通过管道组合

    **管道 (`|`) 详解**：

    * **基本语法**：
      ```bash theme={null}
      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`：将错误日志保存到文件

    **函数详解**：

    * **为什么需要函数？**
      * **代码复用**：避免重复编写相同代码
      * **模块化**：将复杂脚本拆分成小函数
      * **可维护性**：修改一处，所有调用处都生效
      * **可读性**：函数名就是注释，代码更清晰

    * **函数定义**：
      ```bash theme={null}
      function function_name() {
          # 函数体
          echo "Hello from function"
      }

      # 或者（更简洁）
      function_name() {
          echo "Hello from function"
      }
      ```

    * **函数调用**：
      ```bash theme={null}
      function_name  # 直接调用函数名
      ```

    * **函数参数**：
      ```bash theme={null}
      function greet() {
          echo "Hello, $1"  # $1 是第一个参数
          echo "Age: $2"    # $2 是第二个参数
      }

      greet "Alice" 25  # 调用函数并传递参数
      ```

    * **返回值**：
      * Shell 函数通过 `return` 返回退出码（0-255）
      * 通过 `echo` 输出返回值，调用者用 `$(function_name)` 捕获
  </Accordion>

  <Accordion title="SQL 高级查询 (15 mins)" icon="database">
    **什么是聚合函数？**

    聚合函数对一组值执行计算并返回单个值。它们是数据分析的基础工具。

    **为什么需要聚合函数？**

    * **统计分析**：计算总数、平均值、最大值、最小值
    * **数据汇总**：将多行数据汇总成一行
    * **报表生成**：生成各种统计报表
    * **业务分析**：分析业务指标和趋势

    **聚合函数详解**：

    * **COUNT()**：统计行数
      ```sql theme={null}
      SELECT COUNT(*) FROM users;  -- 统计所有用户数
      SELECT COUNT(email) FROM users;  -- 统计有邮箱的用户数（NULL 不计数）
      ```

    * **SUM()**：求和
      ```sql theme={null}
      SELECT SUM(amount) FROM orders;  -- 计算订单总金额
      ```

    * **AVG()**：平均值
      ```sql theme={null}
      SELECT AVG(amount) FROM orders;  -- 计算平均订单金额
      ```

    * **MAX() / MIN()**：最大值/最小值
      ```sql theme={null}
      SELECT MAX(amount) FROM orders;  -- 最大订单金额
      SELECT MIN(created_at) FROM users;  -- 最早注册时间
      ```

    **GROUP BY 详解**：

    * **什么是 GROUP BY？**
      * 将数据按照指定字段分组
      * 对每个分组应用聚合函数
      * 每个分组返回一行结果

    * **基本语法**：
      ```sql theme={null}
      SELECT column1, aggregate_function(column2)
      FROM table
      GROUP BY column1;
      ```

    * **实际应用**：
      ```sql theme={null}
      -- 统计每个用户的订单数
      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（最常用）**：
      ```sql theme={null}
      SELECT u.username, o.amount
      FROM users u
      LEFT JOIN orders o ON u.id = o.user_id;
      ```
      * 返回所有用户，即使没有订单
      * 没有订单的用户，订单相关字段为 NULL
    * **INNER JOIN**：
      ```sql theme={null}
      SELECT u.username, o.amount
      FROM users u
      INNER JOIN orders o ON u.id = o.user_id;
      ```
      * 只返回有订单的用户
      * 没有订单的用户不会出现在结果中
  </Accordion>
</AccordionGroup>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="code" size={26} color="#059669" /> 代码任务 (90 mins)</span>

<Steps>
  <Step title="Shell 函数与日志处理">
    编写 `03_log_cleaner.sh`：

    **任务分解**：

    1. 定义带时间戳的日志函数
    2. 使用函数记录操作日志
    3. 查找日志文件并记录到文件
    4. 使用管道和重定向处理数据

    <CodeGroup>
      ```bash 03_log_cleaner.sh theme={null}
      #!/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"
      ```
    </CodeGroup>

    **常见错误**：

    <Warning>
      * ❌ `log_msg "message"` - 函数未定义就调用，会报错
      * ❌ `function log_msg { ... }` - 语法错误，应该是 `function log_msg() { ... }`
      * ❌ `find ./logs -name *.log` - 通配符未加引号，会被 Shell 展开
      * ✅ `function log_msg() { ... }` - 正确写法
      * ✅ `find ./logs -name "*.log"` - 正确写法
    </Warning>

    **验证步骤**：

    1. 确保 logs 目录存在（Day 02 已创建）：
       ```bash theme={null}
       ls -la logs/
       ```

    2. 运行脚本：
       ```bash theme={null}
       chmod +x 03_log_cleaner.sh
       ./03_log_cleaner.sh
       ```

    3. 检查输出：
       * 应该看到带时间戳的日志信息
       * 应该看到找到的日志文件数量
       * 应该看到前 5 个日志文件列表

    4. 检查生成的文件：
       ```bash theme={null}
       cat found_logs.txt
       ```

    5. 测试边界情况：
       * 删除 logs 目录后运行脚本
       * 创建空的 logs 目录后运行脚本
  </Step>

  <Step title="SQL 联表分析">
    新建 `orders` 表并练习联表查询 `03_joins.sql`：

    **任务分解**：

    1. 创建订单表
    2. 插入测试数据
    3. 使用聚合函数统计订单
    4. 使用 GROUP BY 分组统计
    5. 使用 LEFT JOIN 联表查询

    <CodeGroup>
      ```sql 03_joins.sql theme={null}
      -- ============================================
      -- 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;  -- 按总消费倒序排列
      ```
    </CodeGroup>

    **字段类型选择详解**：

    <AccordionGroup>
      <Accordion title="DECIMAL vs FLOAT vs DOUBLE">
        * **DECIMAL(10, 2)**：精确小数，适合金额
          * 存储：固定精度，不会丢失精度
          * 范围：取决于定义的长度
          * 适用：金额、价格等需要精确计算的场景
        * **FLOAT / DOUBLE**：浮点数，有精度损失
          * 存储：近似值，可能有精度损失
          * 范围：更大
          * 适用：科学计算、不需要精确值的场景
        * **选择依据**：金额必须用 DECIMAL，保证精确性
      </Accordion>

      <Accordion title="LEFT JOIN vs INNER JOIN">
        **LEFT JOIN**：

        * 返回左表所有记录
        * 右表没有匹配的用 NULL 填充
        * 适用：需要显示所有用户，即使没有订单

        **INNER JOIN**：

        * 只返回两表都有匹配的记录
        * 没有匹配的记录不显示
        * 适用：只需要有订单的用户

        **示例对比**：

        ```sql theme={null}
        -- 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;
        -- 结果：只有有订单的用户
        ```
      </Accordion>

      <Accordion title="COALESCE 函数">
        **作用**：返回第一个非 NULL 值

        **语法**：`COALESCE(value1, value2, ...)`

        **应用场景**：

        * 处理 LEFT JOIN 产生的 NULL 值
        * 提供默认值

        **示例**：

        ```sql theme={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;
        ```
      </Accordion>
    </AccordionGroup>

    **验证步骤**：

    1. 连接 MySQL：
       ```bash theme={null}
       docker exec -it mysql-learn mysql -uroot -proot
       ```

    2. 执行 SQL 脚本：
       ```sql theme={null}
       USE infra_db;
       source /path/to/03_joins.sql;
       ```
       或者直接复制粘贴 SQL 语句执行

    3. 验证表创建：
       ```sql theme={null}
       DESCRIBE orders;
       ```

    4. 验证数据插入：
       ```sql theme={null}
       SELECT * FROM orders;
       -- 应该看到 3 条订单记录
       ```

    5. 验证聚合函数：
       ```sql theme={null}
       SELECT COUNT(*) as total_orders, SUM(amount) as total_amount FROM orders;
       -- 应该看到 total_orders = 3, total_amount = 618.50
       ```

    6. 验证 GROUP BY：
       ```sql theme={null}
       SELECT user_id, COUNT(*) as order_count, SUM(amount) as total_spend
       FROM orders
       GROUP BY user_id;
       -- 应该看到每个用户的订单统计
       ```

    7. 验证 LEFT JOIN：
       ```sql theme={null}
       SELECT u.username, o.amount
       FROM users u
       LEFT JOIN orders o ON u.id = o.user_id;
       -- 应该看到用户名和对应的订单金额
       ```

    **常见错误**：

    <Warning>
      * ❌ `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 存在）
    </Warning>
  </Step>
</Steps>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="lightbulb" size={26} color="#059669" /> 拓展任务 (30 mins)</span>

<CardGroup cols={2}>
  <Card title="Shell 挑战" icon="terminal">
    **任务**：学习 `grep`、`awk`、`sed` 的简单组合用法，编写一个脚本统计日志文件中的错误数量。

    **提示**：

    * 使用 `grep "ERROR" log.txt` 过滤错误行
    * 使用 `awk '{print $2}'` 提取第二列
    * 使用 `sed 's/old/new/g'` 替换文本
    * 思考：如何用管道组合这些命令？
  </Card>

  <Card title="SQL 挑战" icon="database">
    **任务**：学习 `HAVING` 子句，过滤分组后的数据（如：找出消费总额 > 100 的用户）。

    **提示**：

    * 使用 `HAVING SUM(amount) > 100` 过滤分组结果
    * 思考：HAVING 和 WHERE 有什么区别？
    * 什么时候用 WHERE，什么时候用 HAVING？
  </Card>
</CardGroup>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="file" size={26} color="#059669" /> 今日产出物</span>

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

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="github" size={26} color="#059669" /> 参考代码</span>

<CardGroup cols={2}>
  <Card title="查看参考代码" icon="code" href="https://github.com/akriamail/insightful-ops/tree/main/docs/mintlify/scripts/training/30daystudy/week-1/day-03">
    在 GitHub 查看完整的示例代码

    <br />

    <small>包含 Shell 脚本和 SQL 文件</small>
  </Card>

  <Card title="在线运行" icon="play" href="https://replit.com">
    使用在线编辑器测试代码

    <br />

    <small>无需本地环境配置</small>
  </Card>
</CardGroup>

***

## 实际应用场景

<CardGroup cols={2}>
  <Card title="Shell 管道和函数在运维中的应用" icon="server">
    * **日志分析**：使用管道组合 grep、awk、sed 分析日志
    * **数据提取**：从命令输出中提取特定信息
    * **批量处理**：通过函数封装重复操作，提高代码复用
    * **脚本模块化**：将复杂脚本拆分成多个函数，便于维护
    * **错误处理**：使用函数统一处理错误和日志输出
  </Card>

  <Card title="SQL 聚合和联表在生产环境的应用" icon="database">
    * **数据统计**：使用聚合函数生成各种统计报表
    * **业务分析**：通过 GROUP BY 分析业务指标
    * **关联查询**：使用 JOIN 获取分散在多个表中的数据
    * **报表生成**：结合聚合和联表生成复杂的业务报表
    * **数据分析**：为数据分析和决策提供支持
  </Card>
</CardGroup>

<Tip>
  **与 Day 02 和 Day 04 的关联**：

  * Day 02 学习了基础的流程控制和 CRUD，今天学习了如何通过函数和管道组合这些操作
  * Day 04 将学习更高级的文本处理工具（grep/awk/sed），今天的管道知识是基础
  * 掌握今天的函数和联表查询，明天的学习会更轻松
</Tip>

***

<CardGroup cols={2}>
  <Card title="上一天: 流程控制" href="/training/30daystudy/day-02">
    Day 02 | Shell 流程控制与 SQL CRUD
  </Card>

  <Card title="下一天: 三剑客优化" href="/training/30daystudy/day-04">
    Day 04 | Shell 三剑客与 SQL 优化
  </Card>
</CardGroup>
