news 2026/10/17 13:07:16

PyTorch时序预测实战:LSTM数据集构建的完整指南与避坑技巧

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyTorch时序预测实战:LSTM数据集构建的完整指南与避坑技巧

1. 时序预测项目里,数据集构建为什么比模型本身还重要

做过时序预测的人大概都有这种体会:模型结构调了半天,学习率、层数、隐藏单元数试了一轮又一轮,结果误差曲线该震荡还是震荡,该滞后还是滞后。最后回头一查,问题根本不在网络结构上,而是数据集的构建方式从一开始就埋了雷。我见过太多项目,LSTM 网络搭得漂漂亮亮,训练代码写得工工整整,但预测出来的曲线永远比真实值慢半拍,或者干脆把未来信息泄露进了训练集,离线指标好得离谱,一上线就崩。

这篇内容就围绕PyTorch 框架下时序预测中 LSTM 数据集构建这件事展开。说白了,就是把原始的时间序列数据,加工成 LSTM 能吃的、格式正确的、不泄露信息的监督学习样本。这件事听起来简单,但里面涉及的窗口切分、归一化时机、批次组织、张量维度对齐、训练验证划分等环节,每一个都有坑。适合正在做时序预测的算法工程师、数据科学从业者,以及刚接触 LSTM 时间序列建模的学生和爱好者。不管你是用单变量做销量预测,还是用多变量做设备状态预测,数据集构建的底层逻辑是相通的。

我会从整体设计思路讲起,把每个关键决策背后的原因说清楚,然后给出可以直接复现的 PyTorch 代码,最后把我自己踩过的坑和排查经验整理出来。目标只有一个:让你在数据集这一步不再反复返工。

2. 整体设计思路与方案选型拆解

2.1 时序预测任务的本质与 LSTM 的输入要求

时序预测的本质,是用过去一段时间的数据去推断未来一段时间的数据。假设你有一条按小时采样的温度序列,长度为 10000 个点,你想用过去 24 小时预测未来 1 小时。那么对 LSTM 来说,它需要的是一个个形状为(batch_size, sequence_length, input_size)的三维张量。其中sequence_length就是 24,input_size是特征维度,单变量时为 1,多变量时等于特征数量。

这里有个容易被忽略的点:LSTM 不会自动帮你把一维序列切成窗口。很多人第一次写的时候,直接把整条序列扔进去,结果网络学到的是一整段序列的全局模式,而不是“给定过去 24 小时推未来 1 小时”的映射关系。所以数据集构建的核心任务,就是把一条长序列,通过滑动窗口的方式,切分成大量的(输入窗口, 目标值)样本对。

为什么用滑动窗口而不是随机采样?因为时序数据有严格的先后顺序,随机打乱会破坏时间依赖结构。滑动窗口能保证每个样本内部的时序关系完整,同时通过步长控制样本之间的重叠程度。步长为 1 时样本量最大,但相邻样本高度相关;步长等于窗口长度时样本之间完全不重叠,但样本量会大幅减少。实际项目中,步长通常取 1 到窗口长度的三分之一之间,需要在样本量和相关性之间做权衡。

2.2 单变量与多变量场景的差异

单变量时序预测,输入只有一条序列,input_size=1,目标也是这条序列的未来值。这种场景下,数据集构建相对直接,重点在于窗口切分和归一化。

多变量场景就复杂一些。比如你有温度、湿度、风速三个特征,想预测未来一小时的温度。这时候输入是三个特征的历史窗口,形状为(sequence_length, 3),但目标只有一个温度值。这就涉及到一个关键决策:目标变量是否也作为输入特征之一。如果温度既是输入特征又是预测目标,那叫自回归预测;如果只用湿度和风速预测温度,温度不作为输入,那叫外生变量预测。两种方式的建模逻辑不同,数据集构建时目标列的选取也不同。

还有一种情况是多变量多步预测,即输入多个特征的历史窗口,输出多个特征未来多个时间步的值。这时候目标张量的形状会变成(sequence_length_out, output_size),数据集构建的复杂度会明显上升。我建议新手先从单变量单步预测做起,把窗口切分、归一化、批次组织这套流程跑通,再逐步扩展到多变量多步。

2.3 归一化为什么必须在切分之前完成

