【问题标题】:Finding each customer group's most recent account查找每个客户组的最新帐户
【发布时间】:2017-02-08 16:49:08
【问题描述】:

我有一个包含客户信息的表格。每个客户都被分配了一个客户 ID(他们的 SSN),他们在开设更多账户时会保留该 ID。两个客户可能在同一个帐户上,每个客户都有自己的 ID。帐号不按日期排序。

我想查找每个客户或客户组的最新帐户。如果两个客户曾经一起使用过一个帐户,我想返回任一客户最近使用过的帐户。

这是一个包含一些可能情况的示例表。

示例表 ACCT:

acctnumber  date            Cust1ID     Cust2ID 
10000       '2016-02-01'    1110        NULL    --Case0-customer has only ever had
                                                --one account

10001       '2016-02-01'    1111        NULL    --Case1-one customer has multiple
10050       '2017-02-01'    1111        NULL    --accounts
400050      '2017-06-01'    1111        NULL
10089       '2017-12-08'    1111        NULL

10008       '2016-02-01'    1120        NULL    --Case2-customer has account(s) and later
10038       '2016-04-01'    1120        NULL
10058       '2017-02-03'    1120        1121    --gets account(s) with another customer

10002       '2016-02-01'    1112        NULL    --Case3-customer has account(s) and later
10052       '2017-02-02'    1113        1112    --becomes the second customer on another
10152       '2017-05-02'    1113        1112    --account(s)

10003       '2016-02-02'    1114        1115    --Case4-customer and second customer
7060        '2017-02-04'    1115        1114    --switch which is first and second

10004       '2016-02-02'    1116        1117    --Case5-second customer later gets
10067       '2017-02-05'    1117        NULL    --separate account(s)
10167       '2018-02-05'    1117        NULL

50013       '2016-01-01'    2008        NULL    --Case5b -customer has account(s) & later
50014       '2017-02-02'    2008        2009    --gets account(s) with second customer &
50015       '2017-04-04'    2008        NULL    --later still first customer gets
100015      '2018-05-05'    2008        NULL    --separate account(s)

30005       '2015-02-01'    1118        NULL    --Case6-customer has account(s) 
10005       '2016-02-01'    1118        NULL
10054       '2017-02-02'    1118        1119    --gets account(s) with another
40055       '2017-03-03'    1118        1119
10101       '2017-04-04'    1119        NULL    --who later gets separate account(s)
10201       '2017-05-05'    1119        NULL
30301       '2017-06-06'    1119        NULL
10322       '2018-01-01'    1119        NULL

10007       '2016-02-01'    1122        1123    --Case7-customers play musical chairs
10057       '2017-02-03'    1123        1124
10107       '2017-06-02'    1124        1125

50001       '2016-01-01'    2001        NULL    --Case8a-customers with account(s)
50002       '2017-02-02'    2001        2002    --together each later get separate
50003       '2017-03-03'    2001        NULL    --account(s)
50004       '2017-04-04'    2002        NULL

50005       '2016-01-01'    2003        NULL    --Case8b-customers with account(s)
50006       '2017-02-02'    2003        2004    --together each later get separate
50007       '2017-03-03'    2004        NULL    --account(s)
50008       '2017-04-04'    2003        NULL
50017       '2018-03-03'    2004        NULL
50018       '2018-04-04'    2003        NULL

50009       '2016-01-01'    2005        NULL    --Case9a-customer has account(s) & later
50010       '2017-02-02'    2005        2006    --gets account(s) with a second customer
50011       '2017-03-03'    2005        2007    --& later still gets account(s) with a
                                                --third customer

50109       '2016-01-01'    2015        NULL    --Case9b starts the same as Case9a, but
50110       '2017-02-02'    2015        2016    
50111       '2017-03-03'    2015        2017    
50112       '2017-04-04'    2015        NULL    --after all accounts with other customers
50122       '2017-05-05'    2015        NULL    --are complete, the original primary
                                                --customer begins opening individual
                                                --accounts again

期望的结果:

acctnumber  date            Cust1ID     Cust2ID 
10000       '2016-02-01'    1110        NULL    --Case0    
10089       '2017-12-08'    1111        NULL    --Case1
10058       '2017-02-03'    1120        1121    --Case2
10152       '2017-05-02'    1113        1112    --Case3
7060        '2017-02-04'    1115        1114    --Case4
10167       '2018-02-05'    1117        NULL    --Case5
100015      '2018-05-05'    2008        NULL    --Case5b
10322       '2018-01-01'    1119        NULL    --Case6
10107       '2017-06-02'    1124        1125    --Case7
50003       '2017-03-03'    2001        NULL    --Case8a result 1
50004       '2017-04-04'    2002        NULL    --Case8a result 2
50017       '2018-03-03'    2004        NULL    --Case8b result 1
50018       '2018-04-04'    2003        NULL    --Case8b result 2
50011       '2017-03-03'    2005        2007    --Case9a
50122       '2017-05-05'    2015        NULL    --Case9b

或者,我会接受案例 7 输出两个不同的客户组:

10007       '2016-02-01'    1122        1123    --Case7 result 1
10107       '2017-06-02'    1124        1125    --Case7 result 2

因为案例 8a 和 8b 代表公司承认客户值得持有单独的帐户,所以我们希望将他们的组视为拆分,因此它具有单独的结果集。

另外,在大多数场景下,客户有很多账户,混合搭配上述情况加班是很常见的。例如,一个客户可以有五个账户(案例 1),然后再与另一个客户开一个或多个账户(案例 3),有时会更换主要账户持有人(案例 4),然后第一个客户再次开始开设个人账户(案例 5b)。


只要帐户编号是唯一的并且任何客户 ID 匹配,我都会尝试将表加入到自身的副本中。但是,这会删除只有一个帐户的客户,因此我添加了一个与 custid 或帐号不匹配的 cust 联合,以及按 custid 划分的组。

不幸的是,第二部分不仅包括案例 0 中的 custid,而且还有一些不应该被排除在外的 custid。

select
    max(date1) as date,
    cust1id1 as cust1id
from
(
select
    acctnumber as [acctnumber1],
    date as [date1],
    cust1id as [cust1id1],
    cust2id as [cust2id1]
from 
    acct
) t1
join
(
select
    acctnumber as [acctnumber2],
    date as [date2],
    cust1id as [cust1id2],
    cust2id as [cust2id2]
from 
    acct
) t2
on t1.date1 > t2.date2 and
(t1.cust1id1 = t2.cust1id2 or
t1.cust1id1 = t2.cust2id2 or
t1.cust2id1 = t2.cust2id2)
Group by
cust1id1
union
select
    max(date1) as date,
    cust1id1 as cust1id
