Skip to content

MySQL - DISTINCT 子句

MySQL 中的 DISTINCT 子句与 SELECT 语句一起使用,用于从结果集中消除重复的行。它确保返回的每一行都基于指定列的值是唯一的。这对于生成唯一的用户位置列表、产品类别或其他任何你只需要查看每个值一次的数据等任务来说至关重要。

使用 DISTINCT 子句的基本语法很简单:

SELECT DISTINCT column1, column2, ...
FROM table_name
WHERE [conditions];

其中:

  • column1, column2, ... 是你希望从中检索唯一值的列。
  • table_name 是你正在查询的表。
  • WHERE [conditions] 是一个可选子句,用于在应用 DISTINCT 操作之前过滤行。

首先,让我们为示例设置一个示例 CUSTOMERS 表。请注意其现代数据类型和约束。

CREATE TABLE CUSTOMERS (
ID INT AUTO_INCREMENT PRIMARY KEY,
NAME VARCHAR(100) NOT NULL,
AGE INT NOT NULL,
ADDRESS VARCHAR(255),
SALARY DECIMAL(10, 2),
JOIN_DATE DATE NOT NULL
);
-- 插入示例数据
INSERT INTO CUSTOMERS(NAME, AGE, ADDRESS, SALARY, JOIN_DATE) VALUES
('Ramesh', 32, 'Hyderabad', 2000.00, '2022-01-15'),
('Khilan', 25, 'Delhi', 1500.00, '2022-03-22'),
('Kaushik', 23, 'Hyderabad', 2000.00, '2023-04-10'),
('Chaitali', 25, 'Mumbai', 6500.00, '2021-11-05'),
('Hardik', 27, 'Vishakapatnam', 8500.00, '2023-01-30'),
('Komal', 22, 'Vishakapatnam', 4500.00, '2022-08-19'),
('Muffy', 24, 'Indore', 10000.00, '2023-05-01'),
('Priya', 25, 'Delhi', NULL, '2023-06-12');

如果我们查询所有地址,将会看到重复项:

SELECT ADDRESS FROM CUSTOMERS;

现在,让我们使用 DISTINCT 来获取所有客户地址的唯一列表。

SELECT DISTINCT ADDRESS FROM CUSTOMERS;

结果集现在只包含唯一的地址,重复项已被移除。

地址
Hyderabad
Delhi
Mumbai
Vishakapatnam
Indore

将 DISTINCT 与聚合函数(COUNT)结合使用

Section titled “将 DISTINCT 与聚合函数(COUNT)结合使用”

你可以将 DISTINCT 与 COUNT()、SUM() 或 AVG() 等聚合函数结合使用,仅对唯一值执行计算。一个常见的用例是统计唯一项的数量。

SELECT COUNT(DISTINCT ADDRESS) AS UniqueLocations FROM CUSTOMERS;

此查询返回唯一地址的数量。

唯一地点数
5

当你在多列上使用 DISTINCT 时,它会返回这些列的唯一 组合。它不会独立地查找每列的唯一值。这是初学者常见的混淆点。

SELECT DISTINCT ADDRESS, AGE FROM CUSTOMERS ORDER BY ADDRESS, AGE;

请注意,‘Delhi’ 和 ‘Hyderabad’ 出现了多次。这是因为 ADDRESS 和 AGE 的 组合 对于每一行来说是唯一的。例如,(‘Delhi’, 25) 是一个唯一的组合。

地址年龄
Delhi25
Hyderabad23
Hyderabad32
Indore24
Mumbai25
Vishakapatnam22
Vishakapatnam27

在 DISTINCT 子句的上下文中,所有 NULL 值都被视为一个单独的组。如果一列包含多个 NULL,DISTINCT 将在结果集中只返回一个 NULL。

SELECT DISTINCT SALARY FROM CUSTOMERS ORDER BY SALARY;

结果中包含 NULL 作为其中一个唯一值。

薪资
NULL
1500.00
2000.00
4500.00
6500.00
8500.00
10000.00

DISTINCT 虽然非常方便,但对于大型表来说可能占用大量资源,因为数据库必须对数据进行排序才能找到并消除重复项。在许多用例中,GROUP BY 可以实现相同的效果,并且有时性能更好,尤其是在分组列上存在索引的情况下。

-- 此查询在功能上等同于 SELECT DISTINCT ADDRESS
SELECT ADDRESS FROM CUSTOMERS GROUP BY ADDRESS;

最佳实践: 对于简单的唯一值检索,DISTINCT 完全可读且可接受。对于更复杂的聚合,或者当大型数据集的性能至关重要时,请考虑 GROUP BY 配合适当的索引是否能提供更好的解决方案。始终使用 EXPLAIN 分析你的查询性能。