这是数据集构建中最容易出错的地方之一。很多人习惯先把数据切成训练集和测试集,然后分别对训练集和测试集做归一化。这种做法在时序预测中是有问题的,因为测试集的均值和方差用到了测试集自身的信息,相当于把未来信息泄露进了预处理环节。

正确的做法是:先用训练集的均值和方差拟合归一化器,然后用这个归一化器去变换训练集、验证集和测试集。也就是说,验证集和测试集的归一化参数必须来自训练集,不能自己算自己的。在 PyTorch 中,你可以手动计算训练集的均值和标准差,也可以用sklearn的StandardScaler,但一定要记住fit只在训练集上调用,transform才用在其他集合上。

注意:如果你的数据有明显的趋势或季节性,简单的标准化可能不够,需要考虑差分或去趋势处理。但差分后的数据在预测结束后需要逆变换回来,这一步很容易被遗漏。

2.4 训练集、验证集、测试集的划分逻辑

时序数据的划分不能像普通机器学习那样随机打乱。必须按时间顺序切分:最早的一段做训练集,中间一段做验证集,最后一段做测试集。常见的比例是 7:1:2 或 8:1:1,但具体取决于数据总量和预测任务的特点。

为什么验证集要放在训练集和测试集之间?因为验证集用于调参和早停,如果它和测试集在时间上重叠或交错,调参过程就会间接看到测试集的信息。按时间顺序切分能最大程度模拟真实部署场景:你用历史数据训练模型,用稍晚的数据验证,用最新的数据测试。

还有一个细节:切分点处的窗口不能跨越边界。比如训练集最后 24 个点,如果窗口长度是 24,那这 24 个点无法构成一个完整的输入窗口,应该丢弃或者留给验证集。实现时可以在切分后分别对每个子集做窗口切分,而不是先切窗口再切数据集。

3. 核心细节解析与实操要点

3.1 滑动窗口切分的具体实现

滑动窗口切分的逻辑可以用两层循环实现,但更高效的方式是用 NumPy 的数组切片。假设原始序列为data,形状为(n_timesteps, n_features),窗口长度为seq_len,预测步长为pred_len,滑动步长为stride。那么第i个样本的输入是data[i:i+seq_len],目标是data[i+seq_len:i+seq_len+pred_len]。

用 Python 实现时,可以预先计算样本数量:n_samples = (n_timesteps - seq_len - pred_len) // stride + 1。然后初始化两个空数组X和y,形状分别为(n_samples, seq_len, n_features)和(n_samples, pred_len, n_features),通过循环填充。这种方式内存可控,逻辑清晰。

如果你追求效率,可以用numpy.lib.stride_tricks.sliding_window_view,但它对多维数组的支持需要额外处理。我个人的习惯是手写循环,因为可读性好,调试方便,而且对于大多数项目的数据量来说,性能瓶颈不在这一步。

import numpy as np def create_sequences(data, seq_len, pred_len, stride=1): n_timesteps, n_features = data.shape n_samples = (n_timesteps - seq_len - pred_len) // stride + 1 X = np.zeros((n_samples, seq_len, n_features), dtype=np.float32) y = np.zeros((n_samples, pred_len, n_features), dtype=np.float32) for i in range(n_samples): start = i * stride X[i] = data[start:start+seq_len] y[i] = data[start+seq_len:start+seq_len+pred_len] return X, y

这段代码看起来简单,但有几个细节值得注意。dtype最好显式指定为float32,因为 PyTorch 默认使用float32,如果数据是float64,后续转张量时还要额外转换,浪费内存。另外,stride的选择会影响样本量和过拟合风险,步长越小样本越多,但相邻样本越相似,模型可能记住训练样本而不是学习泛化模式。

3.2 张量维度对齐与 DataLoader 组织

切分完成后,X和y都是 NumPy 数组。接下来需要把它们转成 PyTorch 的Tensor,并封装成Dataset和DataLoader。这里的关键是维度顺序:PyTorch 的 LSTM 默认输入形状是(batch_size, sequence_length, input_size),而我们的X已经是(n_samples, seq_len, n_features),所以直接转换即可,不需要额外调整。

