【问题标题】:Select rows based on other rows in the same table根据同一个表中的其他行选择行
【发布时间】:2018-03-16 17:01:09
【问题描述】:

我想出了一个非常简单的例子来演示我要解决的问题。

我需要选择客户合同...

1) 已过期或将在未来三个月内过期

2) 尚未签订新合同。
所有过期的合同与新合同一起保留在表格中。

根据这些业务规则,我对测试数据的预期结果将是返回合同 ID 6(客户 3),因为它是过期合同,没有新合同,而合同 ID 7(客户 4)少于 3运行几个月。

我查看了一些解决方案是将表连接到自身的示例

例如how do I query sql for a latest record date for each user

我想我可以只为每个客户选择最近的合同,然后像这样检查它的到期日期,但它只返回合同 ID 6 而不是我期望的 7。我正在使用 SQL 2008 R2。

有什么想法我会出错吗?

SELECT [ContractID]
      ,[StartDate]
      ,[ExpiryDate]
      ,TC.[CustomerID]
  FROM [Test].[dbo].[TestContract] TC
  inner join
  (
    select CustomerID,
    MAX(ExpiryDate) as MaxDate
    From Test.dbo.TestContract
    Group by CustomerID     
  )CM on TC.CustomerID = CM.CustomerID and TC.ExpiryDate = CM.MaxDate
  Where TC.ExpiryDate < DateAdd(DAY, 30, GETDATE())

这是我的测试数据

ContractID  StartDate                 ExpiryDate                CustomerID
1           2017-02-01 00:00:00.000   2018-02-01 00:00:00.000   1
2           2016-01-01 00:00:00.000   2017-01-01 00:00:00.000   1
4           2016-01-01 00:00:00.000   2017-11-01 00:00:00.000   2
5           2017-11-01 00:00:00.000   2018-11-01 00:00:00.000   2
6           2016-10-01 00:00:00.000   2017-10-01 00:00:00.000   3
7           2016-12-01 00:00:00.000   2017-12-01 00:00:00.000   4
8           2015-12-01 00:00:00.000   2016-12-01 00:00:00.000   4
9           2017-06-01 00:00:00.000   2018-06-01 00:00:00.000   5 

这是重新创建我的测试表和数据的脚本。

