同事的一道面试题:
如何将某表中的某字段以逗号分隔拼接起来
在给出答案前,先给出测试用数据,与之前的几篇一样:
--DROP TABLE T
CREATE TABLE T(GRP_A VARCHAR(20),GRP_B VARCHAR(20),GRP_C VARCHAR(20),VAL INT)
INSERT INTO T(GRP_A,GRP_B,GRP_C,VAL)
SELECT 'a1','b1','c1',10 union all
SELECT 'a1','b1','c2',10 union all
SELECT 'a1','b2','c2',40 union all
SELECT 'a1','b2','c3',40 union all
SELECT 'a1','b2','c3',50 union all
SELECT 'a2','b3','c3',12 union all
SELECT 'a2','b3','c3',22 union all
SELECT 'a2','b3','c3',32
SELECT * FROM T
GRP_A GRP_B GRP_C VAL
-------------------- -------------------- -------------------- -----------
a1 b1 c1 10
a1 b1 c2 10
a1 b2 c2 40
a1 b2 c3 40
a1 b2 c3 50
a2 b3 c3 12
a2 b3 c3 22
a2 b3 c3 32
(8 行受影响)
答案1:使用COALESCE函数
DECLARE @T NVARCHAR(200)
--SET @T = ''
SELECT @T = COALESCE(@T,'') + GRP_A + ',' FROM T
SELECT @T
输出结果
------------------------
a1,a1,a1,a1,a1,a2,a2,a2,
答案2:使用ISNULL函数
DECLARE @T NVARCHAR(200)
--SET @T = ''
SELECT @T = ISNULL(@T,'') + GRP_A + ',' FROM T
SELECT @T
输出结果与上边的一致哈不贴了
实际上,您应该已经发现了,这两个函数其实是个障眼术,即只要我的@T变量有初始化,完全可以直接拼接,算是答案3吧,代码如下:
DECLARE @T NVARCHAR(200)
SET @T = ''
SELECT @T = @T + GRP_A + ',' FROM T
SELECT @T
输出结果与上上例一致不贴了
因为我们知道,在SQL中NULL表示UNKNOW类型,与任务字符串累加都会得到NULL值,如
DECLARE @T NVARCHAR(200)
SET @T = NULL
SET @T = @T + 'hello world'
--PRINT @T这里不用PRINT是因为看不到实际输出什么
SELECT @T
实际输出
----
NULL
那么,再回来看上述所谓的答案1,2,3都不够严谨!即,如果该表中有那么一行,它的字段为NULL,会怎么办?!
答案3最终会返回NULL,答案1和答案2则跳过NULL值所在行以前所有的数据,返回NULL行以下的累加!所以嘞?要对GRP_A列进行是否是NULL值的验证!
实事上,COALESCE函数与ISNULL函数原本就是这个功能:返回表达式中第一个不为NULL的值,所谓障眼术即指此
下边的SQL脚本演示了两个函数的基本功能:
SET NOCOUNT ON
DECLARE @T CHAR(6)
SELECT 'COALESCE',COALESCE(@T,NULL,NULL,'1234567890')
SELECT 'ISNULL',ISNULL(@T,'1234567890')
输出结果
-------- ----------
COALESCE 1234567890
------ ------
ISNULL 123456
先声明了一个类型为CHAR(6)的变量@T,没有设置值,默认为NULL
然后分别调用了ISNULL函数和COALESCE函数,ISNULL返回了符合变量定义类型的值,即截断后为CHAR(6)类型,而COALESCE则返回完完整整的字符串
简单对比下两个函数
两个函数都返回第一个不为空的表达式,
其一,ISNULL考虑变量类型,而COALESCE则不考虑
其二,ISNULL只接收两个参数,而COALESCE则可以接收多个参数
再回来看那个面试题,
如何将某表中的某字段以逗号分隔拼接起来?
答案4:
DECLARE @T NVARCHAR(200)
SET @T = ''
SELECT @T = @T + ISNULL(GRP_A,'NULL') + ',' FROM T
SELECT @T
面试题这一部分结束,来看看ISNULL函数的应用实例
1,利用ISNULL函数干掉OR运算!
题目是查询表中VAL小于20的值,包括NULL值:
SELECT * FROM T WHERE ISNULL(VAL,-1) < 20
SELECT * FROM T WHERE VAL IS NULL OR VAL < 20
两个SQL具有相同的输出结果
GRP_A GRP_B GRP_C VAL
-------------------- -------------------- -------------------- -----------
a1 b1 c1 10
a1 b1 c2 10
a2 b3 c3 NULL
a2 b3 c3 NULL
a2 b3 c3 NULL
(5 行受影响)
GRP_A GRP_B GRP_C VAL
-------------------- -------------------- -------------------- -----------
a1 b1 c1 10
a1 b1 c2 10
a2 b3 c3 NULL
a2 b3 c3 NULL
a2 b3 c3 NULL
(5 行受影响)
2,ISNULL非主流更新表存储过程示例
如某更新表存储过程如下:
CREATE PROC UpdateT(
@ID INT,
@GRP_A VARCHAR(10) = NULL,
@GRP_B VARCHAR(10) = NULL,
@GRP_C VARCHAR(10) = NULL,
@VAL INT = 0
)AS
BEGIN
UPDATE T SET
GRP_A = @GRP_A,
GRP_B = @GRP_B,
GRP_C = @GRP_C,
VAL = @VAL
WHERE ID = @ID
END
当我们使用这个存储过程的时候,必须先得该行的所有记录,再把所有记录更新回去,可是这并不总是必须的
有时候手头只有两个数据:ID和VAL,我只想更新这个VAL
又有时候手头有另外两个数据:ID和GRP_A,这时候只更新GRP_A列即可
还有很多情况,如仅更新GRP_A,
仅更新GRP_A,GRP_B
仅更新GRP_A,GRP_B,GRP_C
仅更新GRP_A,GRP_B,GRP_C,VAL
...
这样的组合太多了,要想一劳永逸解决问题那就得更新任何字段前,先得到整行记录,再整行更新回去,于是多了一项工作:先查询,再更新
不爽不爽,那没有办法不先查询直接更新某一列呢?而且列可以任意组合?
在给出答案前,先声明一句:这个方法算不上完美解决方案,仅仅是个思路罢了,虽然我一直认为没什么影响,但如果要在正式项目中使用,建议还是多听听DBA的意见!
非主流更新任意列存储过程:
CREATE PROC UpdateT(
@ID INT,
@GRP_A VARCHAR(10) = NULL,
@GRP_B VARCHAR(10) = NULL,
@GRP_C VARCHAR(10) = NULL,
@VAL INT = 0
)AS
BEGIN
UPDATE T SET
GRP_A = ISNULL(@GRP_A,GRP_A),
GRP_B = ISNULL(@GRP_B,GRP_B),
GRP_C = ISNULL(@GRP_C,GRP_C),
VAL = ISNULL(@VAL,VAL)
WHERE ID = @ID
END
解读1上边的这个存储过程,假设参数@GRP_A为NULL时,经过ISNULL运算返回了GRP_A列!即实际变成了
SET GRP_A = ISNULL(NULL,GRP_A)
再演变为SET GRP_A = GRP_A!神马意思?什么也没更新…把自己更新为自己,等什么也没干,空忙活一场!但是
我们的效果达到了!@GRP_A参数为NULL时(不传递该参数,在定义存储过程时已经设计为可选参数),自己更新自己
当该参数不为NULL时,进行了实际的更新,其余三列以此类推,除@ID参数必须要传外,其它参数都是可选的!谁有值就更新谁,
什么模式?门面模式(又称外观模式),把小碎操作变成一个大的操作
解读2为什么第二部分都使用了ISNULL而不是COALESCE函数?
原因正是ISNULL会考虑第一个参数的类型声明从而自动截断超长部分数据!
如果用COALESCE的话可能会导致返回结果超出列定义!
产生将截断二进制字符串错误
供讨论