自定义Dataset类时,__getitem__返回的是单个样本的(X[i], y[i]),DataLoader会自动把它们堆叠成批次。需要注意的是,DataLoader的shuffle参数在训练集上应该设为True,但在验证集和测试集上必须设为False。为什么?因为验证和测试时你需要保持样本的时间顺序,以便后续做逆变换和可视化对比。如果打乱了,预测值和真实值就对不上了。

import torch from torch.utils.data import Dataset, DataLoader class TimeSeriesDataset(Dataset): def __init__(self, X, y): self.X = torch.tensor(X, dtype=torch.float32) self.y = torch.tensor(y, dtype=torch.float32) def __len__(self): return len(self.X) def __getitem__(self, idx): return self.X[idx], self.y[idx] train_dataset = TimeSeriesDataset(X_train, y_train) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)

批次大小的选择也有讲究。太小会导致训练不稳定,太大则内存吃紧且可能降低泛化能力。对于时序预测任务,我通常从 32 或 64 开始试,根据显存和收敛情况调整。如果序列很长、特征很多,批次大小可能要降到 16 甚至 8。

3.3 归一化的正确姿势与逆变换

前面提到归一化要在切分之前用训练集参数完成。具体操作是:先按时间顺序切分出训练集原始数据,计算训练集的均值和标准差,然后对全体数据做标准化。注意,这里的“全体数据”包括训练、验证、测试,但使用的均值和标准差只来自训练集。

train_mean = train_data.mean(axis=0) train_std = train_data.std(axis=0) train_std[train_std == 0] = 1e-8 # 防止除零 data_normalized = (data - train_mean) / train_std

预测完成后,需要把归一化后的预测值逆变换回原始尺度,才能计算真实的误差指标。逆变换公式是pred * train_std + train_mean。这一步看似简单,但如果你在归一化时对每个特征分别计算了均值和标准差,逆变换时也要对应到正确的特征维度,否则数值会完全错乱。

提示:如果你的目标变量只是多变量中的一个,逆变换时只需要对目标变量对应的维度做逆变换,不需要对所有维度都做。这一点在多变量预测中特别容易搞混。

3.4 缺失值与异常值的处理时机

真实数据几乎不可能干干净净。缺失值和异常值如果在窗口切分之后处理,会破坏样本的完整性。正确的顺序是:先做缺失值填充和异常值处理,再做归一化,最后做窗口切分。

缺失值填充的常用方法有前向填充、线性插值和均值填充。时序数据我推荐前向填充或线性插值,因为这两种方式能保持时间上的连续性。均值填充会引入人为的平坦段,对 LSTM 的学习不利。异常值可以用滑动窗口的均值和标准差来检测,超过 3 倍标准差的点视为异常,用插值替换。

这里有个经验:如果缺失比例超过 10%,填充后的数据质量可能已经不可靠,需要考虑是否放弃这段数据或者换用能处理缺失值的模型结构。不要硬填,填出来的噪声会让模型学到错误的模式。

4. 完整实操流程与核心环节实现

4.1 从原始数据到最终 DataLoader 的完整链路

我把整个流程串一遍,你可以直接照着这个顺序操作。假设你有一个 CSV 文件,包含时间戳和若干数值列,目标是预测其中某一列的未来值。

第一步,读取数据并解析时间戳。用pandas读取后,把时间列转为datetime类型,并按时间排序。如果时间有重复或缺失,需要先处理。排序后把时间列设为索引,方便后续按时间切分。

第二步,选择特征列和目标列。如果是单变量预测,特征列和目标列是同一列。如果是多变量预测,特征列是多列,目标列是其中一列或多列。把选定的列提取成 NumPy 数组,形状为(n_timesteps, n_features)。

第三步,处理缺失值和异常值。用前向填充处理缺失值,用滑动窗口统计检测并替换异常值。处理完后检查一遍数据分布,确保没有明显的异常残留。

第四步,按时间顺序切分训练集、验证集和测试集。假设切分比例为 7:1:2,训练集取前 70%,验证集取接下来 10%,测试集取最后 20%。切分时要注意窗口长度和预测长度,确保每个子集都有足够的样本。

第五步,用训练集的均值和标准差对全体数据做标准化。注意保存均值和标准差,后续逆变换要用。

第六步,对每个子集分别做滑动窗口切分。训练集切出X_train, y_train,验证集切出X_val, y_val,测试集切出X_test, y_test。

