我如何使用行组中的值基于另一个 table 更新 table?
How can I UPDATE a table based on another table, using values from groups of rows?
我有两个table:
CREATE TABLE Employee (
Site ???? ????,
WorkTypeId char(2) NOT NULL,
Emp_NO int NOT NULL,
"Date" ???? NOT NULL
);
CREATE TABLE PTO (
Site ???? ????,
WorkTypeId char(2) NULL,
Emp_NO int NOT NULL,
"Date" ???? NOT NULL
);
我想更新 PTO
的 WorkTypeId
列中的值:
EMP NO
in Employee
(查找 table)和 PTO
应该匹配。
- 一个
WorkTypeId
值应该只从该月的第一次出现中选取。
例如,给定此样本输入数据:
TABLE Employee
:
Site
WorkTypeId
Emp_NO
Date
5015
MB
1005
2022-02-01
5015
MI
1005
2022-02-04
5015
PO
1005
2022-02-04
5015
ME
2003
2022-01-01
5015
TT
2003
2022-01-10
TABLE PTO
:
Site
WorkTypeId
Emp_NO
Date
5015
1005
2022-02-03
5015
1005
2022-02-14
5014
2003
2022-01-09
例如:
- 鉴于
Employee
和 Emp_NO = 1005
...
- ...
Employee
table 中的 Emp_NO
有 3 行,具有 3 个不同的 WorkTypeId
值,但 Date
值不同.
- 所以选择最早的
Date
(2022-02-01)的WorkTypeId
值,即'MB'
- 所以
Emp_NO
得到 WorkTypeId = 'MB'
。
- 然后使用该单个值填充
1005
WorkTypeId
PTO
table 中的单元格。
- 但也可以按月匹配。
所以 PTO
table 中的预期输出是
Site
WorkTypeId
Emp_NO
Date
5015
MB
1005
2022-02-03
5015
MB
1005
2022-02-14
5014
ME
2003
2022-01-09
更新 2002-03-05
留在这里供后人参考,但我建议阅读 。
尝试 CROSS APPLY 获取第一条具有匹配月份和年份的员工记录。
注意:始终对所有 PTO 记录使用 OUTER APPLY,即使未找到匹配的 WorkTypeId。
SELECT p.Site
, e.WorkTypeId
, p.Emp_No
, p.[Date]
FROM PTO p CROSS APPLY
(
SELECT TOP 1 WorkTypeId
FROM Employee e
WHERE e.Emp_No = p.Emp_No
AND MONTH(e.[Date]) = MONTH(p.[Date])
AND YEAR(e.[Date]) = YEAR(p.[Date])
ORDER BY [Date] ASC
)e
结果:
Site | WorkTypeId | Emp_No | Date
---: | :--------- | -----: | :---------
5015 | MB | 1005 | 2022-02-03
5015 | MB | 1005 | 2022-02-14
5014 | ME | 2003 | 2022-01-09
db<>fiddle here
从与GROUP BY
查询中MIN
/MAX
表达式中使用的列不同的列中获取值仍然存在在 SQL 中做一件非常困难的事情,虽然现代版本的 SQL 语言(和 SQL 服务器)使它变得更容易,但它们完全是 non-obvious 和 counter-intuitive 对大多数人来说,因为它必然涉及更高级的主题,如 CTE、derived-tables(又名 inner-queries)、self-joins 和 windowing-functions 尽管 查询的概念简单。
无论如何,as-ever 在现代 SQL 中,通常有 3 或 4 种不同的方法来完成相同的任务,其中有一些陷阱。
前言:
由于Site
、Date
、Year
、Month
都是T-SQL中的关键字,所以我转义了使用 double-quotes,这是 ISO/ANSI SQL 符合标准的转义保留字的方式。
- SQL 服务器默认支持。如果(出于某些不敬虔的原因)您有
SET QUOTED IDENTIFIER OFF
,则将 double-quotes 更改为 square-brackets:[]
我假设两个table中的Site
列只是一个普通的'ol'数据列,因此:
- 它不是
PRIMARY KEY
成员栏。
- 不应用作
GROUP BY
。
- 不应在
JOIN
谓词中使用。
以下所有方法都假定此数据库状态:
CREATE TABLE "Employee" (
"Site" int NOT NULL,
WorkTypeId char(2) NOT NULL,
Emp_NO int NOT NULL,
"Date" date NOT NULL
);
CREATE TABLE "PTO" (
"Site" int NOT NULL,
WorkTypeId char(2) NULL,
Emp_NO int NOT NULL,
"Date" date NOT NULL
);
GO
INSERT INTO "Employee" ( "Site", WorkTypeId, Emp_NO, "Date" )
VALUES
( 5015, 'MB', 1005, '2022-02-01' ),
( 5015, 'MI', 1005, '2022-02-04' ),
( 5015, 'PO', 1005, '2022-02-04' ),
( 5015, 'ME', 2003, '2022-01-01' ),
( 5015, 'TT', 2003, '2022-01-10' );
INSERT INTO "PTO" ( "Site", WorkTypeId, Emp_NO, "Date" )
VALUES
( 5015, NULL, 1005, '2022-02-03' ),
( 5015, NULL, 1005, '2022-02-14' ),
( 5014, NULL, 2003, '2022-01-09' );
- 这两种方法都定义了 CTE
e
和 p
,它们分别扩展 Employee
和 PTO
以添加计算的 "Year"
和 "Month"
列,这避免了在 GROUP BY
和 JOIN
表达式中重复使用 YEAR( "Date" ) AS "Year"
。
- 我建议您将它们作为 computed-columns 添加到您的基础 table 中,如果可以的话,因为它们通常都会有用。也不要忘记适当地为它们编制索引。
方法 1:使用基本聚合组合 CTE,然后 UPDATE
:
WITH
-- Step 1: Extend both the `Employee` and `PTO` tables with YEAR and MONTH columns (this simplifies things later on):
e AS (
SELECT
Emp_No,
"Site",
WorkTypeId,
"Date",
YEAR( "Date" ) AS "Year",
MONTH( "Date" ) AS "Month"
FROM
Employee
),
p AS (
SELECT
Emp_No,
"Site",
WorkTypeId,
"Date",
YEAR( "Date" ) AS "Year",
MONTH( "Date" ) AS "Month"
FROM
PTO
),
-- Step 2: Get the MIN( "Date" ) value for each group:
minDatesForEachEmployeeMonthYearGroup AS (
SELECT
e.Emp_No,
e."Year",
e."Month",
MIN( "Date" ) AS "FirstDate"
FROM
e
GROUP BY
e.Emp_No,
e."Year",
e."Month"
),
-- Step 3: INNER JOIN back on `e` to get the first WorkTypeId in each group:
firstWorkTypeIdForEachEmployeeMonthYearGroup AS (
/* WARNING: This query will fail if multiple rows (for the same Emp_NO, Year and Month) have the same "Date" value. This can be papered-over with GROUP BY and MIN, but I don't think that's a good idea at all). */
SELECT
e.Emp_No,
e."Year",
e."Month",
e.WorkTypeId AS FirstWorkTypeId
FROM
e
INNER JOIN minDatesForEachEmployeeMonthYearGroup AS q ON
e.Emp_NO = q.Emp_NO
AND
e."Date" = q.FirstDate
)
-- Step 4: Do the UPDATE.
-- *Yes*, you can UPDATE a CTE (provided the CTE is "simple" and has a 1:1 mapping back to source rows on-disk).
UPDATE
p
SET
p.WorkTypeId = f.FirstWorkTypeId
FROM
p
INNER JOIN firstWorkTypeIdForEachEmployeeMonthYearGroup AS f ON
p.Emp_No = f.Emp_No
AND
p."Year" = f."Year"
AND
p."Month" = f."Month"
WHERE
p.WorkTypeId IS NULL;
这是 SSMS 的屏幕截图,显示了上述查询运行前后 PTO
table 的内容:
方法二:跳过self-JOIN
with FIRST_VALUE
:
这种方法提供了更短、更简单的查询,但需要 SQL Server 2012 或更高版本(并且您的数据库是 运行 in compatibility-level 110 或更高版本)。
令人惊讶的是,,尽管它与 MIN
有明显的相似之处,但可以使用 SELECT DISTINCT
:
构建等效查询
WITH
-- Step 1: Extend the `Employee` table with YEAR and MONTH columns:
e AS (
SELECT
Emp_No,
"Site",
WorkTypeId,
"Date",
YEAR( "Date" ) AS "Year",
MONTH( "Date" ) AS "Month"
FROM
Employee
),
firstWorkTypeIdForEachEmployeeMonthYearGroup AS (
SELECT
DISTINCT
e.Emp_No,
e."Year",
e."Month",
FIRST_VALUE( WorkTypeId ) OVER (
PARTITION BY
Emp_No,
e."Year",
e."Month"
ORDER BY
"Date" ASC
) AS FirstWorkTypeId
FROM
e
)
-- Step 3: UPDATE PTO:
UPDATE
p
SET
p.WorkTypeId = f.FirstWorkTypeId
FROM
PTO AS p
INNER JOIN firstWorkTypeIdForEachEmployeeMonthYearGroup AS f ON
p.Emp_No = f.Emp_No
AND
YEAR( p."Date" ) = f."Year"
AND
MONTH( p."Date" ) = f."Month"
WHERE
p.WorkTypeId IS NULL;
在此运行后执行 SELECT * FROM PTO
得到与方法 2 完全相同的输出。
方法 2b,但更短:
只是为了让@SOS 不觉得 太自鸣得意 他们的 SQL 比我的短得多,方法 2 SQL 上面可以压缩成这样:
WITH empYrMoGroups AS (
SELECT
DISTINCT
e.Emp_No,
YEAR( e."Date" ) AS "Year",
MONTH( e."Date" ) AS "Month",
FIRST_VALUE( e.WorkTypeId ) OVER (
PARTITION BY
e.Emp_No,
YEAR( e."Date" ),
MONTH( e."Date" )
ORDER BY
e."Date" ASC
) AS FirstWorkTypeId
FROM
Employee AS e
)
UPDATE
p
SET
p.WorkTypeId = f.FirstWorkTypeId
FROM
PTO AS p
INNER JOIN empYrMoGroups AS f ON
p.Emp_No = f.Emp_No
AND
YEAR( p."Date" ) = f."Year"
AND
MONTH( p."Date" ) = f."Month"
WHERE
p.WorkTypeId IS NULL;
- 方法 2 和 方法 2b 的 execution-plans 几乎相同,除了 方法2b 由于某种原因有一个额外的计算标量步骤。
- 方法 1 和 方法 2 的执行计划非常不同,但是,方法 1 比 方法 2 有更多的分支,尽管它们具有相似的语义。
- 但是我的 execution-plans 与你的不匹配,因为它非常 context-dependent,尤其是 w.r.t。你有什么索引和主键,是否涉及任何其他列等。
方法 1 的计划如下所示:
方法 2b 的计划如下所示:
相比之下,@SOS 的计划要简单得多...老实说,我不知道为什么,但它确实显示了 SQL 服务器的查询优化器如今有多好:
我有两个table:
CREATE TABLE Employee (
Site ???? ????,
WorkTypeId char(2) NOT NULL,
Emp_NO int NOT NULL,
"Date" ???? NOT NULL
);
CREATE TABLE PTO (
Site ???? ????,
WorkTypeId char(2) NULL,
Emp_NO int NOT NULL,
"Date" ???? NOT NULL
);
我想更新 PTO
的 WorkTypeId
列中的值:
EMP NO
inEmployee
(查找 table)和PTO
应该匹配。- 一个
WorkTypeId
值应该只从该月的第一次出现中选取。
例如,给定此样本输入数据:
TABLE Employee
:
Site | WorkTypeId | Emp_NO | Date |
---|---|---|---|
5015 | MB | 1005 | 2022-02-01 |
5015 | MI | 1005 | 2022-02-04 |
5015 | PO | 1005 | 2022-02-04 |
5015 | ME | 2003 | 2022-01-01 |
5015 | TT | 2003 | 2022-01-10 |
TABLE PTO
:
Site | WorkTypeId | Emp_NO | Date |
---|---|---|---|
5015 | 1005 | 2022-02-03 | |
5015 | 1005 | 2022-02-14 | |
5014 | 2003 | 2022-01-09 |
例如:
- 鉴于
Employee
和Emp_NO = 1005
...- ...
Employee
table 中的Emp_NO
有 3 行,具有 3 个不同的WorkTypeId
值,但Date
值不同. - 所以选择最早的
Date
(2022-02-01)的WorkTypeId
值,即'MB'
- 所以
Emp_NO
得到WorkTypeId = 'MB'
。 - 然后使用该单个值填充
1005
WorkTypeId
PTO
table 中的单元格。 - 但也可以按月匹配。
- ...
所以 PTO
table 中的预期输出是
Site | WorkTypeId | Emp_NO | Date |
---|---|---|---|
5015 | MB | 1005 | 2022-02-03 |
5015 | MB | 1005 | 2022-02-14 |
5014 | ME | 2003 | 2022-01-09 |
更新 2002-03-05
留在这里供后人参考,但我建议阅读
尝试 CROSS APPLY 获取第一条具有匹配月份和年份的员工记录。
注意:始终对所有 PTO 记录使用 OUTER APPLY,即使未找到匹配的 WorkTypeId。
SELECT p.Site
, e.WorkTypeId
, p.Emp_No
, p.[Date]
FROM PTO p CROSS APPLY
(
SELECT TOP 1 WorkTypeId
FROM Employee e
WHERE e.Emp_No = p.Emp_No
AND MONTH(e.[Date]) = MONTH(p.[Date])
AND YEAR(e.[Date]) = YEAR(p.[Date])
ORDER BY [Date] ASC
)e
结果:
Site | WorkTypeId | Emp_No | Date ---: | :--------- | -----: | :--------- 5015 | MB | 1005 | 2022-02-03 5015 | MB | 1005 | 2022-02-14 5014 | ME | 2003 | 2022-01-09
db<>fiddle here
从与GROUP BY
查询中MIN
/MAX
表达式中使用的列不同的列中获取值仍然存在在 SQL 中做一件非常困难的事情,虽然现代版本的 SQL 语言(和 SQL 服务器)使它变得更容易,但它们完全是 non-obvious 和 counter-intuitive 对大多数人来说,因为它必然涉及更高级的主题,如 CTE、derived-tables(又名 inner-queries)、self-joins 和 windowing-functions 尽管 查询的概念简单。
无论如何,as-ever 在现代 SQL 中,通常有 3 或 4 种不同的方法来完成相同的任务,其中有一些陷阱。
前言:
由于
Site
、Date
、Year
、Month
都是T-SQL中的关键字,所以我转义了使用 double-quotes,这是 ISO/ANSI SQL 符合标准的转义保留字的方式。- SQL 服务器默认支持。如果(出于某些不敬虔的原因)您有
SET QUOTED IDENTIFIER OFF
,则将 double-quotes 更改为 square-brackets:[]
- SQL 服务器默认支持。如果(出于某些不敬虔的原因)您有
我假设两个table中的
Site
列只是一个普通的'ol'数据列,因此:- 它不是
PRIMARY KEY
成员栏。 - 不应用作
GROUP BY
。 - 不应在
JOIN
谓词中使用。
- 它不是
以下所有方法都假定此数据库状态:
CREATE TABLE "Employee" (
"Site" int NOT NULL,
WorkTypeId char(2) NOT NULL,
Emp_NO int NOT NULL,
"Date" date NOT NULL
);
CREATE TABLE "PTO" (
"Site" int NOT NULL,
WorkTypeId char(2) NULL,
Emp_NO int NOT NULL,
"Date" date NOT NULL
);
GO
INSERT INTO "Employee" ( "Site", WorkTypeId, Emp_NO, "Date" )
VALUES
( 5015, 'MB', 1005, '2022-02-01' ),
( 5015, 'MI', 1005, '2022-02-04' ),
( 5015, 'PO', 1005, '2022-02-04' ),
( 5015, 'ME', 2003, '2022-01-01' ),
( 5015, 'TT', 2003, '2022-01-10' );
INSERT INTO "PTO" ( "Site", WorkTypeId, Emp_NO, "Date" )
VALUES
( 5015, NULL, 1005, '2022-02-03' ),
( 5015, NULL, 1005, '2022-02-14' ),
( 5014, NULL, 2003, '2022-01-09' );
- 这两种方法都定义了 CTE
e
和p
,它们分别扩展Employee
和PTO
以添加计算的"Year"
和"Month"
列,这避免了在GROUP BY
和JOIN
表达式中重复使用YEAR( "Date" ) AS "Year"
。- 我建议您将它们作为 computed-columns 添加到您的基础 table 中,如果可以的话,因为它们通常都会有用。也不要忘记适当地为它们编制索引。
方法 1:使用基本聚合组合 CTE,然后 UPDATE
:
WITH
-- Step 1: Extend both the `Employee` and `PTO` tables with YEAR and MONTH columns (this simplifies things later on):
e AS (
SELECT
Emp_No,
"Site",
WorkTypeId,
"Date",
YEAR( "Date" ) AS "Year",
MONTH( "Date" ) AS "Month"
FROM
Employee
),
p AS (
SELECT
Emp_No,
"Site",
WorkTypeId,
"Date",
YEAR( "Date" ) AS "Year",
MONTH( "Date" ) AS "Month"
FROM
PTO
),
-- Step 2: Get the MIN( "Date" ) value for each group:
minDatesForEachEmployeeMonthYearGroup AS (
SELECT
e.Emp_No,
e."Year",
e."Month",
MIN( "Date" ) AS "FirstDate"
FROM
e
GROUP BY
e.Emp_No,
e."Year",
e."Month"
),
-- Step 3: INNER JOIN back on `e` to get the first WorkTypeId in each group:
firstWorkTypeIdForEachEmployeeMonthYearGroup AS (
/* WARNING: This query will fail if multiple rows (for the same Emp_NO, Year and Month) have the same "Date" value. This can be papered-over with GROUP BY and MIN, but I don't think that's a good idea at all). */
SELECT
e.Emp_No,
e."Year",
e."Month",
e.WorkTypeId AS FirstWorkTypeId
FROM
e
INNER JOIN minDatesForEachEmployeeMonthYearGroup AS q ON
e.Emp_NO = q.Emp_NO
AND
e."Date" = q.FirstDate
)
-- Step 4: Do the UPDATE.
-- *Yes*, you can UPDATE a CTE (provided the CTE is "simple" and has a 1:1 mapping back to source rows on-disk).
UPDATE
p
SET
p.WorkTypeId = f.FirstWorkTypeId
FROM
p
INNER JOIN firstWorkTypeIdForEachEmployeeMonthYearGroup AS f ON
p.Emp_No = f.Emp_No
AND
p."Year" = f."Year"
AND
p."Month" = f."Month"
WHERE
p.WorkTypeId IS NULL;
这是 SSMS 的屏幕截图,显示了上述查询运行前后 PTO
table 的内容:
方法二:跳过self-JOIN
with FIRST_VALUE
:
这种方法提供了更短、更简单的查询,但需要 SQL Server 2012 或更高版本(并且您的数据库是 运行 in compatibility-level 110 或更高版本)。
令人惊讶的是,MIN
有明显的相似之处,但可以使用 SELECT DISTINCT
:
WITH
-- Step 1: Extend the `Employee` table with YEAR and MONTH columns:
e AS (
SELECT
Emp_No,
"Site",
WorkTypeId,
"Date",
YEAR( "Date" ) AS "Year",
MONTH( "Date" ) AS "Month"
FROM
Employee
),
firstWorkTypeIdForEachEmployeeMonthYearGroup AS (
SELECT
DISTINCT
e.Emp_No,
e."Year",
e."Month",
FIRST_VALUE( WorkTypeId ) OVER (
PARTITION BY
Emp_No,
e."Year",
e."Month"
ORDER BY
"Date" ASC
) AS FirstWorkTypeId
FROM
e
)
-- Step 3: UPDATE PTO:
UPDATE
p
SET
p.WorkTypeId = f.FirstWorkTypeId
FROM
PTO AS p
INNER JOIN firstWorkTypeIdForEachEmployeeMonthYearGroup AS f ON
p.Emp_No = f.Emp_No
AND
YEAR( p."Date" ) = f."Year"
AND
MONTH( p."Date" ) = f."Month"
WHERE
p.WorkTypeId IS NULL;
在此运行后执行 SELECT * FROM PTO
得到与方法 2 完全相同的输出。
方法 2b,但更短:
只是为了让@SOS 不觉得 太自鸣得意 他们的 SQL 比我的短得多,方法 2 SQL 上面可以压缩成这样:
WITH empYrMoGroups AS (
SELECT
DISTINCT
e.Emp_No,
YEAR( e."Date" ) AS "Year",
MONTH( e."Date" ) AS "Month",
FIRST_VALUE( e.WorkTypeId ) OVER (
PARTITION BY
e.Emp_No,
YEAR( e."Date" ),
MONTH( e."Date" )
ORDER BY
e."Date" ASC
) AS FirstWorkTypeId
FROM
Employee AS e
)
UPDATE
p
SET
p.WorkTypeId = f.FirstWorkTypeId
FROM
PTO AS p
INNER JOIN empYrMoGroups AS f ON
p.Emp_No = f.Emp_No
AND
YEAR( p."Date" ) = f."Year"
AND
MONTH( p."Date" ) = f."Month"
WHERE
p.WorkTypeId IS NULL;
- 方法 2 和 方法 2b 的 execution-plans 几乎相同,除了 方法2b 由于某种原因有一个额外的计算标量步骤。
- 方法 1 和 方法 2 的执行计划非常不同,但是,方法 1 比 方法 2 有更多的分支,尽管它们具有相似的语义。
- 但是我的 execution-plans 与你的不匹配,因为它非常 context-dependent,尤其是 w.r.t。你有什么索引和主键,是否涉及任何其他列等。
方法 1 的计划如下所示:
方法 2b 的计划如下所示:
相比之下,@SOS 的计划要简单得多...老实说,我不知道为什么,但它确实显示了 SQL 服务器的查询优化器如今有多好: