这里创建一个简化的进销系统,系统中只有销售单和采购单,不存在红冲单据以及库存、退货等单据。由于销售单和采购单存在主从结构,所以将这两张表中的主从数据分别保存在不同的表中。下面是这个系统中表之间的关系图:
FId 字段为主键,
FNumber 字段为人员工号,
FName 字段为人员姓名,
FManagerId 字段为上级主管主键(指向T_Person 表的FId 字段的外键)。
FId 字段为主键,
FNumber 字段为产品编号,
FName 字段为商品名,
FPrice 为商品价格;
FNumber 字段为销售单编号,
FBillMakerId 字段为开单人主键(指向T_Person 表的FId 字段的外键),
FMakeDate 字段为制单日期,
FConfirmDate 字段为确认日期;
FId 字段为主键,
FBillId 字段为主表主键(指向T_SaleBill 表的FId 字段的外键),
FMerchandiseId 字段为商品主键(指向T_Merchandise 表的FId 字段的外键),
FCount 字段为销售数量。
FNumber 字段为采购单编号,
FBillMakerId 字段为开单人主键(指向 T_Person 表的FId 字段的外键)
FMakeDate 字段为制单日期,
FConfirmDate 字段为确认日期;
FId 字段为主键,
FBillId 字段为主表主键(指向 T_PurchaseBill 表的FId 字段的外键),
FMerchandiseId 字段为商品主键(指向T_Merchandise 表的FId 字段的外键),
FCount 字段为采购数量。
根据表的设计,我们编写了下面的建表SQL 语句,请在相应的DBMS 中执行对应的
DROP TABLE IF EXISTS `T_Person`;
CREATE TABLE `T_Person` (
`FId` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键',
`FNumber` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '人员工号',
`FName` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '人员姓名',
`FManagerId` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '上级主管主键(指向T_Person 表的FId 字段的外键)',
PRIMARY KEY (`FId`) USING BTREE,
INDEX `FManagerId`(`FManagerId`) USING BTREE,
CONSTRAINT `T_Person_ibfk_1` FOREIGN KEY (`FManagerId`) REFERENCES `T_Person` (`FId`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '人员表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
DROP TABLE IF EXISTS `T_Merchandise`;
CREATE TABLE `T_Merchandise` (
`FId` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键',
`FNumber` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '产品编号',
`FName` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品名',
`FPrice` int(11) NULL DEFAULT NULL COMMENT '价格',
PRIMARY KEY (`FId`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '为商品表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
DROP TABLE IF EXISTS `T_SaleBill`;
CREATE TABLE `T_SaleBill` (
`FId` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`FNumber` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '销售单编号',
`FBillMakerId` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '开单人主键(指向T_Person 表的FId 字段的外键',
`FMakeDate` datetime(0) NULL DEFAULT NULL COMMENT '制单日期',
`FConfirmDate` datetime(0) NULL DEFAULT NULL COMMENT '确认日期',
PRIMARY KEY (`FId`) USING BTREE,
INDEX `FBillMakerId`(`FBillMakerId`) USING BTREE,
CONSTRAINT `T_SaleBill_ibfk_1` FOREIGN KEY (`FBillMakerId`) REFERENCES `T_Person` (`FId`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '销售单' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
DROP TABLE IF EXISTS `T_SaleBillDetail`;
CREATE TABLE `T_SaleBillDetail` (
`FId` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`FBillId` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'FBillId 字段为主表主键(指向T_SaleBill 表的FId 字段的外键',
`FMerchandiseId` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品主键(指向T_Merchandise 表的FId 字段的外键',
`FCount` int(11) NULL DEFAULT NULL COMMENT '销售数量',
PRIMARY KEY (`FId`) USING BTREE,
INDEX `FBillId`(`FBillId`) USING BTREE,
INDEX `FMerchandiseId`(`FMerchandiseId`) USING BTREE,
CONSTRAINT `T_SaleBillDetail_ibfk_1` FOREIGN KEY (`FBillId`) REFERENCES `T_SaleBill` (`FId`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `T_SaleBillDetail_ibfk_2` FOREIGN KEY (`FMerchandiseId`) REFERENCES `T_Merchandise` (`FId`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '销售单明细记录' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
DROP TABLE IF EXISTS `T_PurchaseBill`;
CREATE TABLE `T_PurchaseBill` (
`FId` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`FNumber` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '采购单编号',
`FBillMakerId` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'FBillMakerId 字段为开 \r\n单人主键(指向 T_Person 表的FId 字段的外键',
`FMakeDate` datetime(0) NULL DEFAULT NULL COMMENT '制单日期',
`FConfirmDate` datetime(0) NULL DEFAULT NULL COMMENT '确认日期',
PRIMARY KEY (`FId`) USING BTREE,
INDEX `FBillMakerId`(`FBillMakerId`) USING BTREE,
CONSTRAINT `T_PurchaseBill_ibfk_1` FOREIGN KEY (`FBillMakerId`) REFERENCES `T_Person` (`FId`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '采购单' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
DROP TABLE IF EXISTS `T_PurchaseBillDetail`;
CREATE TABLE `T_PurchaseBillDetail` (
`FId` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`FBillId` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '主表主键(指向 T_PurchaseBill 表的FId 字段的外键)',
`FMerchandiseId` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品主键(指 \r\n向 T_Merchandise 表的FId 字段的外键',
`FCount` int(11) NULL DEFAULT NULL COMMENT '采购数量',
PRIMARY KEY (`FId`) USING BTREE,
INDEX `FBillId`(`FBillId`) USING BTREE,
INDEX `FMerchandiseId`(`FMerchandiseId`) USING BTREE,
CONSTRAINT `T_PurchaseBillDetail_ibfk_1` FOREIGN KEY (`FBillId`) REFERENCES `T_PurchaseBill` (`FId`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `T_PurchaseBillDetail_ibfk_2` FOREIGN KEY (`FMerchandiseId`) REFERENCES `T_Merchandise` (`FId`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '采购单明细记录' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
除了创建数据表,还需要预置一些演示数据。首先向T_Person、T_Merchandise 两张表中插入演示数据:
insert into T_Person(FId,FNumber,FName,FManagerId)
values('00001','1','Robert',NULL);
insert into T_Person(FId,FNumber,FName,FManagerId)
values('00002','2','John','00001');
insert into T_Person(FId,FNumber,FName,FManagerId)
values('00003','3','Tom','00001');
insert into T_Person(FId,FNumber,FName,FManagerId)
values('00004','4','Jim','00003');
insert into T_Person(FId,FNumber,FName,FManagerId)
values('00005','5','Lily','00002');
insert into T_Person(FId,FNumber,FName,FManagerId)
values('00006','6','Merry','00003');
insert into T_Merchandise(FId,FNumber,FName,FPrice)
values('00001','1','Bacon',30);
insert into T_Merchandise(FId,FNumber,FName,FPrice)
values('00002','2','Cake',2);
insert into T_Merchandise(FId,FNumber,FName,FPrice)
values('00003','3','Apple',6);
向T_SaleBill 和T_PurchaseBill 表中插入演示数据:
insert into T_SaleBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00001','1','00006','2007-03-15','2007-05-15');
insert into T_SaleBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00002','2',null,'2006-01-25','2006-02-03');
insert into T_SaleBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00003','3','00001','2006-02-12','2007-01-11');
insert into T_SaleBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00004','4','00003','2008-05-25','2008-06-15');
insert into T_SaleBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00005','5','00005','2008-03-17','2007-04-15');
insert into T_SaleBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00006','6','00002','2002-02-03','2007-11-11');
insert into T_PurchaseBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00001','1','00006','2007-02-15','2007-02-15');
insert into T_PurchaseBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00002','2','00004','2003-02-25','2006-03-03');
insert into T_PurchaseBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00003','3','00001','2007-02-12','2007-07-12');
insert into T_PurchaseBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00004','4','00002','2007-05-25','2007-06-15');
insert into T_PurchaseBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00005','5','00002','2007-03-17','2007-04-15');
insert into T_PurchaseBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00006','6',null,'2006-02-03','2006-11-20');
由于Oracle 中日期常量的表示方法与其他DBMS 不同,所以在Oracle 总必须执行下面的SQL 语句
T_SaleBill 中插入演示数据:
insert into T_SaleBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00001','1','00006',TO_DATE('2007-03-15','YYYY-MM-DD HH24:MI:SS'),TO_DATE('2007-05-15', 'YYYY-MM-DD HH24:MI:SS'));
insert into T_SaleBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00002','2',null,TO_DATE('2006-01-25','YYYY-MM-DD HH24:MI:SS'),TO_DATE('2006-02-03', 'YYYY-MM-DD HH24:MI:SS'));
insert into T_SaleBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00003','3','00001',TO_DATE('2006-02-12','YYYY-MM-DD HH24:MI:SS'),TO_DATE('2007-01-11', 'YYYY-MM-DD HH24:MI:SS'));
insert into T_SaleBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00004','4','00003',TO_DATE('2008-05-25','YYYY-MM-DD HH24:MI:SS'),TO_DATE('2008-06-15', 'YYYY-MM-DD HH24:MI:SS'));
insert into T_SaleBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00005','5','00005',TO_DATE('2008-03-17','YYYY-MM-DD HH24:MI:SS'),TO_DATE('2007-04-15', 'YYYY-MM-DD HH24:MI:SS'));
insert into T_SaleBill(FId,FNumber,FBillMakerId,FMakeDate,FConfirmDate)
values('00006','6','00002',TO_DATE('2002-02-03','YYYY-MM-DD HH24:MI:SS'),TO_DATE('2007-11-11', 'YYYY-MM-DD HH24:MI:SS'));
INSERT INTO T_PurchaseBill (FId, FNumber, FBillMakerId, FMakeDate, FConfirmDate)
VALUES ('00001', '1', '00006', TO_DATE('2007-02-15', 'YYYY-MM-DD HH24:MI:SS'),
TO_DATE('2007-02-15', 'YYYY-MM-DD HH24:MI:SS'));
INSERT INTO T_PurchaseBill (FId, FNumber, FBillMakerId, FMakeDate, FConfirmDate)
VALUES ('00002', '2', '00004', TO_DATE('2003-02-25', 'YYYY-MM-DD HH24:MI:SS'),
TO_DATE('2006-03-03', 'YYYY-MM-DD HH24:MI:SS')) ;
INSERT INTO T_PurchaseBill (FId, FNumber, FBillMakerId, FMakeDate, FConfirmDate)
VALUES ('00003', '3', '00001', TO_DATE('2007-02-12', 'YYYY-MM-DD HH24:MI:SS'),
TO_DATE('2007-07-12', 'YYYY-MM-DD HH24:MI:SS')) ;
INSERT INTO T_PurchaseBill (FId, FNumber, FBillMakerId, FMakeDate, FConfirmDate)
VALUES ('00004', '4', '00002', TO_DATE('2007-05-25', 'YYYY-MM-DD HH24:MI:SS'),
TO_DATE('2007-06-15', 'YYYY-MM-DD HH24:MI:SS')) ;
INSERT INTO T_PurchaseBill (FId, FNumber, FBillMakerId, FMakeDate, FConfirmDate)
VALUES ('00005', '5', '00002', TO_DATE('2007-03-17', 'YYYY-MM-DD HH24:MI:SS'),
TO_DATE('2007-04-15', 'YYYY-MM-DD HH24:MI:SS')) ;
INSERT INTO T_PurchaseBill (FId, FNumber, FBillMakerId, FMakeDate, FConfirmDate)
VALUES ('00006', '6', null, TO_DATE('2006-02-03', 'YYYY-MM-DD HH24:MI:SS'),
TO_DATE('2006-11-20', 'YYYY-MM-DD HH24:MI:SS')) ;
--向T_SaleBillDetail 表和T_PurchaseBillDetail 表中插入演示数据:
insert into T_SaleBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00001','00001','00003',20);
insert into T_SaleBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00002','00001','00001',30);
insert into T_SaleBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00003','00001','00002',22);
insert into T_SaleBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00004','00002','00003',12);
insert into T_SaleBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00005','00002','00002',11);
insert into T_SaleBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00006','00003','00001',60);
insert into T_SaleBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00007','00003','00002',2);
insert into T_SaleBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00008','00003','00003',5);
insert into T_SaleBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00009','00004','00001',16);
insert into T_SaleBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00010','00004','00002',8);
insert into T_SaleBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00011','00004','00003',9);
insert into T_SaleBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00012','00005','00001',6);
insert into T_SaleBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00013','00005','00003',26);
insert into T_SaleBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00014','00006','00001',66);
insert into T_SaleBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00015','00006','00002',518);
insert into T_PurchaseBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00001','00001','00002',12);
insert into T_PurchaseBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00002','00001','00001',20);
insert into T_PurchaseBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00003','00002','00001',32);
insert into T_PurchaseBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00004','00002','00003',18);
insert into T_PurchaseBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00005','00002','00002',88);
insert into T_PurchaseBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00006','00003','00003',19);
insert into T_PurchaseBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00007','00003','00002',6);
insert into T_PurchaseBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00008','00003','00001',2);
insert into T_PurchaseBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00009','00004','00001',20);
insert into T_PurchaseBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00010','00004','00003',18);
insert into T_PurchaseBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00011','00005','00002',19);
insert into T_PurchaseBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00012','00005','00001',26);
insert into T_PurchaseBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00013','00006','00003',3);
insert into T_PurchaseBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00014','00006','00001',22);
insert into T_PurchaseBillDetail(FId,FBillId,FMerchandiseId,FCount)
values('00015','00006','00002',168);
数据库中的数据是以关系表的形式保存的,非技术人员很难看懂这些表中数据的意思,必须将其转换为业务人员可以看得懂的形式,这就是报表。复杂的分析报表可能需要数据挖掘等技术才能实现,不过普通的报表一般使用SELECT 语句就可以完成。
要求显示每张销售单的编号、制单人、制单日期等信息,可以使用简单的SELECT 语句来完成这个任务:
SELECT FNumber, FBillMakerId, FMakeDate
FROM T_SaleBill
执行完毕我们就能在输出结果中看到下面的执行结果:
这里的FBillMakerId
显示的是制单人在T_Person
表中的主键,业务人员很难将这个编号与人名对应起来,因此必须将其转换为制单人的姓名。FBillMakerId
字段保存的是T_Person
表的主键,而T_Person
表的FName
字段则为人员的名称,
因此将这两个表做连接查询即可,SQL 语句如下:
SELECT salebill.FNumber,person.FName,salebill.FMakeDate
FROM T_SaleBill salebill
INNER JOIN T_Person person
ON salebill.FBillMakerId=person.FId;
执行完毕我们就能在输出结果中看到下面的执行结果:
这个查询结果已经能够显示开票人的姓名,不过仔细观察会发现编号为2 的记录并没有 显示在执行结果中,这是因为这条记录的FBillMakerId
字段为空值,所以不能与T_Person
表中的任何记录进行匹配,而内连接不会显示没有匹配的行。一般情况下即使没有开单人也要将这张单据显示出来,这时就要使用外部连接了,如下:
SELECT salebill.FNumber,person.FName,salebill.FMakeDate
FROM T_SaleBill salebill
LEFT OUTER JOIN T_Person person
ON salebill.FBillMakerId=person.FId;
因为T_SaleBill
表中的记录必须全部显示到结果集中,而T_SaleBill
为左表,所以使用 左外部连接。执行完毕我们就能在输出结果中看到下面的执行结果:
这样没有开票人的单据也显示出来了,不过这里其对应的开票人处显示的是NULL,这让业务人员感到难以理解,我们使用 COALESCE()函数
来解决这个问题。
COALESCE()函数
支持多个参数,该函数返回参数中的第一个非空值,这样COALESCE(f1,f2)
就可以实现“如果f1为空则将f2做为返回值” 这样的空值处理逻辑了。将SQL语句做如下改造:
SELECT salebill.FNumber,
COALESCE(person.FName,'没有开单人'),
salebill.FMakeDate
FROM T_SaleBill salebill
LEFT OUTER JOIN T_Person person
ON salebill.FBillMakerId=person.FId;
要求列出所有销售单的详细信息,每行显示销售单的每一条销售记录,同时每行头部要显示此行所属的销售单的信息,比如单号、开单人、开单日期等。T_SaleBillDetail
表保存的是销售单的每一条销售记录,T_SaleBill
表保存的是销售单的头信息,T_SaleBillDetail
表的FMerchandiseId
字段保存的是销售的商品主键,而T_SaleBill
表的FBillMakerId
字段保存的是开单人的主键,只要对这四张表做连接查询即可。由于T_SaleBill
表的FBillMakerId
字段有可能为空,所以在T_SaleBill
表和T_Person
表进行连接的时候要使用左外连接,而为了提高查询效率其他连接都使用内连接。SQL 语句如下:
SELECT
saleBill.FNumber,person.FName,saleBill.FMakeDate,merchandise.FName,salebilldetail.FCount
FROM T_SaleBill saleBill
INNER JOIN T_SaleBillDetail salebilldetail
ON salebilldetail.FBillId=saleBill.FId
INNER JOIN T_Merchandise merchandise
ON salebilldetail.FMerchandiseId=merchandise.FId
LEFT OUTER JOIN T_Person person
ON saleBill.FBillMakerId=person.FId
执行完毕我们就能在输出结果中看到下面的执行结果:
通过上述结果可以看出同一订单下的商品信息,但是同一订单的商品显示并没有都在一起,可以根据saleBill.FNumber
排序优化
SELECT
saleBill.FNumber,person.FName,saleBill.FMakeDate,merchandise.FName,salebilldetail.FCount
FROM T_SaleBill saleBill
INNER JOIN T_SaleBillDetail salebilldetail
ON salebilldetail.FBillId=saleBill.FId
INNER JOIN T_Merchandise merchandise
ON salebilldetail.FMerchandiseId=merchandise.FId
LEFT OUTER JOIN T_Person person
ON saleBill.FBillMakerId=person.FId
ORDER BY saleBill.FNumber
要求计算每种商品的总收益,受收益的定义为所有的销售单中该商品的销售总额减去所有的采购单中该商品的购买总额。T_SaleBillDetail
表中保存的所有的销售单详细记录,因此下面的SQL 语句可以检索
所有产品的销售记录,包括产品名和销售额:
SELECT merchandise.FName AS MerchandiseName,
merchandise.FPrice*salebilldetail.FCount AS Amount
FROM T_SaleBillDetail salebilldetail
INNER JOIN T_Merchandise merchandise
ON salebilldetail.FMerchandiseId=merchandise.FId
这里将T_SaleBillDetail
表和T_Merchandise
进行连接查询就可以取得每种商品的名称和单价,而单价与销售量的乘积即为销售额。执行完毕我们就能在输出结果中看到下面的执行结果:
同理下面的SQL语句则可以检索所有产品的购买记录,包括产品名和采购额:
SELECT merchandise.FName AS MerchandiseName,
merchandise.FPrice*purchasebilldetail.FCount AS Amount
FROM T_PurchaseBillDetail purchasebilldetail
INNER JOIN T_Merchandise merchandise
ON purchasebilldetail.FMerchandiseId=merchandise.FId
执行完毕我们就能在输出结果中看到下面的执行结果:
将上述两个SQL 语句进行UNION
运算就可以将两个检索的结果集合并了,不过这样就无法区分销售单和采购单了。为了区分销售单和采购单,同时方便后续运算,我们将检索产品的购买记录的金额全部取负值,这样就可以表示采购行为的金额为负值:
SELECT merchandise.FName AS MerchandiseName,
merchandise.FPrice*purchasebilldetail.FCount*(-1) AS Amount
FROM T_PurchaseBillDetail purchasebilldetail
INNER JOIN T_Merchandise merchandise
ON purchasebilldetail.FMerchandiseId=merchandise.FId
这样就可以将销售记录和采购记录同时显示了:
SELECT merchandise.FName AS MerchandiseName,
merchandise.FPrice*salebilldetail.FCount AS Amount
FROM T_SaleBillDetail salebilldetail
INNER JOIN T_Merchandise merchandise
ON salebilldetail.FMerchandiseId=merchandise.FId
UNION ALL
SELECT merchandise.FName AS MerchandiseName,
merchandise.FPrice*purchasebilldetail.FCount*(-1) AS Amount
FROM T_PurchaseBillDetail purchasebilldetail
INNER JOIN T_Merchandise merchandise
ON purchasebilldetail.FMerchandiseId=merchandise.FId
因为有可能存在重复的记录(即同一种商品有同样的交易额),所以要使用UNION ALL
,以防止重复的记录被合并。执行完毕我们就能在输出结果中看到下面的执行结果:
这个结果集中列出了每一条详细交易记录,包括商品名和交易金额,销售行为的交易额为正值,而购买行为的交易额为负值。有个这个执行结果,只要将这个SQL 语句做为子查询,然后按照商品名进行分组,然后计算交易金额的总和。SQL 语句如下:
SELECT details.MerchandiseName,SUM(details.Amount)
FROM
(
SELECT merchandise.FName AS MerchandiseName,
merchandise.FPrice*salebilldetail.FCount AS Amount
FROM T_SaleBillDetail salebilldetail
INNER JOIN T_Merchandise merchandise
ON salebilldetail.FMerchandiseId=merchandise.FId
UNION ALL
SELECT merchandise.FName AS MerchandiseName,
merchandise.FPrice*purchasebilldetail.FCount*(-1) AS Amount
FROM T_PurchaseBillDetail purchasebilldetail
INNER JOIN T_Merchandise merchandise
ON purchasebilldetail.FMerchandiseId=merchandise.FId
) details
GROUP BY details.MerchandiseName
要求统计每种产品的销售额并且在报表最后列出销售额总计。检索每种产品的销售额可以到T_SaleBillDetail
中按照产品进行分组,然后使用聚合函数SUM()
来计算每种产品的销售额。因为T_SaleBillDetail
只保存了销售量,价格和产品名称保存在T_Merchandise
表中,因此还需要与T_Merchandise
表进行连接运算。SQL 语句如
下:
SELECT merchandise.FName AS MerchandiseName,
SUM(merchandise.FPrice*salebilldetail.FCount) AS Amount
FROM T_SaleBillDetail salebilldetail
INNER JOIN T_Merchandise merchandise
ON salebilldetail.FMerchandiseId=merchandise.FId
GROUP BY merchandise.FName
统计销售额总计同样需要与T_Merchandise
表进行连接运算以取得价格,然后使用聚合函数SUM()
来计算总销售额。SQL 语句如下:
SELECT SUM(merchandise.FPrice*salebilldetail.FCount) AS Amount
FROM T_SaleBillDetail salebilldetail
INNER JOIN T_Merchandise merchandise
ON salebilldetail.FMerchandiseId=merchandise.FId
由于这两个查询结构是异构的,如果要将第二个查询的结果集添加到第一个查询的结果集后,那么就需要首先为第二个查询增加一列以保证和第一个查询的列数相同,然后将这两个查询语句使用UNION ALL 联合起来。SQL 语句如下:
SELECT merchandise.FName AS MerchandiseName,
SUM(merchandise.FPrice*salebilldetail.FCount) AS Amount
FROM T_SaleBillDetail salebilldetail
INNER JOIN T_Merchandise merchandise
ON salebilldetail.FMerchandiseId=merchandise.FId
GROUP BY merchandise.FName
UNION ALL
SELECT '总计',
SUM(merchandise.FPrice*salebilldetail.FCount) AS Amount
FROM T_SaleBillDetail salebilldetail
INNER JOIN T_Merchandise merchandise
ON salebilldetail.FMerchandiseId=merchandise.FId
要求统计所有的销售明细,并且计算每一笔销售记录中销售量占同产品总销售量的百分比。这里要求列出每一笔销售记录中销售量,而且还要计算销售量占同产品总销售量的百分比,很显然这要使用窗口函数来完成。SQL 语句如下:
SELECT merchandise.FName,FCount,
FCount*1.0/SUM(salebilldetail.FCount) OVER(PARTITION BY salebilldetail.FMerchandiseId)
FROM T_SaleBillDetail salebilldetail
INNER JOIN T_Merchandise merchandise
ON salebilldetail.FMerchandiseId=merchandise.FId
为了取得每一笔销售记录中的商品名称,需要连接T_Merchandise
表;为了统计同产品的总销量,需要使用按FMerchandiseId
进行分区的SUM()
窗口函数来统计同产品总销售量;由于FCount
是整数类型,在进行除法运算的时候为了避免精度问题,需要将FCount
乘以 1.0
这样就可以进行高精度运算了。由于这里使用来的窗口函数,所以这个SQL 语句不能在MYSQL
中运行。
执行完毕我们就能在输出结果中看到下面的执行结果:
统计每张采购单的单号、总采购额,并且对于总采购额小于等于500 元的显示为“小额”、 总采购额大于等于1000 元的显示为“大额”、介于 500 元与1000 元之间的显示为“普通”。对于这种复杂的问题可以分解成几个小步骤进行处理。首先统计所有采购单的单号以及 它们的每条交易明细的交易额,这只要将 T_PurchaseBill
、T_PurchaseBillDetail
和 T_Merchandise
这三张表进行连接查询就可以得到。
SQL 语句如下:
SELECT purchasebill.FNumber,purchasebilldetail.FCount*merchandise.FPrice
FROM T_PurchaseBillDetail purchasebilldetail
INNER JOIN T_PurchaseBill purchasebill
ON purchasebilldetail.FBillId = purchasebill.FId
INNER JOIN T_Merchandise merchandise
ON purchasebilldetail.FMerchandiseId = merchandise.FId;
执行完毕我们就能在输出结果中看到下面的执行结果:
接着需要统计每张采购单的总交易额,因为我们不关系每张采购单内部的交易明细。将上面的结果集按照采购单号进行分组,然后使用聚合函数SUM()就可以轻松的统计每张采购单的总交易额了。SQL 语句如下:
SELECT purchasebill.FNumber,
SUM(purchasebilldetail.FCount*merchandise.FPrice)
FROM T_PurchaseBillDetail purchasebilldetail
INNER JOIN T_PurchaseBill purchasebill
ON purchasebilldetail.FBillId = purchasebill.FId
INNER JOIN T_Merchandise merchandise
ON purchasebilldetail.FMerchandiseId = merchandise.FId
GROUP BY purchasebill.FNumber;
执行完毕我们就能在输出结果中看到下面的执行结果:
这样便统计出了采购单的总交易额,接着需要按照每张采购单的总交易额对采购单进行 分级,显然使用CASE……WHEN
语句很容易的实现这个功能:
SELECT purchasebill.FNumber,
SUM(purchasebilldetail.FCount*merchandise.FPrice),
CASE
WHEN SUM(purchasebilldetail.FCount*merchandise.FPrice)<=500 THEN '小额'
WHEN SUM(purchasebilldetail.FCount*merchandise.FPrice)>=1000 THEN '大额'
ELSE '普通'
END
FROM T_PurchaseBillDetail purchasebilldetail
INNER JOIN T_PurchaseBill purchasebill
ON purchasebilldetail.FBillId = purchasebill.FId
INNER JOIN T_Merchandise merchandise
ON purchasebilldetail.FMerchandiseId = merchandise.FId
GROUP BY purchasebill.FNumber;
执行完毕我们就能在输出结果中看到下面的执行结果:
这样就完成了要求的功能。不过上面的SQL 有一个问题,那就是计算每张采购单总交易额的聚合函数“SUM(purchasebilldetail.FCount*merchandise.FPrice)
”出现了很多次,这违反了DRY 原则造成后期维护的困难,而且有可能造成性能问题,因为乘法运算是非常低效的。可以使用子查询来解决这个问题,在子查询中计算每张采购单总交易额,这样在外部只要引用计算结果就可以了。SQL 语句如下:
SELECT
t.BillNumber,
t.TotalAmount,
( CASE WHEN t.TotalAmount <= 500 THEN '小额'
WHEN t.TotalAmount >= 1000 THEN '大额'
ELSE '普通' END ) NAME
FROM
(
SELECT
purchasebill.FNumber AS BillNumber,
SUM( purchasebilldetail.FCount * merchandise.FPrice ) AS TotalAmount
FROM
T_PurchaseBillDetail purchasebilldetail
INNER JOIN T_PurchaseBill purchasebill ON purchasebilldetail.FBillId = purchasebill.FId
INNER JOIN T_Merchandise merchandise ON purchasebilldetail.FMerchandiseId =merchandise.FId
GROUP BY
purchasebill.FNumber
)t
要求按照主键排序,检索所有制单人不为空的销售单,并且为每行显示一个行号。在MSSQLServer
、Oracle
、DB2
等支持窗口函数的DBMS中,使用窗口函数ROW_NUMBER()
可以完成这个功能:
SELECT ROW_NUMBER() OVER(ORDER BY FId) AS rn,
FNumber,FMakeDate
FROM T_SaleBill
WHERE FBillMakerId IS NOT NULL
对于MYSQL以及MSSQLServer2000
等不支持窗口函数的DBMS函数可以使用子查询来完成这个功能:
SELECT
(
SELECT COUNT(*) FROM T_SaleBill t1
WHERE t1.FId<=t2.FId
AND t1.FBillMakerId IS NOT NULL
) AS rn,
t2.FNumber,t2.FMakeDate
FROM T_SaleBill t2
WHERE t2.FBillMakerId IS NOT NULL
ORDER BY t2.FId
由于是按照FId
排序,而且FId
的值是唯一的,所以使用相关子查询计算小于等于当前FId
值的行的个数就可以得到当前行的行号。执行完毕我们就能在输出结果中看到下面的执行结果:
要求将每张销售单中销售量最大的明细记录标记出来。尝试使用下面的SQL 语句来来完成要求的功能:
SELECT FId,FBillId,FMerchandiseId,FCount,
CASE
WHEN FCount=MAX(FCount)
THEN '单内最大值'
ELSE ''
END
FROM T_SaleBillDetail
GROUP BY FBillId
在这个SQL 语句中,首先按照FBillId进行分组,然后使用聚合函数MAX()
来计算组内FCount
的最大值,最后使用CASE函数
判断每一行的FCount
是否等于这个最大值。执行这个SQL 语句后DBMS 会报出如下的错误信息:
选择列表中的列 ‘T_SaleBillDetail.FId’ 无效,因为该列没有包含在聚合函数或 GROUP BY 子句中。出现这个错误的原因是因为出现在SELECT 列表中的所有列如果不是在聚合函数中使用则必须加入GROUP BY 子 句中。
为了保证这个SQL 语句能够正确运行,需要将用到的所有列放到GROUP BY 子句中,SQL 语句如下:
SELECT FId,FBillId,FMerchandiseId,FCount,
CASE
WHEN FCount=MAX(FCount)
THEN '单内最大值'
ELSE ''
END
FROM T_SaleBillDetail
GROUP BY FId,FBillId,FMerchandiseId,FCount;
执行完毕我们就能在输出结果中看到下面的执行结果:
虽然SQL 语句能够执行通过了,不过非常遗憾的是,这个执行结果是错误的,因为将SELECT
列表中的所有列都放到GROUP BY
子句中会破坏原有的分组。这里将讲解使用聚合函数而又不必将SELECT
列表中的所有列都放到GROUP BY
子句中的技巧。在介绍窗口函数的时候曾经提到,使用窗口函数将无需使用GROUP BY
子句,而且窗口函数中的聚合计算不会影响其他的列,因此对于支持窗口函数的DBMS 可以使用如下的SQL 语句:
SELECT FId,FBillId,FMerchandiseId,FCount,
CASE
WHEN FCount=MAX(FCount) OVER(PARTITION BY FBillId)
THEN '单内最大值'
ELSE ''
END
FROM T_SaleBillDetail;
这里使用窗口函数“MAX(FCount) OVER(PARTITION BY FBillId)
”计算每一行所属
的销售单中的销售量的最大值,然后将其与FCount
进行比较,如果等于FCount
则表示当前行是销售量的最大值所在的行执行完毕我们就能在输出结果中看到下面的执行结果:
对于MYSQL
、MSSQLServer2000
等不支持窗口函数的DBMS 来说,可以使用相关子查询来达到相同的效果。SQL 语句如下:
SELECT FId,FBillId,FMerchandiseId,FCount,
CASE
WHEN FCount=
(
SELECT MAX(t1.FCount) FROM T_SaleBillDetail t1
WHERE t1.FBillId=t2.FBillId
)
THEN '单内最大值'
ELSE ''
END
FROM T_SaleBillDetail t2;
这里使用相关子查询来计算每一个销售单中的销售量的最大值,其余部分与使用窗口函数是一样的。需要注意的是相关子查询中的WHERE
子句中将t1.FBillId
和t2.FBillId
进行了相等性过滤,这样就达到了窗口函数中“PARTITION BY FBillId
”一样的分区计算最大值的效果,因此这个WHERE
语句是不能遗漏的。这个案例是非常典型的,当需要使用聚合计算,但是又不希望由于引入聚合函数而需要添加额外的GROUP BY
子句的话可以使用这里介绍的方案,那就是:支持窗口函数的DBMS 使用窗口函数,不支持窗口函数的DBMS使用子查询。
在一些业务报表中经常需要对多组数据进行比较,在技术实现上就是表间的数据比较。使用连接、IN、EXISTS等都可以解决表间比较的问题,不过这些方式中存在着一些差异,本节将通过例子对这些差异进行比较。
要求检索出制作过采购单的人制作的销售单,也就是销售单的制单人必须在采购单中有其制作的单据。
使用子查询可以解决这个问题,对于T_SaleBill 中的每一条记录的制单人都到T_PurchaseBill 表中查看是否存在于T_PurchaseBill 表中。SQL语句如下:
SELECT * FROM T_SaleBill
WHERE FBillMakerId IN
(
SELECT FBillMakerId FROM T_PurchaseBill
);
要求检索出没有制作过采购单的人制作的销售单,也就是销售单的制单人必须不在采购单中有其制作的单据。
参照上的例子我们也尝试使用子查询解决这个问题,对于T_SaleBill 中的每一条记录的制单人都到T_PurchaseBill 表中查看是否存在于T_PurchaseBill 表中。只需要将IN运算
符修改为NOT IN即可,SQL语句如下:
SELECT * FROM T_SaleBill
WHERE FBillMakerId NOT IN
(
SELECT FBillMakerId FROM T_PurchaseBill
);
执行完毕我们就能在输出结果中看到下面的执行结果:
这个执行结果出乎我们的意料,因为返回的查询结果是空的,主键为00003和00005的人员只做了销售单、没做过采购单,但是它们并没有被输出的执行结果中。这是因为
SELECT
FBillMakerId FROM T_PurchaseBill
返回的结果中有一个空值,如下:
我们知道,IN
和NOT IN
运算符本质上是OR
运算,因而必须从OR
运算符对于NULL
的处理来考虑IN
和NOT IN
运算符的运算结果。比如下面的两句SQL语句是等价的:
SELECT * FROM T_SaleBill
WHERE FBillMakerId IN
('00006','00004','00001','00002', NULL);
SELECT * FROM T_SaleBill
WHERE FBillMakerId ='00006' OR FBillMakerId ='00004'
OR FBillMakerId ='00001' OR FBillMakerId ='00002' OR FBillMakerId =NULL;
而对于NOT IN运算符来说,下面的两句SQL语句也是等价的:
SELECT * FROM T_SaleBill
WHERE FBillMakerId NOT IN('00006','00004','00001','00002', NULL);
SELECT * FROM T_SaleBill
WHERE NOT(FBillMakerId ='00006' OR FBillMakerId ='00004'
OR FBillMakerId ='00001' OR FBillMakerId ='00002' OR FBillMakerId =NULL);
可以看到,条件
FBillMakerId NOT IN('00006','00004','00001','00002', NULL)
等价于
NOT(FBillMakerId ='00006' OR FBillMakerId ='00004' OR
FBillMakerId ='00001' OR FBillMakerId ='00002' OR FBillMakerId =NULL)
在这种情况下,假设当FBillMakerId等于’00005’时,表达式的输出为:
NOT('00005'='00006' OR '00005'='00004' OR '00005'='00001' OR
'00005'='00002' OR '00005'=NULL)
我们知道,在SQL中NULL代表“值未知,“‘00005’=NULL”表示判断’00005’是否等于NULL,一个已知的值’00005’是无法确定是否等于一个未知的值的,所以“‘00005’=NULL”的返回值也是未知的,所以“‘00005’=NULL”的计算结果为NULL。因此上边的条件可以简化为:
NOT(FALSE OR FALSE OR FALSE OR NULL)
布尔值只有TRUE
和FALSE
两种取值,对于布尔类型而言NULL表示不知道是TRUE还是FALSE
。
TRUE和任何布尔值的OR运算结果都是TRUE,因此TRUE和未知值NULL的OR运算结果都是TRUE,因此“TURE OR NULL”的计算结果是TRUE;FALSE和TRUE的OR运算结果是TRUE,
而FALSE和FALSE的OR运算结果是FALSE
因此FALSE和未知值NULL的OR运算结果是未知的, 因此“FALSE OR NULL”的结果结果是NULL。
根据布尔值与NULL的这个运算特性我们得知“FALSE OR FALSE OR FALSE OR NULL”的运算结果为NULL,而一个未知的值做NOT运算,其结果同样是未知的,因此“NOTNULL”的运算结果为NULL,这样“NOT(FALSE OR FALSE OR FALSE OR NULL)”的运算结果为NULL。 既然WHERE条件是未知的,那么结果集中的结果是否符合WHERE条件也就是未知的了,因此检索不出任何数据是非常合理的
既然问题出在空值上面,只要将空值从子查询中过滤掉即可。为子查询中的查询语句增加一个WHERE条件,将所有空值过滤掉。SQL语句如下:
SELECT * FROM T_SaleBill
WHERE FBillMakerId NOT IN
(
SELECT FBillMakerId FROM T_PurchaseBill
WHERE FBillMakerId IS NOT NULL
);
在进行数据的备份等场景下,需要创建和指定表格式完全相同的表,甚至要将源表中的数据也原封不动的复制到目标表中,而且完成这一任务的时候无需关心源表的具体结构。
下面演示以T_Person
表为源表创建目标表T_Person2
,并且将T_Person
中的表复制到 T_Person2
表。在MYSQL
和ORACLE
中支持从结果集来创建一张表,并且将结果集中的数据填充到新创建的表中,使用方法也非常简单,只要在CREATE TABLE
语句后使用“AS 查询语句
”就可以。比如:
CREATE TABLE T_Person2
AS
SELECT * FROM T_Person
在MSSQLServer
中可以使用SELECT……INTO
语句来将检索的结果插入到一张新表中,如下:
SELECT * INTO T_Person2
FROM T_Person
在DB2
中可以使用带LIKE
子句的CREATE TABLE
语句来创建目标表,比如下面的SQL语句表示创建和T_Person
表格式一样的表T_Person2
:
CREATE TABLE T_Person2 LIKE T_Person;
不过这个语句只能复制表结构,不能同时将T_Person
表中的数据复制到T_Person2
表中的。前面介绍过使用INSERT……SELECT
语句可以快速将结果集插入到目标表中,因此在执行完上面的CREATE TABLE
语句后还需再执行一个INSERT
语句来将T_Person
表中的输入插入到T_Person2
表中:
INSERT INTO T_Person2
SELECT * FROM T_Person;
本案例讲解完毕,请删除T_Person2
表:
DROP TABLE T_Person2;
下面演示以T_Person
表为源表创建目标表T_Person2
,但是并不将T_Person
中的表复制到T_Person2
表。MYSQL
、MSSQLServer
和Oracle
中都是从结果集来创建表的,而且会将结果集中的数据插入到目标表中,因此如果不需要将T_Person
中的表复制到T_Person2
表只需要让结果集为空就可以了。注意结果集为空并不表示结果集没有意义,因为空的结果集中仍然包含了结果集的列信息。创建一个空结果集的最简单方式就是使用永远为FALSE的WHERE条件“WHERE1<>1”
。
MYSQL和ORACLE:
CREATE TABLE T_Person2
AS
SELECT * FROM T_Person
WHERE 1<>1
MSSQLServer:
SELECT * INTO T_Person2
FROM T_Person
WHERE 1<>1
“WHERE 1<>1
”这样的查询条件有可能造成全表扫描从而带来性能问题,因此如果数据量比较大的话可以用取结果集前0条这样的方式来得到一个空结果集,而DB2中的带LIKE子句的CREATETABLE语句本来就不复制数据,因此只要执行下面的SQL
语句就可以了:
CREATE TABLE T_Person2 LIKE T_Person;
本案例讲解完毕,请删除T_Person2表:
DROP TABLE T_Person2;
“去掉最高分和最低分,最后平均得分……”,这是在很多比赛中经常听到的。最高和最
低分对平均分的影响较大,为了避免个别评委的不公平评分而造成得分的不公正,去掉最高和最低分的做法是有效方法。去除最高值、最低值有两种策略:去除所有最低最高值和只去除一个最低最高值。本节将以计算销售单中的销售量的平均值为例进行介绍。
使用子查询可以排除所有的最高值和最低值,子查询返回表中的最大值和最小值,针对返
回的值使用NOT IN就可以从所有值中排除所有的最大值和最小值,然后使用AVG()函数计算平均值。SQL语句如下:
SELECT AVG(FCount) FROM T_SaleBillDetail
WHERE FCount NOT IN
(
(SELECT MIN(FCount) FROM T_SaleBillDetail),
(SELECT MAX(FCount) FROM T_SaleBillDetail)
)
在MSSQLServer
、Oracle
、DB2
等支持窗口函数
的DBMS中还可以使用窗口函数来完成这 个功能。SQL语句如下:
SELECT AVG(t.FCount)
FROM
(
SELECT FCount,MIN(FCount) OVER() min_count,
MAX(FCount) OVER() max_count
FROM T_SaleBillDetail
)t
WHERE t.FCount NOT IN(min_count, max_count)
子查询t
返回所有的值(包括最大值和最小值)、最大值和最小值,这样从每一行中都可以访问最大值和最小值,因此找出哪些值是最大值和最小值非常简单。外层的查询语句对子查询t
中的行做筛选,这样所有与min_count
和max_count
相等的行都会被排除。
上面的实现方式如果存在多个最高值或者最低值,那么它们都会被排除。如果只想排除一个最高值和一个最低值,那么只需要从总和SUM()中减去最高值MAX()和最低值MIN(),然后除以COUNT()-2就可以得到平均值了:
SELECT (SUM(FCount)-MIN(FCount)-MAX(FCount))/(COUNT(FCount)-2)
FROM T_SaleBillDetail;
注意这个SQL语句是存在BUG的,也就是如果表T_SaleBillDetail
中恰好只有两条记录,那么COUNT(FCount)-2
就会返回0,这就会造成以0为除数的计算错误。可以使用CASE…… WHEN
语句来修复这个BUG,SQL语句如下:
SELECT
CASE COUNT(FCount)
WHEN 2 THEN NULL
ELSE (SUM(FCount)-MIN(FCount)-MAX(FCount))/(COUNT(FCount)-2)
END
FROM T_SaleBillDetail;
在这个SQL语句中,如果恰好只有两条记录那么就将NULL做为平均值返回。
在实际应用中,很多数据是时间相关的,因此灵活的使用SQL进行日期处理将会大大提高开发速度,本节将会介绍SQL中的常见的日期相关的应用。
要求计算销售单的确认日和制单日之间相差的天数。
不同的DBMS中计算两个日期之间相差的天数的方式是不同的。
MYSQL:
SELECT FNumber,FMakeDate,FConfirmDate,
DATEDIFF(FConfirmDate,FMakeDate) AS DaysBetween
FROM T_SaleBill
MSSQLServer:
SELECT FNumber,FMakeDate,FConfirmDate,
DATEDIFF(day,FMakeDate,FConfirmDate) AS DaysBetween
FROM T_SaleBill
ORACLE:
SELECT FNumber,FMakeDate,FConfirmDate,
FConfirmDate-FMakeDate AS DaysBetween
FROM T_SaleBill
DB2:
SELECT FNumber,FMakeDate,FConfirmDate,
DAYS(FConfirmDate)-DAYS(FMakeDate) AS DaysBetween
FROM T_SaleBill
使用SQL语句可以创建非常复杂的报表,不过出于美观性和兼容使用者喜欢的考虑经常需要调整报表的输出格式,通过结果集转置可以快速调整输出格式,本节将介绍几种常见的结果集转置方式。
假设只有Bacon、Cake和Apple三种产品,要求统计每种产品的销售量。要求以下面的格式显示:
使用带GROUP BY
子句的SELECT
语句可以很容易的统计每种产品的销售量,SQL语句如下:
SELECT merchandise.FName,SUM(salebilldetail.FCount)
FROM T_SaleBillDetail salebilldetail
INNER JOIN T_Merchandise merchandise
ON salebilldetail.FMerchandiseId = merchandise.FId
GROUP BY merchandise.FName
执行完毕我们就能在输出结果中看到下面的执行结果:
这样虽然可以统计每种产品的总销量,不过显示格式与要求的不同,必须要将其向顺时针方向转90度成下面的格式:
SELECT
SUM(CASE WHEN FMerchandiseId='00003' THEN FCount ELSE 0 END) AS Apple,
SUM(CASE WHEN FMerchandiseId='00001' THEN FCount ELSE 0 END) AS Bacon,
SUM(CASE WHEN FMerchandiseId='00002' THEN FCount ELSE 0 END) AS Cake
FROM T_SaleBillDetail
执行完毕我们就能在输出结果中看到下面的执行结果:
这与要求的格式完全一致。如果不能理解这种用法,可以首先去掉聚合函数SUM()
,执行下面的SQL语句:
SELECT
CASE WHEN FMerchandiseId='00003' THEN FCount ELSE 0 END AS Apple,
CASE WHEN FMerchandiseId='00001' THEN FCount ELSE 0 END AS Bacon,
CASE WHEN FMerchandiseId='00002' THEN FCount ELSE 0 END AS Cake
FROM T_SaleBillDetail
以Apple列为例,通过CASE函数判断FMerchandiseId是否等于’00003’,如果等于 '00003’则表示它是Apple,因此列出FCount,如果不等于’00003’则表示它不是Apple,因此列出0值。执行完毕我们就能在输出结果中看到下面的执行结果:
结果集中每行对应一个销售单明细,每一列对应一种商品,如果此销售单明细中的商品是对应的产品则列的值为此产品的销量,否则列的值为0。接下来只要统计每一列的值的总和就可以得到每种商品的总销量了:
SELECT
SUM(CASE WHEN FMerchandiseId='00003' THEN FCount ELSE 0 END) AS Apple,
SUM(CASE WHEN FMerchandiseId='00001' THEN FCount ELSE 0 END) AS Bacon,
SUM(CASE WHEN FMerchandiseId='00002' THEN FCount ELSE 0 END) AS Cake
FROM T_SaleBillDetail
因为列数是固定的,无法实现动态列,所以实现这种转置要求产品的种类是确定的。
要求显示每张销售单中明细记录的产品编号。如果使用简单的SQL语句可以显示如下的格式:
希望重新调整结果集的格式,使每张销售单使用一列(假设只有这六张销售单)
首先,使用窗口函数ROW_NUMBER(
)使为每个FBillId
、FMerchandiseId
组合是设定一个编号:
SELECT FBillId,FMerchandiseId,
ROW_NUMBER() OVER(PARTITION BY FBillId ORDER BY FMerchandiseId) rn
FROM T_SaleBillDetail
执行完毕我们就能在输出结果中看到下面的执行结果:
rn
为每个FBillId
、FMerchandiseId
组合指定了一个编号,由于窗口函数是按照
FBillId字段进行分区的,所以这个编号是FBillId相同的组内唯一的。
接着,使用CASE表达式,把FMerchandiseId组合对应的FBillId:
SELECT
MAX(CASE WHEN FBillId='00001' THEN FMerchandiseId ELSE '' END) AS
Bill_00001,
MAX(CASE WHEN FBillId='00002' THEN FMerchandiseId ELSE '' END) AS
Bill_00002,
MAX(CASE WHEN FBillId='00003' THEN FMerchandiseId ELSE '' END) AS
Bill_00003,
MAX(CASE WHEN FBillId='00004' THEN FMerchandiseId ELSE '' END) AS
Bill_00004,
MAX(CASE WHEN FBillId='00005' THEN FMerchandiseId ELSE '' END) AS
Bill_00005,
MAX(CASE WHEN FBillId='00006' THEN FMerchandiseId ELSE '' END) AS
Bill_00006
FROM
(
SELECT FBillId,FMerchandiseId,
ROW_NUMBER() OVER(PARTITION BY FBillId ORDER BY FMerchandiseId) rn
FROM T_SaleBillDetail
) t
GROUP BY rn
执行完毕我们就能在输出结果中看到下面的执行结果:
在MYSQL、MSSQLServer2000等DBMS中并不支持窗口函数,所以上面的SQL语句是无法
正确执行的。上面的SQL语句中是使用窗口函数ROW_NUMBER()来为行设定一个编号的,在本书
的前面章节已经介绍了在不支持窗口函数的DBMS中使用子查询来变通实现窗口函数
ROW_NUMBER(),在此不做赘述。在不支持窗口函数的DBMS中可以使用下面的SQL语句来完成
同样的功能:
SELECT
MAX(CASE WHEN FBillId='00001' THEN FMerchandiseId ELSE '' END) AS
Bill_00001,
MAX(CASE WHEN FBillId='00002' THEN FMerchandiseId ELSE '' END) AS
Bill_00002,
MAX(CASE WHEN FBillId='00003' THEN FMerchandiseId ELSE '' END) AS
Bill_00003,
MAX(CASE WHEN FBillId='00004' THEN FMerchandiseId ELSE '' END) AS
Bill_00004,
MAX(CASE WHEN FBillId='00005' THEN FMerchandiseId ELSE '' END) AS
Bill_00005,
MAX(CASE WHEN FBillId='00006' THEN FMerchandiseId ELSE '' END) AS
Bill_00006
FROM
(
SELECT t2.FBillId AS FBillId,t2.FMerchandiseId AS FMerchandiseId,
(
SELECT COUNT(*) FROM T_SaleBillDetail t1
WHERE t2.FBillId=t1.FBillId AND
t2.FMerchandiseId<t1.FMerchandiseId
) AS rn
FROM T_SaleBillDetail t2
) t
GROUP BY rn;