跳到主要内容

sql优化工具——学会看执行计划

· 阅读需 6 分钟

预备知识

查询 sql 语句执行顺序

(8) SELECT (9)DISTINCT<Select_list>
(1) FROM <left_table> (3) <join_type>JOIN<right_table>
(2) ON<join_condition>
(4) WHERE<where_condition>
(5) GROUP BY<group_by_list>
(6) WITH {CUBE|ROLLUP}
(7) HAVING<having_condtion>
(10) ORDER BY<order_by_list>
(11) LIMIT<limit_number>

具体说明详见:


SQL 的书写顺序和执行顺序 https://zhuanlan.zhihu.com/p/77847158


优化目标

在前一段时间写了不下百段的逻辑库后,sql 优化将是我下一阶段的学习方向,它和业务表现息息相关。奇怪的是,它并没有在任何一个路线图上出现,尽管它其实非常重要。

首先明确什么是 sql 优化:

SQL 优化,就是指将一条 SQL 写的更加简洁,让 SQL 的执行速度更快,易读性与维护性更好。

其中,快自然是首要的优化目标,至于易读性嘛,只能说兼顾,毕竟大家都是在屎山上拉屎的那个。那么如何定义一段好的 sql 语句呢,标准如下:

减小查询的数据量、提升 SQL 的索引命中率

另外,从测试指标上,最基本的有执行时间,这在 navicat 上都有显示。遇到一条执行慢的 sql,应该先用 explain 命令得到查询计划,里面有很多有用的信息。

看懂 pgsql 上的执行计划

首先,最重要的,分清一次查询有没有走了索引: seq scan 是全表扫描,index scan / Bitmap Index Scan / Index Only Scan 是走了索引。

rows 是优化器预估的返回的行数,不是扫描的行数。rows 主要是为优化器选择合适的执行计划做参考的。

条件过滤:出现 Filter。

嵌套循环连接,一般来说,这个是导致 sql 变慢的重要原因之一。在联结(join)操作时就会出现。优化的基本原则是小表驱动大表。下面是一个包含 Nest Loop Join 的简单执行计划的示例:

EXPLAIN SELECT *
FROM table1
JOIN table2 ON table1.column_id = table2.column_id;

这里是一个示例执行计划的输出:

Nested Loop Join
(cost=1000.00..2500.00 rows=100 width=32)
-> Seq Scan on table1
(cost=0.00..500.00 rows=50 width=16)
-> Index Scan using index_column_id on table2
(cost=500.00..1000.00 rows=50 width=16)
Index Cond: (table1.column_id = table2.column_id)

在这个执行计划中,可以看到:

  • Nested Loop Join 表示使用了 Nest Loop Join。
  • Seq Scan on table1 表示对 table1 进行了顺序扫描,即全表扫描。
  • Index Scan using index_column_id on table2 表示对 table2 使用了索引扫描。
  • Index Cond: (table1.column_id = table2.column_id) 表示连接条件是基于列 column_id 的相等条件。

散列连接(Hash Join)是数据库查询中一种常见的连接算法,用于将两个表的数据连接起来。与 Nest Loop Join 不同,Hash Join 的连接过程不是基于嵌套循环,而是通过散列算法将连接条件的列的值映射到一个散列表中,然后在散列表中查找匹配的行。

以下是 Hash Join 的基本步骤:

  • 构建散列表:将连接条件的列的值通过散列算法映射到散列表中。

  • 将第一个表的每一行添加到散列表中。

  • 遍历第二个表的每一行,通过散列算法找到散列表中匹配的行。 相比于 Nest Loop Join,Hash Join 的优势主要体现在以下几个方面:

  • 性能: 在某些情况下,Hash Join 的性能可能比 Nest Loop Join 更好。特别是在连接大型表时,Hash Join 的性能通常更高效,因为它可以利用散列表的快速查找特性。

  • 适用于等值连接: Hash Join 通常用于等值连接(即连接条件是相等关系),而 Nest Loop Join 更适合处理其他类型的连接条件。如果连接条件是等值关系,Hash Join 可能会更为高效。

  • 适用于大型表: 当连接的表很大时,Hash Join 可以更好地利用内存,因为它在内存中构建散列表。这有助于减少 I/O 操作,提高查询性能。

然而,Hash Join 也有一些限制,例如对内存的需求较高,如果内存不足可能导致性能下降。因此,在选择连接算法时,需要根据具体的查询和表结构来进行优化。数据库优化器通常会根据统计信息和查询条件选择合适的连接策略。