USE [Test]
GO
/****** Object:  Table [dbo].[TestContract]    Script Date: 10/05/2017 17:07:33 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[TestContract](
    [ContractID] [int] IDENTITY(1,1) NOT NULL,
    [StartDate] [datetime] NOT NULL,
    [ExpiryDate] [datetime] NOT NULL,
    [CustomerID] [int] NOT NULL,
 CONSTRAINT [PK_TestContract] PRIMARY KEY CLUSTERED 
(
    [ContractID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET IDENTITY_INSERT [dbo].[TestContract] ON
INSERT [dbo].[TestContract] ([ContractID], [StartDate], [ExpiryDate], [CustomerID]) VALUES (1, CAST(0x0000A70D00000000 AS DateTime), CAST(0x0000A87A00000000 AS DateTime), 1)
INSERT [dbo].[TestContract] ([ContractID], [StartDate], [ExpiryDate], [CustomerID]) VALUES (2, CAST(0x0000A58000000000 AS DateTime), CAST(0x0000A6EE00000000 AS DateTime), 1)
INSERT [dbo].[TestContract] ([ContractID], [StartDate], [ExpiryDate], [CustomerID]) VALUES (4, CAST(0x0000A58000000000 AS DateTime), CAST(0x0000A81E00000000 AS DateTime), 2)
INSERT [dbo].[TestContract] ([ContractID], [StartDate], [ExpiryDate], [CustomerID]) VALUES (5, CAST(0x0000A81E00000000 AS DateTime), CAST(0x0000A98B00000000 AS DateTime), 2)
INSERT [dbo].[TestContract] ([ContractID], [StartDate], [ExpiryDate], [CustomerID]) VALUES (6, CAST(0x0000A69200000000 AS DateTime), CAST(0x0000A7FF00000000 AS DateTime), 3)
INSERT [dbo].[TestContract] ([ContractID], [StartDate], [ExpiryDate], [CustomerID]) VALUES (7, CAST(0x0000A6CF00000000 AS DateTime), CAST(0x0000A83C00000000 AS DateTime), 4)
INSERT [dbo].[TestContract] ([ContractID], [StartDate], [ExpiryDate], [CustomerID]) VALUES (8, CAST(0x0000A56100000000 AS DateTime), CAST(0x0000A6CF00000000 AS DateTime), 4)
INSERT [dbo].[TestContract] ([ContractID], [StartDate], [ExpiryDate], [CustomerID]) VALUES (9, CAST(0x0000A78500000000 AS DateTime), CAST(0x0000A8F200000000 AS DateTime), 5)
SET IDENTITY_INSERT [dbo].[TestContract] OFF

【问题讨论】:

  • 你的预期输出是什么
  • @TheGameiswar 问题中写有 :) ID 6 和 7
  • Anagha 选择了我的愚蠢错误。 30 应该是 90。我现在用我的真实数据去测试它,这显然会更复杂。感谢大家提出的解决方案。我只需要一个有效的:) 我会看看我是否能理解你所描述的,因为我确信那里有很好的学习机会。 .

标签: sql-server


【解决方案1】:

你说“过期或将在未来三个月内过期”的意思是应该有DateAdd(DAY, 90, GETDATE())这样的条件而不是30

修改查询后:

SELECT [ContractID]
      ,[StartDate]
      ,[ExpiryDate]
      ,TC.[CustomerID]
  FROM [TestContract] TC
  inner join
  (
    select CustomerID,
    MAX(ExpiryDate) as MaxDate
    From TestContract
    Group by CustomerID     
  )CM on TC.CustomerID = CM.CustomerID and TC.ExpiryDate = CM.MaxDate
  Where TC.ExpiryDate <  DateAdd(DAY, 90, GETDATE())

另一种选择:

select [ContractID],[StartDate],[ExpiryDate] ,[CustomerID]
from (select [ContractID],[StartDate],ExpiryDate ,TC.[CustomerID],
      ROW_NUMBER() over (partition by customerid order by ExpiryDate desc) rn
FROM [TestContract] TC ) a
where rn =1 
and ExpiryDate <  DateAdd(DAY, 90, GETDATE())

【讨论】:

  • 你选的 :) 我不敢相信我错过了。我非常确信我的解决方案行不通,我什至没有寻找愚蠢的错误。我只是将我的 30 改为 90,我得到了预期的结果。
  • 有时会发生 :)
【解决方案2】:

按照我对问题的理解,像 ROW_NUMBER() 这样的窗口函数应该不是必需的。

使用连接方法...

SELECT
    TC.ContractID
  , TC.StartDate
  , TC.ExpiryDate
  , TC.CustomerID
FROM dbo.TestContract TC
LEFT JOIN dbo.TestContract TC2
    ON TC2.CustomerID = TC.CustomerID
    AND TC2.StartDate > TC.StartDate
WHERE
    TC.ExpiryDate < dateadd(day, 90, getdate())
    AND TC2.ContractID is null

但是,由于我们不需要 select 子句中连接表中的任何列,因此我会选择使用 NOT EXISTS。虽然两者都不是很复杂,但这对我来说更容易推理,因为当阅读它时更接近于业务规则,并且不需要考虑确定连接是否会产生额外的行......

SELECT
    TC.ContractID
  , TC.StartDate
  , TC.ExpiryDate
  , TC.CustomerID
FROM dbo.TestContract TC
WHERE
    TC.ExpiryDate < dateadd(day, 90, getdate())
    AND NOT EXISTS (
        SELECT *
        FROM dbo.TestContract TC2
        WHERE
            TC2.CustomerID = TC.CustomerID
            AND TC2.StartDate > TC.StartDate
        )

当您有仅在其连接条件中使用的内连接表或仅在其自己的连接条件和 where 子句中使用的外连接表时,您通常可以使用 EXISTS 或 NOT EXISTS 重写查询.

【讨论】:

  • 谢谢杰夫,这两个答案都很好,而且比我想出的更简单。我想我会尝试实施您的第二个答案,因为不需要包含外部联接。
  • 不客气,感谢您回来评论。很确定这些解决方案将比行号方法执行得更好。但是,如果您没有太多数据,它可能只有在查看查询计划时才会注意到。
【解决方案3】:

在子查询中使用ROW_NUMBER() 以获取每个客户的最新合同,然后检查expirydate

SELECT ContractID,StartDate,ExpiryDate,CustomerID
FROM (
       SELECT ContractID,StartDate,ExpiryDate,CustomerID,
              ROW_NUMBER() OVER (PARTITION BY CustomerId ORDER BY ExpiryDate DESC) AS RN
       FROM [YourTable]
) X
WHERE X.RN=1 AND X.ExpiryDate < DateAdd(DAY, 90, GETDATE())

【讨论】:

  • 谢谢,这也有效,但我的原始解决方案非常接近工作,尽管它不是很漂亮。我给第一个发现我错误的人打勾。我会看看哪个更容易实现。
【解决方案4】:
SELECT ContractID,StartDate,ExpiryDate,CustomerID
FROM (
       SELECT ContractID,StartDate,ExpiryDate,CustomerID,
              ROW_NUMBER() OVER (PARTITION BY CustomerId ORDER BY ExpiryDate DESC) AS RN
       FROM TestContract
) a
WHERE a.RN=1 AND ( a.ExpiryDate < DateAdd(DAY, 30, GETDATE()) OR a.ExpiryDate <= DateAdd(M, 3, GETDATE()) )

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-02-06
    • 2021-07-15
    • 1970-01-01
    • 2010-09-08
    • 1970-01-01
    • 2022-01-12
    • 1970-01-01
    相关资源
    最近更新 更多