from
(
select
    acctnumber as [acctnumber1],
    date as [date1],
    cust1id as [cust1id1],
    cust2id as [cust2id1]
from 
    acct
) t1
join
(
select
    acctnumber as [acctnumber2],
    date as [date2],
    cust1id as [cust1id2],
    cust2id as [cust2id2]
from 
    acct
) t2
on (t1.acctnumber1 != t2.acctnumber2 and
t1.cust1id1 != t2.cust1id2 and
t1.cust1id1 != t2.cust2id2 and
t1.cust2id1 != t2.cust2id2)
group by
cust1id1

更新

感谢您迄今为止提供的所有出色答案和 cmets。我一直在尝试查询并比较结果。

@VladimirBaranov 提出了一个我以前在 cmets 中没有考虑过其他答案的罕见案例。

与案例 7 类似,如果案例 8 被处理,将是一个奖励,但不是预期的。

案例 9 很重要,应处理 9a 和 9b 的结果。

更新 2

我注意到我原来的 7 个案例存在问题。

在最近的帐户中,当客户不再在帐户中时,总是留下第二个借款人。这完全是无意的,您可以查看这些示例中的任何一个,其中任一客户都可能是最近帐户中的剩余客户。

此外,每个案例都有最少数量的帐户来准确显示案例正在测试的内容,但这并不常见。通常在每个案例的每个步骤中,在客户切换到添加第二个客户之前,可以有 5、10、15 个或更多帐户,然后这两个帐户可以一起拥有许多帐户。

查看我看到的许多答案都有索引、创建、更新和其他特定于能够编辑数据库的子句。不幸的是,我在这个数据库的消费者端,所以我有只读访问权限,我可以用来与数据库交互的程序会自动拒绝它们。

【问题讨论】:

  • 这似乎是一个非常糟糕的数据库设计。
  • 不幸的是,我在这个数据库的消费者端,他们至少有一个想法是正确的:它被设置为只读。
  • 方括号表示 Microsoft SQL Server 语法。 MySQL 不在标识符周围使用方括号。我将更改此问题的标签以表明这一点。
  • 为什么 CustID 1116 没有结果集?还是1118?
  • 不确定这个数据库设计,你不支持第三个客户吗?但是,这里适用 SQL 最佳实践,即您需要将数据重新格式化为清晰易读的内容,即使您能够编写超级查询来完成这项工作......不要。视图、CTE 和临时表按此顺序出现。

标签: sql sql-server tsql greatest-n-per-group


【解决方案1】:

我要感谢 Jeff Breadner 提供带有示例数据的 DDL。

您必须逐步运行以下查询,逐个 CTE 并检查中间结果以了解它的作用。它假定AcctNumber 在给定表中是唯一的。

首先,我想为每个客户找到最新的帐户。这是一个简单的top-n-per-group 查询,我在这里使用ROW_NUMBER 方法。

CTE_CustomersCust1IDCust2ID 放在一起,创建了所有个人客户的简单列表。 CTE_RN 为它们分配行号。 CTE_LatestAccounts 为每个客户提供最新帐户:

+------------------+------------+--------+
| LatestAcctNumber |  LatestDT  | CustID |
+------------------+------------+--------+
|            10000 | 2016-02-01 |   1110 |
|            10050 | 2017-02-01 |   1111 |
|            10052 | 2017-02-02 |   1112 |
|            10052 | 2017-02-02 |   1113 |
|             7060 | 2017-02-04 |   1114 |
|             7060 | 2017-02-04 |   1115 |
|            10004 | 2016-02-02 |   1116 |
|            10067 | 2017-02-05 |   1117 |
|            10054 | 2017-02-03 |   1118 |
|            10101 | 2017-06-02 |   1119 |
|            10058 | 2017-02-03 |   1120 |
|            10058 | 2017-02-03 |   1121 |
|            10007 | 2016-02-01 |   1122 |
|            10057 | 2017-02-03 |   1123 |
|            10107 | 2017-06-02 |   1124 |
|            10107 | 2017-06-02 |   1125 |
+------------------+------------+--------+

让客户对将最新帐户“传播”给另一个客户,因此任务变得复杂。

客户对是在原始表中定义的,因此CTE_MaxLatestAccounts 从原始表中获取每一行并将最新的帐户连接到其中两次 - 分别为 Cust1DCust2ID。对于每一对,我都会选择两个最新帐户中的一个——最近的一个。因此,属于一对的客户可能会从其合作伙伴那里获得一个帐户。

+---------+---------+-------------+---------------------+
| Cust1ID | Cust2ID | MaxLatestDT | MaxLatestAcctNumber |
+---------+---------+-------------+---------------------+
|    1110 | NULL    | 2016-02-01  |               10000 |
|    1111 | NULL    | 2017-02-01  |               10050 |
|    1111 | NULL    | 2017-02-01  |               10050 |
|    1120 | NULL    | 2017-02-03  |               10058 |
|    1120 | 1121    | 2017-02-03  |               10058 |
|    1112 | NULL    | 2017-02-02  |               10052 |
|    1113 | 1112    | 2017-02-02  |               10052 |
|    1114 | 1115    | 2017-02-04  |                7060 |
|    1115 | 1114    | 2017-02-04  |                7060 |
|    1116 | 1117    | 2017-02-05  |               10067 |
|    1117 | NULL    | 2017-02-05  |               10067 |
|    1118 | NULL    | 2017-02-03  |               10054 |
|    1118 | 1119    | 2017-06-02  |               10101 |
|    1119 | NULL    | 2017-06-02  |               10101 |
|    1122 | 1123    | 2017-02-03  |               10057 |
|    1123 | 1124    | 2017-06-02  |               10107 |
|    1124 | 1125    | 2017-06-02  |               10107 |
+---------+---------+-------------+---------------------+

这里的MaxLatestAcctNumber 适用于Cust1IDCust2ID。同一客户可能在此处多次列出,我们需要使用最新的帐户再次选择条目。这是一对的最新帐户,不适用于个人客户。

方法与开始时相同。将Cust1IDCust2ID 客户放在一个列表中:CTE_CustomersWithLatestAccountFromPair。在CTE_CustomersWithLatestAccountFromPairRN 中分配行号并在CTE_FinalAccounts 中选择最终帐户。

