从刚开始带新人的那阵子起,我就发现一个规律:十个刚接触C#/.NET的初学者,至少有七八个第一周就会被数据库连接卡住。不是连接字符串写错,就是不知道SqlCommand和SqlDataReader是干什么用的,再要么就在DataTable和DataReader之间纠结半天。说到底,C#和SQL Server之间的这条数据通道,看起来就几个类的事,但对新手来说,缺少的从来不是某个API的拼写,而是一条“先干什么、再干什么、为什么这么干”的完整链路。
这篇内容就是把我在实际开发中用得最多、也最常给新人讲的一批C#/.NET + SQL Server常用命令整理出来,用大白话拆开讲透。目标很简单:让你看完之后,遇到“用程序读写SQL Server数据”的需求不再发怵,能照着写、照着改,也知道出错了应该往哪个方向查。
1. 先搞懂C#连接SQL Server的三步套路
很多新手一上来就背代码,今天记住了SqlConnection conn = new SqlConnection(...),明天换了个需求就不会了。其实没必要死记,你只需要理解C#和SQL Server之间的交互永远就那么三步。
1.1 三条命令,一个目的
第一步是建立连接,用SqlConnection。第二步是准备一条要执行的命令,用SqlCommand。第三步是执行命令并处理结果,这一步根据查询类型不同,会用到ExecuteNonQuery、ExecuteScalar、ExecuteReader这几个方法。
打个比方,SqlConnection就像是你拨出去的电话线路,SqlCommand是你在电话里要说的话,执行方法则是你决定“对方听后是挂断、回答一句,还是一口气跟你说一堆”。理解了这三层,你再看下面的代码就不会觉得乱了。
// 第一步:建立连接 using SqlConnection conn = new SqlConnection(connectionString); conn.Open(); // 第二步:准备命令 using SqlCommand cmd = new SqlCommand("SELECT Name FROM Users WHERE Id = 1", conn); // 第三步:执行命令并拿结果 string name = (string)cmd.ExecuteScalar();1.2 新手最容易卡住的地方:连接字符串
上面代码里的connectionString,就是新手的第一道坎。它长这样:
Server=localhost;Database=MyShop;User Id=sa;Password=123456;或者这样:
Server=localhost;Database=MyShop;Trusted_Connection=True;第一种用的是SQL Server账号登录,第二种用的是Windows当前登录身份。很多新手在自己电脑上连接没问题,换到别人电脑上就报登录失败,原因就是这两套登录方式搞混了。我的建议是,自己本机开发用Trusted_Connection=True最省事,部署到服务器上再根据环境决定用哪种。
2. 连接字符串逐一拆解,写对了才能不报错
连接字符串看着就是几个分号隔开的键值对,但里面每一个参数都有各自的讲究,踩坑的时候你就知道它们的分量了。
2.1 核心参数各是什么作用
先从最常见的几个参数说起,我把它们的含义整理成了一张表,方便你随时查。
| 参数 | 作用 | 例子 | 踩坑提醒 |
|---|---|---|---|
Server或Data Source | 指定数据库实例所在的位置 | Server=localhost、Server=192.168.1.10 | 连接本机SQL Server默认实例时,localhost通常可以,但命名实例要写成localhost\SQLEXPRESS,很多新手漏掉反斜杠后面的实例名 |
Database或Initial Catalog | 指定要连哪个数据库 | Database=MyShop | 你连的是库,不是服务器。只写服务器不写库,程序会不知道连到哪 |
User Id | 登录账号 | User Id=sa | 用SQL Server账号登录时必须提供 |
Password | 账号密码 | Password=123456 | 密码里有特殊字符时,要确认是否正确转义 |
Trusted_Connection=True | 是否使用Windows身份认证 | Trusted_Connection=True | 和User Id/Password二选一,同时写容易造成混淆 |
Encrypt | 是否加密连接 | Encrypt=True或Encrypt=False | 新版驱动默认可能开启加密,老程序连不上有时就是这个问题 |
TrustServerCertificate | 是否信任服务器证书 | TrustServerCertificate=True | 开发环境经常和Encrypt=True搭配使用,生产环境建议不要盲目设置 |
2.2 开发机上最让人头疼的两个坑
第一个坑是“命名实例”。很多人安装SQL Server时选了“命名实例”,比如机器名加SQLEXPRESS。这时候如果你只写Server=localhost,会看到A network-related or instance-specific error。解决方式就是补全实例名:
Server=localhost\\SQLEXPRESS;Database=MyShop;Trusted_Connection=True;第二个坑是“默认端口”。如果你的SQL Server没跑在默认的1433端口上,而是自定义成14330之类,你就得显式指定端口:
Server=localhost,14330;Database=MyShop;Trusted_Connection=True;别小看这个逗号,很多人会习惯性写成冒号,结果连不上。
3. 执行命令的三种方法,别再选错了
SqlCommand准备好之后,具体怎么执行,取决于你这条SQL的返回结果到底是什么。我在带新人时发现,很多人不管拿到什么SQL都用同一个方法去执行,最后要么拿不到数据,要么报错,要么多此一举。
3.1 增删改用ExecuteNonQuery
如果你执行的是INSERT、UPDATE、DELETE,或者建表、删表这类不返回结果集的SQL,用ExecuteNonQuery。它的返回值是受影响的行数。
using SqlConnection conn = new SqlConnection(connectionString); conn.Open(); string sql = "UPDATE Users SET Name = @Name WHERE Id = @Id"; using SqlCommand cmd = new SqlCommand(sql, conn); cmd.Parameters.AddWithValue("@Name", "张三"); cmd.Parameters.AddWithValue("@Id", 1); int affected = cmd.ExecuteNonQuery(); Console.WriteLine($"影响了 {affected} 行");这里有两个隐藏细节。第一,ExecuteNonQuery返回的是受影响行数,但有些操作比如CREATE DATABASE这类是不返回行数的,返回-1也不算出错。第二,如果你执行的是UPDATE但影响行数为0,不代表SQL错了,可能只是条件没匹配上任何行。
3.2 只取一个值就用ExecuteScalar
ExecuteScalar是用来取“单个结果”的。比如统计总数、查询某一行某一列的值,用这个最合适——它只返回结果集第一行第一列的值,多余的数据不会取出来,效率高,语义也清晰。
using SqlConnection conn = new SqlConnection(connectionString); conn.Open(); string sql = "SELECT COUNT(*) FROM Users"; using SqlCommand cmd = new SqlCommand(sql, conn); int count = (int)cmd.ExecuteScalar(); Console.WriteLine($"总用户数:{count}");注意返回的其实是object类型,所以你需要强转或转换。如果是COUNT(*),通常转成int没问题,但如果是SUM(Price)这类,可能会返回decimal或NULL,直接转int会抛异常。遇到可能为NULL的情况,要提前处理。
3.3 读多行数据用ExecuteReader或SqlDataAdapter
要取多条记录,最基础的做法是ExecuteReader,它返回一个SqlDataReader,你可以一行一行读。这个我在后面的章节里详细说。
很多新手的问题是:明明结果集有好几行,却用了ExecuteScalar,结果只拿到第一行第一列,还以为是SQL写错了。所以选方法前,先问自己一个问题:我到底要拿一个单元格,还是要拿多行多列?这样思路就清楚了。
3.4 关于using,建议不要省
上面示例里我用了using关键字,这不是装样子。SqlConnection和SqlCommand都实现了IDisposable接口,它们各自占着数据库连接资源和命令的托管资源。如果你不用using,连接对象没有被及时释放,久而久之会出现连接池耗尽的问题——报错信息通常是“The connection pool has timed out waiting for a free connection”。
最简单的做法就是像我那样写成using SqlConnection conn = new SqlConnection(...),这是C# 8.0以后支持的简化写法,作用域结束自动释放。
4. SQL Server日常增删改查,小白真正用得上的语法
C#这边只是“管道”,我们真正要下发的还得是SQL语句本身。很多新手天天写CRUD,以为SQL就这么点东西,其实常用的语句拆开也就是几类,但你得知道每个语法的适用范围。
4.1 SELECT查询与WHERE筛选
查数据的灵魂是WHERE。少写一个条件,你可能把整个表扫一遍;多写一个错误的条件,数据就查不出来。
SELECT Id, Name, Age FROM Users WHERE Age > 18 ORDER BY Age DESC;这个语句干了三件事:选某些列、筛选行、排序。ORDER BY可以放在最后,但它只影响展示顺序,不影响筛选结果。如果你想看前几条,用TOP或OFFSET ... FETCH。
SELECT TOP 10 Id, Name FROM Users ORDER BY Id;4.2 模糊查询与范围筛选
LIKE做模糊匹配时,%代表任意长度的字符,_代表一个字符。比如查找名字以“张”开头的用户:
SELECT Id, Name FROM Users WHERE Name LIKE '张%';范围筛选用BETWEEN或IN,都是高频:
SELECT Id, Name FROM Users WHERE Age BETWEEN 18 AND 30; SELECT Id, Name FROM Users WHERE City IN ('北京', '上海', '广州');4.3 INSERT、UPDATE、DELETE的固定套路
增删改的SQL语法非常固定:
-- 插入 INSERT INTO Users (Name, Age, City) VALUES ('李四', 25, '深圳'); -- 修改 UPDATE Users SET Age = 26 WHERE Name = '李四'; -- 删除 DELETE FROM Users WHERE Id = 10;这里我只提一个意识上的问题:写UPDATE和DELETE的时候,先确认WHERE条件,再执行。你永远不知道这会触碰到多少条数据。尤其在测试库里还好,生产库一条没有WHERE的UPDATE能让你整个周末都在补数据。
4.4 聚合统计:GROUP BY、COUNT、SUM
统计场景是新手最容易写错SQL的地方。比如统计每个城市的用户数:
SELECT City, COUNT(*) AS UserCount FROM Users GROUP BY City;GROUP BY之后,SELECT里能出现的列就必须是分组列或聚合函数,不然SQL会报错。这是数据库的一致性规则,多写几次就记住了。
5. C#读取结果集的两种做法,各有各的适用场景
从C#侧读多行结果,最常见的两种方式:SqlDataAdapter填充DataTable,以及SqlDataReader逐行读取。很多教程都会写,但对什么时候用哪个,说得不够清楚。
5.1 SqlDataAdapter:一次性搬回来
SqlDataAdapter的使用逻辑是“跑一趟SQL,把整个结果集一次性放到内存里的DataTable中”。适合数据量不大、可以整体处理的情况。
using SqlConnection conn = new SqlConnection(connectionString); conn.Open(); string sql = "SELECT Id, Name, Age FROM Users"; using SqlDataAdapter adapter = new SqlDataAdapter(sql, conn); DataTable table = new DataTable(); adapter.Fill(table); foreach (DataRow row in table.Rows) { Console.WriteLine($"{row["Id"]} - {row["Name"]} - {row["Age"]}"); }它的好处是拿完就断,你不需要一直盯着连接;操作DataTable的方式对新手也友好。缺点就是如果结果集巨大(比如几十万行),全塞内存里会吃紧。
5.2 SqlDataReader:边读边处理
SqlDataReader则是“开着管道,一条一条读”。你必须保持连接打开,而且只能向前读取,不能倒退回去看已读过的行。
using SqlConnection conn = new SqlConnection(connectionString); conn.Open(); string sql = "SELECT Id, Name, Age FROM Users"; using SqlCommand cmd = new SqlCommand(sql, conn); using SqlDataReader reader = cmd.ExecuteReader(); while (reader.Read()) { int id = reader.GetInt32(0); string name = reader.GetString(1); int age = reader.GetInt32(2); Console.WriteLine($"{id} - {name} - {age}"); }新手最容易犯的错有两个:一个是忘了reader.Read()那一步就开始取值,直接报“数据读取操作无效”;另一个是拿到reader["Name"]之后不知道要转换成具体类型,一用就报InvalidCastException。
5.3 我给你的选型建议
我个人的习惯是这样的:数据量在几百到几千行以内,又需要做一些遍历或转对象操作,直接用SqlDataAdapter填DataTable,省心;数据量成千上万甚至更多,或者你只需要顺序流式处理,用SqlDataReader。记住“少即是多”的道理,别什么都往内存里塞。
6. 参数化查询不是选择题,是必答题
这是我反复跟新人强调的一点:任何从用户输入拼接到SQL里的值,都必须用参数化方式传,不要手拼字符串。
6.1 正确写法和错误写法对比
先看错误写法:
string name = txtName.Text; // 用户输入 string sql = "SELECT * FROM Users WHERE Name = '" + name + "'";这样写的问题显而易见:用户要是输入一段包含单引号的恶意内容,SQL就变成了另一条完全不同的语句。防范手段其实很简单:
string sql = "SELECT * FROM Users WHERE Name = @Name"; using SqlCommand cmd = new SqlCommand(sql, conn); cmd.Parameters.AddWithValue("@Name", name);SQL里的@Name是一个占位符,AddWithValue负责把值安全地传进去。数据库拿到的是参数值,不会再被当作SQL代码去解析。这就是“参数化查询”的核心逻辑。
6.2 参数化还有性能上的好处
除了防注入,参数化查询能让SQL Server更好地复用执行计划。你想想,如果每次WHERE的条件都是嵌入式字符串,数据库等于每次都看到一句“新SQL”,编译一次、缓存一次;参数化之后,SQL模板是固定的,只是参数在变,执行计划能直接复用,执行效率自然更高。
以我实测的经验来说,一个循环一万次的插入操作,用参数化和不用参数化,性能差距不是一点半点。
6.3 什么时候要小心AddWithValue
AddWithValue虽然方便,但在某些场景下可能会把类型推断错。比如SQL Server里的字段是nvarchar(50),你却传了一个长度很大或类型不明确的字符串,可能导致索引失效。更严谨的做法是显式指定参数类型:
cmd.Parameters.Add("@Name", SqlDbType.NVarChar, 50).Value = name; cmd.Parameters.Add("@Age", SqlDbType.Int).Value = age;这个习惯在写生产级代码时尤其重要。后面你维护老项目、接手别人的代码,会感谢当年自己有这么一手。
7. 存储过程怎么调,小白也能看懂的调用方式
调用存储过程是C#连接SQL Server的另一个高频场景。很多团队为了权限可控、执行计划稳定、业务逻辑下沉,都会要求所有数据操作走存储过程。新手看到存储过程就觉得难,其实C#这边调用反而比写散装SQL更简单。
7.1 设置CommandType为StoredProcedure
关键就一句话:告诉SqlCommand,你这儿不是一条文本SQL,而是一个存储过程的名字。
using SqlConnection conn = new SqlConnection(connectionString); conn.Open(); using SqlCommand cmd = new SqlCommand("sp_GetUserById", conn); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.AddWithValue("@UserId", 1); using SqlDataReader reader = cmd.ExecuteReader(); while (reader.Read()) { Console.WriteLine(reader["Name"]); }存储过程名后面不用加括号,更不用写参数定义——参数列表完全由Parameters集合给出。这是新手最常蒙圈的地方。
7.2 带输出参数和返回值的场景
有些存储过程除了返回结果集,还会带输出参数。调用时要先注册输出参数,再执行,执行完再去读它的值:
using SqlConnection conn = new SqlConnection(connectionString); conn.Open(); using SqlCommand cmd = new SqlCommand("sp_CheckUserExists", conn); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.AddWithValue("@UserName", "张三"); SqlParameter outputParam = new SqlParameter("@Exists", SqlDbType.Bit); outputParam.Direction = ParameterDirection.Output; cmd.Parameters.Add(outputParam); cmd.ExecuteNonQuery(); bool exists = (bool)outputParam.Value;这里一定要把Direction设成Output,不然存储过程那边的输出参数你根本收不到。这类问题排查时最容易被忽视——程序不报错,但值就是不对。
8. 常遇报错的排查思路,比记住代码更有用
命令写多了,谁都会遇到报错。真正拉开差距的不是“会不会报错”,而是“报错之后能不能快速定位”。这里分享几个最常遇到的场景和我的排查顺序。
8.1 登录失败:Login failed for user xxx
这个报错一出现,先别急着改密码。按这个顺序排查:
- 连接字符串中的认证方式对不对?本机开发用
Trusted_Connection=True,就不要同时传User Id和Password。 - SQL Server是否开启了对应的登录模式?如果是用账号密码登录,服务器需要开启“混合认证模式”。
- 账号有没有访问目标数据库的权限?一个常见问题是账号能连上服务器,但连不上数据库。
我遇到过很多次,问题根本不是密码错,而是SQL Server实例没开启混合登录,导致账号登录被直接拒绝。
8.2 超时错误:Timeout expired
Timeout expired是连接或命令执行超时的统称。先确认SQL本身是不是跑得太慢。很多新手遇到超时第一反应是加长超时时间,我的建议相反:先查看SQL执行计划,看是不是索引没建、是不是扫全表。治标更要治本。
命令超时和连接超时是两回事:
cmd.CommandTimeout = 30; // 命令执行超时,单位秒 conn.ConnectionString = "Server=localhost;Database=MyShop;Connect Timeout=15;Trusted_Connection=True;";Connect Timeout控制“连不上多久算失败”,CommandTimeout控制“连上之后SQL跑多久算失败”。搞混这两个,排查方向就错了。
8.3 找不到服务器实例:Instance-specific error
看到“A network-related or instance-specific error occurred”这类报错,九成是连接字符串里的Server写错了,或者SQL Server服务没启动。排查顺序很直接:
- 确认SQL Server服务真的在跑。按Win+R输入
services.msc,找到SQL Server相关的服务,看状态是否为“正在运行”。 - 确认实例名称对不对。本机默认实例写
localhost,命名实例写localhost\\实例名。 - 确认防火墙有没有挡掉1433端口。开发本机连接一般没事,但跨机器连接时防火墙是最大嫌疑。
8.4 附带要说的小细节
写SQL在C#字符串里,多行SQL建议用@原样字符串符,避免转义地狱:
string sql = @" SELECT Id, Name FROM Users WHERE Age > @Age ORDER BY Id";另外,我强烈建议你给所有跟数据库相关的操作,至少包一层try...catch,吐出明确的错误信息。不一定要写得多复杂,但连数据库的程序,异常处理绝对不能省。
9. 把常用命令整理成一张速查表
到最后,我把最常用到的命令和代码套路整理成一张速查表,方便你工作中直接翻。这张表我做了简化,只保留真正的高频内容。
| 场景 | 关键代码/命令 |
|---|---|
| 建立连接 | SqlConnection conn = new SqlConnection(connectionString); |
| 执行增删改 | cmd.ExecuteNonQuery(); |
| 查询单值 | cmd.ExecuteScalar(); |
| 查询多行(流式) | cmd.ExecuteReader(); while(reader.Read()){...} |
| 查询多行(整体) | SqlDataAdapter adapter = new SqlDataAdapter(sql, conn); adapter.Fill(table); |
| 参数化传值 | cmd.Parameters.AddWithValue("@Name", value); |
| 调用存储过程 | cmd.CommandType = CommandType.StoredProcedure; |
| 防注入 | 永远别拼接用户输入进SQL |
| 释放资源 | using关键字自动释放连接和命令 |
这张表不能代表所有场景,但覆盖了日常业务开发的八成需求。
以我带新人的经验,很多人能做到“程序跑通”,但说不出“为什么”。其实SQL Server和C#之间的交互远没有想象中复杂。你先在连接字符串上较真一次,再把三个执行方法用明白,最后学会参数化查询,基本上日常的数据读写需求就都可以应对了。剩下的,就是多写多踩坑,遇到报错不要慌,按着连接、命令、结果集、异常这条线去排查就行。