es6重要特性

· 阅读需 3 分钟

前言

语言学习看文档是非常无聊的,所以找来视频来学,并做了笔记。

模板语法

当返回一串比较复杂的字符串时,优先使用模板语法:

`${var1}, ${var2}`;

优点:模板所见即所得,即不需要敲换行、空格等占位符。

解构和扩展运算符

一切可迭代的数据类型(数组、对象等)都可以解构。解构出的是子元素。扩展运算府(三个点)也是将子元素拿出来放到一个新数据里。他们两个容易弄混:

解构:
let income = [10, 20, 30];
let [a,b,c]=income //a=10,b=20,c=30

扩展运算符:
let x=[...income] //x=[10,20,30]

看出差别了吧,解构是为了把数据取出来赋值给分散的新变量,而扩展运算符是把数据取出来包裹起来作为整体赋给一个变量。

对象字面量

如果对象的属性是从之前声明的变量获得的,那么在创建变量时,可以直接用变量来进行声明,而不需要写完整的键值对:

let name, age;
(name = "jason"), (age = 18);
let user = {
name,
age,
};

剩余操作符

"Rest operator" 可以翻译为 "剩余操作符" 或者 "剩余参数"。在 JavaScript 中,这个概念指的是使用三个点符号(...)来收集函数中的剩余参数,将它们组合成一个数组。

举个例子,假设有一个函数用来计算总和:

function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3, 4)); // 输出 10

let const

let/const 一直是 ES6 经久不衰的一个重要知识点。首先必须要知道,他俩为什么要发明出来取代 var。因为 var 有一个让人非常不爽的特点:变量提升。这是一个十分臭名昭著的特性,给 JavaScript 代码带来了很多 bug。所以 let、const 被提出,他俩就非常老实,只在自己的块级作用域内生效。也许 javascript 一开始就本该如此,谁知道创始人发了什么疯呢。

export import

见之前博文:javascript 模块系统

Class

见之前博文:面向对象编程及其在 javascript 中的实现

async/await promise

见之前博文:同步异步任务在 javascript 中的实现

pgsql常用且容易忘记的一些用法

· 阅读需 3 分钟
Jason Lee
The owner of this blog site

总结一些工作中非常有用的 pgsql 用法。

日期类

加入 time 列是 timestamp 类型的,那么

需要获得昨日的数据:select ... where time between current_date-1 and current_date;

过去 24 小时:select ... where time > current_timestamp - interval '1 day';

今日的数据:select ... where time > current_date

过去七天的数据并按天列出:select ... where time > current_date-7 group by extract(day from parking_time)

需要注意的是,在 where 子句中慎用强转符号::,因为这会让该列索引失效,从而搜索速度变得很慢。应该变通成上面这些形式,即 sql 会在 timestamp 类型和 date 类型比较时自动将 date 类型转为 timestamp 类型。

