SQL 多表联查 +ADO.NET学习总结:从基础到实战
数据库查询与 C# 代码落地是后端学习的核心环节 —— 多表联查涉及关联逻辑的梳理,ADO.NET包含多个抽象对象的使用。本文以 “基础概念→详细语法→实战案例→注意事项” 为逻辑,将知识点拆解细化,结构清晰、内容通俗,适合作为复习资料,帮你扎实掌握从 SQL 查询到 C# 数据库访问的全流程。
一、SQL 多表联查:关联查询核心逻辑
多表联查的核心是 “从多张关联表中提取目标数据”,核心原则是通过 “关联条件” 去除无用的笛卡尔积(两张表无条件连接会产生 “表 1 行数 × 表 2 行数” 的冗余数据),关联条件通常基于主外键关系(如 emp 表的 deptno 与 dept 表的 deptno 关联)。
1.1 多表联查的应用场景
当数据分散在多张表时,需通过联查获取完整信息。例如:员工姓名(存储在 emp 表)、部门名称(存储在 dept 表),需通过共同的 deptno 字段关联,才能同时获取这两类信息。
1.2 三大核心联查方式
1.2.1 联合查询(UNION/UNION ALL/INTERSECT/EXCEPT)
作用:合并多个 SELECT 语句的结果集,要求所有 SELECT 语句的列数、列类型完全一致。
表格
| 运算符 | 核心作用 | 特点 | 适用场景 |
|---|---|---|---|
| UNION | 合并结果集 + 自动去除重复项 | 去重,性能稍慢 | 合并无重复需求的数据 |
| UNION ALL | 合并结果集 + 保留重复项 | 不去重,性能更优 | 无需去重的批量数据合并 |
| INTERSECT | 取两个结果集的交集 | 只保留共同记录,自动去重 | 筛选两个查询的共同数据 |
| EXCEPT | 取第一个结果集的独有关联 | 只保留第一个查询的独有数据 | 对比两个查询的差异数 |
实战案例:
-- 合并“薪资<2000”和“10号部门”的员工数据(优先使用UNION ALL提升性能) SELECT ename, sal, deptno FROM emp WHERE sal < 2000 UNION ALL SELECT ename, sal, deptno FROM emp WHERE deptno = 10; -- 注意:列数不一致会直接报错(如下为错误示例) SELECT ename, sal, deptno FROM emp WHERE sal < 2000 UNION ALL SELECT ename, sal FROM emp WHERE deptno = 10; -- 缺少deptno列,运行报错1.2.2 连接查询(INNER/LEFT/RIGHT JOIN)
通过关联条件匹配两张表的数据,是最常用的联查方式,重点掌握内连接和外连接。
(1)内连接(INNER JOIN)
作用:只返回两张表中满足关联条件的记录,不满足条件的记录会被过滤。
语法结构:
SELECT 目标字段 FROM 表1 别名1 INNER JOIN 表2 别名2 ON 关联条件; -- 必须指定,否则产生笛卡尔积实战案例:
-- 查询员工姓名、薪资、部门名称(emp与dept通过deptno关联) SELECT e.ename AS 员工姓名, e.sal AS 薪资, d.dname AS 部门名称 FROM emp e INNER JOIN dept d ON e.deptno = d.deptno; -- 关联条件:员工表与部门表的部门编号一致(2)外连接(LEFT/RIGHT JOIN)
核心逻辑:保留某一张表的所有记录,另一张表不匹配的字段显示为 NULL。
表格
| 连接方式 | 核心逻辑 | 适用场景 |
|---|---|---|
| 左外连接(LEFT JOIN) | 保留左表所有记录,右表不匹配显示 NULL | 需展示左表完整数据(如所有员工,无部门则标注) |
| 右外连接(RIGHT JOIN) | 保留右表所有记录,左表不匹配显示 NULL | 需展示右表完整数据(如所有部门,无员工则标注) |
实战案例:
-- 左外连接:显示所有员工,无部门则标注“无部门” SELECT e.ename AS 员工姓名, ISNULL(d.dname, '无部门') AS 部门名称 -- ISNULL函数:将NULL替换为指定值 FROM emp e LEFT JOIN dept d ON e.deptno = d.deptno;(3)自连接
作用:将单张表视为两张表,查询表内关联数据(如员工与直属领导的关系)。
核心要求:必须给表起不同别名,避免字段归属混淆。
实战案例:
-- 查询员工姓名及其直属领导姓名(emp表的managerid关联自身empno) SELECT a.ename AS 员工姓名, ISNULL(b.ename, '无领导') AS 领导姓名 FROM emp a -- 表1:员工表 LEFT JOIN emp b -- 表2:领导表(同一张表的不同别名) ON a.managerid = b.empno; -- 关联条件:员工的领导ID=领导的员工ID1.3 子查询(嵌套查询)
定义:SELECT 语句中包含另一个 SELECT 语句,用一个查询的结果作为另一个查询的条件,按返回结果分为三类。
(1)标量子查询
返回结果为单个值(数字、字符串、日期),常用在 =、>、< 等比较运算符后。
语法结构:SELECT 字段 FROM 表 WHERE 条件 = (子查询);
实战案例:
-- 查询薪资高于ALLEN的员工 SELECT * FROM emp WHERE sal > (SELECT sal FROM emp WHERE ename = 'ALLEN'); -- 子查询返回ALLEN的薪资(单个值)注意:标量子查询需返回单个值,若返回多个结果会直接报错。
(2)列子查询
返回结果为一列数据(如多个部门编号、多个薪资),常用运算符:IN、NOT IN、ANY、ALL。
表格
| 运算符 | 说明 |
|---|---|
| IN | 匹配集合内任意一个值 |
| NOT IN | 不匹配集合内的所有值 |
| ANY | 满足集合中任意一个条件即可 |
| ALL | 满足集合中所有条件 |
实战案例:
-- 查询“销售部(SALES)”和“财务部(ACCOUNTING)”的员工 SELECT * FROM emp WHERE deptno IN (SELECT deptno FROM dept WHERE dname IN ('SALES', 'ACCOUNTING'));(3)表子查询
返回结果为多行多列的临时表,常用在 JOIN 后作为关联表。
实战案例:
-- 查询员工编号7369的姓名、薪资、部门名称 SELECT e.ename, e.sal, d.dname FROM emp e JOIN (SELECT deptno, dname FROM dept) d -- 子查询作为临时表 ON e.deptno = d.deptno WHERE e.empno = 7369;1.4 常用 SQL 函数(辅助多表联查)
函数可简化数据处理,以下为联查中高频使用的函数,搭配案例说明用法。
1.4.1 字符串函数
表格
| 函数名 | 作用 | 语法示例 | 实战案例 |
|---|---|---|---|
| CONCAT | 拼接字符串 | CONCAT (字段 1, 字段 2) | 拼接员工姓名和职位:CONCAT (ename, '-', job) |
| LEN | 计算字符串长度 | LEN (字段) | 查询姓名长度大于 3 的员工:LEN (ename) > 3 |
| TRIM | 去除字符串前后空格 | TRIM (字段) | 清洁员工姓名:TRIM (sname) |
| REPLACE | 替换字符串中的内容 | REPLACE (字段,旧值,新值) | 替换性别标识:REPLACE (gender, 'male', ' 男 ') |
1.4.2 日期时间函数
表格
| 函数名 | 作用 | 语法示例 | 实战案例 |
|---|---|---|---|
| GETDATE() | 获取当前系统日期时间 | GETDATE() | 查询当日新增员工:create_time = GETDATE () |
| YEAR/MONTH/DAY | 提取年 / 月 / 日 | YEAR (日期字段) | 计算入职年份:YEAR (GETDATE ()) - YEAR (hiredate) |
| DATEDIFF | 计算两个日期的间隔 | DATEDIFF (单位,日期 1, 日期 2) | 计算入职年限:DATEDIFF (YEAR, hiredate, GETDATE ()) |
1.4.3 流程控制函数
表格
| 函数名 | 作用 | 语法示例 | 实战案例 |
|---|---|---|---|
| ISNULL | 替换 NULL 值 | ISNULL (字段,默认值) | 无奖金显示 0:ISNULL (comm, 0) |
| IIF | 二选一判断 | IIF (条件,结果 1, 结果 2) | 判断奖金状态:IIF (comm IS NULL, ' 无 ', ' 有 ') |
| CASE WHEN | 多条件判断 | CASE WHEN 条件 1 THEN 结果 1 ELSE 结果 2 END | 薪资分级:CASE WHEN sal>=2500 THEN ' 高工资 ' ELSE ' 普通工资 ' END |
函数 + 多表联查实战:
-- 查询员工姓名、薪资、奖金状态、入职年限、部门名称 SELECT e.ename AS 员工姓名, e.sal AS 薪资, IIF(e.comm IS NULL OR e.comm=0, '无奖金', '有奖金') AS 奖金状态, DATEDIFF(YEAR, e.hiredate, GETDATE()) AS 入职年限, d.dname AS 部门名称 FROM emp e JOIN dept d ON e.deptno = d.deptno;二、ADO.NET编程:C# 操作数据库核心实现
ADO.NET是 C# 与数据库交互的核心技术,核心流程为 “连接→执行 SQL→处理结果”,需掌握关键对象的作用及使用方法。
2.1 ADO.NET核心对象速查表
表格
| 对象名称 | 核心作用 | 常用方法 / 属性 |
|---|---|---|
| SqlConnection | 建立 C# 与 SQL Server 的连接 | Open ()(打开)、Close ()(关闭) |
| SqlCommand | 执行 SQL 语句或存储过程 | ExecuteNonQuery ()(增删改)、ExecuteReader ()(查询)、ExecuteScalar ()(查单个值) |
| SqlDataReader | 逐行读取查询结果(连接式访问) | Read ()(读下一行)、["字段名"](取数据) |
| SqlDataAdapter | 填充数据到内存(断开式访问) | Fill ()(填充)、Update ()(更新) |
| DataSet | 内存中的离线数据库 | Tables(包含的表集合) |
| DataTable | 内存中的数据表(DataSet 的核心) | Rows(数据行)、Columns(数据列) |
2.2 前期准备
2.2.1 引入命名空间
C# 操作 SQL Server 需引入以下命名空间,复制到代码顶部即可:
using System.Data; // 包含DataTable、DataSet等 using System.Data.SqlClient; // 包含SqlConnection、SqlCommand等 // .NET Core/.NET 5+ 版本使用:using Microsoft.Data.SqlClient;2.2.2 编写连接字符串
连接字符串是 C# 访问数据库的 “地址和凭证”,套用以下模板(修改括号内内容):
// 模板1:SQL Server身份验证(需账号密码) string connectionString = "Server=localhost;Database=数据库名;User Id=sa;Password=登录密码;"; // 模板2:Windows身份验证(无需账号密码) string connectionString = "Server=localhost;Database=数据库名;Integrated Security=True;";2.3 核心流程 1:连接数据库
连接数据库的核心流程为 “创建连接对象→打开连接→使用→关闭连接”,连接用完需及时关闭,避免占用数据库资源。
完整代码(带异常处理):
using System; using System.Data; using System.Data.SqlClient; namespace ADO.NET基础 { class Program { static void Main(string[] args) { // 1. 定义连接字符串(替换为实际数据库信息) string connectionString = "Server=localhost;Database=testdb;Integrated Security=True;"; // 2. 使用using自动释放资源 using (SqlConnection conn = new SqlConnection(connectionString)) { try { // 3. 打开连接 conn.Open(); Console.WriteLine("数据库连接成功!"); Console.WriteLine("当前连接状态:" + conn.State); // 输出Open // 后续执行SQL语句的代码写在此处 } catch (Exception ex) { // 捕获连接异常 Console.WriteLine("数据库连接失败:" + ex.Message); } // using会自动关闭连接,无需手动调用Close() } } } }2.4 核心流程 2:执行增删改 SQL(ExecuteNonQuery)
执行 INSERT、UPDATE、DELETE 语句时,使用 SqlCommand 的 ExecuteNonQuery () 方法,返回 “受影响的行数”,可通过返回值判断操作是否成功。
实战案例:添加员工
/// <summary> /// 向emp表添加员工 /// </summary> /// <param name="ename">员工姓名</param> /// <param name="sal">薪资</param> /// <param name="deptno">部门编号</param> /// <returns>受影响的行数</returns> public static int AddEmployee(string ename, decimal sal, int deptno) { string connectionString = "Server=localhost;Database=testdb;Integrated Security=True;"; // 采用参数化查询,避免SQL注入 string sql = "INSERT INTO emp(ename, sal, deptno) VALUES(@ename, @sal, @deptno)"; using (SqlConnection conn = new SqlConnection(connectionString)) { try { conn.Open(); using (SqlCommand cmd = new SqlCommand(sql, conn)) { // 给参数赋值 cmd.Parameters.AddWithValue("@ename", ename); cmd.Parameters.AddWithValue("@sal", sal); cmd.Parameters.AddWithValue("@deptno", deptno); // 执行增删改,返回受影响行数 int affectedRows = cmd.ExecuteNonQuery(); Console.WriteLine("添加成功,受影响行数:" + affectedRows); return affectedRows; } } catch (Exception ex) { Console.WriteLine("添加失败:" + ex.Message); return 0; } } } // 调用方法 AddEmployee("张三", 2500, 10);2.5 核心流程 3:执行查询 SQL
查询数据有两种常用方式:连接式访问(SqlDataReader)和断开式访问(SqlDataAdapter+DataTable)。
2.5.1 连接式访问(SqlDataReader)
适用于查询大量数据、仅需读取一次的场景,特点是逐行读取、只能读不能改,需保持连接打开。
实战案例:多表联查并读取结果
/// <summary> /// 查询指定部门的员工信息(多表联查) /// </summary> /// <param name="deptno">部门编号</param> public static void GetEmployeesByDept(int deptno) { string connectionString = "Server=localhost;Database=testdb;Integrated Security=True;"; // 多表联查SQL string sql = @" SELECT e.ename AS 员工姓名, e.sal AS 薪资, d.dname AS 部门名称 FROM emp e JOIN dept d ON e.deptno = d.deptno WHERE e.deptno = @deptno"; using (SqlConnection conn = new SqlConnection(connectionString)) { try { conn.Open(); using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@deptno", deptno); // 执行查询,返回SqlDataReader对象 using (SqlDataReader reader = cmd.ExecuteReader()) { Console.WriteLine("员工姓名\t薪资\t部门名称"); Console.WriteLine("---------------------------"); // 循环读取每一行数据 while (reader.Read()) { // 通过字段名取数据(需做类型转换) string ename = reader["员工姓名"].ToString(); decimal sal = (decimal)reader["薪资"]; string dname = reader["部门名称"].ToString(); Console.WriteLine($"{ename}\t{sal}\t{dname}"); } } } } catch (Exception ex) { Console.WriteLine("查询失败:" + ex.Message); } } } // 调用方法(查询10号部门员工) GetEmployeesByDept(10);2.5.2 断开式访问(SqlDataAdapter+DataTable)
适用于需修改查询结果、多次使用数据的场景,特点是数据加载到内存后可断开连接,离线操作。
实战案例:查询所有员工并存储到内存
/// <summary> /// 查询所有员工,保存到DataTable(内存表) /// </summary> /// <returns>内存中的员工表</returns> public static DataTable GetAllEmployees() { string connectionString = "Server=localhost;Database=testdb;Integrated Security=True;"; string sql = "SELECT e.ename, e.sal, d.dname FROM emp e JOIN dept d ON e.deptno = d.deptno;"; // 创建DataTable(内存中的表) DataTable dt = new DataTable("员工表"); // 创建SqlDataAdapter(数据库与内存的桥梁) using (SqlDataAdapter adapter = new SqlDataAdapter(sql, connectionString)) { try { // 填充数据(自动处理连接的打开与关闭) adapter.Fill(dt); Console.WriteLine("数据填充成功,共" + dt.Rows.Count + "条记录"); } catch (Exception ex) { Console.WriteLine("填充失败:" + ex.Message); } } // 断开连接后仍可操作数据 foreach (DataRow row in dt.Rows) { string ename = row["ename"].ToString(); decimal sal = (decimal)row["sal"]; Console.WriteLine($"{ename}的薪资:{sal}"); } return dt; }2.6 事务处理(保证数据一致性)
事务的核心是 “一组操作要么全成功,要么全失败”,典型应用场景为银行转账、订单提交等。
核心流程
- 打开数据库连接;
- 开启事务(SqlTransaction);
- 执行多个 SQL 操作;
- 全部成功→提交事务(Commit ());
- 任意失败→回滚事务(Rollback ())。
实战案例:银行转账
/// <summary> /// 银行转账:张三给李四转2000元 /// </summary> /// <returns>转账是否成功</returns> public static bool TransferMoney() { string connectionString = "Server=localhost;Database=testdb;Integrated Security=True;"; string sql1 = "UPDATE bank SET umoney -= 2000 WHERE uname = 'zhangsan';"; // 张三扣钱 string sql2 = "UPDATE bank SET umoney += 2000 WHERE uname = 'lisi';"; // 李四加钱 using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlTransaction tran = null; try { // 开启事务 tran = conn.BeginTransaction(); // 创建命令对象,关联事务 using (SqlCommand cmd = new SqlCommand()) { cmd.Connection = conn; cmd.Transaction = tran; // 执行第一个操作(张三扣钱) cmd.CommandText = sql1; cmd.ExecuteNonQuery(); // 执行第二个操作(李四加钱) cmd.CommandText = sql2; cmd.ExecuteNonQuery(); } // 提交事务 tran.Commit(); Console.WriteLine("转账成功!"); return true; } catch (Exception ex) { // 回滚事务 tran?.Rollback(); Console.WriteLine("转账失败:" + ex.Message); return false; } } }2.7 通用工具类:DBHelper 封装
封装 DBHelper 类可简化重复代码,提高开发效率,直接复制到项目中即可使用。
using System; using System.Data; using System.Data.SqlClient; /// <summary> /// SQL Server数据库辅助类 /// </summary> public static class DBHelper { // 连接字符串(替换为实际数据库信息) private static string connectionString = "Server=localhost;Database=testdb;Integrated Security=True;"; #region 1. 执行增删改SQL(返回受影响行数) public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddRange(parameters); return cmd.ExecuteNonQuery(); } } } #endregion #region 2. 执行查询SQL(返回DataTable) public static DataTable ExecuteDataTable(string sql, params SqlParameter[] parameters) { DataTable dt = new DataTable(); using (SqlDataAdapter adapter = new SqlDataAdapter(sql, connectionString)) { adapter.SelectCommand.Parameters.AddRange(parameters); adapter.Fill(dt); } return dt; } #endregion #region 3. 执行查询SQL(返回单个值) public static object ExecuteScalar(string sql, params SqlParameter[] parameters) { using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddRange(parameters); return cmd.ExecuteScalar(); } } } #endregion }DBHelper 使用示例
// 1. 添加员工 string addSql = "INSERT INTO emp(ename, sal, deptno) VALUES(@ename, @sal, @deptno)"; SqlParameter[] addParams = { new SqlParameter("@ename", "李四"), new SqlParameter("@sal", 3000), new SqlParameter("@deptno", 20) }; int rows = DBHelper.ExecuteNonQuery(addSql, addParams); // 2. 多表联查 string querySql = @" SELECT e.ename, e.sal, d.dname FROM emp e JOIN dept d ON e.deptno = d.deptno WHERE e.deptno = @deptno"; SqlParameter[] queryParams = { new SqlParameter("@deptno", 20) }; DataTable dt = DBHelper.ExecuteDataTable(querySql, queryParams); // 3. 查询员工总数 string countSql = "SELECT COUNT(*) FROM emp"; object total = DBHelper.ExecuteScalar(countSql); Console.WriteLine("员工总数:" + total);三、核心复习要点
1. 多表联查
- 核心原则:多表查询必须指定关联条件,避免笛卡尔积;
- 连接方式:内连接(匹配数据)、左外连接(保留左表)、右外连接(保留右表);
- 子查询:标量子查询返回单个值,列子查询用 IN,表子查询作为临时表;
- 函数应用:ISNULL 替换 NULL,DATEDIFF 计算日期差,CASE WHEN 实现多条件判断。
2. ADO.NET
- 核心对象:SqlConnection(连接)、SqlCommand(执行 SQL)、SqlDataReader(读数据)、DataTable(内存表);
- 连接字符串:需准确配置,Windows 身份验证无需账号密码;
- 安全规范:采用参数化查询,避免 SQL 注入;
- 资源管理:使用 using 包裹对象,自动释放资源;
- 工具类:DBHelper 封装后可简化重复代码,提升开发效率。
3. 常见问题与避坑指南
- SQL 语法:字符串需用单引号,多表联查时表别名需规范,避免字段混淆;
- 数据类型:读取数据时需做类型转换,确保与数据库字段类型一致;
- 事务特性:事务需在同一个连接中执行,否则回滚无效;
- 报错排查:连接失败先检查连接字符串,SQL 执行失败先在数据库中测试语句正确性。
通过梳理核心知识点与实战案例,可系统掌握多表联查与ADO.NET的核心用法。建议先在数据库中验证 SQL 语句,再通过 C# 代码落地,逐步积累实战经验,夯实后端开发基础。