在客户端应用程序中实现 DISTINCT(现代示例)

Section titled “在客户端应用程序中实现 DISTINCT(现代示例)”

以下是如何在现代应用程序代码中执行 DISTINCT 查询,其中强调了安全性以及连接管理和参数化查询等最佳实践。

Node.js (async/await)
Python (mysql-connector-python)
Java (JDBC with try-with-resources)
PHP (PDO)
此示例使用 `mysql2/promise` 库实现现代异步代码,利用连接池进行高效数据库连接,并使用 try/catch/finally 块进行健壮的错误处理。
```javascript
// main.js
const mysql = require('mysql2/promise');
async function getUniqueCustomerCities() {
let connection;
try {
// 使用连接池以获得更好的性能和资源管理
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'TUTORIALS',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
connection = await pool.getConnection();
console.log('成功连接到数据库。');
const sql = "SELECT DISTINCT ADDRESS FROM CUSTOMERS WHERE ADDRESS IS NOT NULL ORDER BY ADDRESS;";
const [rows] = await connection.execute(sql);
console.log('唯一客户城市:');
rows.forEach(row => {
console.log(`- ${row.ADDRESS}`);
});
return rows;
} catch (error) {
console.error('数据库查询失败:', error);
} finally {
if (connection) connection.release(); // 将连接释放回连接池
}
}
getUniqueCustomerCities();

此 Python 示例使用 with 语句(上下文管理器)来确保数据库连接和游标自动关闭。它还使用 try/except 块进行错误处理。

main.py
import mysql.connector
from mysql.connector import errorcode
def get_unique_customer_cities():
try:
# 连接详情理想情况下应存储在配置文件中
connection = mysql.connector.connect(
host='localhost',
user='root',
password='password',
database='TUTORIALS'
)
# 使用 with 语句确保游标自动关闭
with connection.cursor(dictionary=True) as cursor:
query = "SELECT DISTINCT ADDRESS FROM CUSTOMERS WHERE ADDRESS IS NOT NULL ORDER BY ADDRESS;"
cursor.execute(query)
results = cursor.fetchall()
print("唯一客户城市:")
for row in results:
print(f"- {row['ADDRESS']}")
except mysql.connector.Error as err:
if err.errno == errorcode.ER_ACCESS_DENIED_ERROR:
print("用户名或密码有误")
elif err.errno == errorcode.ER_BAD_DB_ERROR:
print("数据库不存在")
else:
print(err)
finally:
if 'connection' in locals() and connection.is_connected():
connection.close()
print("MySQL 连接已关闭")
if __name__ == "__main__":
get_unique_customer_cities()

此现代 Java 示例使用 try-with-resources 语句,该语句自动关闭 Connection、PreparedStatement 和 ResultSet。这可以防止资源泄漏并简化代码。

DistinctExample.java
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class DistinctExample {
private static final String DB_URL = "jdbc:mysql://localhost:3306/TUTORIALS";
private static final String USER = "root";
private static final String PASS = "password";
public static void main(String[] args) {
String sql = "SELECT DISTINCT ADDRESS FROM CUSTOMERS WHERE ADDRESS IS NOT NULL ORDER BY ADDRESS";
List<String> cities = new ArrayList<>();
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
System.out.println("成功连接到数据库。");
while (rs.next()) {
cities.add(rs.getString("ADDRESS"));
}
} catch (SQLException e) {
System.err.println("数据库操作失败:" + e.getMessage());
e.printStackTrace();
}
System.out.println("唯一客户城市:");
cities.forEach(city -> System.out.println("- " + city));
}
}

此 PHP 示例使用 PDO(PHP Data Objects),这是在 PHP 中与数据库交互的现代推荐方式。它支持预处理语句以防止 SQL 注入,并为不同的数据库系统提供一致的接口。错误处理通过异常进行管理。

main.php
<?php
$host = 'localhost';
$db = 'TUTORIALS';
$user = 'root';
$pass = 'password';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 以关联数组形式获取结果
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
echo "成功连接到数据库。\n";
$sql = "SELECT DISTINCT ADDRESS FROM CUSTOMERS WHERE ADDRESS IS NOT NULL ORDER BY ADDRESS";
$stmt = $pdo->query($sql);
echo "唯一客户城市:\n";
while ($row = $stmt->fetch()) {
echo "- " . $row['ADDRESS'] . "\n";
}
} catch (\PDOException $e) {
// 在实际应用中,你会记录此错误,而不是向用户显示。
error_log($e->getMessage());
// http_response_code(500);
die("数据库连接失败:" . $e->getMessage());
}
?>