第七步,封装成Dataset和DataLoader。训练集的shuffle=True,验证集和测试集的shuffle=False。

import pandas as pd import numpy as np import torch from torch.utils.data import Dataset, DataLoader # 1. 读取数据 df = pd.read_csv('data.csv', parse_dates=['timestamp']) df = df.sort_values('timestamp').set_index('timestamp') # 2. 选择特征 features = ['temperature', 'humidity', 'wind_speed'] target = ['temperature'] data = df[features].values.astype(np.float32) # 3. 缺失值处理 df[features] = df[features].ffill().bfill() # 4. 切分数据集 n = len(data) train_end = int(n * 0.7) val_end = int(n * 0.8) train_data = data[:train_end] val_data = data[train_end:val_end] test_data = data[val_end:] # 5. 归一化 train_mean = train_data.mean(axis=0) train_std = train_data.std(axis=0) train_std[train_std == 0] = 1e-8 data_norm = (data - train_mean) / train_std train_norm = data_norm[:train_end] val_norm = data_norm[train_end:val_end] test_norm = data_norm[val_end:] # 6. 窗口切分 seq_len = 24 pred_len = 1 stride = 1 X_train, y_train = create_sequences(train_norm, seq_len, pred_len, stride) X_val, y_val = create_sequences(val_norm, seq_len, pred_len, stride) X_test, y_test = create_sequences(test_norm, seq_len, pred_len, stride) # 7. DataLoader train_loader = DataLoader(TimeSeriesDataset(X_train, y_train), batch_size=64, shuffle=True) val_loader = DataLoader(TimeSeriesDataset(X_val, y_val), batch_size=64, shuffle=False) test_loader = DataLoader(TimeSeriesDataset(X_test, y_test), batch_size=64, shuffle=False)

4.2 窗口长度和预测长度的选择依据

窗口长度seq_len决定了 LSTM 能看到多长的历史信息。太短,模型捕捉不到周期性;太长,计算量增加且可能引入噪声。对于有明显日周期的数据,窗口长度至少覆盖一个完整周期。比如小时级数据,日周期是 24 小时,窗口长度取 24 或 48 比较合理。对于周周期,窗口长度可能要取 168。

预测长度pred_len取决于业务需求。如果只需要预测下一个时间点,pred_len=1。如果需要预测未来 24 小时,pred_len=24。多步预测的难度明显高于单步,因为误差会累积。如果业务允许,可以先用单步预测滚动输出多步结果,这样模型只需要学单步映射,稳定性更好。

我通常的做法是先用seq_len=48, pred_len=1跑一个基线,看看误差水平,再根据业务需求调整。如果多步预测误差太大,就退回单步滚动方案。

4.3 批次组织与内存优化

当数据量很大时,一次性把所有窗口加载到内存可能会爆。这时候可以用惰性加载的方式,在Dataset的__getitem__里实时切窗口,而不是预先切好所有样本。这种方式内存占用小,但每次取数据都要做切片操作,速度稍慢。

class LazyTimeSeriesDataset(Dataset): def __init__(self, data, seq_len, pred_len, stride=1): self.data = torch.tensor(data, dtype=torch.float32) self.seq_len = seq_len self.pred_len = pred_len self.stride = stride self.n_samples = (len(data) - seq_len - pred_len) // stride + 1 def __len__(self): return self.n_samples def __getitem__(self, idx): start = idx * self.stride x = self.data[start:start+self.seq_len] y = self.data[start+self.seq_len:start+self.seq_len+self.pred_len] return x, y

惰性加载的另一个好处是方便做数据增强。比如你可以在__getitem__里随机加一点噪声,或者随机缩放,增加样本多样性。但要注意,验证集和测试集不能用数据增强,否则评估结果不可靠。

4.4 多变量多步预测的目标张量组织

多变量多步预测时,目标y的形状是(n_samples, pred_len, n_outputs)。如果n_outputs等于特征数,那就是预测所有特征的未来值。如果只预测其中一部分,需要在切窗口时只取目标列。