+---------------------+
| MaxLatestAcctNumber |
+---------------------+
|               10000 |
|               10050 |
|               10052 |
|               10052 |
|                7060 |
|                7060 |
|               10067 |
|               10067 |
|               10101 |
|               10101 |
|               10058 |
|               10058 |
|               10057 |
|               10107 |
|               10107 |
|               10107 |
+---------------------+

现在我们只需要过滤原始表并只保留出现在此列表中的那些行(帐户)。看看下面的最终结果。

样本数据

declare @ACCT table (
    AcctNumber int,
    dt date,
    Cust1ID int,
    Cust2ID int
);

insert into @ACCT values 
(10000, '2016-02-01', 1110, null),
(10001, '2016-02-01', 1111, null),
(10050, '2017-02-01', 1111, null),
(10008, '2016-02-01', 1120, null),
(10058, '2017-02-03', 1120, 1121),
(10002, '2016-02-01', 1112, null),
(10052, '2017-02-02', 1113, 1112),
(10003, '2016-02-02', 1114, 1115),
(7060,  '2017-02-04', 1115, 1114),
(10004, '2016-02-02', 1116, 1117),
(10067, '2017-02-05', 1117, null),
(10005, '2016-02-01', 1118, null),
(10054, '2017-02-03', 1118, 1119),
(10101, '2017-06-02', 1119, null),
(10007, '2016-02-01', 1122, 1123),
(10057, '2017-02-03', 1123, 1124),
(10107, '2017-06-02', 1124, 1125);

查询

WITH
CTE_Customers
AS
(
    SELECT
        AcctNumber
        ,dt
        ,Cust1ID AS CustID
    FROM @ACCT
    WHERE Cust1ID IS NOT NULL

    UNION ALL

    SELECT
        AcctNumber
        ,dt
        ,Cust2ID AS CustID
    FROM @ACCT
    WHERE Cust2ID IS NOT NULL
)
,CTE_RN
AS
(
    SELECT
        AcctNumber
        ,dt
        ,CustID
        ,ROW_NUMBER() OVER (PARTITION BY CustID ORDER BY dt DESC) AS rn
    FROM CTE_Customers
)
,CTE_LatestAccounts
-- this gives one row per CustID
AS
(
    SELECT
        AcctNumber AS LatestAcctNumber
        ,dt AS LatestDT
        ,CustID
    FROM CTE_RN
    WHERE rn = 1
)
,CTE_MaxLatestAccounts
AS
(
    SELECT
        A.Cust1ID
        ,A.Cust2ID
        ,CASE WHEN ISNULL(A1.LatestDT, '2000-01-01') > ISNULL(A2.LatestDT, '2000-01-01')
        THEN A1.LatestDT ELSE A2.LatestDT END AS MaxLatestDT
        ,CASE WHEN ISNULL(A1.LatestDT, '2000-01-01') > ISNULL(A2.LatestDT, '2000-01-01')
        THEN A1.LatestAcctNumber ELSE A2.LatestAcctNumber END AS MaxLatestAcctNumber
    FROM
        @ACCT AS A
        LEFT JOIN CTE_LatestAccounts AS A1 ON A1.CustID = A.Cust1ID
        LEFT JOIN CTE_LatestAccounts AS A2 ON A2.CustID = A.Cust2ID
)
,CTE_CustomersWithLatestAccountFromPair
AS
(
    SELECT
        Cust1ID AS CustID
        ,MaxLatestDT
        ,MaxLatestAcctNumber
    FROM CTE_MaxLatestAccounts
    WHERE Cust1ID IS NOT NULL

    UNION ALL

    SELECT
        Cust2ID AS CustID
        ,MaxLatestDT
        ,MaxLatestAcctNumber
    FROM CTE_MaxLatestAccounts
    WHERE Cust2ID IS NOT NULL
)
,CTE_CustomersWithLatestAccountFromPairRN
AS
(
    SELECT
        CustID
        ,MaxLatestDT
        ,MaxLatestAcctNumber
        ,ROW_NUMBER() OVER (PARTITION BY CustID ORDER BY MaxLatestDT DESC) AS rn
    FROM CTE_CustomersWithLatestAccountFromPair
)
,CTE_FinalAccounts
AS
(
    SELECT MaxLatestAcctNumber
    FROM CTE_CustomersWithLatestAccountFromPairRN
    WHERE rn = 1
)
SELECT *
FROM @ACCT AS A
WHERE A.AcctNumber IN (SELECT MaxLatestAcctNumber FROM CTE_FinalAccounts)
;

结果

+------------+------------+---------+---------+
| AcctNumber |     dt     | Cust1ID | Cust2ID |
+------------+------------+---------+---------+
|      10000 | 2016-02-01 |    1110 | NULL    |
|      10050 | 2017-02-01 |    1111 | NULL    |
|      10058 | 2017-02-03 |    1120 | 1121    |
|      10052 | 2017-02-02 |    1113 | 1112    |
|       7060 | 2017-02-04 |    1115 | 1114    |
|      10067 | 2017-02-05 |    1117 | NULL    |
|      10101 | 2017-06-02 |    1119 | NULL    |
|      10057 | 2017-02-03 |    1123 | 1124    |
|      10107 | 2017-06-02 |    1124 | 1125    |
+------------+------------+---------+---------+

此结果与您想要的结果相匹配,除了最后一种情况 7。

我的查询不会尝试跟踪任意长度的链接客户链,并且仅限于一次处理一对。这就是案例 7 结果不是一行的原因。 查询将始终选择具有最后日期 (10107) 的行/帐户,它也可能选择链中间的帐户。在这种情况下,它选择了一行10057,而不是10007,因为这是客户11221123 的后续帐户。


当我查看执行计划时,我发现CTE_LatestAccounts 后面的查询基本上运行了四次。

如果您将CTE_LatestAccounts 的结果保存到具有适当索引的临时表中,整体性能可能会更好。

类似这样的:

DECLARE @LatestAccounts TABLE 
    (LatestAcctNumber int, LatestDT date, CustID int PRIMARY KEY);

