跳转至

Blog

Python 语言进阶 —— yield表达式

介绍

在 Python 的 官网 中,对 yield 表达式的解释如下:

yield 表达式在定义 generator 函数或 asynchronous generator 函数时才会用到因此只能在函数定义的内部使用。 在一个函数体内使用 yield 表达式会使这个函数变成一个生成器函数,而在一个 async def 函数的内部使用它则会让这个协程函数变成一个异步生成器函数。

简单而言就是:在函数中添加 yield 关键字会使该函数返回一个生成器对象,该对象可以被迭代。

def simple():
    x = 1
    yield x
    yield x + 1
    yield x + 2


s = simple()
print(s)

输出的是一个迭代器对象:

<generator object simple at 0x0000022568E34B80>

迭代器对象

yield 的作用和 return 很像,都是用于函数的返回。但是 yieldreturn 不同的是,return 返回的是某个值,在返回时会清空函数产生的临时变量。 而 yield 返回的是一个可以迭代的生成器。当获取到 yield 返回的对象时,可以通过 for 循环或者是 next() 函数进行迭代。

因为是一个迭代器对象,就可以通过迭代的方式读取其中的值:

def simple():
    x = 1
    yield x
    yield x + 1
    yield x + 2


if __name__ == '__main__':
    s = simple()
    for i in s:
        print(i)

输出:

1
2
3

也可以使用 next() 函数进行迭代:

def simple():
    x = 1
    yield x
    yield x + 1
    yield x + 2


if __name__ == '__main__':
    s = simple()
    print(next(s))
    print(next(s))
    print(next(s))
    print(next(s)) # 这里会报错,因为此处已经不可迭代。

运行可以得到相同的结果。因为在 simple() 函数中只 yield 了三次,因此当进行第 4 次迭代的时候会报错:

1
2
3
Traceback (most recent call last):
  File "D:\Projects\Python\PythonDemos\yielddemo.py", line 13, in <module>
    print(next(s))
          ^^^^^^^
StopIteration

不清理局部变量

yield 的返回不会像 return 一样清理函数中局部变量。更准确地说,应该是 yield 会阻止函数退出,直到下次调用 next() 方法。当调用时,将从之前暂停的位置继续开始执行。

def simple():
    num = 1
    print("First time execution of the function")
    yield num  # 第 1 次调用在此处停止,不清理局部变量 num 的值
    num = 10
    print("Second time execution of the function")
    yield num  # 第 2 次调用在此处停止,不清理局部变量 num 的值
    num = 100
    print("Third time execution of the function")
    yield num  # 第 3 次调用在此处停止,不清理局部变量 num 的值


if __name__ == '__main__':
    s = simple()
    print('--- First time start ---')
    print(next(s))
    print('--- First time end ---\n')

    print('--- Second time start ---')
    print(next(s))
    print('--- Second time end ---\n')

    print('--- Third time start ---')
    print(next(s))
    print('--- Third time end ---\n')

执行上述代码,输出如下:

--- First time start ---
First time execution of the function
1
--- First time end ---

--- Second time start ---
Second time execution of the function
10
--- Second time end ---

--- Third time start ---
Third time execution of the function
100
--- Third time end ---

与 return 的关系

Return Yield
将结果返回给调用者(函数外部) 用于将函数转换为生成器。暂停函数执行并保留其状态
执行完毕后销毁变量(局部变量生命周期结束) yield 不会销毁函数的局部变量。它会保留状态。
通常每个函数只有一个 return 语句 可以有一个或多个 yield 语句
如果再次执行函数,则从开头开始执行 从上一次暂停的地方开始执行

Android 性能优化利器 —— Simpleperf

Simpleperf 是 Android 官方推出的底层性能分析工具,专为移动端场景设计,可帮助开发者进行函数级 CPU 性能剖析、调用图分析和热点定位。作为 Android NDK 工具链的重要组成部分,它能够以极低开销捕获应用程序和系统底层的运行时行为,为性能优化提供数据支撑。

基本原理

Simpleperf 是基于 Linux 内核性能事件采集机制(perf_event)的 Android 性能分析工具,其核心能力依赖于 CPU 内置的 PMU(Performance Monitoring Unit,性能监控单元)。

PMU 是 CPU 的硬件模块,通过专用计数器实时监测底层性能事件,包括 CPU 周期、指令执行数、缓存命中/失效、分支预测错误等, 精度可达指令级。PMU 通过周期性中断或溢出采样触发数据采集,并将原始数据通过 Linux 内核的 perf_event 子系统传递给用户态工具,比如 Simpleperf。

Simplerperf 工作原理

如上图所示,完整工作流程分为三个阶段:

  1. 配置阶段:Simpleperf 通过 perf_event_open() 系统调用初始化 PMU 寄存器,指定监测事件(如 cpu-cycles)。

  2. 采集阶段:PMU 触发硬件级中断,采样数据通过内核的 mmap 内存映射高效传输至用户空间。

  3. 分析阶段:Simpleperf 解析二进制数据,生成火焰图、热点函数统计等可视化报告。

整个流程通过内核桥接硬件与工具链,实现低开销(<3%) 的性能分析。Android 针对移动端场景优化了该路径,包括 JNI 符号自动解析、动态频率调节 等特性,确保高性能与低功耗的平衡。

下载工具

下载 Simpleperf 工具的方式主要有如下三种:

  1. 通过 AOSP 源码,编译 Simpleperf;
  2. 通过下载 Android NDK 文件,直接获取 Simpleperf;
  3. 通过 ADB 进入设备的 Shell 环境,使用内置的 Simpleperf。
对比项 源码编译 NDK 获取 设备内置
易用性 简单
推荐程度 不推荐 强烈推荐 推荐
优点 可以根据不同的源码,编译针对 Android 版本的工具 提供了 Simpleperf 封装的高级脚本 不需要下载其他的库和工具,原生自持
缺点 费时。对于初学者不友好,需要搭建源码编译环境 需要下载 NDK 文件,国内下载速度较慢 只能够获取到文本型数据,解析数据还需要借助外部工具和第三方库

AOSP 源码编译

访问 Simpleperf 的 源码仓库 获取源码,然后阅读 README 文件进行源码编译。

Android NDK 获取

下载 Android NDK 文件,直接获取 Simpleperf 工具:

  1. 解压 NDK 文件,找到 simpleperf 文件夹;
  2. 不同的操作系统,在寻找可执行文件存在细微差异。比如在 Windows 设备上,依次进入:simpleperf > bin > windows > x86_64,找到 simpleperf.exe 可执行文件;
  3. 执行 simpleperf -h 命令,获取帮助信息。
$ simpleperf -h
Usage: simpleperf [common options] subcommand [args_for_subcommand]
common options:
    -h/--help     Print this help information.
    --log <severity> Set the minimum severity of logging. Possible severities
                     include verbose, debug, warning, info, error, fatal.
                     Default is info.
    --version     Print version of simpleperf.
subcommands:
    dump                dump perf record file
    help                print help information for simpleperf
    inject              parse etm instruction tracing data
    kmem                collect kernel memory allocation information
    merge               merge multiple perf.data into one
    report              report sampling information in perf.data
    report-sample       report raw sample information in perf.data

simpleperf 目录中,相关文件或脚本的作用:

  1. bin/:包括可执行文件和共享库;
  2. bin/android/${arch}/simpleperf:用于设备上的静态 Simpleperf 可执行文件;
  3. bin/${host}/${arch}/simpleperf:用于主机的 Simpleperf 可执行文件;
  4. bin/${host}/${arch}/libsimpleperf_report.${so/dylib/dll}:用于主机的报告共享库;
  5. *.py, inferno, purgatorio:高级的 Python 封装脚本。

设备内置

通常在高版本的 Android 设备中,内置了 Simpleperf,检查方式如下:

$ adb shell 
> cd /system/bin
> ls | grep simpleperf # 查看设备使用有 Simpleperf 工具,较低 Android 版本可能没有
simpleperf
simpleperf_app_runner
> simpleperf -h # 查看 Simpleperf 的帮助信息

若设备没有内置 Simpleperf,可以在 Android NDK 中的 simpleperf 目录,找到 android 目录(不是 windows 目录)中simpleperf 可执行文件,然后将其 push 到设备中:

$ adb push android-ndk-版本号\simpleperf\bin\android\arm64\simpleperf /data/local/tmp
$ adb shell
> cd /data/local/tmp # 推荐此路径,其他路径可能无法添加权限
> chmod a+x simpleperf
> ls -al | grep simpleperf
-rwxrwxrwx 1 shell shell 3853120 2023-01-25 13:24 simpleperf
> simpleperf -h

Warning

从 Android 15 开始,Android 兼容性定义文档(Android CDD)开始要求设备必须支持 Simpleperf。

基础使用

通过 Android NDK 获取到的 Simpleperf 和在 Android Shell 环境下使用内置的 Simpleperf 使用方式有点区别,但是功能基本相同。首先,我们看下这 两种方式的帮助信息:

Android Shell 环境下的帮助信息
$ simpleperf --help                                                              
Usage: simpleperf [common options] subcommand [args_for_subcommand]                       
common options:                                                                           
    -h/--help     Print this help information.                                            
    --log <severity> Set the minimum severity of logging. Possible severities             
                     include verbose, debug, warning, info, error, fatal.                 
                     Default is info.                                                     
    --log-to-android-buffer  Write log to android log buffer instead of stderr.           
    --version     Print version of simpleperf.                                            
subcommands:                                                                              
    api-collect         Collect recording data generated by app api                       
    api-prepare         Prepare recording via app api                                     
    boot-record         record at boot time                                               
    debug-unwind        Debug/test offline unwinding.                                     
    dump                dump perf record file                                             
    help                print help information for simpleperf                             
    inject              parse etm instruction tracing data                                
    kmem                collect kernel memory allocation information                      
    list                list available event types                                        
    merge               merge multiple perf.data into one                                 
    monitor             monitor events and print their textual representations to stdout  
    record              record sampling info in perf.data                                 
    report              report sampling information in perf.data                          
    report-sample       report raw sample information in perf.data                        
    stat                gather performance counter information                            
    trace-sched         Trace system-wide process runtime events.                         
