去重这件事,SQL层面就能做完
做列表筛选、下拉选项、报表分组时,经常遇到一个需求:某个字段的取值只需要出现一次。比如前端要渲染一个部门下拉框,数据源是员工表,同一个部门有几十号人,直接查department列会返回几十条重复值,交给JavaScript用Set去重当然也能做,但数据已经从MySQL传到Node.js再过滤,网络传输和内存都白花了。
SELECT DISTINCT把去重放在数据库侧完成,返回的就是已经去重的结果集。这一节围绕Node.js+mysql2执行SELECT DISTINCT展开,把单列、多列组合、条件过滤和参数化写法讲清楚。
DISTINCT到底做了什么
DISTINCT是SELECT的一个关键字,作用是去掉查询结果中的重复行。它作用在结果集上,不改动表里的数据。表里有多少条重复记录,执行完之后还是那么多条,只是返回给你的结果里每个值只出现一次。
环境准备
动手前确认:
-
系统已安装Node.js
-
MySQLServer已安装并处于运行状态
-
项目里已安装mysql2包
-
目标数据库和Employees表已存在并有数据
安装mysql2:
npm install mysql2
示例统一使用下面这张Employees表:
| employeeId | name | department | city |
|---|---|---|---|
| 1 | John | Sales | London |
| 2 | Emma | HR | Manchester |
| 3 | Michael | Sales | London |
| 4 | Sophia | IT | Birmingham |
| 5 | Daniel | HR | London |
先看看不去重是什么效果:
SELECT department FROM Employees;
返回:
Sales
HR
Sales
IT
HR
Sales出现两次,HR出现两次。加上DISTINCT:
SELECT DISTINCT department FROM Employees;
返回:
Sales
HR
IT
重复值被去掉了。
SELECT DISTINCT的基本语法
SELECT DISTINCT column_name FROM table_name;
SELECT指定要读取的数据,DISTINCT表示对结果去重,column_name是要读取的列,table_name是目标表。
在Node.js中查询单列唯一值
const mysql = require('mysql2');
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'your_password',
database: 'companydb'
});
const sql = "SELECT DISTINCT department FROM Employees";
connection.query(sql, (err, result) => {
if (err) {
console.log("错误:", err.message);
return;
}
console.log(result);
});
connection.end();
返回:
[
{ department: 'Sales' },
{ department: 'HR' },
{ department: 'IT' }
]
每个部门只出现一次。结果依然是数组,数组元素是对象,键为列名。
查询另一列的唯一值
把列换成city,就是城市去重:
const sql = "SELECT DISTINCT city FROM Employees";
connection.query(sql, (err, result) => {
if (err) {
console.log("错误:", err.message);
return;
}
console.log(result);
});
返回:
[
{ city: 'London' },
{ city: 'Manchester' },
{ city: 'Birmingham' }
]
London在表里出现了三次,去重后只返回一次。
多列组合去重
DISTINCT后面跟多列时,去重的判断依据是这些列组合起来的值,而不是单列。这一点经常被误解。
const sql = "SELECT DISTINCT department, city FROM Employees";
connection.query(sql, (err, result) => {
if (err) {
console.log("错误:", err.message);
return;
}
console.log(result);
});
返回:
[
{ department: 'Sales', city: 'London' },
{ department: 'HR', city: 'Manchester' },
{ department: 'IT', city: 'Birmingham' },
{ department: 'HR', city: 'London' }
]
结果里有两条HR,但对应的city不同,一条是Manchester,一条是London,所以(HR,Manchester)和(HR,London)是两个不同的组合,都保留。Sales和London这个组合在表里出现了两次(John和Michael),去重后只返回一次。
这里有个实际项目里踩过的坑:有人写SELECT DISTINCTdepartment,city,本意是“把部门去重,顺便把城市也带出来”,结果发现部门还是重复的。原因是DISTINCT作用在整行上,不是只作用在第一列。要单列去重就只写那一列。
配合WHERE先过滤再去重
WHERE在DISTINCT之前生效,先把不满足条件的行筛掉,再对剩下的结果去重。
const sql = "SELECT DISTINCT department FROM Employees WHERE city = 'London'";
connection.query(sql, (err, result) => {
if (err) {
console.log("错误:", err.message);
return;
}
console.log(result);
});
返回:
[
{ department: 'Sales' },
{ department: 'HR' }
]
逻辑顺序是:先找出city为London的员工(John、Michael、Daniel),取出他们的department(Sales、Sales、HR),再去重得到Sales和HR。
配合ORDERBY排序
DISTINCT返回的顺序不保证稳定,想要按字母或数值排序要显式加ORDERBY。
const sql = "SELECT DISTINCT department FROM Employees ORDER BY department";
connection.query(sql, (err, result) => {
if (err) {
console.log("错误:", err.message);
return;
}
console.log(result);
});
返回:
[
{ department: 'HR' },
{ department: 'IT' },
{ department: 'Sales' }
]
这里ORDERBY的字段必须是SELECT里出现的字段,否则MySQL在某些配置下会报错。如果确实需要按没选出来的字段排序,可以用聚合函数或者改写成GROUPBY。
参数化查询处理用户输入
城市名来自前端时,不能拼接字符串:
const sql = "SELECT DISTINCT department FROM Employees WHERE city = ?";
connection.query(sql, [city], (err, result) => {
if (err) {
console.log("错误:", err.message);
return;
}
console.log(result);
});
完整一点,用readline模拟用户输入:
const mysql = require('mysql2');
const readline = require('readline');
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'your_password',
database: 'companydb'
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('请输入城市: ', (city) => {
const sql = "SELECT DISTINCT department FROM Employees WHERE city = ?";
connection.query(sql, [city], (err, result) => {
if (err) {
console.log("错误:", err.message);
return;
}
console.log(result);
connection.end();
rl.close();
});
});
输入London后返回:
[
{ department: 'Sales' },
{ department: 'HR' }
]
?占位符把用户输入当作纯值处理,即使输入里带引号或SQL关键字,也不会被当作语句的一部分执行。
列名写错时怎么暴露问题
const sql = "SELECT DISTINCT dept FROM Employees";
connection.query(sql, (err, result) => {
if (err) {
console.log("错误:", err.message);
return;
}
console.log(result);
});
输出:
错误: Unknown column 'dept' in 'field list'
实际列名是department,写成dept,MySQL直接报字段不存在。回调里判断err并return,后面的代码就不会拿着undefined继续执行。
DISTINCT和GROUPBY的选择
两者都能实现去重,但语义和使用场景有区别。
SELECT DISTINCTdepartmentFROMEmployees和SELECTdepartmentFROMEmployeesGROUPBYdepartment在这个场景下返回结果一样。DISTINCT表达的是“去重”,GROUPBY表达的是“分组”,后者通常还伴随聚合函数,比如SELECTdepartment,COUNT(*)FROMEmployeesGROUPBYdepartment。
单纯去重就用DISTINCT,需要同时统计每组数量就用GROUPBY。执行计划上两者在MySQL里经常被优化成相似的路径,性能差异不大,选哪个更多看语义是否清晰。
关于性能的一点经验
DISTINCT需要对结果做去重,MySQL内部通常会用到临时表或排序。数据量大、去重列没有索引时,开销会比较明显。
如果去重列上有索引,MySQL可以直接利用索引有序性跳过重复值,效率高很多。所以对经常需要DISTINCT的列,考虑加索引。
另一个思路是从数据模型上避免。如果部门是固定的几个值,单独建一张departments表,员工表用外键关联,查询部门列表直接查departments表,根本不需要DISTINCT。这是更根本的解法,只是改造成本高,要看项目阶段决定。
现在写法:Promise与async/await
const mysql = require('mysql2/promise');
async function getUniqueDepartments(city) {
const connection = await mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'your_password',
database: 'companydb'
});
try {
const sql = "SELECT DISTINCT department FROM Employees WHERE city = ? ORDER BY department";
const [rows] = await connection.execute(sql, [city]);
return rows;
} catch (err) {
console.error("查询失败:", err.message);
throw err;
} finally {
await connection.end();
}
}
解构赋值const[rows]取出结果行数组,第二个元素是字段元信息,一般用不上。finally保证连接释放,异常情况下也不会泄漏。
本节课程知识要点
-
DISTINCT作用在结果集上,不修改表中的数据
-
多列DISTINCT按列组合去重,不是按第一列去重
-
WHERE在DISTINCT之前生效,先过滤再去重
-
ORDERBY的字段需要在SELECT列表中出现
-
用户输入走
?占位符,避免SQL注入 -
去重列有索引时性能更好,数据模型合理时可以从源头避免去重需求
-
单纯去重用DISTINCT,需要聚合统计用GROUPBY
-
mysql2/promise配合async/await更适合现在项目结构
练习思考题
-
写一条SELECT DISTINCT语句,查询Employees表中所有不重复的city,并按city字母顺序排序。
-
查询department和city的唯一组合,只保留city为London的记录,用参数化查询实现。
-
解释
SELECT DISTINCT department,city FROM Employees和SELECT DISTINCT depart ment FROM Employees返回结果的区别,并说明原因。 -
如果一张表有百万级数据,需要对某个列做DISTINCT查询,从索引和数据模型两个角度说说可以怎么优化。