WITH
CTE_Customers
AS
(
    SELECT
        AcctNumber
        ,dt
        ,Cust1ID AS CustID
    FROM @ACCT
    WHERE Cust1ID IS NOT NULL

    UNION ALL

    SELECT
        AcctNumber
        ,dt
        ,Cust2ID AS CustID
    FROM @ACCT
    WHERE Cust2ID IS NOT NULL
)
,CTE_RN
AS
(
    SELECT
        AcctNumber
        ,dt
        ,CustID
        ,ROW_NUMBER() OVER (PARTITION BY CustID ORDER BY dt DESC) AS rn
    FROM CTE_Customers
)
,CTE_LatestAccounts
-- this gives one row per CustID
AS
(
    SELECT
        AcctNumber AS LatestAcctNumber
        ,dt AS LatestDT
        ,CustID
    FROM CTE_RN
    WHERE rn = 1
)
INSERT INTO @LatestAccounts (LatestAcctNumber, LatestDT, CustID)
SELECT LatestAcctNumber, LatestDT, CustID
FROM CTE_LatestAccounts;


WITH
CTE_MaxLatestAccounts
AS
(
    SELECT
        A.Cust1ID
        ,A.Cust2ID
        ,CASE WHEN ISNULL(A1.LatestDT, '2000-01-01') > ISNULL(A2.LatestDT, '2000-01-01')
        THEN A1.LatestDT ELSE A2.LatestDT END AS MaxLatestDT
        ,CASE WHEN ISNULL(A1.LatestDT, '2000-01-01') > ISNULL(A2.LatestDT, '2000-01-01')
        THEN A1.LatestAcctNumber ELSE A2.LatestAcctNumber END AS MaxLatestAcctNumber
    FROM
        @ACCT AS A
        LEFT JOIN @LatestAccounts AS A1 ON A1.CustID = A.Cust1ID
        LEFT JOIN @LatestAccounts AS A2 ON A2.CustID = A.Cust2ID
)
,CTE_CustomersWithLatestAccountFromPair
AS
(
    SELECT
        Cust1ID AS CustID
        ,MaxLatestDT
        ,MaxLatestAcctNumber
    FROM CTE_MaxLatestAccounts
    WHERE Cust1ID IS NOT NULL

    UNION ALL

    SELECT
        Cust2ID AS CustID
        ,MaxLatestDT
        ,MaxLatestAcctNumber
    FROM CTE_MaxLatestAccounts
    WHERE Cust2ID IS NOT NULL
)
,CTE_CustomersWithLatestAccountFromPairRN
AS
(
    SELECT
        CustID
        ,MaxLatestDT
        ,MaxLatestAcctNumber
        ,ROW_NUMBER() OVER (PARTITION BY CustID ORDER BY MaxLatestDT DESC) AS rn
    FROM CTE_CustomersWithLatestAccountFromPair
)
,CTE_FinalAccounts
AS
(
    SELECT MaxLatestAcctNumber
    FROM CTE_CustomersWithLatestAccountFromPairRN
    WHERE rn = 1
)
SELECT *
FROM @ACCT AS A
WHERE A.AcctNumber IN (SELECT MaxLatestAcctNumber FROM CTE_FinalAccounts)
;

如果您确实需要在链的长度是任意的时将所有链接的客户合并/分组为一行,您可以使用递归查询来完成,例如,这里:How to find all connected subgraphs of an undirected graph

用某个 GroupID 标记每个客户后,找到每个客户的最新帐户,如本查询开头所示。然后在组中找到最新的帐户(而不是像此查询中的简单对)。

在链接问题中查找无向图的所有子图的查询对于大型数据集可能会非常慢,并且有有效的基于非集合的算法可以做到这一点。

如果你知道链的最大长度不能超过某个数字,就有可能使这个递归查询更高效。

【讨论】:

  • 这可以得到想要的结果+1,谢谢。它比 iSR5 慢七倍。
  • @GoldenRatio,这是可以理解的。 iSR5 的查询和我的查询可能会产生不同的结果,这取决于您表中的实际数据。如果您的数据使得我们的两个查询都产生相同的结果,那么 iSR5 的更简单版本就是最佳选择。
  • 如果您能想到一个示例,说明您的查询在 iSR5 不适用的情况下有效,我肯定会使用您的而不是他的。在这种情况下,准确性对我来说是更重要的因素。
  • @GoldenRatio,我的查询和 iSR5 的查询在案例 7、8a、8b 中产生不同的结果。查询使用不同的逻辑。很可能还有其他未在问题中列出的情况,结果会有所不同。完全由您决定哪种逻辑适合您。比较这些查询的性能没有多大意义,因为它们会产生不同的结果。
  • 我发现你的例子对于至少一些案例 1 客户来说也不准确。客户可能有 10 或 15 个帐户,并且查询似乎是随机选择的最终结果。我会尝试进一步调查。
【解决方案2】:

要将逻辑应用于每个子集,一个好的运算符是CROSS APPLY 运算符。这使我们能够找到每个客户 ID 的最新帐户。

设置

DECLARE @Stage TABLE
(
    AcctNumber INT
    ,[Date] DATETIME
    ,Cust1Id INT
   ,Cust2Id INT
)

INSERT INTO @Stage (AcctNumber, [Date] ,Cust1Id ,Cust2Id)
VALUES
(10000,'2.1.16',1110,NULL)
,(10001,'2.1.16',1111,NULL)
,(10050,'2.1.17',1111,NULL)
,(10008,'2.1.16',1120,NULL)
,(10058,'2.3.17',1120,1121)
,(10002,'2.1.16',1112,NULL)
,(10052,'2.2.17',1113,1112)
,(10003,'2.2.16',1114,1115)
,(7060,'2.4.17',1115,1114)
,(10004,'2.2.16',1116,1117)
,(10067,'2.5.17',1117,NULL)
,(10005,'2.1.16',1118,NULL)
,(10054,'2.3.17',1118,1119)
,(10101,'6.2.17',1119,NULL)
,(10007,'2.1.16',1122,1123)
,(10057,'2.3.17',1123,1124)
,(10107,'6.2.17',1124,1125)

--Additional Cases to cover
,(50001, '2016-01-01', 2001, NULL)
,(50002, '2017-02-02', 2001, 2002)
,(50003, '2017-03-03', 2001, NULL)
,(50004, '2017-04-04', 2002, NULL)

,(50005, '2016-01-01', 2003, NULL)
,(50006, '2017-02-02', 2003, 2004)
,(50007, '2017-03-03', 2004, NULL)
,(50008, '2017-04-04', 2003, NULL)

执行

交叉应用

