当前位置: 首页 > news >正文

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=领导的员工ID
1.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 事务处理(保证数据一致性)

事务的核心是 “一组操作要么全成功,要么全失败”,典型应用场景为银行转账、订单提交等。

核心流程
  1. 打开数据库连接;
  2. 开启事务(SqlTransaction);
  3. 执行多个 SQL 操作;
  4. 全部成功→提交事务(Commit ());
  5. 任意失败→回滚事务(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# 代码落地,逐步积累实战经验,夯实后端开发基础。

http://www.cnnetsun.cn/news/1433812.html

相关文章:

  • Windows EFS文件加密系统
  • 2026年AIGC检测能查出用了降AI工具吗?真相可能和你想的不一样
  • el-input输入限制全攻略:从整数到小数,再到特殊符号过滤
  • 嵌入式软件测试工具选型与工程实践指南
  • CAM++系统效果实测:说话人验证相似度计算,结果清晰直观
  • Starry Night效果惊艳展示:15步内生成1024px高清幻想画作集
  • GeoScene Pro实战:5步搞定FLUS模型土地利用预测(附避坑指南)
  • C语言 函数的递归和迭代
  • M2LOrder安全部署指南:防范模型投毒与对抗样本攻击
  • AI元人文:以伦理中间件为桥,锚定PKSP与人类责任主义的意义共生
  • Z-Image Turbo极简部署:免环境配置生成高质量图像
  • vue框架header固定导航header
  • Linemod算法实战:在ROS+Realsense D435i上实现工业零件的实时抓取定位
  • 银河麒麟v10下Anaconda与PyCharm的极简安装指南
  • YOLOv8与YOLOv5深度对比:Anchor-Free带来的性能提升与迁移学习实践
  • 模型预测控制在空调加热器中的应用与实现
  • Arduino 24LC64F EEPROM 驱动库:字节级擦写与I²C高可靠实现
  • 653基于放大电路传感器气体烟雾检测仪
  • 深度学习与机器学习如何选择?
  • 零基础玩转Pi0具身智能:浏览器一键体验机器人动作生成
  • CosyVoice3快速部署指南:一键运行,开启你的语音克隆之旅
  • 2025 高效整理雪球内容:自动化下载与多格式导出实战
  • 5年下滑50万辆,东风本田的表现到底该怎么看?
  • SmolVLA构建智能客服:微信小程序端对话机器人集成
  • ESP32+手机热点5分钟搭建个人WebServer(附完整代码)
  • PARL核心架构深度解析:Model、Algorithm、Agent三要素
  • 终极TensorPack图像增强全攻略:从基础变换到高级技巧
  • SaaS Boilerplate桌面化:Electron与Tauri跨平台方案深度测评
  • 产品经理必看!用UML用例图搞定需求沟通的5个实战技巧
  • 从Pending到Running:Calico网络组件镜像拉取故障的深度排查与实战解决