Android NDK 的帮助信息
$ simpleperf.exe --help
Usage: simpleperf [common options] subcommand [args_for_subcommand]
common options:
    -h/--help     Print this help information.
    --log <severity> Set the minimum severity of logging. Possible severities
                     include verbose, debug, warning, info, error, fatal.
                     Default is info.
    --version     Print version of simpleperf.
subcommands:
    dump                dump perf record file
    help                print help information for simpleperf
    inject              parse etm instruction tracing data
    kmem                collect kernel memory allocation information
    merge               merge multiple perf.data into one
    report              report sampling information in perf.data
    report-sample       report raw sample information in perf.data                        

上述帮助信息可以整理成如下的表格:

子命令/选项 Shell 环境 Android NDK 说明
通用选项
-h/--help 打印帮助信息
--log <severity> 设置日志级别(verbose/debug/warning/info/error/fatal),默认为 info
--log-to-android-buffer 将日志写入Android日志缓冲区(仅Shell环境支持)
--version 打印版本信息
子命令
api-collect 收集通过应用 API 生成的录制数据
api-prepare 通过应用 API 准备录制
boot-record 在启动时录制
debug-unwind 调试/测试离线栈展开
dump 转储 perf 录制文件
help 打印帮助信息
inject 解析 ETM 指令追踪数据
kmem 收集内核内存分配信息
list 列出可用事件类型
merge 合并多个 perf.data 文件
monitor 监控事件并输出文本表示
record 录制采样信息到 perf.data
report 报告 perf.data 中的采样信息
report-sample 报告 perf.data 中的原始采样信息
stat 收集性能计数器信息
trace-sched 追踪系统级进程运行时事件

说明:

  1. ✅ 表示支持,❌ 表示不支持。
  2. NDK 环境缺少部分系统级功能(如 boot-recordmonitor 等),但保留了核心分析功能(如 reportdump 等)。
  3. Shell 环境特有的 --log-to-android-buffer 选项在 NDK 中不可用。

也就是说,相比 Android NDK 提供的 Simpleperf,Android Shell 环境内置的 Simpleperf 支持更多功能。为了可以更好的了解 Simpleperf 的基本使用,本章节的命令特别的说明,默认均在 Android Shell 环境下执行。

list

list 命令用于枚举当前设备支持的所有性能监控事件(PMU events)。由于不同设备的硬件架构(如 CPU/GPU 型号)和内核版本存在差异, 所支持的事件集合也会有所不同。

$ simpleperf list
List of hw-cache events:    # 硬件缓存事件(需CPU缓存支持)
  branch-loads             # 分支预测加载
  ...
List of hardware events:    # 硬件性能计数器事件
  cpu-cycles               # CPU时钟周期
  instructions             # 退休指令数
  ...
List of software events:    # 软件模拟事件(通过内核计数)
  cpu-clock                # CPU时间
  task-clock               # 任务占用CPU时间
  ...

Danger

部分事件可能因 CPU 型号不同而不可用,尝试执行会返回 <not supported> 提示。

在基于 ARM 架构的设备上,输出会额外包含原始PMU事件(raw events):

  1. 这些事件直接来自ARM性能监控单元(PMU)的硬件能力
  2. 内核已对部分常用原始事件做了标准化封装:
    • raw-cpu-cycles → 标准化为 cpu-cycles
    • raw-instruction-retired → 标准化为 instructions
  3. 保留原始事件的目的:
    • 访问设备特有的高级监控能力(如特定微架构事件)
    • 兼容尚未被内核标准化的新硬件特性

stat

stat 命令用于获取调试线程的 event 的计数器的值。通过参数传递,可以选择使用哪个事件,哪个进程/线程被监控,需要监控多长时间和打印间隔。

# 使用默认事件(cpu-cycles、instructions 等),监控进程 7394,持续 10 秒钟。
$ simpleperf stat -p 7394 --duration 10 
Performance counter statistics:

#         count  event_name                # count / runtime
     16,513,564  cpu-cycles                # 1.612904 GHz
      4,564,133  stalled-cycles-frontend   # 341.490 M/sec
      6,520,383  stalled-cycles-backend    # 591.666 M/sec
      4,900,403  instructions              # 612.859 M/sec
         47,821  branch-misses             # 6.085 M/sec
  25.274251(ms)  task-clock                # 0.002520 cpus used
              4  context-switches          # 158.264 /sec
            466  page-faults               # 18.438 K/sec

Total test time: 10.027923 seconds.

stat 命令可以传递 list 命令中获取到的事件参数:

# 选择 cpu-cycles 事件
$ simpleperf stat -e cpu-cycles -p 11904 --duration 10

# 选择 cache-references 和 cache-misses 事件
$ simpleperf stat -e cache-references,cache-misses -p 11904 --duration 10

想要了解 stat 命令的参数含义可以执行 simpleperf stat --help 获取。下表是 stat 命令参数的含义:

选项 描述
-a 收集系统全局的性能计数器信息
--app package_name 分析指定 Android 应用的进程(非 root 设备需为可调试应用)
--cpu cpu_item1,cpu_item2,... 仅在选定的 CPU 上收集信息(支持单个 CPU 号或范围,如 0-3
--csv 以 CSV 格式输出报告
--duration time_in_sec 监控指定秒数(支持浮点数)
--interval time_in_ms 每隔指定毫秒打印一次统计(支持浮点数,默认累计统计)
--interval-only-values 仅打印每个间隔内的事件数值(需配合 --interval
-e event1[:modifier],... 选择要计数的事件列表(事件名或原始 PMU 事件如 r1b
修饰符:
- u:仅监控用户空间事件
- k:仅监控内核空间事件
--group event1[:modifier],... 将事件分组监控(同组事件同时调度,减少多路复用影响)
--no-inherit 不统计创建的子线程/进程
-o output_filename 将报告写入指定文件(默认输出到标准输出)
--per-core 为每个 CPU 核心单独打印计数器
--per-thread 为每个线程单独打印计数器
-p pid1,pid2,... 监控现有进程(与 -a 互斥)
-t tid1,tid2,... 监控现有线程(与 -a 互斥)
--print-hw-counter 测试并打印设备可用的 CPU PMU 硬件计数器
--sort key1,key2,... 指定排序键(配合 --per-thread--per-core 使用)
可选键:
- count:事件计数
- count_per_thread:线程在所有 CPU 上的计数
- cpu:CPU ID
- pid:进程 ID
- tid:线程 ID
- comm:线程名称
默认排序键:count_per_thread,tid,cpu,count
--use-devfreq-counters 在高通 SOC 设备上临时释放被内存延迟监控占用的硬件计数器(可能影响功耗)
--verbose 显示详细模式的结果

record

record 命令用于转储所分析进程的样本。每个样本可以包含样本生成时间、自上次样本以来的事件数、线程的程序计数器、线程的调用链等信息。

# 在进程 7394 上记录 10 秒,使用默认事件(cpu-cycles),使用默认采样频率(每秒 4000 个样本),将记录写入 perf.data 文件
$ simpleperf record -p 7394 --duration 10
simpleperf I cmd_record.cpp:316] Samples recorded: 21430. Samples lost: 0.

# 使用事件 instructions 进行记录。
$ simpleperf record -e instructions -p 11904 --duration 10

# 使用 task-clock 进行记录,该事件显示以纳秒为单位的 CPU 经过时间。
$ simpleperf record -e task-clock -p 11904 --duration 10

Warning

执行 record 命令时若报如下的错误:

$ adb shell "simpleperf record -p 1816 --duration 10"
simpleperf E cmd_record.cpp:480] Can't create output file in directory .: Read-only file system
说明文件存储的路径没有权限,需要通过 -o 的方式将其保存到 /data/local/tmp/ 目录下:

$ adb shell "simpleperf record -p 1816 --duration 10 -o /data/local/tmp/perf.data"

Note

Q: recordstat 命令的关系与区别? stat 直接统计性能事件的总次数,适合快速定位问题。record 记录详细执行过程,生成 perf.data 文件用于深入分析代码瓶颈。 二者通常配合使用,先通过 stat 发现异常,再用 record 查明原因。

想要了解 record 命令的参数含义可以执行 simpleperf record --help 获取。下表是 record 命令参数的含义:

选项 描述
-a 系统级采集(需配合 --exclude-perf 排除 simpleperf 自身样本)
--app package_name 分析指定 Android 应用的进程(非 root 设备需为可调试应用)
-p pid1,pid2,... 监控现有进程(与 -a 互斥)
-t tid1,tid2,... 监控现有线程(与 -a 互斥)
-e event1[:modifier],... 选择记录的事件(支持事件名、原始PMU事件如 r1b、kprobe事件)
修饰符:u(用户空间)、k(内核空间)
--group event1[:modifier],... 将事件分组监控(同组事件同时调度)
--trace-offcpu 记录线程被调度下CPU的事件(等效于 -c 1 -e sched:sched_switch
--kprobe kprobe_event 添加动态kprobe事件(格式参见内核文档)
--add-counter event1,... 在采样中附加事件计数(如同时记录cycles和instructions)
-f freq 设置采样频率(Hz,默认4000)
-c count 设置采样周期(每N次事件记录一次)
--call-graph fp/dwarf 启用调用图记录(默认dwarf,65528)
-g 等效于 --call-graph dwarf
--clockid clock_id 指定采样时间戳时钟(realtime/monotonic等)
--cpu cpu_items 仅在指定CPU核心采集(支持单个号或范围如0-3)
--duration 秒数 设置监控时长(支持小数)
-j branch_filters 启用分支栈采样(any/any_call/any_ret等过滤器)
-b 等效于 -j any
--addr-filter 为指令追踪设置地址过滤器(格式:filter/start/stop)
--tp-filter 设置tracepoint事件过滤器(格式参见内核文档)
--post-unwind 控制DWARF解栈时机(默认实时解栈)
--no-unwind 禁用调用栈解栈
--no-callchain-joiner 禁用调用链合并(默认启用以突破64K栈限制)
--exclude-perf 排除simpleperf自身样本
--exclude-pid/tid 排除指定进程/线程
--include-pid/tid 仅包含指定进程/线程
--exclude-process-name 通过正则排除进程名
--include-uid 仅包含指定UID的进程
-o 文件名 设置输出文件名(默认perf.data)
--size-limit SIZE 设置最大记录大小(支持K/M/G单位)
--symfs <dir> 指定符号文件搜索目录
--no-dump-symbols 禁止在记录文件中转储符号
--exit-with-parent 随父进程退出停止记录
--stdio-controls-profiling 通过stdin/stdout控制采样启停
--in-app 声明已在应用上下文中运行

说明:

  1. 默认行为:不加选项时默认记录 cpu-cycles 事件,采样频率 4000Hz,输出到 perf.data
  2. DWARF 解栈:默认实时解栈用户调用栈(可通过 --post-unwind 调整);
  3. 分支分析:-j 支持多种分支类型过滤(如函数调用/返回);
  4. Android 适配:--app--in-app 选项专门用于 Android 应用分析。

report

report 命令用于解析 record 命令生成的采样数据(如 perf.data),生成可读的性能分析报告,帮助开发者定位性能瓶颈。

# 基础报告
simpleperf report -i perf.data
# 生成调用图
simpleperf report -g caller --sort comm,symbol
# 过滤特定动态库
simpleperf report --dsos libc.so

想要了解 report 命令的参数含义可以执行 simpleperf report --help 获取。下表是 report 命令参数的含义:

选项 描述
-b 使用分支目标地址代替指令地址(需配合 -b/-j 录制的数据)
--children 显示调用链累计开销(Children列包含子函数开销)
--csv 以CSV格式输出报告
--csv-separator 设置CSV列分隔符(默认逗号)
--full-callgraph 打印完整调用图(需配合 -g
-g [callee\|caller] 打印调用图(callee模式显示被谁调用,caller模式显示调用谁,默认caller)
-i <file> 指定输入文件(默认perf.data)
--kallsyms <file> 指定内核符号文件路径
--max-stack <frames> 设置调用图最大显示栈帧数
-n 显示每个条目的采样次数
--no-demangle 禁止符号名反混淆
--no-show-ip 对未知符号不显示虚拟地址
-o <file> 设置报告输出文件(默认stdout)
--percent-limit <percent> 设置报告条目最小百分比阈值
--print-event-count 显示每个条目的事件计数(需record时用--add-counter
--raw-period 显示原始周期计数而非百分比
--sort key1,key2,... 设置报告排序键
可选键:pid/tid/comm/dso/symbol/vaddr_in_file
分支专用键:dso_from/dso_to/symbol_from/symbol_to
默认:comm,pid,tid,dso,symbol
--symfs <dir> 指定符号文件搜索目录
--vmlinux <file> 指定内核符号文件
--comms comm1,comm2,... 仅显示指定线程名的样本
--cpu cpu_items 仅显示指定CPU的样本(支持单号或范围如0-3)
--dsos dso1,dso2,... 仅显示指定动态库的样本
--pids/--tids 仅显示指定进程/线程的样本
--symbols sym1;sym2;... 仅显示指定函数的样本
--exclude-pid/tid 排除指定进程/线程样本
--include-process-name 通过正则包含进程名样本
--filter-file <file> 使用时间戳过滤样本(文件格式见doc)

高级脚本

这里说的 “高级脚本” 指的是在 Android NDK 下的将 simpleperf 命令使用高级脚本语言(比如:Python)进行了二次封装的脚本。 按照功能的不同,这些脚本可以分成三种类型:

  1. 用于记录的脚本(功能和 record 命令相似):app_profiler.pyrun_simpleperf_without_usb_connection.py
  2. 用于报告的脚本(功能和 report 命令相似):report.pyreport_html.pyinferno
  3. 用于分析的脚本:simpleperf_report_lib.py

Warning

这些脚本需要在 Python 3.9 及其以上的版本中才可以正常运行。

记录的脚本

app_profiler.py

app_profiler.py 脚本是对 simpleperf record 命令的高级封装,用于记录(采集)应用程序或 Native 程序的数据。 默认记录完成后,会在本地生成一个 perf.data 文件。

# 记录一个 Android 应用程序。
$ ./app_profiler.py -p simpleperf.example.cpp

# 记录一个将 Java 代码编译为本机指令的 Android 应用程序。
$ ./app_profiler.py -p simpleperf.example.cpp --compile_java_code

# 记录启动 Android 应用程序的 Activity。
$ ./app_profiler.py -p simpleperf.example.cpp -a .SleepActivity

# 记录一个本机进程。
$ ./app_profiler.py -np surfaceflinger

# 记录给定 pid 的本机进程。
$ ./app_profiler.py --pid 11324

# 记录一个命令。
$ ./app_profiler.py -cmd \
    "dex2oat --dex-file=/data/local/tmp/app-debug.apk --oat-file=/data/local/tmp/a.oat"

# 记录一个 Android 应用程序,并使用 -r 将自定义选项发送到记录命令。
$ ./app_profiler.py -p simpleperf.example.cpp \
    -r "-e cpu-clock -g --duration 30"

# 同时记录 CPU 时间和非 CPU 时间。
$ ./app_profiler.py -p simpleperf.example.cpp \
    -r "-e task-clock -g -f 1000 --duration 10 --trace-offcpu"

# 将分析数据保存在一个自定义文件中(如 perf_custom.data),而不是 perf.data。
$ ./app_profiler.py -p simpleperf.example.cpp -o perf_custom.data
run_simpleperf_on_device.py

有时候会有想要分析应用启动(冷热启动)的需求。此时,可以考虑使用 run_simpleperf_on_device.py 脚本。通过添加参数 --app 监控某个应用的进程是否存在。如果不存在,会以 1ms 的间隔循环轮询应用程序进程。

$ ./run_simpleperf_on_device.py record --app simpleperf.example.cpp \
    -g --duration 1 -o /data/local/tmp/perf.data
# 运行完命令后,手动启动应用或者通过 am start 的方式启动应用
api_profiler.py

api_profiler.py 用于控制应用程序代码中的记录。 它在记录前进行准备工作,并在记录后收集分析数据文件。

run_simpleperf_without_usb_connection.py

在设备没有通过 USB 连接电脑时,也可以使用 Simpleperf 记录数据。

$ ./run_simpleperf_without_usb_connection.py start -p simpleperf.example.cpp
# 在命令成功完成后,拔掉 USB 数据线,运行 SimpleperfExampleCpp 应用程序。
# 几秒钟后,再次插入 USB 数据线。

$ ./run_simpleperf_without_usb_connection.py stop
# 停止可能需要一点时间。等待停止后,性能分析数据会保存在本地的 perf.data 文件中
binary_cache_builder.py

binary_cache_builder.py 文件可以从 Android 设备中提取二进制文件,也可以在本地的目录中查找二进制文件。默认情况下,执行完 app_profiler.py 脚本后,会在本地生成一个 binary_cache 的目录,该目录用于保存在分析数据时(生成报告时)所需要的调试信息和符号表。

# 通过从设备中拉取二进制文件,为 perf.data 生成 binary_cache
$ ./binary_cache_builder.py

# 通过从设备中拉取二进制文件并在 SimpleperfExampleCpp 中查找二进制文件,生成 binary_cache
$ ./binary_cache_builder.py -lib path_of_SimpleperfExampleCpp
run_simpleperf_on_device.py

run_simpleperf_on_device.py 脚本将 simpleperf 可执行文件推送到设备上,并在设备上运行 simpleperf 命令。 它比手动运行 adb 命令更方便。

报告的脚本

report.py

report.py 是对 simpleperf record 命令的高级封装,它可以接收 record 命令的所有参数。

# 调用图
$ ./report.py -g

# 在由 Python Tk 实现的 GUI 窗口中报告调用图。
$ ./report.py -g --gui
report_html.py

report_html.py 可以将本地的 perf.data 数据转换成一个 HTML 文件,然后通过浏览器可以直接访问查看性能数据信息,包括图表统计、示例表、火焰图、每个函数的带注释的源代码、每个函数的带注释的反汇编等。

# 生成基于 perf.data 的图表统计、样本表和火焰图。
$ ./report_html.py

# 添加源代码。
$ ./report_html.py --add_source_code --source_dirs path_of_SimpleperfExampleCpp

# 添加反汇编内容。
$ ./report_html.py --add_disassembly

# 添加所有二进制文件的反汇编内容可能会耗费大量时间。因此,我们可以选择只为选定的二进制文件添加反汇编内容。
$ ./report_html.py --add_disassembly --binary_filter libgame.so

# report_html.py 接受多个记录数据文件。
$ ./report_html.py -i perf1.data perf2.data

Danger

注意:生成的 Html 可能会加载失败,这是无法访问 JavaScript 源,解决办法参考 生成火焰图无法加载

inferno

inferno 是一个用于在 html 文件中生成火焰图的工具。

# 基于 perf.data 生成火焰图。
# 在 Windows 上,请使用 inferno.bat 而不是 ./inferno.sh。
$ ./inferno.sh -sc --record_file perf.data

# 记录一个本地程序并生成火焰图。
$ ./inferno.sh -np surfaceflinger
report_sample.py

report_sample.py 将分析数据文件转换为 linux-perf-tool 输出的 perf 脚本文本格式。

该格式可以导入到:

# 将性能分析记录到 perf.data 中。
$ ./app_profiler.py <args>

# 将当前目录中的 perf.data 转换为 FlameGraph 使用的格式。
$ ./report_sample.py --symfs binary_cache > out.perf

$ git clone https://github.com/brendangregg/FlameGraph.git
$ FlameGraph/stackcollapse-perf.pl out.perf > out.folded
$ FlameGraph/flamegraph.pl out.folded > a.svg

脚本总结

下表是对上水提到的脚本功能的总结:

脚本名称 功能描述
性能分析相关脚本
app_profiler.py 用于分析应用程序性能,收集性能数据(如CPU使用率、内存等)
run_simpleperf_on_device.py 在设备上运行Simpleperf工具进行性能分析(需USB连接)
api_profiler.py 分析应用程序的API调用性能
run_simpleperf_without_usb_connection.py 在无USB连接的情况下运行Simpleperf进行性能分析
binary_cache_builder.py 构建二进制缓存,用于优化性能分析过程中的符号解析
报告生成相关脚本
report.py 生成性能分析报告(文本格式)
report_html.py 生成HTML格式的性能分析报告,支持可视化展示
inferno 将Simpleperf数据转换为火焰图(FlameGraph)的工具
report_sample.py 生成采样报告,展示具体的采样数据点

Simpleperf 的脚本很多,到底应该如何选择这些脚本呢?首先,我们需要弄清楚这些脚本的之间的关系:

脚本的选择

上图中提到了一些 Simpleperf 以外的性能分析工具,比如 Firefox Profiler、FlameGrap、PProf 和 Android Studio 等。通过 Simpleperf 提供的脚本可以将数据很轻松地转换成其他工具可以识别的格式,从而在不同的工具上打开和访问它们。以下是这些工具的基本介绍:

  • Firefox Profiler:火狐性能分析器。是一个用于分析和优化 Firefox 浏览器性能的工具。通过 Firefox Profiler,用户可以捕获和分析浏览器在运行过程中的各种性能数据,包括 CPU 使用率、内存占用、网络请求、渲染时间等指标。这些数据可以帮助开发者识别潜在的性能瓶颈,并进行针对性的优化。它提供了直观的可视化界面,包括图表和时间线,帮助用户更好地理解性能数据。开发者可以利用这些信息来改进网站性能、优化代码以及提升用户体验。
  • FlameGrap:是一种用于可视化软件程序性能分析数据的工具,最初由 Brendan Gregg 创建。它以图形方式呈现了程序在执行过程中的调用栈信息和函数调用关系,帮助开发人员快速了解程序中的性能瓶颈和优化方向。该工具被广泛应用于分析各种软件系统的性能特征,包括操作系统、应用程序和服务端程序等。
  • PProf:PProf 是一个性能分析工具,通常用于分析 Go 语言程序的性能特征。它可以帮助开发人员定位并解决程序中的性能瓶颈,提高程序的运行效率。PProf 可以生成可视化的分析报告,展示程序在运行过程中 CPU 使用情况、内存分配情况、函数调用频率等信息。开发人员可以通过这些报告快速了解程序的性能特征,并据此进行优化。
  • Android Studio:本身就内置很多 Android 开发中常使用的工具,比如 Profiler、Logcat 等。Android Studio 自身就是一个分析 Android 性能的工具之一。

其实,通过上图不难看出来:Simpleperf 中 perf.data 可以说是一个“万金油”的文件,只要想办法获取到 perf.data 文件就可以通过已有的脚本将其转换成任意格式的文件,然后在不同的性能分析工具上打开它们。

实际生产过程中,如何选择脚本呢?首先,确定你想要什么样的格式文件,然后选择一个最短路径。比如:

  1. 希望获取系统状态的火焰图及其系统信息,那么最短路径就是:perf.data > report.html,脚本选择 report_html.py
  2. 希望获取到差分火焰图,那么最短路径就是:perf.data > a.folded > 火焰图 > 差分火焰图,脚本选择:stackcollapse.py > flamegrap.pl

Note

flamegrap.pldifffolded.pl 不是 Simpleperf 工具自带的,需要单独到 Github 下载

Framework

代码插桩

在 Framework 开发过程中,想要在某个位置抓取 Simpleperf 的数据,可以添加如下的代码:

try {
  // for capability check
  Os.prctl(OsConstants.PR_CAP_AMBIENT, OsConstants.PR_CAP_AMBIENT_RAISE,
           OsConstants.CAP_SYS_PTRACE, 0, 0);
  // Write to /data instead of /data/local/tmp. Because /data can be written by system user.
  Runtime.getRuntime().exec("/system/bin/simpleperf record -g -p " + String.valueOf(Process.myPid())
            + " -o /data/perf.data --duration 30 --log-to-android-buffer --log verbose");
} catch (Exception e) {
  Slog.e(TAG, "error while running simpleperf");
  e.printStackTrace();

Danger

在设备上,需要先关闭 SElinux:adb shell setenforce 0。因为 SELinux 仅允许 Simpleperf 运行在 Shell 或者是可以 debug 和可分析的应用中;

设备启动分析

在 userdebug/eng/userroot 设备上,Simpleperf 可以获取到设备启动时的数据:

Step 1:设置系统 persist 属性

adb shell setprop persist.simpleperf.boot_record true

在 Android 系统中,persist.simpleperf.boot_record 用于控制 Simpleperf 是否在每次系统启动时记录性能数据。如果这个属性被设置为 true,则表示系统会在每次启动时自动记录性能数据;如果设置为 false,则不会。

Step 2:重启设备

adb reboot

当重启设备时,init 查询到这个 persist 属性已经设置的话,它就会 fork 一个后台进程执行 Simpleperf 录制 boot-time 的 profile。init 启动 Simpleperf 在 zygote-start 阶段(即 zygote 启动后)。

Step 3:获取数据

重启完成后,在 /data/simpleperf_boot_data 目录下会存在记录到的数据:

$ adb shell ls /data/simpleperf_boot_data
perf-20220126-11-47-51.data

以下是一个获取到的数据例子。从时间戳看,第一个 sample 被生成大约在启动后的 4.5 秒:

设备启动数据

高级进阶

了解 stat 的工作原理

stat 命令的核心工作原理可分为事件配置、数据采集、结果计算三个阶段:

  1. 事件配置阶段初始化性能监控环境,确定要测量的硬件/软件事件及其监控方式。
    • 事件选择:通过 -e 参数指定监控事件(如 cpu-cyclescache-misses),支持的事件列表可通过 simpleperf list 查询。
    • 多路复用处理:若事件数超过 CPU PMU 硬件计数器数量(如 ARM 通常 4-6 个),内核自动启用时间分片轮询,分组轮流测量事件(默认每组监控 1-10ms)。
    • 监控目标绑定:确定监控范围(如特定进程 -p PID、整个系统 -a 或 Android 应用 --app package_name)。
  2. 数据采集阶段通过 CPU PMU 硬件计数器实时采集性能数据。
    • PMU 寄存器配置:通过 Linux perf_event_open 系统调用,初始化 PMU 计数器并开始计数。
    • 数据读取机制:通过 -I 参数设置读取间隔(如 -I 100 表示每 100ms 读取一次计数器值)。
    • 内核将计数器数据写入 perf buffer,用户态工具(Simpleperf)从中读取。
    • 若启用多路复用,内核按时间片切换事件组,记录每个事件的 time_enabled(总启用时间)和 time_running(实际计数时间)。
  3. 结果计算阶段对原始计数器数据进行处理,生成用户可读的统计结果。
    • 若发生多路复用,按公式缩放数据:真实估值 = 测量值 × (time_enabled / time_running)
    • 指标计算:绝对值计算直接输出事件计数,如 1,000,000 cpu-cycles)。比率指标计算,如 IPC = instructions / cpu-cycles

Note

在执行 Simpleperf 命令时,可以通过 -v 参数查看详细日志。

PMU 多路复用问题

在 Simpleperf 工具中,PMU(Performance Monitoring Unit)多路复用问题 指的是由于 CPU 硬件性能计数器的数量有限(如 ARM Cortex-A77 仅有 6 个物理 PMU 计数器),当使用 stat 命令监控的硬件事件数量(如 CPU 周期、缓存命中/失效等)超过可用计数器时, 内核会通过时间片轮转方式共享硬件寄存器,导致每个事件仅能部分时间被监测,从而引发测量精度下降或某些事件无法同时测量的问题。

在 Simpleperf 输出中,如果看到如下警告,说明发生了多路复用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ simpleperf stat -p 7394 -e cache-references,cache-references:u,cache-references:k \
      -e cache-misses,cache-misses:u,cache-misses:k,instructions --duration 1
Performance counter statistics:

#   count  event_name           # count / runtime
  490,713  cache-references     # 151.682 M/sec
  899,652  cache-references:u   # 130.152 M/sec
  855,218  cache-references:k   # 111.356 M/sec
   61,602  cache-misses         # 7.710 M/sec
   33,282  cache-misses:u       # 5.050 M/sec
   11,662  cache-misses:k       # 4.478 M/sec
        0  instructions         #

Total test time: 1.000867 seconds.
simpleperf W cmd_stat.cpp:946] It seems the number of hardware events are more than the number of
available CPU PMU hardware counters. That will trigger hardware counter
multiplexing. As a result, events are not counted all the time processes
running, and event counts are smaller than what really happen

Simpleperf 默认允许多路复用,但可通过 --no-multiplexing 强制禁用:

$ simpleperf stat --no-multiplexing -e cpu-cycles,branch-misses --app com.example.app

或者在执行命令前,先查询每个 CPU 可用的计数器:

1
2
3
4
5
6
7
8
9
$ simpleperf stat --print-hw-counter
There are 2 CPU PMU hardware counters available on cpu 0.
There are 2 CPU PMU hardware counters available on cpu 1.
There are 2 CPU PMU hardware counters available on cpu 2.
There are 2 CPU PMU hardware counters available on cpu 3.
There are 2 CPU PMU hardware counters available on cpu 4.
There are 2 CPU PMU hardware counters available on cpu 5.
There are 2 CPU PMU hardware counters available on cpu 6.
There are 2 CPU PMU hardware counters available on cpu 7.

为了降低 PMU 多路复用对采集到的数据的影响,建议可以从这些方面进行优化。

优化方向 1:减少监控事件的数量,只选择最关键的几个核心事件进行监控(如 cpu-cycles + instructions):

优化方向 2:使用事件分组测量,将需要监控的事件分成多组,分别进行测量。

# 第一次测量 CPU 相关事件
simpleperf stat -e cpu-cycles,instructions -p 1234
# 第二次测量缓存相关事件
simpleperf stat -e cache-references,cache-misses -p 1234

或者,添加 --group 参数将参数进行分组监控。将多个事件绑定为一组,确保它们在同一时间片被测量,避免因多路复用导致的时间片轮换误差:

# 确保 cycles 和 instructions 同步测量,避免比值失真。
simpleperf stat --group cpu-cycles,instructions -p 1234
# 缓存相关事件和分支事件分别成组,减少组内误差。
simpleperf stat \
 --group cache-references,cache-misses \
 --group branch-instructions,branch-misses \
 --app com.example.app

注意:并不是所有的事件都支持分组,比如 ARM 的 stalled-cycles 就有可能独占计数器。也可以手动将某个事件强制独占计数器:

$ simpleperf stat -e 'cpu-cycles:e'  # 保证全程监控

优化方向 3:调整采样参数。可以增大采样间隔(减少多路复用切换频率)或者延长监控时长(提高数据准确性):

simpleperf stat -I 100 -e event1,event2...  # 100ms 采样间隔
simpleperf stat --duration 10 -e event1,event2...  # 监控 10 秒

优化方向 4:绑定到特定CPU核心测量(减少多核竞争):

simpleperf stat --cpu 0 -e event1,event2...  # 仅监控 CPU 0

FAQs

火焰图无法加载

使用 Android NDK 中的 Simpleperf 生成的网页无法打开或加载的问题,一直困扰了很多使用 Simpleperf 的 Android 性能开发工作者。这是因为本地生成的页面依赖一些 JavaScript 文件,国内的网络访问不到 CDN 源,此时需要将这些源替换成国内可以访问的源。

以下是两种解决办法:

  1. 修改 Simpleperf 生成的 HTML 文件,将 JavaScript 源替换成国内网络可以访问到的源;
  2. 修改 Python 脚本,使其在生成 HTML 前完成 JavaScript 源的替换工作。

这两种方法前者仅一次生效,后者永久生效。

修改 HTML 文件

因为这个方法是使用了 Simpleperf 的 Python 生成 HTML 后再去做替换,所以最大的弊端就是每次都需要手动替换。具体的替换步骤如下:

  1. 使用记事本软件打开 HTML 文件(我本地这个文件名字叫:report.html);
  2. 按照如下的方式进行替换:

替换前:

<html>
<head>
    <link rel="stylesheet" type="text/css"
          href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css"></link>
    <link rel="stylesheet" type="text/css"
          href="https://cdn.datatables.net/1.10.19/css/dataTables.bootstrap4.min.css"></link>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/js/bootstrap.min.js"></script>
    <script src="https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js"></script>
    <script src="https://cdn.datatables.net/1.10.19/js/dataTables.bootstrap4.min.js"></script>
    <script src="https://www.gstatic.com/charts/loader.js"></script>
    <script>google.charts.load('current', {'packages': ['corechart', 'table']});</script>
    <style type="text/css">
        .colForLine {
            width: 50px;
        }

        .colForCount {
            width: 100px;
        }

        .tableCell {
            font-size: 17px;
        }

        .boldTableCell {
            font-weight: bold;
            font-size: 17px;
        }
    </style>
</head>
<body>
<script>

替换后:

<html>
<head>
    <link rel="stylesheet" type="text/css"
          href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.1.2/css/bootstrap.min.css"></link>
    <link rel="stylesheet" type="text/css"
          href="https://cdn.datatables.net/1.10.19/css/dataTables.bootstrap4.min.css"></link>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.1.2/js/bootstrap.min.js"></script>
    <script src="https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js"></script>
    <script src="https://cdn.datatables.net/1.10.19/js/dataTables.bootstrap4.min.js"></script>
    <script src="https://www.gstatic.com/charts/loader.js"></script>
    <script>google.charts.load('current', {'packages': ['corechart', 'table']});</script>
    <style type="text/css">
        .colForLine {
            width: 50px;
        }

        .colForCount {
            width: 100px;
        }

        .tableCell {
            font-size: 17px;
        }

        .boldTableCell {
            font-weight: bold;
            font-size: 17px;
        }
    </style>
</head>
<body>
<script>
修改 Python 脚本

因为这个方法是在 HTML 生成之前就做了替换,所以最大的优点就是不需要每次手动替换。在获取到 perf.data 文件后,我们会使用 Simpleperf 中的 report_html.py 脚本进行渲染出 report.html 文件:

python ./report_html.py -i D:\temp\perf.data -o D:\temp\report.html

因此,需要修改的是 report_html.py 文件。具体步骤如下:

  1. 使用记事本打开 report_html.py;
  2. 搜索关键字“URLS”,找到 JavaScript 数据源;
  3. 按照如下的方式进行替换:

替换前:

URLS = {
    'jquery': 'https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js',
    'bootstrap4-css': 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css',
    'bootstrap4-popper':
        'https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js',
    'bootstrap4': 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/js/bootstrap.min.js',
    'dataTable': 'https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js',
    'dataTable-bootstrap4': 'https://cdn.datatables.net/1.10.19/js/dataTables.bootstrap4.min.js',
    'dataTable-css': 'https://cdn.datatables.net/1.10.19/css/dataTables.bootstrap4.min.css',
    'gstatic-charts': 'https://www.gstatic.com/charts/loader.js',
}

替换后:

URLS = {
    'jquery': 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js',
    'bootstrap4-css': 'https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.1.2/css/bootstrap.min.css',
    'bootstrap4-popper':
        'https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js',
    'bootstrap4': 'https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.1.2/js/bootstrap.min.js',
    'dataTable': 'https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js',
    'dataTable-bootstrap4': 'https://cdn.datatables.net/1.10.19/js/dataTables.bootstrap4.min.js',
    'dataTable-css': 'https://cdn.datatables.net/1.10.19/css/dataTables.bootstrap4.min.css',
    'gstatic-charts': 'https://www.gstatic.com/charts/loader.js',
}

快速入门 SQL 语言

介绍

SQL(Structured Query Language:结构化查询语言)是一种用于管理关系型数据库系统的标准语言。它提供了一套强大的语句和命令,用于执行各种数据库操作,包括数据的定义、查询、更新和删除。SQL 支持数据库结构的创建和修改,允许用户定义表结构、索引以及其他数据库对象。同时,它还负责控制数据访问权限,确保数据的安全性和完整性。通过 SQL,用户可以高效地处理和分析数据,支持从简单查询到复杂的数据操作和数据管理任务。

发展历史

SQL 语言的历史可以追溯到20世纪70年代。最初由 IBM 的 Donald D. Chamberlin 和 Raymond F. Boyce 开发,用于管理关系型数据库模型。1974年,SQL 首次发布为 SEQUEL(Structured English Query Language),其目的是简化数据访问和操作。1979年,Oracle 发布了第一个商业化的 SQL 数据库。1986年,ANSI(美国国家标准学会)首次发布 SQL 标准,标志着 SQL 成为数据库行业的标准语言。 随后,SQL 经历了多次版本更新和扩展,增加了更多功能,逐渐成为现代关系型数据库的核心语言。

RDBMS

RDBMS 指关系型数据库管理系统,全称 Relational Database Management System。 RDBMS 是 SQL 的基础,同样也是所有现代数据库系统的基础,比如 MS SQL Server、IBM DB2、Oracle、MySQL 以及 Microsoft Access。 RDBMS 中的数据存储在被称为表的数据库对象中。表是相关的数据项的集合,它由列和行组成。

语法树

Warning

SQL 语言对字母的大小写不敏感。也就是说,SELECT 和 select 是一个意思。

SQL 语法树 SQL 语法树

数据定义语言(DDL)

数据定义语言(DDL,Data Definition Language)是一类用于定义和管理数据库结构的 SQL(Structured Query Language)子集。DDL 主要用于创建、修改和删除数据库对象,如表、视图、索引、用户等。

CREATE

创建数据库

CREATE DATABASE 用于创建数据库,语法如下:

CREATE
DATABASE database_name
创建表

CREATE 用于创建新的数据库对象,语法如下:

CREATE TABLE table_name
(
    column1 data_type,
    column2 data_type,
    column3 data_type,
)
数据类型

数据类型(data_type)规定了列可容纳何种数据类型。 在 MySQL 中,有三种主要的类型:文本、数字和日期/时间类型。

Text 类型
数据类型 描述
CHAR(size) 保存固定长度的字符串(可包含字母、数字以及特殊字符)。在括号中指定字符串的长度。最多 255 个字符。
VARCHAR(size) 保存可变长度的字符串(可包含字母、数字以及特殊字符)。在括号中指定字符串的最大长度。最多 255 个字符。注释:如果值的长度大于 255,则被转换为 TEXT 类型。
TINYTEXT 存放最大长度为 255 个字符的字符串。
TEXT 存放最大长度为 65,535 个字符的字符串。
BLOB 用于 BLOBs (Binary Large OBjects)。存放最多 65,535 字节的数据。
MEDIUMTEXT 存放最大长度为 16,777,215 个字符的字符串。
MEDIUMBLOB 用于 BLOBs (Binary Large OBjects)。存放最多 16,777,215 字节的数据。
LONGTEXT 存放最大长度为 4,294,967,295 个字符的字符串。
LONGBLOB 用于 BLOBs (Binary Large OBjects)。存放最多 4,294,967,295 字节的数据。
ENUM(x,y,z,etc.) 允许你输入可能值的列表。可以在 ENUM 列表中列出最大 65535 个值。如果列表中不存在插入的值,则插入空值。注释:这些值是按照你输入的顺序存储的。可以按照此格式输入可能的值:ENUM('X','Y','Z')
SET 与 ENUM 类似,SET 最多只能包含 64 个列表项,不过 SET 可存储一个以上的值。
Number 类型
数据类型 描述
TINYINT(size) -128 到 127 常规。0 到 255 无符号*。在括号中规定最大位数。
SMALLINT(size) -32768 到 32767 常规。0 到 65535 无符号*。在括号中规定最大位数。
MEDIUMINT(size) -8388608 到 8388607 普通。0 to 16777215 无符号*。在括号中规定最大位数。
INT(size) -2147483648 到 2147483647 常规。0 到 4294967295 无符号*。在括号中规定最大位数。
BIGINT(size) -9223372036854775808 到 9223372036854775807 常规。0 到 18446744073709551615 无符号*。在括号中规定最大位数。
FLOAT(size,d) 带有浮动小数点的小数字。在括号中规定最大位数。在 d 参数中规定小数点右侧的最大位数。
DOUBLE(size,d) 带有浮动小数点的大数字。在括号中规定最大位数。在 d 参数中规定小数点右侧的最大位数。
DECIMAL(size,d) 作为字符串存储的 DOUBLE 类型,允许固定的小数点。

这些整数类型拥有额外的选项 UNSIGNED。通常,整数可以是负数或正数。如果添加 UNSIGNED 属性,那么范围将从 0 开始,而不是某个负数。

Date 类型
数据类型 描述
DATE() 日期。格式:YYYY-MM-DD注释:支持的范围是从 '1000-01-01' 到 '9999-12-31'
DATETIME() *日期和时间的组合。格式:YYYY-MM-DD HH:MM:SS注释:支持的范围是从 '1000-01-01 00:00:00' 到 '9999-12-31 23:59:59'
TIMESTAMP() *时间戳。TIMESTAMP 值使用 Unix 纪元('1970-01-01 00:00:00' UTC) 至今的描述来存储。格式:YYYY-MM-DD HH:MM:SS注释:支持的范围是从 '1970-01-01 00:00:01' UTC 到 '2038-01-09 03:14:07' UTC
TIME() 时间。格式:HH:MM:SS注释:支持的范围是从 '-838:59:59' 到 '838:59:59'
YEAR() 2 位或 4 位格式的年。注释:4 位格式所允许的值:1901 到 2155。2 位格式所允许的值:70 到 69,表示从 1970 到 2069。

即便 DATETIME 和 TIMESTAMP 返回相同的格式,它们的工作方式很不同。在 INSERT 或 UPDATE 查询中,TIMESTAMP 自动把自身设置为当前的日期和时间。TIMESTAMP 也接受不同的格式,比如 YYYYMMDDHHMMSS、YYMMDDHHMMSS、YYYYMMDD 或 YYMMDD。

约束

SQL 中约束用于限制加入表的数据的类型。可以在创建表时规定约束(通过 CREATE TABLE 语句),或者在表创建之后也可以(通过 ALTER TABLE 语句)。常见的约束有:

  • NOT NULL
  • UNIQUE
  • PRIMARY KEY
  • FOREIGN KEY
  • CHECK
  • DEFAULT
NOT NULL

NOT NULL 约束强制列不接受 NULL 值。NOT NULL 约束强制字段始终包含值。这意味着,如果不向字段添加值,就无法插入新记录或者更新记录。

CREATE TABLE Persons
(
    Id_P      int          NOT NULL,
    LastName  varchar(255) NOT NULL,
    FirstName varchar(255),
    Address   varchar(255),
    City      varchar(255)
)
UNIQUE

UNIQUE 约束唯一标识数据库表中的每条记录。UNIQUE 和 PRIMARY KEY 约束均为列或列集合提供了唯一性的保证。PRIMARY KEY 拥有自动定义的 UNIQUE 约束。

Warning

每个表可以有多个 UNIQUE 约束,但是每个表只能有一个 PRIMARY KEY 约束。

CREATE TABLE Persons
(
    Id_P      int          NOT NULL,
    LastName  varchar(255) NOT NULL,
    FirstName varchar(255),
    Address   varchar(255),
    City      varchar(255),
    UNIQUE (Id_P)
)

如果需要命名 UNIQUE 约束,以及为多个列定义 UNIQUE 约束,请使用下面的 SQL 语法:

CREATE TABLE Persons
(
    Id_P      int          NOT NULL,
    LastName  varchar(255) NOT NULL,
    FirstName varchar(255),
    Address   varchar(255),
    City      varchar(255),
    CONSTRAINT uc_PersonID UNIQUE (Id_P, LastName)
)

当表已被创建时,如需在 "Id_P" 列创建 UNIQUE 约束,请使用下列 SQL:

ALTER TABLE Persons
    ADD UNIQUE (Id_P)

如需命名 UNIQUE 约束,并定义多个列的 UNIQUE 约束,请使用下面的 SQL 语法:

ALTER TABLE Persons
    ADD CONSTRAINT uc_PersonID UNIQUE (Id_P, LastName)

如需撤销 UNIQUE 约束,请使用下面的 SQL:

ALTER TABLE Persons
DROP INDEX uc_PersonID
PRIMARY KEY

PRIMARY KEY 约束唯一标识数据库表中的每条记录。主键必须包含唯一的值。主键列不能包含 NULL 值。每个表都应该有一个主键,并且每个表只能有一个主键。

下面的 SQL 在 "Persons" 表创建时在 "Id_P" 列创建 PRIMARY KEY 约束:

CREATE TABLE Persons
(
    Id_P      int          NOT NULL,
    LastName  varchar(255) NOT NULL,
    FirstName varchar(255),
    Address   varchar(255),
    City      varchar(255),
    PRIMARY KEY (Id_P)
)

如果需要命名 PRIMARY KEY 约束,以及为多个列定义 PRIMARY KEY 约束,请使用下面的 SQL 语法:

CREATE TABLE Persons
(
    Id_P      int          NOT NULL,
    LastName  varchar(255) NOT NULL,
    FirstName varchar(255),
    Address   varchar(255),
    City      varchar(255),
    CONSTRAINT pk_PersonID PRIMARY KEY (Id_P, LastName)
)

如果在表已存在的情况下为 "Id_P" 列创建 PRIMARY KEY 约束,请使用下面的 SQL:

ALTER TABLE Persons
    ADD PRIMARY KEY (Id_P)

如果需要命名 PRIMARY KEY 约束,以及为多个列定义 PRIMARY KEY 约束,请使用下面的 SQL 语法:

ALTER TABLE Persons
    ADD CONSTRAINT pk_PersonID PRIMARY KEY (Id_P, LastName)

如需撤销 PRIMARY KEY 约束,请使用下面的 SQL:

ALTER TABLE Persons
DROP
PRIMARY KEY
FOREIGN KEY

一个表中的 FOREIGN KEY 指向另一个表中的 PRIMARY KEY。让我们通过一个例子来解释外键。请看下面两个表:

"Persons" 表:

Id_P LastName FirstName Address City
1 Adams John Oxford Street London
2 Bush George Fifth Avenue New York
3 Carter Thomas Changan Street Beijing

"Orders" 表:

Id_O OrderNo Id_P
1 77895 3
2 44678 3
3 22456 1
4 24562 1

"Orders" 中的 "Id_P" 列指向 "Persons" 表中的 "Id_P" 列。"Persons" 表中的 "Id_P" 列是 "Persons" 表中的 PRIMARY KEY。" Orders" 表中的 "Id_P" 列是 "Orders" 表中的 FOREIGN KEY。FOREIGN KEY 约束用于预防破坏表之间连接的动作。 FOREIGN KEY 约束也能防止非法数据插入外键列,因为它必须是它指向的那个表中的值之一。

下面的 SQL 在 "Orders" 表创建时为 "Id_P" 列创建 FOREIGN KEY:

CREATE TABLE Orders
(
    Id_O    int NOT NULL,
    OrderNo int NOT NULL,
    Id_P    int,
    PRIMARY KEY (Id_O),
    FOREIGN KEY (Id_P) REFERENCES Persons (Id_P)
)

如果需要命名 FOREIGN KEY 约束,以及为多个列定义 FOREIGN KEY 约束,请使用下面的 SQL 语法:

CREATE TABLE Orders
(
    Id_O    int NOT NULL,
    OrderNo int NOT NULL,
    Id_P    int,
    PRIMARY KEY (Id_O),
    CONSTRAINT fk_PerOrders FOREIGN KEY (Id_P)
        REFERENCES Persons (Id_P)
)

如果在 "Orders" 表已存在的情况下为 "Id_P" 列创建 FOREIGN KEY 约束,请使用下面的 SQL:

ALTER TABLE Orders
    ADD FOREIGN KEY (Id_P)
        REFERENCES Persons (Id_P)

如果需要命名 FOREIGN KEY 约束,以及为多个列定义 FOREIGN KEY 约束,请使用下面的 SQL 语法:

ALTER TABLE Orders
    ADD CONSTRAINT fk_PerOrders
        FOREIGN KEY (Id_P)
            REFERENCES Persons (Id_P)

如需撤销 FOREIGN KEY 约束,请使用下面的 SQL:

ALTER TABLE Orders
DROP
FOREIGN KEY fk_PerOrders
CHECK

CHECK 约束用于限制列中的值的范围。如果对单个列定义 CHECK 约束,那么该列只允许特定的值。如果对一个表定义 CHECK 约束,那么此约束会在特定的列中对值进行限制。

下面的 SQL 在 "Persons" 表创建时为 "Id_P" 列创建 CHECK 约束。CHECK 约束规定 "Id_P" 列必须只包含大于 0 的整数。

CREATE TABLE Persons
(
    Id_P      int          NOT NULL,
    LastName  varchar(255) NOT NULL,
    FirstName varchar(255),
    Address   varchar(255),
    City      varchar(255),
    CHECK (Id_P > 0)
)

如果需要命名 CHECK 约束,以及为多个列定义 CHECK 约束,请使用下面的 SQL 语法:

CREATE TABLE Persons
(
    Id_P      int          NOT NULL,
    LastName  varchar(255) NOT NULL,
    FirstName varchar(255),
    Address   varchar(255),
    City      varchar(255),
    CONSTRAINT chk_Person CHECK (Id_P > 0 AND City = 'Sandnes')
)

如果在表已存在的情况下为 "Id_P" 列创建 CHECK 约束,请使用下面的 SQL:

ALTER TABLE Persons
    ADD CHECK (Id_P > 0)

如果需要命名 CHECK 约束,以及为多个列定义 CHECK 约束,请使用下面的 SQL 语法:

ALTER TABLE Persons
    ADD CONSTRAINT chk_Person CHECK (Id_P > 0 AND City = 'Sandnes')

如需撤销 CHECK 约束,请使用下面的 SQL:

ALTER TABLE Persons
DROP
CHECK chk_Person
DEFAULT

DEFAULT 约束用于向列中插入默认值。如果没有规定其他的值,那么会将默认值添加到所有的新记录。

下面的 SQL 在 "Persons" 表创建时为 "City" 列创建 DEFAULT 约束:

CREATE TABLE Persons
(
    Id_P      int          NOT NULL,
    LastName  varchar(255) NOT NULL,
    FirstName varchar(255),
    Address   varchar(255),
    City      varchar(255) DEFAULT 'Sandnes'
)

通过使用类似 GETDATE() 这样的函数,DEFAULT 约束也可以用于插入系统值:

CREATE TABLE Orders
(
    Id_O      int NOT NULL,
    OrderNo   int NOT NULL,
    Id_P      int,
    OrderDate date DEFAULT GETDATE()
)

如果在表已存在的情况下为 "City" 列创建 DEFAULT 约束,请使用下面的 SQL:

ALTER TABLE Persons
    ALTER City SET DEFAULT 'SANDNES'

如需撤销 DEFAULT 约束,请使用下面的 SQL:

ALTER TABLE Persons
    ALTER City DROP DEFAULT
CREATE INDEX

CREATE INDEX 语句用于在表中创建索引。在不读取整个表的情况下,索引使数据库应用程序可以更快地查找数据。用户无法看到索引,它们只能被用来加速搜索/查询。

Warning

更新一个包含索引的表需要比更新一个没有索引的表更多的时间,这是由于索引本身也需要更新。因此,理想的做法是仅仅在常常被搜索的列(以及表)上面创建索引。

在表上创建一个简单的索引。允许使用重复的值:

--    "column_name" 规定需要索引的列
CREATE INDEX index_name
    ON table_name (column_name)

在表上创建一个唯一的索引。唯一的索引意味着两个行不能拥有相同的索引值。

CREATE UNIQUE INDEX index_name
    ON table_name (column_name)

希望以降序索引某个列中的值,可以在列名称之后添加保留字 DESC:

CREATE INDEX Index_Pers
    ON Person (LastName DESC) 

希望索引不止一个列,您可以在括号中列出这些列的名称,用逗号隔开:

CREATE INDEX Index_Pers
    ON Person (LastName, FirstName)

操作表结构

ALTER

ALTER TABLE 语句用于在已有的表中添加、修改或删除列。如需在表中添加列,请使用下列语法:

ALTER TABLE table_name
    ADD column_name datatype

要删除表中的列,请使用下列语法:

ALTER TABLE table_name
DROP
COLUMN column_name

要改变表中列的数据类型,请使用下列语法:

ALTER TABLE table_name
ALTER
COLUMN column_name datatype
DROP

我们可以使用 DROP INDEX 命令删除表格中的索引。

DROP INDEX table_name.index_name

DROP TABLE 语句用于删除表(表的结构、属性以及索引也会被删除):

DROP TABLE table_name
TRUNCATE

如果仅仅需要删除表格的内容,而不删除表的话,可以使用 TRUNCATE TABLE:

TRUNCATE TABLE table_name

数据查询语言(DML)

数据查询语言(DML, Data Manipulation Language)是用于操作数据库中数据的编程语言。DML 主要用于执行对数据的增、删、改、查操作,允许用户和应用程序在数据库中插入、更新、删除和检索数据。

SELECT

SELECT 用于从数据库表中查询数据。用户可以指定要检索的列、表以及查询条件。SELECT 的语法如下:

SELECT column_name
FROM table_name

使用 SELECT 的时候,可以为列名称和表名称指定别名(Alias):

SELECT column_name(s)
FROM table_name
         AS alias_name
SELECT column_name AS alias_name
FROM table_name
DISTINCT

当使用 SELECT 查询到的数据时,想要仅仅列出不同(DISTINCT)的值,可以使用 DISTINCT 关键字。 DISTINCT 关键字用于返回唯一不同的值:

SELECT DISTINCT column_name
FROM table_name
WHERE

如需有条件地从表中选取数据,可将 WHERE 子句添加到 SELECT 语句。WHERE 的语法如下:

SELECT column_name
FROM table_name
WHERE column operator value

operator 的类型如下:

操作符 描述 备注
= 等于
<> 不等于 在某些版本的 SQL 中,操作符 <> 可以写为 !=
> 大于
< 小于
>= 大于等于
<= 小于等于
BETWEEN 在某个范围内
LIKE 搜索某种模式

例如,选取居住在城市 "Beijing" 中的人,我们需要向 SELECT 语句添加 WHERE 子句:

SELECT *
FROM Persons
WHERE city = 'Beijing'

Warning

在 SQL 中,要是文本值(字符串)类型需要使用单引号引起来。如果是数值,则不需要使用引号。

SELECT * FROM Persons WHERE FirstName='Bush'
SELECT * FROM Persons WHERE Year>1965
LIKE

LIKE 操作符用于在 WHERE 子句中搜索列中的指定模式,使用 % 定义通配符(模式中缺少的字母)。

SELECT column_name(s)
FROM table_name
WHERE column_name LIKE pattern

希望从 "Persons" 表中选取居住在以 "g" 结尾的城市里的人:

SELECT *
FROM Persons
WHERE City LIKE '%g'

使用 NOT LIKE 选择不包含的数据。可以从 "Persons" 表中选取居住在不包含 "lon" 的城市里的人:

SELECT *
FROM Persons
WHERE City NOT LIKE '%lon%'

除了 % 以外,还有其他的通配符可以在 LIKE 语言中使用。下表是支持的通配符:

通配符 描述
% 代表零个或多个字符
_ 仅替代一个字符
[charlist] 字符列中的任何单一字符
[^charlist] 或者 [!charlist] 不在字符列中的任何单
IN

IN 操作符允许我们在 WHERE 子句中规定多个值。

SELECT column_name(s)
FROM table_name
WHERE column_name IN (value1, value2, . . .)

例如,选取所有 LastName 为 Adams 和 Carter 的人:

SELECT *
FROM Persons
WHERE LastName IN ('Adams', 'Carter')
BETWEEN AND

操作符 BETWEEN ... AND 会选取介于两个值之间的数据范围。这些值可以是数值、文本或者日期。

SELECT column_name(s)
FROM table_name
WHERE column_name
          BETWEEN value1 AND value2

以字母顺序显示介于 "Adams"(包括)和 "Carter"(不包括)之间的人,请使用下面的 SQL:

SELECT *
FROM Persons
WHERE LastName
          BETWEEN 'Adams' AND 'Carter'

Danger

不同的数据库对 BETWEEN...AND 操作符的处理方式是有差异的。某些数据库会列出介于 "Adams" 和 "Carter" 之间的人,但不包括 "Adams" 和 "Carter" ;某些数据库会列出介于 "Adams" 和 "Carter" 之间并包括 "Adams" 和 "Carter" 的人;而另一些数据库会列出介于 "Adams" 和 "Carter" 之间的人,包括 "Adams" ,但不包括 "Carter" 。 所以,请检查你的数据库是如何处理 BETWEEN....AND 操作符的!

AND、OR

AND 和 OR 可在 WHERE 子语句中把两个或多个条件结合起来。如果第一个条件和第二个条件都成立,则 AND 运算符显示一条记录。 如果第一个条件和第二个条件中只要有一个成立,则 OR 运算符显示一条记录。

SELECT *
FROM Persons
WHERE FirstName = 'Thomas'
  AND LastName = 'Carter'
SELECT *
FROM Persons
WHERE firstname = 'Thomas'
   OR lastname = 'Carter'

Warning

对于一些复杂的 AND 和 OR 语句,可以使用圆括号()括起来,组成复杂的查询语句:

SELECT * FROM Persons WHERE (FirstName='Thomas' OR FirstName='William')
AND LastName='Carter'
ORDER BY

ORDER BY 语句用于根据指定的列对结果集进行排序。ORDER BY 语句默认按照升序对记录进行排序。希望按照降序对记录进行排序,可以使用 DESC 关键字。

SELECT Company, OrderNumber
FROM Orders
ORDER BY Company, OrderNumber
SELECT Company, OrderNumber
FROM Orders
ORDER BY Company DESC

INSERT INTO

INSERT INTO 用于向数据库表中插入新记录。用户需要指定要插入的数据值。语法如下:

INSERT INTO table_name
VALUES (value1, value2, ....)

指定所要插入数据的列:

INSERT INTO table_name (column1, column2, ...)
VALUES (value1, value2, ....)

UPDATE

UPDATE 用于更新数据库表中的现有记录。用户需要指定更新的列和新的值,并可以设置条件来限制更新的范围。语法如下:

UPDATE table_name
SET column1 = new_value1,
    column2 = new_value2
WHERE column1 = old_value_1

DELETE

DELETE 用于从数据库表中删除记录。用户可以设置条件来确定哪些记录将被删除。

DELETE
FROM table_name
WHERE column1 = value1, ...

可以在不删除表的情况下删除所有的行。这意味着表的结构、属性和索引都是完整的:

DELETE
FROM table_name

或者:

DELETE
* FROM table_name

事务控制语言(TCL)

事务控制语言(TCL, Transaction Control Language)用于管理数据库事务的执行和控制。事务是指一系列操作的集合,这些操作要么全部成功,要么全部失败,以保证数据的一致性和完整性。

COMMIT

COMMIT 用于提交当前事务的所有操作,将其永久保存到数据库中。如果事务中的所有操作都成功执行,使用 COMMIT 确保这些更改是最终的。

COMMIT;

ROLLBACK

ROLLBACK 用于撤销当前事务的所有操作,将数据库恢复到事务开始前的状态。如果事务中的操作出现错误或不满足某些条件,可以使用 ROLLBACK 撤销所有更改。

ROLLBACK;

SAVEPOINT

SAVEPOINT 用于在事务中设置一个保存点,允许在事务中指定一个恢复点,方便在发生错误时仅撤销到该保存点,而不是整个事务。

SAVEPOINT savepoint_name;

SET TRANSACTION

SET TRANSACTION 用于设置事务的属性,比如隔离级别和访问模式,以影响事务的执行和并发控制。

SET TRANSACTION ISOLATION LEVEL level;

事务案例

假设我们有一个银行系统,涉及两个账户:AccountA 和 AccountB。我们要将 100 元从 AccountA 转账到 AccountB。以下是一个完整的事务示例:

BEGIN;

-- 从 AccountA 扣款
UPDATE Accounts
SET balance = balance - 100
WHERE account_id = 'AccountA';

-- 向 AccountB 存款
UPDATE Accounts
SET balance = balance + 100
WHERE account_id = 'AccountB';

-- 提交事务
COMMIT;

如果在扣款或存款过程中出现错误,可以使用 ROLLBACK 撤销所有操作,以保持数据的一致性。

高阶查询

高阶 SQL 查询包括复杂的查询功能和技巧,用于处理和分析数据库中的数据。高阶查询可以分成如下类型:

  • 子查询:在主查询中嵌套一个查询,用于提供主查询所需的数据。
  • 联接:结合多个表的数据。
  • 聚合函数:对数据进行统计和汇总。
  • 窗口函数:对结果集进行排序和计算窗口内的值。
  • CTE(公用表达式):用于简化复杂查询,提供临时的结果集。

子查询

在主查询中嵌套一个查询,用于提供主查询所需的数据。

SELECT employee_name
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

联接(JOIN)

联接(JOIN)指的是通过使用 JOIN...ON 关键字结合两个或多个表的数据进行查询。联接有如下几种:

  • INNER JOIN:返回两个表中匹配的记录。
  • LEFT JOIN(或 LEFT OUTER JOIN):返回左表中的所有记录,以及右表中匹配的记录;如果右表没有匹配的记录,则结果中右表的列会显示为 NULL。
  • RIGHT JOIN(或 RIGHT OUTER JOIN):返回右表中的所有记录,以及左表中匹配的记录;如果左表没有匹配的记录,则结果中左表的列会显示为 NULL。
  • FULL JOIN(或 FULL OUTER JOIN):返回两个表中的所有记录,包括那些在一个表中没有匹配的记录;这些记录在没有匹配的表中的列会显示为 NULL。
  • CROSS JOIN:返回两个表的笛卡尔积,即每一条记录都会与另一个表的每一条记录组合,结果集的大小是两个表记录数的乘积。

上述的关系可以总结成如下的一张图:

SQL Joins

假设我们有两个表:employees 和 departments:

表:employees

employee_id name department_id
1 Alice 1
2 Bob 2
3 Charlie 3
4 David NULL

表:departments

department_id department_name
1 HR
2 Engineering
3 Marketing
4 Sales
INNER JOIN

INNER JOIN(内联接)和 JOIN 是相同的。

SELECT e.name, d.department_name
FROM employees e
         INNER JOIN departments d
                    ON e.department_id = d.department_id;
name department_name
Alice HR
Bob Engineering
Charlie Marketing

在 INNER JOIN 语法中,INNER 关键字可以经常会被省略。也就是说:INNER JOIN 和 JOIN 在 SQL 中实际上是相同的。INNER JOIN 是一种明确指定的连接类型,而 JOIN 默认情况下也是 INNER JOIN。两者都会返回两个表中匹配条件的记录。

SELECT e.name, d.department_name
FROM employees e
         JOIN departments d
              ON e.department_id = d.department_id;

当两个表中有同名的列时,可以使用 USING 指定这些列进行连接。

上面的 SQL 语句也可以写成:

SELECT e.name, d.department_name
FROM employees e
         JOIN departments d USING (department_id)
LEFT JOIN

LEFT JOIN(或 LEFT OUTER JOIN):返回左表中的所有记录,以及右表中匹配的记录;如果右表没有匹配的记录,则结果中右表的列会显示为 NULL。

SELECT e.name, d.department_name
FROM employees e
         LEFT JOIN departments d
                   ON e.department_id = d.department_id;
name department_name
Alice HR
Bob Engineering
Charlie Marketing
David NULL
RIGHT JOIN

RIGHT JOIN(或 RIGHT OUTER JOIN):返回右表中的所有记录,以及左表中匹配的记录;如果左表没有匹配的记录,则结果中左表的列会显示为 NULL。

SELECT e.name, d.department_name
FROM employees e
         RIGHT JOIN departments d
                    ON e.department_id = d.department_id;
name department_name
Alice HR
Bob Engineering
Charlie Marketing
NULL Sales
FULL JOIN

FULL JOIN(或 FULL OUTER JOIN):返回两个表中的所有记录,包括那些在一个表中没有匹配的记录;这些记录在没有匹配的表中的列会显示为 NULL。

SELECT e.name, d.department_name
FROM employees e
         FULL JOIN departments d
                   ON e.department_id = d.department_id;
name department_name
Alice HR
Bob Engineering
Charlie Marketing
David NULL
NULL Sales
CROSS JOIN

CROSS JOIN 返回两个表的笛卡尔积,即每一条记录都会与另一个表的每一条记录组合,结果集的大小是两个表记录数的乘积。

SELECT e.name, d.department_name
FROM employees e
         CROSS JOIN departments d;
name department_name
Alice HR
Alice Engineering
Alice Marketing
Alice Sales
Bob HR
Bob Engineering
Bob Marketing
Bob Sales
Charlie HR
Charlie Engineering
Charlie Marketing
Charlie Sales
David HR
David Engineering
David Marketing
David Sales

聚合函数

聚合函数用于对一组值进行计算并返回一个单一的值。常见的函数如下表所示:

函数 描述
AVG(column) 返回某列的平均值
BINARY_CHECKSUM 计算行的二进制校验和,用于数据更改检测
CHECKSUM 计算单个或多个列的校验和
CHECKSUM_AGG 计算一组值的校验和的总和
COUNT(column) 返回某列的行数(不包括NULL值)
COUNT(*) 返回被选行数
COUNT(DISTINCT column) 返回相异结果的数目
FIRST(column) 返回在指定的域中第一个记录的值(SQLServer2000 不支持)
LAST(column) 返回在指定的域中最后一个记录的值(SQLServer2000 不支持)
MAX(column) 返回某列的最高值
MIN(column) 返回某列的最低值
STDEV(column) 计算某列的样本标准差
STDEVP(column) 计算某列的总体标准差
SUM(column) 返回某列的总和
VAR(column) 计算某列的样本方差
VARP(column) 计算某列的总体方差

COUNT():返回行数。例如,COUNT(*) 计算表中的总行数,COUNT(column_name) 计算非 NULL 值的行数。

SELECT COUNT(*)
FROM employees;

SUM():计算指定列的总和。例如,SUM(salary) 计算薪资列的总和。

SELECT SUM(salary)
FROM employees;

窗口函数

窗口函数用于对结果集中的行进行分析,通常与 OVER() 子句一起使用。窗口函数不会像聚合函数那样将多行合并为一行,而是提供额外的分析能力。常见的窗口函数有:

函数 描述
ROW_NUMBER() 为结果集中的每一行分配唯一的序号
RANK() 为结果集中的每一行分配排名,如果存在相同的值,则分配相同的排名,并跳过后续的排名
DENSE_RANK() 为结果集中的每一行分配排名,如果存在相同的值,则分配相同的排名,不跳过排名
NTILE(n) 将结果集划分为 n 个桶,并为每一行分配一个桶编号
SUM() 计算指定列的累计和,在窗口内对数据进行汇总
AVG() 计算指定列的累计平均值,在窗口内对数据进行计算
MIN() 返回指定列的最小值,在窗口内对数据进行计算
MAX() 返回指定列的最大值,在窗口内对数据进行计算
COUNT() 计算指定列的行数,在窗口内对数据进行计算
LEAD() 返回当前行后面指定行偏移量的值
LAG() 返回当前行前面指定行偏移量的值
FIRST_VALUE() 返回窗口内第一个值
LAST_VALUE() 返回窗口内最后一个值
NTH_VALUE(n) 返回窗口内第 n 个值

在 MySQL 中,OVER() 子句用于与窗口函数一起使用,以定义窗口的范围。窗口函数允许你在查询结果中执行一些复杂的分析操作,而不需要子查询或自连接。OVER() 子句的主要作用是指定窗口函数操作的数据范围或“窗口”。

<窗口函数> OVER ([PARTITION BY <列名>] [ORDER BY <列名>] [ROWS/ RANGE <窗口范围>])

其中:

  • PARTITION BY:将数据分区。窗口函数在每个分区内独立计算。如果省略此部分,整个结果集被视为一个分区。
  • ORDER BY:定义数据的排序顺序。窗口函数在分区内按此顺序操作。如果省略此部分,窗口函数的计算顺序可能是不确定的。
  • ROWS/RANGE:指定窗口的范围(可选)。ROWS 和 RANGE 用于定义窗口的起始和结束行。具体用法取决于需要计算的函数类型。

以下是一些使用 OVER() 子句的例子:

例子 1:查询计算员工按薪水排序的排名

SELECT employee_id,
       salary,
       RANK() OVER (ORDER BY salary DESC) AS salary_rank
FROM employees;

例子 2:计算每个部门的员工数量

SELECT department_id,
       employee_id,
       COUNT(*) OVER (PARTITION BY department_id) AS department_employee_count
FROM employees;

例子 3:查询计算每行的过去 7 天(包括当前行)的销售总和

SELECT order_date,
       sales_amount,
       SUM(sales_amount) OVER (ORDER BY order_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS rolling_sum
FROM sales;

CTE

CTE(Common Table Expression,公用表表达式)是一种 SQL 语法,用于在查询中定义一个临时的结果集,这个结果集可以在同一查询中的其他部分进行引用。CTE 通常用来简化复杂的查询,增加代码的可读性,并在多个地方重用查询结果。CTE 的基本语法如下:

WITH cte_name AS (
    -- <CTE 查询>
)
SELECT <列名>
FROM cte_name
WHERE <条件>;

其中:

  • WITH cte_name AS:定义 CTE 的名称(cte_name)和查询语句()。
  • CTE 查询:定义 CTE 的查询,它可以是任何合法的 SQL 查询。
  • SELECT:从 CTE 中选择数据, 的结果作为临时表使用。

例 1:查询 sales 表中销售额大于 1000 的记录

WITH SalesCTE AS (SELECT order_date,
                         sales_amount
                  FROM sales
                  WHERE sales_amount > 1000)
SELECT order_date,
       sales_amount
FROM SalesCTE
ORDER BY order_date;

例 2:递归 CTE。递归 CTE 允许处理层级结构或递归数据。递归 CTE 包含两个部分:锚查询和递归查询。

WITH RECURSIVE EmployeeHierarchy AS (
    -- 锚查询
    SELECT employee_id,
           manager_id,
           employee_name
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    -- 递归查询
    SELECT e.employee_id,
           e.manager_id,
           e.employee_name
    FROM employees e
             INNER JOIN EmployeeHierarchy eh
                        ON e.manager_id = eh.employee_id)
SELECT *
FROM EmployeeHierarchy;

在这个例子中,EmployeeHierarchy 是一个递归 CTE,首先选择所有没有上级的员工(锚查询),然后递归地选择这些员工的下属(递归查询)。