;WITH Results AS(
    SELECT DISTINCT S2.*
    FROM @Stage S1
    CROSS APPLY (
        SELECT TOP 1 S2.*
        FROM @Stage S2
        WHERE 
            (S1.Cust1Id = S2.Cust1Id
            OR S1.Cust1Id = S2.Cust2Id
            OR S1.Cust2Id = S2.Cust1Id
            OR S1.Cust2Id = S2.Cust2Id)
        ORDER BY S2.[Date] DESC
            ) S2
)
SELECT R1.*
FROM Results R1
    LEFT JOIN Results R2
        ON R1.Cust2Id = R2.Cust1Id
WHERE R1.[Date] > R2.[Date]
    OR R2.AcctNumber IS NULL

CROSS APPLY 操作员将案例向后遍历,以将逻辑应用于每个联合帐户案例,同时确保结转最近的帐户。仅此一项就涵盖了大多数情况。唯一挥之不去的案例是 3 个帐户在 3 个客户之间转移的案例。最终选择中的自联接和WHERE 子句涵盖了这些。

结果

+------------+------------+---------+---------+
| AcctNumber | Date       | Cust1Id | Cust2Id |
| 7060       | 2017-02-04 | 1115    | 1114    |
| 10000      | 2016-02-01 | 1110    | NULL    |
| 10050      | 2017-02-01 | 1111    | NULL    |
| 10052      | 2017-02-02 | 1113    | 1112    |
| 10058      | 2017-02-03 | 1120    | 1121    |
| 10067      | 2017-02-05 | 1117    | NULL    |
| 10101      | 2017-06-02 | 1119    | NULL    |
| 10107      | 2017-06-02 | 1124    | 1125    |
| 50003      | 2017-03-03 | 2001    | NULL    |
| 50004      | 2017-04-04 | 2002    | NULL    |
| 50007      | 2017-03-03 | 2004    | NULL    |
| 50008      | 2017-04-04 | 2003    | NULL    |
+------------+------------+---------+---------+

【讨论】:

  • 您是否应该在过滤器中再添加一个OR S1.Cust2Id = S2.Cust2Id?我不确定。
  • @VladimirBaranov 不错!我已经添加了。它不会改变 OP 案例中的任何内容,但它会捕获其他预期案例。谢谢!
  • 不过,至少有一个案例(OP 没有列出)并没有像我预期的那样成功。尝试将这些行添加到示例数据中:(50001, '2016-01-01', 2001, NULL), (50002, '2017-02-02', 2001, 2002), (50003, '2017-03-03', 2001, NULL), (50004, '2017-04-04', 2002, NULL), 我希望最终结果应该只有一个额外的行(50004, '2017-04-04', 2002, NULL),但您的查询返回两个:(50003, '2017-03-03', 2001, NULL), (50004, '2017-04-04', 2002, NULL),
  • @VladimirBaranov 我在另一个嵌套的CROSS APPLY 中添加了这将多个帐户向后移动。这涵盖了您所说的新案例。我还添加了另一个在 Cust1IdCust2IdDate 之间有不同关系的案例。
  • 我喜欢你最初的想法和查询看起来非常优雅。不幸的是,当所有情况都得到处理时,它变得不那么漂亮了。
【解决方案3】:

我确信有一个更简单的方法,但这是我的想法:

SELECT 
    a.acctnumber, 
    a.date, 
    a.Cust1ID, 
    a.Cust2ID 
FROM acct a
OUTER APPLY (
SELECT acctnumber
FROM (
SELECT *, 
    ROW_NUMBER() OVER(PARTITION BY acctnumber ORDER BY [date] DESC) AS ACC_RN,
    ROW_NUMBER() OVER(PARTITION BY CustomerID ORDER BY [date] DESC) AS RN
FROM (
SELECT 
     a1.acctnumber,
     a1.[date],
     a1.Cust1ID AS CustomerID
FROM acct a1
UNION 
SELECT 
     a2.acctnumber,
     a2.[date],
     a2.Cust2ID
FROM acct a2
) D
) C
WHERE 
    RN = 1
AND CustomerID IS NOT NULL
AND ACC_RN = 2
) acc
WHERE a.acctnumber IN(acc.acctnumber)

【讨论】:

  • 与 Matt Rowland 的回答相同。如果您像 Matt ,(50001, '2016-01-01', 2001, NULL) ,(50002, '2017-02-02', 2001, 2002) ,(50003, '2017-03-03', 2001, NULL) ,(50004, '2017-04-04', 2002, NULL) ,(50005, '2016-01-01', 2003, NULL) ,(50006, '2017-02-02', 2003, 2004) ,(50007, '2017-03-03', 2004, NULL) ,(50008, '2017-04-04', 2003, NULL) 那样在示例数据中添加更多行,您会发现您的查询返回的行数比合理预期的要多。
  • @VladimirBaranov 您能解释一下其他案例背后的逻辑吗?查询将根据 OP 逻辑显示 (50003, 50004, 50007, 50008),并且根据其他案例,我看到 Cust1ID #2001 已经开设了一个帐户,然后一年后,又获得了另一个客户的第二个帐户,然后一个月后,他们俩都有了新的独立帐户。 2003 年和 2004 年也发生了同样的事情。这是您要涵盖的逻辑吗?
  • 与问题中的案例6类似。只有 OP 可以告诉我们他想在这个特定示例中看到什么结果,但根据我的理解,我认为查询应该只返回 5000450008
  • @VladimirBaranov 如果我正确理解这些案例,由于我之前提到的相同逻辑,预期结果将是 (50003, 50004, 50007, 50008)。如果你想复制案例 6,那么我们需要删除 50003 或 50004。在 50007 和 50008 上也是一样的。因为 OP 示例中的案例 6 显示 118 有一个单独的帐户,然后与 119 共享它,然后 118 关闭了帐户,而 119 得到了一个单独的帐户,这就是为什么 118 被从结果中删除了。这就是我对OP逻辑的理解,并与您的案例进行了比较。
  • 最好问OP。希望他正在阅读 cmets 并可以对他的问题进行澄清。我的理由是:客户20012002 在某个时间点共享了一个帐户。这就是为什么结果应该有最后一个帐户,它属于20012002 或两者,即50004。与第二个示例相同。客户20032004 在某个时间点共享了一个帐户。与20032004 关联的最新帐户是50008
【解决方案4】:

您是否可以仅使用左连接将帐户与可能较晚日期的其他“链接”帐户连接起来,然后仅过滤掉“以后帐户”表不为空的记录?像这样的:

select ThisAccount.* 
from Accounts ThisAccount
left join Accounts LaterAccount on
    LaterAccount.AcctNumber <> ThisAccount.AcctNumber
    and LaterAccount.dt > ThisAccount.dt
    and
    (   LaterAccount.Cust1ID = ThisAccount.Cust1ID
        or LaterAccount.Cust2ID = ThisAccount.Cust1ID
        or LaterAccount.Cust1ID = ThisAccount.Cust2ID
        or LaterAccount.Cust2ID = ThisAccount.Cust2ID
    )
where LaterAccount.AcctNumber is null
order by ThisAccount.AcctNumber

这应该会按预期返回结果:

AcctNo  Dt          Cust1   Cust2
7060    2017-02-04  1115    1114
10000   2016-02-01  1110    NULL
10050   2017-02-01  1111    NULL
10052   2017-02-02  1113    1112
10058   2017-02-03  1120    1121
10067   2017-02-05  1117    NULL
10101   2017-06-02  1119    NULL
10107   2017-06-02  1124    1125
50003   2017-03-03  2001    NULL
50004   2017-04-04  2002    NULL
50007   2017-03-03  2004    NULL
50008   2017-04-04  2003    NULL