def create_sequences_multi(data, target_indices, seq_len, pred_len, stride=1): n_timesteps, n_features = data.shape n_samples = (n_timesteps - seq_len - pred_len) // stride + 1 X = np.zeros((n_samples, seq_len, n_features), dtype=np.float32) y = np.zeros((n_samples, pred_len, len(target_indices)), dtype=np.float32) for i in range(n_samples): start = i * stride X[i] = data[start:start+seq_len] y[i] = data[start+seq_len:start+seq_len+pred_len][:, target_indices] return X, y

这种组织方式下,模型的输出层维度要对应len(target_indices),损失函数计算时也要注意维度对齐。如果目标有多个,可以考虑对每个目标分别计算损失再加权求和,或者直接用 MSE 对所有目标一起算。

5. 常见问题与排查技巧实录

5.1 预测曲线整体滞后怎么办

这是时序预测中最常见的问题。模型预测的曲线形状和真实曲线很像,但整体向右偏移了一个或几个时间步。原因通常有两个:一是窗口长度不够,模型看不到足够的历史信息来判断趋势变化;二是目标对齐方式有问题,比如把t时刻的输入对应到了t时刻的目标,而不是t+1时刻。

排查方法:先检查窗口切分代码,确认y的起始位置是start+seq_len而不是start+seq_len-1。然后尝试增大窗口长度,看看滞后是否改善。如果数据有很强的趋势性,可以考虑对数据做差分,让模型学习增量而不是绝对值。

5.2 训练损失下降但验证损失震荡

这种情况通常是批次大小太小或者学习率太高。时序数据的样本之间相关性高,小批次会导致梯度估计方差大,损失曲线抖动。可以尝试增大批次大小,或者使用梯度累积来模拟大批次。另外,验证集的窗口切分方式要和训练集一致,如果验证集用了不同的stride或seq_len,评估结果会不可比。

还有一个容易被忽略的原因:验证集的归一化参数如果来自验证集自身,而不是训练集,会导致验证损失异常低或异常高。检查归一化代码,确保fit只在训练集上调用。

5.3 常见问题速查表

问题现象可能原因排查方向解决方法
预测曲线整体滞后窗口长度不足或目标对齐错误检查y起始位置和seq_len增大窗口长度,修正对齐
训练损失正常但验证损失爆炸归一化参数泄露或验证集分布不同检查归一化fit范围只用训练集拟合归一化器
预测值全部接近均值模型欠拟合或窗口太短检查模型容量和窗口长度增加层数或增大窗口
多步预测误差累积快单步误差大或目标维度错位检查每步误差和y形状改用单步滚动或调整损失权重
DataLoader 报维度错误张量形状不匹配打印X和y的 shape确保(batch, seq, feat)顺序
逆变换后数值完全错误均值和标准差对应错特征检查train_mean维度按特征维度正确广播

5.4 几个我踩过的坑

第一个坑:归一化时用了全体数据的均值和标准差。当时觉得“全体数据”更稳定,结果测试集的信息泄露进了训练过程,离线指标虚高,上线后误差翻倍。后来改成只用训练集拟合,指标虽然降了一点,但线上表现和离线一致了。

第二个坑:窗口切分时stride设成了seq_len,导致样本量太少,模型欠拟合。后来把stride改成 1,样本量上来了,但训练时间也上来了。最后的折中方案是stride=seq_len//3,样本量和训练时间都能接受。

第三个坑:多变量预测时,目标列和特征列的归一化参数混用了。因为目标列只是特征列的一个子集,逆变换时应该只取目标列对应的均值和标准差,但我一开始用了全量的,导致预测值缩放错误。这个问题的排查花了不少时间,因为损失曲线看起来很正常,只是逆变换后的数值不对。

第四个坑:验证集的shuffle忘了设成False,导致验证损失和测试损失对不上。因为验证时打乱了顺序,某些批次的样本分布和测试集不同,评估结果波动很大。后来统一设成False,问题消失。

提示:每次修改数据集构建代码后,建议先打印几个样本的X和y,肉眼检查窗口切分是否正确。特别是y的第一个值是否等于X最后一个值之后的下一个时间点,这个对齐关系一旦错了,模型再强也学不对。

5.5 数据量不足时的应对策略

如果数据量本身就不大,窗口切分后样本更少,模型很容易过拟合。这时候可以考虑几种策略:一是增大stride的重叠度,让样本尽可能多;二是使用数据增强,比如加噪声、缩放、时间扭曲;三是降低模型复杂度,减少 LSTM 层数和隐藏单元数;四是使用迁移学习,先在类似任务上预训练,再在目标数据上微调。