在 PostgreSQL 中,可以使用不同的函数和模式来将 timestamp 数据类型转换为特定的时间格式。以下是几种常见的方法:

  1. 使用 TO_CHAR 函数:TO_CHAR(timestamp, 'format') 函数将 timestamp 转换为指定的时间格式。例如,要将 timestamp 转换为年-月-日 小时:分钟:秒 的格式,可以使用以下代码:

    SELECT TO_CHAR(timestamp_column, 'YYYY-MM-DD HH24:MI:SS') FROM your_table;
    ```

  2. 使用 EXTRACT 函数:EXTRACT(field FROM timestamp) 函数允许提取 timestamp 中的特定时间部分(如年、月、日、小时等)。然后,可以将提取的时间部分按照需要的格式进行拼接。例如,以下代码将 timestamp 转换为年-月-日 格式:

    SELECT EXTRACT(YEAR FROM timestamp_column) || '-' || EXTRACT(MONTH FROM timestamp_column) || '-' || EXTRACT(DAY FROM timestamp_column) FROM your_table;
    ```

  3. 使用 TO_TIMESTAMP 函数和 TO_CHAR 函数的组合:如果要在转换过程中进行一些计算或调整,可以使用 TO_TIMESTAMP 函数将 timestamp 转换为特定格式的时间戳,然后再使用 TO_CHAR 函数将其格式化为字符串。例如,以下代码将 timestamp 转换为带有 AM/PM 标记的小时:分钟:秒 格式:

    SELECT TO_CHAR(TO_TIMESTAMP(timestamp_column), 'HH12:MI:SS AM') FROM your_table;
    ```

这些方法只是其中的几种,具体的选择取决于所需的时间格式和转换的要求。可以根据具体情况选择合适的方法。

having 和 where 的区别

这里有另一种理解方法,WHERE 在数据分组前进行过滤,HAVING 在数据分组后进行过滤。这是一个重要的区别,WHERE 排除的行不包括在分组中。这可能会改变计算值,从而影响 HAVING 子句中基于这些值过滤掉的分组。

python机器学习-模型评估与参数调优

· 阅读需 3 分钟
Jason Lee
The owner of this blog site

性能指标(二分类问题)

混淆矩阵

首先,为什么在有准确率(accuracy)的情况下,还要引入别的指标呢?因为受困于数据收集的客观限制,容易出现不平衡问题,比如正类的数量远远大于负类。这样,即使模型将所有样本预测为正类,损失函数也很低,达到了欺骗的效果。因此,我们需要将正类和负类分别的预测结果列出来,如混淆矩阵。

16981566899581698156689045.png

准确率和召回率

基于混淆矩阵,得到了准确率(precision)和召回率(recall):

precision = TP/(TP+FP)
recall = TP/(TP+FN)

ROC 曲线

我们通过真正率(TPR)和假真率(FPR)来衡量分类器的性能。

通过 ROC 空间,我们明白了一条 ROC 曲线其实代表了无数个分类器。那么我们为什么常常用一条 ROC 曲线来描述一个分类器呢?仔细观察 ROC 曲线,发现其都是上升的曲线(斜率大于 0),且都通过点(0,0)和点(1,1)。其实,这些点代表着一个分类器在不同阈值下的分类效果,具体的,曲线从左往右可以认为是阈值从 0 到 1 的变化过程。当分类器阈值为 0,代表不加以识别全部判断为 0,此时 TP=FP=0,TPR=TP/P=0,FPR=FP/N=0;当分类器阈值为 1,代表不加以识别全部判断为 1,此时 FN=TN=0,P=TP+FN=TP, TPR=TP/P=1,N=FP+TN=FP, FPR=FP/N=1。所以,ROC 曲线描述的其实是分类器性能随着分类器阈值的变化而变化的过程。对于 ROC 曲线,一个重要的特征是它的面积,面积为 0.5 为随机分类,识别能力为 0,面积越接近于 1 识别能力越强,面积等于 1 为完全识别。

python机器学习-数据降维

· 阅读需 6 分钟
Jason Lee
The owner of this blog site

PCA(主成分分析)

example:

import numpy as np
from sklearn.decomposition import PCA

# 创建一个示例数据集
data = np.array([
[1.5, 2.0, 3.0, 4.5],
[2.2, 2.8, 3.9, 4.2],
[1.9, 2.5, 3.2, 4.7],
[2.7, 3.0, 3.6, 4.0],
[2.3, 2.9, 3.7, 4.3]
])

# 初始化 PCA 模型,指定主成分的数量
pca = PCA(n_components=2)

# 对数据进行拟合
pca.fit(data)

# 获取主成分
principal_components = pca.components_

# 获取主成分的方差贡献比例
explained_variance_ratio = pca.explained_variance_ratio_

# 对数据进行 PCA 转换
transformed_data = pca.transform(data)

print("原始数据:\n", data)
print("主成分:\n", principal_components)
print("主成分的方差贡献比例:", explained_variance_ratio)
print("PCA 转换后的数据:\n", transformed_data)

输出:

原始数据:
[[1.5 2. 3. 4.5]
[2.2 2.8 3.9 4.2]
[1.9 2.5 3.2 4.7]
[2.7 3. 3.6 4. ]
[2.3 2.9 3.7 4.3]]
主成分:
[[-0.61305201 -0.55633419 -0.46392272 0.31533349]
[-0.53989171 -0.03347232 0.83292811 0.11673604]]
主成分的方差贡献比例: [0.87919714 0.0703791 ]
PCA 转换后的数据:
[[ 1.00928239 -0.02497259]
[-0.37705186 0.28493986]
[ 0.45617665 -0.0677326 ]
[-0.71873459 -0.2649261 ]
[-0.36967259 0.07269143]]

在深入之前,有必要解释一下 PCA 为什么在高维数据中找最大方差的方向而不是别的指标(比如最大值),来降维的?找到最大方差的方向意味着在这个方向上数据的变化最为显著,即信息越丰富。以我之前接触到的人脸图像为例,不同位置的像素有差异,才能让分类器有能力识别。

为了将我视频学习到的和这个例子对应起来,将输出中的几个部分进行解释:

首先,这个例子中的原始数据是有五个样本,每个样本有四个维度的特征(一开始我还真没搞明白这个)。然后,pca 算法找出四个主成分(找的过程是通过计算协方差矩阵和特征向量等步骤得来的,视频举的例子是几何平面便于理解,而书上的公式是一种通用的方法,适合任意数量纬度特征,二者本质相同),也就是输出中的主成分。主成分本质上是一个方向向量,且模为 1,所以本例中的主成分有两组,每组四个值,对应四种特征,哪个特征的绝对值最大,说明哪个特征在这个主成分中占主导。方差贡献比例就是,依次计算所有数据在某一主成分上的投影点到距离之和,然后全部求和,最后计算一个主成分所占的比例,比例越高,说明方差越大,数据在这个主成分上差异越明显。最后的转换后数据,则是将所有数据在某一主成分上的投影点到距离,作为以各主成分为新坐标轴的空间内的坐标。

LDA(线性判别分析)

不同于 PCA,LDA 有类别标签参与训练,所以是有监督的。LDA 训练过程中的优化目标也变成了最大化不同类之间的类间差距,而不是 PCA 的找样本内方差最大的方向,可以说是完全不同的方法,但是都能实现降维。

LDA 在一条线上投影数据点,并且让投影点的不同类之间的均值差距最大,而同类的方差最小,这样就让不同类之间的类间差距最大化。

可以说,LDA 的适用范围更窄些,它要求数据有明确的标签,且只适用分类问题;而 PCA 则适用范围更广,因为它只要求提供特征,能适用于不限于分类的问题。

example:

# 导入必要的库
from sklearn import datasets
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
import matplotlib.pyplot as plt

# 加载示例数据集(这里以Iris数据集为例)
iris = datasets.load_iris()
X = iris.data
y = iris.target

# 创建LDA模型
lda = LinearDiscriminantAnalysis(n_components=2) # 我们希望将数据降至二维

# 拟合模型并进行降维
X_lda = lda.fit_transform(X, y)

# 绘制降维后的数据
plt.figure(figsize=(8, 6))
colors = ['navy', 'turquoise', 'darkorange']
lw = 2

for color, i, target_name in zip(colors, [0, 1, 2], iris.target_names):
plt.scatter(X_lda[y == i, 0], X_lda[y == i, 1], alpha=0.8, color=color,
label=target_name)
plt.legend(loc='best', shadow=False, scatterpoints=1)
plt.title('LDA of IRIS dataset')
plt.show()

16978904104791697890409581.png

工作中的一些工具使用技巧和方法论

· 阅读需 2 分钟
Jason Lee
The owner of this blog site

我的工作是后端开发,这几天的内容就是做一套增删改查接口,暂时还没涉及复杂的逻辑。之前在外包里面搞前端,工具就两个:vscode 和浏览器。现在包括但不限于:navicat(操纵数据库)、vscode(写代码)、winscp(把本地写好的代码传到测试服务器)、postman(测试接口)、xshell(查看服务报错日志来 debug)、金山文档(写接口文档)。项目是没有办法在本地运行的,都要上传到测试服务器,通过请求接口后才能知道有无 bug。

之前的一个痛点是,每次写完代码,要在 scp 里面改目录,拖进去;如果每次修改 model、service、controller,就要拖动三次,还容易看错目录,搞得很紧张。后来,自己想的办法是,把 winscp 的文本编辑器由默认的内置改为 vscode,这样,直接从远程服务器上下载一个文件到本地,用 vscode 打开,编辑完保存,改动就同步到服务器上了。

《python 机器学习》数据预处理

· 阅读需 3 分钟
Jason Lee
The owner of this blog site

缺失数据值的处理

在 scikit-learn 中处理缺失数据的方法:

SimpleImputer: SimpleImputer 是 scikit-learn 中用于填充缺失数据的类。你可以使用不同的策略来填充缺失值,包括均值、中位数、众数等。例如:

from sklearn.impute import SimpleImputer

imputer = SimpleImputer(strategy='mean') # 使用均值填充缺失值
X_imputed = imputer.fit_transform(X)

KNNImputer: KNNImputer 使用 K-最近邻算法来填充缺失值,根据相邻样本的值进行估计。

from sklearn.impute import KNNImputer

imputer = KNNImputer(n_neighbors=5) # 使用5个最近邻的值来估计缺失值
X_imputed = imputer.fit_transform(X)

在 pandas 中处理缺失数据的方法:

isna() 和 dropna(): isna() 方法用于检测缺失值,dropna() 方法用于删除包含缺失值的行或列。

import pandas as pd

df = pd.DataFrame({'A': [1, 2, None, 4], 'B': [None, 6, 7, 8]})
df.isna() # 检测缺失值
df.dropna() # 删除包含缺失值的行
df.dropna(axis=1) # 删除包含缺失值的列

fillna(): fillna() 方法用于填充缺失值,你可以指定要使用的值或方法。

df.fillna(value=0)  # 用0填充缺失值
df.fillna(method='ffill') # 使用前一个非缺失值进行前向填充
df.fillna(method='bfill') # 使用后一个非缺失值进行后向填充

处理类别数据

这里对类别数据做一个定义:一般以字符串形式出现,表示类别。类别数据分为两种,一种是内在有数值大小关系的,比如衣服尺寸:S,M,L;另一种是标称特征,不具备排序特征。既然是字符串,我们都要处理为数字方便训练。

sklearn

这个函数是处理标签列的,本质上是通过枚举,将所有独立的标签从 0 开始编号。之所以可以这样处理,因为我们不关心类标被赋予哪个整数值。

from sklearn.preprocessing import LabelEncoder
class_le = LabelEncoder()
y = class_le.fir_transform(df['label'].values)

// 如果要将整数类标还原到字符串,可以用:
class_le.inverse_transform(y)

如果是属于特征的类别数据,特别是无序数据,用枚举则是不合适的。这是因为,这会使得模型假定不同类别之间有顺序关系。所以,独热编码更合适。

from sklearn.preprocessing import OneHotEncoder
ohe = OneHotEncoder()
ohe.fit_transform(df['color'].values)

pandas

pandas 有 get_dummies 可以用于实现独热编码

pd.get_dummies(df) // 只对字符串列进行转化

《python机器学习》分类算法

· 阅读需 3 分钟
Jason Lee
The owner of this blog site

前言

我打算开一个全新的系列,记录我在学习《python 机器学习》这本书过程中的要点和感想。

决策边界图

分类任务中的一种图,可以看出模型在整个网格内的预测结果。

16962158450461696215844398.png

代码如下:

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.svm import SVC

# 步骤2:加载鸢尾花数据集
iris = datasets.load_iris()
X = iris.data[:, :2] # 使用前两个特征以便进行可视化
y = iris.target

# 步骤3:训练一个SVM分类器
clf = SVC(kernel='linear')
clf.fit(X, y)

# 步骤4:创建一个网格来覆盖特征空间
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.01), np.arange(y_min, y_max, 0.01))

# 步骤5:对网格上的每个点使用分类器进行预测,获取决策边界
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)

# 步骤6:可视化数据点、决策边界和决策区域
plt.contourf(xx, yy, Z, cmap=plt.cm.Spectral, alpha=0.8)
plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.Spectral)
plt.xlabel('Sepal Length')
plt.ylabel('Sepal Width')
plt.title('Decision Boundary Visualization')
plt.show()

主要的函数有 meshgrid,功能是生成网格坐标数据。还有 contourf,绘制决策边界图。

逻辑回归

线性计算方式,激活函数采用 sigmoid,从而得出属于正类别的概率。

决策树/随机森林

决策树就是一种二叉树,通过计算一种特征能否尽可能的将类别区分来决定选择哪一种特征作为节点。随机森林则是决策树的集合,通过随机抽样得到训练样本来引入随机性,并且最后由多数投票来确定预测结果。

基于eggjs, sequelize和pgsql的小需求

· 阅读需 5 分钟
Jason Lee
The owner of this blog site

前言

这是我在部门遇到的第一个小需求。这一次,我写的是后端代码。刚开始,我还陷入在一种思维定式里,似乎一定要在现有项目的工程里添加代码。如果这样做,就需要在本地或者测试服务器启动项目,然而不熟悉服务器环境和配置的我,容易卡在一个环节上无法继续。我后面想明白了,完全可以将其视作一个独立的小项目。

pgsql 启动入口

不同于 mysql 直接通过命令行工具启动,pgsql 的启动方式有两种。一种是自带的 pgadmin4 这款数据库可视化软件,只要第一次连接上,后续打开都会自动连接。另一种是 sql shell, 也是随 pgsql 一起安装的。打开后,前面几个参数都是默认,最后输入口令(密码)。

16959671454581695967145365.png

eggjs+sequelize 的开发流程

初始化 egg 项目和安装 npm 包

$ npm init egg --type=simple
$ npm i

# sequelize 和 nodejs的pgsql驱动
npm install --save egg-sequelize pg pg-hstore

在 config/plugin.js 中引入 egg-sequelize 插件

'use strict';

exports.sequelize = {
enable: true,
package: 'egg-sequelize',
};

在 config/config.default.js 中编写 sequelize 配置

/* eslint valid-jsdoc: "off" */

'use strict';

module.exports = appInfo => {
/**
* built-in config
* @type {Egg.EggAppConfig}
**/
const config = exports = {};

// use for cookie sign key, should change to your own and keep security
config.keys = appInfo.name + '_1696034448361_9424';

// add your middleware config here
config.middleware = [];

// mysql

// config.sequelize = {
// dialect: 'mysql', // support: mysql, mariadb, postgres, mssql
// database: 'egg-sequelize-example-dev',
// host: '127.0.0.1',
// port: 3306,
// username: 'root',
// password: 'root',
// };

// pgsql

config.sequelize = {
database: 'egg-sequelize-example-dev',
username: 'postgres',
password: 'root',
host: 'localhost', // 或者你的数据库主机地址
port: 5432, // 或者你的数据库端口号
dialect: 'postgres', // 指定数据库类型为PostgreSQL
};

// add your user config here
const userConfig = {
// myAppName: 'egg',
};

return {
...config,
...userConfig,
};
};

创建数据库和数据表(在 pgadmin4 等工具中写 sql 语句)

CREATE DATABASE IF NOT EXISTS 'egg-sequelize-doc-default';
create table [name];

编写 model

注意字段要和数据表中定义的完全一致。这时候,model 可以通过 ctx.model 访问到,在编写 controller 或 service 层时可通过 ide 检查能否找到 model,若不能,检查 typings 文件夹下相应的 index.d.ts 文件,看看名称能否对应上。

# app/model/station.js

'use strict';

module.exports = (app) => {
const { STRING, INTEGER, TEXT, DATE } = app.Sequelize;
const { DataTypes } = require('sequelize');

const Station = app.model.define('Station', {
id: { type: INTEGER, primaryKey: true, autoIncrement: true },
name: { type: STRING(30), allowNull: false },
associated_yard: { type: STRING(30), allowNull: false },
address: { type: STRING(50) },
longitude: { type: DataTypes.NUMERIC(5), allowNull: false},
latitude: { type: DataTypes.NUMERIC(5), allowNull: false },
province: {type: STRING(32), allowNull: false},
city: {type: STRING(32), allowNull: false},
county: { type: STRING(32), allowNull: false },
tag: { type: DataTypes.ARRAY(DataTypes.TEXT) },
property_unit: { type: STRING(30) },
property_contact: { type: STRING(30) },
property_phone: { type: STRING(11) },
status: { type: STRING(10), allowNull: false },
map_induction: { type: DataTypes.BOOLEAN, allowNull: false },
area: { type: STRING(30) },
accounting_policy: { type: STRING(30), allowNull: false },
operating_description: { type: TEXT, allowNull: false },
comment: { type: TEXT },
created_at: DATE,
updated_at: DATE,
});

return Station;
};

上述代码中需要注意 sequelize 的表名推导问题。在const Station = app.model.define('Station', {...})中,第一个 Station 是模型名称,是 eggjs 中调用 app.model.[name]用到的名称。然后 define 函数中的'Station'和数据库中的表明存在对应关系,具体的对应规则是:

  1. 默认的是单数转复数:默认情况下,Sequelize 会将模型的名称转换为复数形式,并将其用作数据库表的名称。比如,station 转成 stations。所以创建表时,应该把表命名为复数形式。
  2. 自定义表名:可以在定义模型时明确指定要映射到的表的名称。
const User = sequelize.define('User', {
// 模型属性定义
}, {
tableName: 'custom_users_table' // 自定义表名
});

编写 controller 和 service 层,在 router.js 中添加路由。