【讨论】:

    【解决方案5】:

    我的回答是错误的,抱歉过早发布。我正在研究一个不同的想法,我很快就会回来。


    原始回复:

    假设您的日期格式是 MM.DD.YY,我得到的代码如下所示。我不明白为什么您想要的结果集不包含 CustID 1116 或 1118 的行,但我确实看到包含它们将如何分别重复 1117 和 1119,除非修改源数据以从中删除这些重复的 1117 和 1119 值结果。目前,我有这个临时解决方案,等待您的回复。

    declare @ACCT table (
      acctnumber int,
      date date,
      Cust1ID int,
      Cust2ID int
    );
    
    insert into @ACCT values (10000, '2016-02-01', 1110, null);
    insert into @ACCT values (10001, '2016-02-01', 1111, null);
    insert into @ACCT values (10050, '2017-02-01', 1111, null);
    insert into @ACCT values (10008, '2016-02-01', 1120, null);
    insert into @ACCT values (10058, '2017-02-03', 1120, 1121);
    insert into @ACCT values (10002, '2016-02-01', 1112, null);
    insert into @ACCT values (10052, '2017-02-02', 1113, 1112);
    insert into @ACCT values (10003, '2016-02-02', 1114, 1115);
    insert into @ACCT values (7060,  '2017-02-04', 1115, 1114);
    insert into @ACCT values (10004, '2016-02-02', 1116, 1117);
    insert into @ACCT values (10067, '2017-02-05', 1117, null);
    insert into @ACCT values (10005, '2016-02-01', 1118, null);
    insert into @ACCT values (10054, '2017-02-03', 1118, 1119);
    insert into @ACCT values (10101, '2017-06-02', 1119, null);
    insert into @ACCT values (10007, '2016-02-01', 1122, 1123);
    insert into @ACCT values (10057, '2017-02-03', 1123, 1124);
    insert into @ACCT values (10107, '2017-06-02', 1124, 1125);
    
    with
    
    OneCustId as (
    select
      acctnumber,[date], Cust1ID as CustID
    from
      @ACCT
    
    union
    
    select
      acctnumber, [date], Cust2ID
    from
      @ACCT
    ),
    
    SortedByLastUsage as (
    select
      acctnumber, [date], CustID, row_number() over (partition by CustID order by [date] desc) as RowID
    from
      OneCustId
    ),
    
    LastUsage as (
    select
      acctnumber, [date], CustID
    from
      SortedByLastUsage
    where
      RowID = 1
    )
    
    select distinct
      ACCT.acctnumber, ACCT.[date], ACCT.Cust1ID, ACCT.Cust2ID
    from
      @ACCT ACCT
      inner join LastUsage on
        ACCT.acctnumber = LastUsage.acctnumber and
        ACCT.[date] = LastUsage.[date] and
        LastUsage.CustID in (ACCT.Cust1ID, ACCT.Cust2ID)
    order by
      Cust1ID, Cust2ID
    

    结果集:

    acctnumber  date    Cust1ID Cust2ID
    10000   2016-02-01  1110    NULL
    10050   2017-02-01  1111    NULL
    10052   2017-02-02  1113    1112
    7060    2017-02-04  1115    1114
    10004   2016-02-02  1116    1117
    10067   2017-02-05  1117    NULL
    10054   2017-02-03  1118    1119
    10101   2017-06-02  1119    NULL
    10058   2017-02-03  1120    1121
    10007   2016-02-01  1122    1123
    10057   2017-02-03  1123    1124
    10107   2017-06-02  1124    1125
    

    【讨论】:

      【解决方案6】:

      我将保留我原来的答案,因为该方法可能适用于其他搜索此问题的人。

      如果没有光标,我无法弄清楚如何做到这一点。因此,任何其他提供正确答案(不使用光标)的答案都将胜过这个答案。我不够聪明,无法弄清楚它是什么样子,但它必须包含一个令人讨厌的递归 CTE。

      真正的诀窍是将所有相互关联的帐户组合在一起。这是在顶部的大光标 if/then/else 链中完成的,可以稍微清理一下。我已经将调试print 语句留在原处,它们显然可以被删除。

      您也可以使关联表永久化,而不是使用表变量。

      再一次,在性能方面,这将是非常非常糟糕的,但它确实有效。我很期待看到其他人的想法。也感谢您提出的高质量问题,这让生活变得更加轻松。

      代码:

      declare @Associations table (
        GroupID int,
        CustID int
      );
      
      declare @NextGroupID int = 0;
      declare @FoundGroup1ID int;
      declare @FoundGroup2ID int;
      declare @Cust1 int;
      declare @Cust2 int;
      
      declare db_cursor cursor for
      select Cust1ID, Cust2ID from @ACCT;
      
      open db_cursor;
      fetch next from db_cursor into @Cust1, @Cust2;
      
      while @@fetch_status = 0
      begin
        set @FoundGroup1ID = null;
        set @FoundGroup2ID = null;
        print '----------------------------'
        print 'Cust1 = ' + isnull(cast(@Cust1 as varchar(max)), 'NULL')
        print 'Cust2 = ' + isnull(cast(@Cust2 as varchar(max)), 'NULL')
      
        select @FoundGroup1ID = GroupID from @Associations where CustID = @Cust1
        print 'FoundGroup1ID = ' + isnull(cast(@FoundGroup1ID as varchar(max)), 'NULL')
      
        if @Cust2 is null
        begin
          if @FoundGroup1ID is null 
          begin
            set @NextGroupID = @NextGroupID +1
            print 'Adding Cust1 to new group ' + cast(@NextGroupID as varchar(max))
            insert into @Associations (GroupID, CustID) values (@NextGroupID, @Cust1)
          end
        end 
        else -- @Cust2 is not null
        begin
      
          print 'FoundGroup2ID = ' + isnull(cast(@FoundGroup2ID as varchar(max)), 'NULL')
          select @FoundGroup2ID = GroupID from @Associations where CustID = @Cust2
      
          if @FoundGroup1ID is null and @FoundGroup2ID is null
          begin
            set @NextGroupID = @NextGroupID +1
            print 'Adding both to new group ' + cast(@NextGroupID as varchar(max))
            insert into @Associations (GroupID, CustID) values (@NextGroupID, @Cust1)
            insert into @Associations (GroupID, CustID) values (@NextGroupID, @Cust2)
          end 
          else if @FoundGroup1ID is not null and @FoundGroup2ID is null
          begin
            print 'Adding Cust2 to existing group ' + cast(@FoundGroup1ID as varchar(max))
            insert into @Associations (GroupID, CustID) values (@FoundGroup1ID, @Cust2)
          end
          else if @FoundGroup1ID is null and @FoundGroup2ID is not null
          begin
            print 'Adding Cust1 to existing group ' + cast(@FoundGroup2ID as varchar(max))
            insert into @Associations (GroupID, CustID) values (@FoundGroup2ID, @Cust1)
          end
          else -- Neither is null
          begin
            print 'Switching all of GroupID ' + cast(@FoundGroup2ID as varchar(max)) + ' to GroupID ' + cast(@FoundGroup1ID as varchar(max))
            update @Associations set GroupID = @FoundGroup1ID where GroupID = @FoundGroup2ID
          end
        end
        fetch next from db_cursor into @Cust1, @Cust2;
      end
      close db_cursor;
      deallocate db_cursor;
      
      ;with
      
      AddedGroupID as (
      select
        ACCT.acctnumber,
        ACCT.[date],
        ACCT.Cust1ID,
        ACCT.Cust2ID,
        Associations.GroupID,
        row_number() over (partition by Associations.GroupID order by ACCT.[date] desc) as RowID
      from
        @ACCT ACCT
        inner join @Associations Associations on
          Associations.CustID in (ACCT.Cust1ID, ACCT.Cust2ID)
      )
      
      select 
        acctnumber, [date], Cust1ID, Cust2ID
      from 
        AddedGroupID
      where
        RowID = 1
      

      结果:

      acctnumber  date    Cust1ID Cust2ID
      10000   2016-02-01  1110    NULL
      10050   2017-02-01  1111    NULL
      10058   2017-02-03  1120    1121
      10052   2017-02-02  1113    1112
      7060    2017-02-04  1115    1114
      10067   2017-02-05  1117    NULL
      10101   2017-06-02  1119    NULL
      10107   2017-06-02  1124    1125
      

      【讨论】:

      • 我在这个数据库的消费者端,所以它是只读的,任何使用关键字插入、创建或更新的查询都会被自动拒绝。
      • 即使是表变量?如果您要使用不同的 SQL 机器并通过链接服务器访问您的主要数据会怎样?有办法解决它。但是,我可以理解想要优先考虑更简单的解决方案。不幸的是,我在这里提出的解决方案确实需要能够以某种形式插入;如果不允许这样做,那么您需要选择不同的解决方案。
      • 我们对数据库的唯一访问是通过 Citrix Portal,然后在他们的软件中,必须将查询粘贴到的文本框界面
      【解决方案7】:

      我们不应该担心使用 EXISTS 因为它在这种情况下运行速度很快 我想是最简单的解决方案:

      SELECT
          A.ACCTNUMBER, A.DT as "date", A.CUST1ID, A.CUST2ID
      FROM
          ACCT A
      WHERE
              NOT EXISTS
              (SELECT
                  *
              FROM
                  ACCT A2
              WHERE
                  (A2.CUST1ID = A.CUST1ID
                  OR A2.CUST2ID = A.CUST1ID
                  OR (A.CUST2ID IS NOT NULL AND A2.CUST1ID = A.CUST2ID)
                  OR (A.CUST2ID IS NOT NULL AND A2.CUST2ID = A.CUST2ID)
                  )
                  AND A2.DT>A.DT
              )
      

      我假设您在 CUST1ID 上有单独的索引 CUST2ID 上的另一个。 您可以在 DT(“日期”)字段上比较结果而不使用升序索引。 它可以加快您的查询速度或减慢速度 - 我不知道您的真实数据是什么样的

      【讨论】:

        【解决方案8】:

        试试下面的查询。它很长,因为需要重复应用窗口函数(您不能将它们嵌套在单个查询中),但查询本身非常简单。核心思想是将不共享帐户的客户与共享帐户的客户分开。之后,对于单账户客户,分组列很简单,Cust1ID,但对于其他的,你必须做一些如下所述的操作,得到分组列:

        要获取分组列(对于多帐户客户),您必须应用以下逻辑:

        使用UNION ALL(CTE 在查询中称为cte)将所有第一个客户和第二个客户放在同一列中。然后,当您按该列排序并检查具有下一行 ID 的两个 ID 时,您可以检查它们是否“已连接”,即它们至少有一个 ID 相同:

        case when Cust1ID in (cust1idLead, cust2idLead) or Cust2ID in (cust1idLead, cust2idLead) then 1 else 0 end SameGroup
        

        通过这种方式,您可以区分组,并在该组内分别取最大值到日期(dt 列)。

        样本数据:

        declare @tbl table (acctnumber int, dt date ,   Cust1ID int,    Cust2ID  int);
        insert into @tbl values
        (10000, '2.1.16', 1110, null),
        (10001, '2.1.16', 1111, null),
        (10050, '2.1.17', 1111, null),
        (10008, '2.1.16', 1120, null),
        (10058, '2.3.17', 1120, 1121),
        (10002, '2.1.16', 1112, null),
        (10052, '2.2.17', 1113, 1112),
        (10003, '2.2.16', 1114, 1115),
        (7060, '2.4.17', 1115, 1114),
        (10004, '2.2.16', 1116, 1117),
        (10067, '2.5.17', 1117, null),
        (10005, '2.1.16', 1118, null),
        (10054, '2.3.17', 1118, 1119),
        (10101, '6.2.17', 1119, null),
        (10007, '2.1.16', 1122, 1123),
        (10057, '2.3.17', 1123, 1124),
        (10107, '6.2.17', 1124, 1125)
        

        T-SQL:

        ;with SingleAccounts as (
            select cust1id from @tbl
            where Cust2ID is null
            except
            select cust1id from @tbl
            where Cust2ID is not null
            except
            select cust2id from @tbl
        ), cte as (
            select  acctnumber, dt, Cust1ID, Cust2ID from @tbl
            where Cust1ID not in (select Cust1ID from SingleAccounts)
            union all
            select  acctnumber, dt, Cust2ID, Cust1ID from @tbl
            where Cust1ID not in (select Cust1ID from SingleAccounts) and Cust2ID is not null
        ), SingleAmountsResult as (
            select acctnumber, dt, cust1id, cust2id,
                   ROW_NUMBER() over (partition by cust1id order by dt desc) rn 
            from @tbl 
            where cust1id in (select Cust1ID from SingleAccounts)
        ), FinalResult as (
            select acctnumber, dt, cust1id, cust2id from SingleAmountsResult
            where rn = 1
            union all
            select acctnumber, dt, cust1id, cust2id
            from (
                select acctnumber, dt, cust1id, cust2id,
                       ROW_NUMBER() over (partition by GroupingColumn order by dt desc) rn
                from (
                    select acctnumber, dt, cust1id, cust2id,
                           SUM(NewGroup) over (order by cust1id, cust2id) GroupingColumn
                    from (
                        select acctnumber, dt, cust1id, cust2id,
                               case when LAG(SameGroup) over (order by cust1id, cust2id) = 0 then 1 else 0 end NewGroup
                        from (
                            select acctnumber, dt, cust1id, cust2id,
                                   case when Cust1ID in (cust1idLead, cust2idLead) or Cust2ID in (cust1idLead, cust2idLead) then 1 else 0 end SameGroup
                            from (
                                select acctnumber, dt, cust1id, cust2id,
                                       LEAD(cust1id) over (order by cust1id, cust2id) cust1idLead,
                                       LEAD(cust2id) over (order by cust1id, cust2id) cust2idLead
                                from cte
                            ) a 
                        ) a
                    ) a 
                ) a 
            ) a where rn = 1
        )
        
        --this final query gets you correct Cust1ID and Cust2ID, as FinalResult might have them switched
        select * from @tbl
        intersect
        select * from (
            select acctnumber, dt, cust1id, cust2id from FinalResult
            union all
            select acctnumber, dt, cust2id, cust1id from FinalResult
        ) fr
        

        更新

        根据 OP 解释,此代码将曾经在一个帐户中的所有客户 ID 视为同一组(这是传递的1)),因此,对于其他情况,8a 和 8b 的结果是:

        acctnumber | dt         | Cust1ID | Cust2ID
        50004      | 2017-04-04 | 2002    | NULL
        50008      | 2017-04-04 | 2003    | NULL 
        

        因为只有 2 组!

        1) 这意味着,如果元素 a 与元素 bb 与元素 c 在同一组中,则意味着 ac 也在同一组中。

        【讨论】:

        • 查询处理器耗尽了内部资源,无法生成查询计划,因此我无法适当地测试此查询,但运行时间过长。
        【解决方案9】:

        这很复杂...

        首先,您要确定客户群。那是所有直接或间接相关的客户。例如,对于 A/B、B/C、D/E、D/F、G/A、H/A、H/F 客户对,您将只有一个组。在 SQL 中,这需要递归查询。

        SQL Server 在递归查询中缺少循环检测。因此,您可以从客户 A/B 获得包含 A 或 B 的所有对,即 B/C、A/B G/A、H/A 和 A/B 本身。甚至,如果我们检测到这个直接循环(同一对),我们会继续使用 B/C 查找包含 B 或 C 的所有记录。其中一个又是 A/B,我们​​又一次处于循环中。解决此问题的一种方法是构建一串尚未访问过的客户,并且不再访问他们。

        我们的结果是所有客户与所有直接或间接连接的其他客户。使用聚合,我们可以将每个客户的最小合作伙伴用作组键。在上面的例子中,所有客户都与 A 相关,所以 A 都是他们的最小合作伙伴,表明他们都属于同一个组。如果我们添加两条记录 X/Y 和 Z/-,那么我们还有两个组:X 和 Y 属于 X 组,Z 属于 Z 组。

        我们使用这些组再次查找原始记录。对于ROW_NUMBER,我们将每个组的最后一条记录编号为#1。然后我们只保留那些,我们就完成了。

        with all_cust(custid) as
        (
          select cust1id from mytable
          union
          select cust2id from mytable where cust2id is not null
        )
        , cte(c1, c2, sofar) as
        (
          select custid, custid, '<' + cast(custid as varchar(max)) + '>' from all_cust
          union all
          select cte.c1, case when cte.c2 = m.cust1id then m.cust2id else m.cust1id end,
             cte.sofar + '<' + cast(case when cte.c2 = m.cust1id then m.cust2id else m.cust1id end as varchar(max)) + '>'
          from mytable m
          join cte on cte.c2 in (m.cust1id, m.cust2id)
          and cte.sofar not like '%' + cast(case when cte.c2 = m.cust1id then m.cust2id else m.cust1id end as varchar(max)) + '%'
        )
        , groups(custid, grp) as
        (
          select c1, min(c2) from cte group by c1
        )
        , ranked as
        (
          select *, row_number() over (partition by g.grp order by date desc) as rn 
          from groups g
          join mytable m on g.custid in (m.cust1id, m.cust2id)
        )
        select acctnumber, date, cust1id, cust2id
        from ranked
        where rn = 1
        order by cust1id;
        

        Rextester 演示:http://rextester.com/RWCQ83881

        【讨论】:

          猜你喜欢
          • 2020-03-10
          • 1970-01-01
          • 2021-11-03
          • 2013-05-18
          • 1970-01-01
          • 2020-08-31
          • 1970-01-01
          • 1970-01-01
          • 2020-06-23
          相关资源
          最近更新 更多