数据增强在时序预测中要谨慎使用。加噪声和缩放比较安全,时间扭曲可能会破坏周期结构。我通常只加轻微的高斯噪声,标准差取原始数据标准差的 1% 到 5%,再大就可能引入虚假模式。

6. 一些关于数据集构建的个人体会

数据集构建这件事,在时序预测项目里的时间占比往往被低估。很多人觉得模型才是核心,数据只要喂进去就行。但实际做下来,数据集构建花的时间可能占到整个项目的四成以上,而且一旦出错,后面所有工作都白费。

我现在的习惯是,每做完一个数据集构建流程,都会写一个小脚本做完整性检查:打印样本数量、检查X和y的形状、随机抽几个样本做逆变换看数值范围、画一条预测对齐示意图。这些检查花不了几分钟,但能避免后面几小时的调试。

另外,窗口长度、预测长度、步长这些参数,不要一次定死。先用一组保守参数跑通全流程,再逐步调整。每次只改一个参数,观察指标变化,这样才能知道哪个参数真正影响了效果。

最后分享一个小技巧:如果你的时序数据有明显的周期性,可以在特征里加入时间编码,比如小时的正弦余弦值、星期几的 one-hot 编码。这些额外特征能帮助 LSTM 更快地学到周期模式,有时候比单纯增大窗口长度更有效。数据集构建不只是切窗口,特征工程也是其中一部分,把这两件事结合起来做,效果会更好。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 13:06:28

分类模型评估指南:从混淆矩阵到业务对齐的完整框架

我一直觉得,分类模型评估是机器学习项目里最容易被低估的环节。模型训练完,很多人第一件事就是看准确率,拿到一个96%的数字就以为大功告成。可真到了线上,业务方跑过来问“为什么这个用户被拒绝了”“为什么那条内容没有推给感兴趣…

作者头像 李华
网站建设 2026/10/17 13:06:27

本地优先多引擎编码Agent:SQLite持久化与成本优化实战

1. 从一次账单焦虑说起:为什么要自己造一个本地优先的编码 Agent事情的起因很朴素。我日常写代码有相当一部分时间花在重复性的重构、补测试、查文档、写样板代码上,用云端编码 Agent 确实爽,但月底一看账单,心里咯噔一下。尤其是…

作者头像 李华
网站建设 2026/10/17 13:06:01

UE4投影矩阵从原理到实战:透视、反向Z与自定义视角改造

开头部分,我先说个一直想聊透的话题。很多同学在UE4里做渲染相关的东西,模型、材质、光照玩得挺6,但只要一碰到“投影矩阵”四个字,要么绕道走,要么直接拿现成函数一顿调用,压根不管底层发生了什么。一直到…

作者头像 李华
网站建设 2026/10/17 13:05:27

太空工程师飞船姿态矫正脚本:PD控制器与陀螺仪实战

玩《太空工程师》的都知道,飞船开着开着就歪了这件事有多烦。造了一艘大船,推进器不对称、货舱装了重东西、或者说你手动转向的时候总得不停按反方向键去“刹车”,姿态一塌糊涂。后来我试着用游戏里的可编程块写脚本做飞船姿态矫正&#xff0…

作者头像 李华
网站建设 2026/10/17 13:05:16

联邦学习梯度泄露风险:视网膜影像中的梯度反转攻击

联邦学习这几年在医疗AI圈子里几乎是绕不开的关键词——“数据不动模型动”这套说法,听着确实很美好。尤其对视网膜影像这类高度敏感的数据来说,医院既希望借助多方数据训练出更准的筛查模型,又不敢把眼底照片、OCT影像这些原始资料往外传&am…

作者头像 李华
网站建设 2026/10/17 13:05:06

SQL子查询实战:从JOIN/临时表分工到性能陷阱与优化建议

子查询这东西,我刚开始学数据库那会儿总觉得它是个“半成品”功能——明明能用JOIN或者临时表解决的事,为什么要绕个弯子?后来在业务里被几个嵌套查询教做人了之后,才慢慢理解子查询真正的用武之地。它不是让你炫技的,…

作者头像 李华