2012-02-17 38 views
3

这里进行切换的情况是:我有一个页面,我可以一次编辑多个记录(假设发票)。它们没有在下面显示,但它们被编辑为一个记录。在页面加载时,只有当它们在整个记录集中相同时,值才会显示在给定字段中。查找其对记录位置可以安全地在组合组记录

现在,最重要的是,我也想编辑子记录(1-n的关系;发票的线)。我设法证明发票行记录是相同throughought所有编辑的发票,像这样:

Invoice 1  Invoice 2     Lines edited 
    A    B        D 
    D    C        G 
    F    D    =>   
    G    E 
       G 

假设A,B,......是发票行

订购事宜,透过线的发票,因此发票的每一行都有一个位置字段。这是我想要做的:允许重新排列发票的行。编辑一张发票时,这是一件容易的事。但是,当一次编辑多个发票时,会出现一些问题。考虑以下几点:

Invoice 1  Invoice 2     Lines edited 
    F(1)    B(1)       F(1) 
    A(2)    C(2)       B(2) 
    B(3)    D(3)    =>   C(3) 
    C(4)    F(4) 
    E(5) 

当线B后移动线F,在发票1,F不仅B之后,还A之后,并且用户不知道它移动;在发票2中,F已​​经在B之后,但用户不知道它。所以B应该放在F(在位置3)之前还是留在原来的位置?这不清楚。

我想要做的是防止重新排序时的行为是不明确(或意外),并允许它在其他情况下。这是我的解决方案:对于发票的每一行,找到它是否可以向上移动一个步骤(position--),以及是否可以向下移动一个步骤(位置++)。怎么样 ?对于在编辑页面中的每个邻居对线(在本例中:F-B; B-C)中,如果在源发票线对应的对是邻居并以相同的顺序中,一对可以切换。因此,在本例中,这意味着B和C可以切换,但不F和B.因此结果将是:

Lines edited move down  move up 
    F    no   no 
    B    yes   no 
    C    no   yes 

这里是或多或少我目前的状况:

CREATE TABLE [InvoiceLine](
    [id] [int] IDENTITY(1,1) NOT NULL, 
    [invoiceId] [int] NOT NULL, 
    [position] [int] NOT NULL, 
    [text] [nvarchar](255) NULL, 
    [price] [decimal](18,2) NULL, 
    CONSTRAINT [PK_InvoiceLine] PRIMARY KEY CLUSTERED ([id] ASC) ON [PRIMARY] 
) 

CREATE TABLE [Invoice](
    [id] [int] IDENTITY(1,1) NOT NULL, 
    [customerId] [int] NULL, 
    CONSTRAINT [PK_Invoice] PRIMARY KEY CLUSTERED ([id] ASC) ON [PRIMARY] 
) 

INSERT INTO [Invoice]([customerId]) VALUES 
(1000), 
(2000); 

INSERT INTO [InvoiceLine]([invoiceId],[position],[text],[price]) VALUES 
(1000,1,'F',10.5), 
(1000,2,'A',3.0), 
(1000,3,'B',4.0), 
(1000,4,'C',1.0), 
(1000,5,'E',1.0), 

(2000,1,'B',4.15), 
(2000,2,'C',1.35), 
(2000,3,'D',1.20), 
(2000,4,'F',12.10); 

DECLARE @ids TABLE(n int); 
INSERT INTO @ids (n) VALUES (1000),(2000); 

DECLARE @n int; 
SET @n = (SELECT COUNT(*) FROM @ids); 

SELECT 
    CAST(text AS nvarchar) AS id, 
    CASE WHEN rank_position = _rankMain THEN position ELSE NULL END AS position, 
    CASE WHEN rank_text = _rankMain THEN text ELSE NULL END AS text, 
    CASE WHEN rank_price = _rankMain THEN price ELSE NULL END AS price, 
    0 AS isRecordDeleted 
FROM (
    SELECT 
     T4.position, RANK() OVER (PARTITION BY T4.text, T4.position ORDER BY id) AS rank_position, 
     T4.text, RANK() OVER (PARTITION BY T4.text ORDER BY id) AS rank_text, 
     T4.price, RANK() OVER (PARTITION BY T4.text, T4.price ORDER BY id) AS rank_price, 
     RANK() OVER (PARTITION BY T4.text ORDER BY id) AS _rankMain, 
     _cnt 
    FROM 
    (
     -- Filter lines 
     SELECT 
      text, 
      (
       SELECT COUNT(id) FROM InvoiceLine WHERE invoiceId IN (SELECT * FROM @ids) AND text = T2.text 
      ) AS _cnt FROM 
     (
      -- add rank on text field (an invoice line is considered equal to another one if both text fields are equal) 
      SELECT RANK() OVER (PARTITION BY text ORDER BY invoiceId) AS rnk, text FROM 
      (
       -- distinct lines 
       SELECT DISTINCT invoiceId, text FROM InvoiceLine WHERE invoiceId IN (SELECT n FROM @ids) 
      ) T1 
     ) T2 
     WHERE rnk = (SELECT COUNT(n) FROM @ids) 
    ) T3 INNER JOIN InvoiceLine T4 ON T4.text = T3.text 
) T5 WHERE _cnt = _rankMain ORDER BY position 

我问题是:

我应该如何变换查询(实际存储过程)获得“向上”和“向下移动”领域?

我得到的第一个想法是从结果中获取所有相邻对,并在每个源发票行中找到它们的距离(距离是位置差的绝对值),并取距离的最大值。如果最大值等于1,并且差异全部具有相同的符号,则可以切换该对的位置。但我不知道怎么翻译,为SQL ...

[编辑]一两件事:顶编辑的记录应始终都动起来=没有,而最低的编辑的记录应该始终有下移=无。

[EDIT 2012-02-23]添加了ORDER BY在查询

[EDIT 2012-02-23] 这里的端部的第二组数据的其预期的输出:

INSERT INTO [Invoice]([customerId]) VALUES 
(1000), 
(2000), 
(3000); 

INSERT INTO [InvoiceLine]([invoiceId],[position],[text],[price]) VALUES 
    (1000,1,'F',10.5), 
    (1000,2,'A',3.0), 
    (1000,3,'B',4.0), 
    (1000,4,'C',1.0), 
    (1000,5,'E',1.0), 
    (1000,6,'G',4.2), 
    (1000,7,'H',9.0), 
    (1000,8,'K',9.0), 

    (2000,1,'B',4.15), 
    (2000,2,'C',1.35), 
    (2000,3,'D',1.20), 
    (2000,4,'F',12.10), 
    (2000,6,'G',4.2), 
    (2000,7,'H',2.7), 
    (2000,8,'I',1.3), 

    (3000,1,'B',41.15), 
    (3000,2,'C',15.35), 
    (3000,3,'D',12.20), 
    (3000,4,'F',11.10), 
    (3000,5,'I',4.0), 
    (3000,6,'G',4.2), 
    (3000,7,'H',6.7), 
    (3000,8,'E',7.3); 


DECLARE @ids TABLE(n int); 
INSERT INTO @ids (n) VALUES (1000),(2000),(3000); 

应该产生:

id position text price isRecordDeleted moveUp moveDown 
B  NULL  B NULL   0   no  yes 
C  NULL  C NULL   0   yes  no 
F  NULL  F NULL   0   no  no 
G  6  G 4.20   0   no  yes 
H  7  H NULL   0   yes  no 

[编辑2012-02-24] 和重复行应只出现一次,并具有为moveUp和下移,只有当他们是直接邻居

这里是第三组数据的其预期输出:

INSERT INTO [Invoice]([customerId]) VALUES 
(1000), 
(2000), 
(3000); 

INSERT INTO [InvoiceLine]([invoiceId],[position],[text],[price]) VALUES 
    (1000,1,'F',10.5), 
    (1000,2,'A',3.0), 
    (1000,3,'B',4.0), 
    (1000,4,'C',1.0), 
    (1000,5,'E',1.0), 
    (1000,6,'J',3.2), 
    (1000,7,'G',4.2), 
    (1000,8,'H',9.0), 
    (1000,9,'K',9.0), 
    (1000,10,'F',3.0), 

    (2000,1,'B',4.15), 
    (2000,2,'C',1.35), 
    (2000,3,'D',1.20), 
    (2000,4,'C',1.35), 
    (2000,5,'F',12.10), 
    (2000,6,'J',6.2), 
    (2000,7,'G',4.2), 
    (2000,8,'H',2.7), 
    (2000,9,'H',3.1), 
    (2000,10,'I',1.3), 

    (3000,1,'B',41.15), 
    (3000,2,'C',15.35), 
    (3000,3,'D',12.20), 
    (3000,4,'F',11.10), 
    (3000,5,'I',4.0), 
    (3000,6,'J',2.3), 
    (3000,7,'G',4.2), 
    (3000,8,'H',6.7), 
    (3000,9,'E',7.3); 


DECLARE @ids TABLE(n int); 
INSERT INTO @ids (n) VALUES (1000),(2000),(3000); 

应该产生:

id position text price isRecordDeleted moveUp moveDown 
B  NULL  B NULL   0   no  no 
C  NULL  C NULL   0   no  no 
F  NULL  F NULL   0   no  no 
J  6  J NULL   0   no  yes 
G  7  G 4.20   0   yes  no 
H  NULL  H NULL   0   no  no 

或者更好:

id position text price isRecordDeleted moveUp moveDown 
B  NULL  B NULL   0   no  no 
C  NULL  C NULL   0   no  no 
F  NULL  F NULL   0   no  no 
J  6  J NULL   0   no  yes 
G  7  G 4.20   0   yes  yes 
H  NULL  H NULL   0   yes  no 

[EDIT 2012-03-02]

第二个结果是更好的,因为虽然中的H发票2000中出现两次,这两条线是邻居,因此它是安全的切换的h和g的位置:均为H线将被切换。

但是这最后的结果可能会导致过于复杂的查询。

+0

+1将表结构和数据作为脚本提供给我们。当您尝试回答问题时,您会惊讶于节省多少时间。 – 2012-02-17 13:22:06

+0

难以帮助,因为我无法想象用户正在使用您的界面。这是什么类型的应用程序,目标是什么?为什么用户一次编辑多个发票?他们是否在发票下看到所有这些行以及“编辑过的行”?当值在给定的“字段”中显示时......您指的是什么“字段”? “编辑过的行”是指所有发票中常见的行吗?这个列表是否因使用界面而改变?为什么用户需要知道编辑过的行?当用户“移动一条线”时,他们是否在发票内水平拖放? – sisdog 2012-02-18 07:06:17

+0

@sisdog,答案1:其实,我正在创建类似于应用程序生成器的东西(更像是脚手架,现在只是部分)。所以这应该适用于1-n关系的一般情况。发票中可以包含n行发票的发票就是这种情况。 – Daniel 2012-02-20 08:20:55

回答

1

这应该工作。

DECLARE @ids TABLE(n int); 
INSERT INTO @ids (n) VALUES (1000); 
INSERT INTO @ids (n) VALUES (2000); 
INSERT INTO @ids (n) VALUES (3000); 

SELECT il.* 
INTO #InvoiceLineQuery 
FROM [InvoiceLine] il 
INNER JOIN @ids i ON il.invoiceId = i.n 


SELECT letter.text id, 
     CASE WHEN pos.maxPos = pos.minPos THEN pos.maxPos ELSE NULL END AS position, 
     letter.text, 
     CASE WHEN price.maxPrice = price.minPrice THEN price.maxPrice ELSE NULL END AS price, 
     0 AS isRecordDeleted, 
     CASE WHEN moveUP.text IS NOT NULL THEN 'yes' ELSE 'no' END AS moveUP, 
     CASE WHEN moveDOWN.text IS NOT NULL THEN 'yes' ELSE 'no' END AS moveDown 
FROM (SELECT il.text 
     FROM #InvoiceLineQuery il 
     GROUP BY il.text 
     HAVING count(il.position) = (SELECT count(1) FROM @ids)) letter 
INNER JOIN (SELECT il.text, MAX(il.position) maxPos, MIN(il.position) minPos 
      FROM #InvoiceLineQuery il 
      GROUP BY il.text) pos on letter.text = pos.text 
INNER JOIN (SELECT il.text, MAX(il.price) maxPrice, MIN(il.price) minPrice 
      FROM #InvoiceLineQuery il 
      GROUP BY il.text) price on letter.text = price.text 
LEFT JOIN (SELECT upNeighbour.text 
      FROM (SELECT mainLine.text text, upLine.text upText 
        FROM #InvoiceLineQuery mainLine 
        INNER JOIN #InvoiceLineQuery upLine ON mainLine.position = upLine.Position + 1 
               AND mainLine.invoiceId = upLine.invoiceId) upNeighbour 
      GROUP BY upNeighbour.text,upNeighbour.upText 
      HAVING count(upNeighbour.upText) = (SELECT count(1) FROM @ids)) moveUP ON letter.text = moveUP.text 
LEFT JOIN (SELECT downNeighbour.text 
      FROM (SELECT mainLine.text text, downLine.text downText 
        FROM #InvoiceLineQuery mainLine 
        INNER JOIN #InvoiceLineQuery downLine ON mainLine.position + 1 = downLine.Position 
               AND mainLine.invoiceId = downLine.invoiceId) downNeighbour 
      GROUP BY downNeighbour.text,downNeighbour.downText 
      HAVING count(downNeighbour.downText) = (SELECT count(1) FROM @ids)) moveDOWN ON letter.text = moveDOWN.text 


DROP TABLE #InvoiceLineQuery 
+0

感谢您的答案,但它不会产生正确的结果。对于给定的数据,C行应该有moveDown = no,但是您的查询使其成为yes。我还没有探索你的查询,所以也许它只是部分错误... – Daniel 2012-02-23 13:06:22

+0

实际上,与其他数据集(请参阅我当天的最后一次编辑),结果是不正确的。所以,不,现在还不行。感谢您的时间,虽然...... – Daniel 2012-02-23 13:40:59

+0

如果我理解你,只有当他们的上邻居在所有发票中相同时,线条才能向上移动。它和下邻居一样工作? – RealUlysse 2012-02-23 14:36:13

1

好吧,我已经失去了睡眠。我想这样的作品:

;with CommonLines as (
    select [text], 
     MIN(position) as minPos,MAX(position) as maxPos, 
     MIN(price) as minPrice,MAX(price) as maxPrice 
    from @InvoiceLine 
    where invoiceId in (select n from @ids) 
    group by [text] 
    having COUNT(*) = (select COUNT(*) from @ids) 
), InvoiceOrders as (
    select invoiceId,[text],ROW_NUMBER() OVER (PARTITION BY InvoiceId order by Position) as rn 
    from @InvoiceLine where [text] in (select [text] from CommonLines) and 
    invoiceId in (select n from @ids) 
), AlwaysAdjacent as (--Ignoring lines that aren't going to appear at all 
    select a1.[text] as FirstText,a2.[text] as SecondText,ROW_NUMBER() OVER (ORDER BY a1.[text]) as Ord 
    from 
     InvoiceOrders a1 
      inner join 
     InvoiceOrders a2 
      on 
       a1.invoiceId = a2.invoiceId and 
       a1.rn = a2.rn - 1 
    group by a1.text,a2.text 
    having COUNT(*) = (select COUNT(*) from @ids) 
) 
select 
    cl.[text], 
    CASE WHEN aa1.Ord IS NOT NULL THEN 1 ELSE 0 END as MoveDown, 
    CASE WHEN aa2.Ord IS NOT NULL THEN 1 ELSE 0 END as MoveUp, 
    CASE WHEN minPrice=maxPrice THEN minPrice END as price, 
    CASE WHEN minPos=maxPos THEN minPos END as Position 
from 
    CommonLines cl 
     left join 
    AlwaysAdjacent aa1 
     on 
      cl.[text] = aa1.FirstText 
     left join 
    AlwaysAdjacent aa2 
     on 
      cl.[text] = aa2.SecondText 
order by 
    COALESCE(aa2.Ord,aa1.Ord), 
    CASE 
     WHEN aa1.Ord IS NOT NULL THEN 0 
     WHEN aa2.Ord IS NOT NULL THEN 1 
     ELSE 2 END 

我把发票数据插入到表变量也,而忽视了发票,因为它似乎并不相关 - 设置数据如下。

有希望,这是合理的容易阅读,但无论如何一些解释。 CommonLinesInvoiceLine进行关系部门,找到我们将要使用的textInvoiceOrders然后,对于每张发票,计算这些text出现的顺序 - 此排序忽略任何其他行,并且仅基于position

AlwaysAdjacent接着执行另一个关系划分,有使用InvoiceOrders两次,以确定出现在所有的发票中相同的(相对)位置那些text值 - 即,它们的排序之间的差为常数,即使绝对值是不同的。

最后,我们输出文本,并使用AlwaysAdjacent来确定是否允许向下移动或向上移动。最后的ORDER BY有点棘手,但我认为是正确的 - 它试图容纳比2更长的运行,其中中间行可以向上和向下移动(即,在1001和F交换FE的位置应该C和G,以及整个集可以被重新排序)之间出现

结果:

text MoveDown MoveUp  price         Position 
---- ----------- ----------- --------------------------------------- ----------- 
F 0   0   NULL         NULL 
B 1   0   NULL         NULL 
C 0   1   NULL         NULL 
G 1   0   4.20         6 
H 0   1   NULL         7 

设置数据:

declare @InvoiceLine table (invoiceId int not null,position int not null,[text] char(1) not null,price decimal(12,2) not null) 

INSERT INTO @InvoiceLine([invoiceId],[position],[text],[price]) VALUES 
    (1000,1,'F',10.5), 
    (1000,2,'A',3.0), 
    (1000,3,'B',4.0), 
    (1000,4,'C',1.0), 
    (1000,5,'E',1.0), 
    (1000,6,'G',4.2), 
    (1000,7,'H',9.0), 
    (1000,8,'K',9.0), 

    (2000,1,'B',4.15), 
    (2000,2,'C',1.35), 
    (2000,3,'D',1.20), 
    (2000,4,'F',12.10), 
    (2000,6,'G',4.2), 
    (2000,7,'H',2.7), 
    (2000,8,'I',1.3), 

    (3000,1,'B',41.15), 
    (3000,2,'C',15.35), 
    (3000,3,'D',12.20), 
    (3000,4,'F',11.10), 
    (3000,5,'I',4.0), 
    (3000,6,'G',4.2), 
    (3000,7,'H',6.7), 
    (3000,8,'E',7.3); 

DECLARE @ids TABLE(n int); 
INSERT INTO @ids (n) VALUES (1000),(2000),(3000); 
+0

感谢您的回答。它适用于该数据,但在仅选择1000和2000的发票时不起作用。结果表明C和F可以切换,而F和G可以切换,这是错误的。 – Daniel 2012-02-24 10:06:28

+0

我刚刚更新了我的答案(因为我正在努力添加额外的列),但尚未查看您的评论。我会看看 – 2012-02-24 10:15:26

+0

@Daniel - 我在“InvoiceLines' CTE上犯了一个错误 - 它应该仅限于感兴趣的发票,所以我在”WHERE“子句中添加了一个额外的过滤器。另外,我在价格和位置列中添加了。 – 2012-02-24 10:20:41

0

根据Damien_The_Unbeliever的回答,这里是第三组数据结果1的解决方案(结果2的解决方案会更好):

DECLARE @InvoiceLine TABLE (
    [id] [int] IDENTITY(1,1) NOT NULL, 
    [invoiceId] [int] NOT NULL, 
    [position] [int] NOT NULL, 
    [text] [nvarchar](255) NULL, 
    [price] [decimal](18,2) NULL 
) 

DECLARE @Invoice TABLE (
    [id] [int] IDENTITY(1,1) NOT NULL, 
    [customerId] [int] NULL 
) 

INSERT INTO @Invoice([customerId]) VALUES 
(1000), 
(2000), 
(3000); 

INSERT INTO @InvoiceLine([invoiceId],[position],[text],[price]) VALUES 
(1000,1,'F',10.5), 
(1000,2,'A',3.0), 
(1000,3,'B',4.0), 
(1000,4,'C',1.0), 
(1000,5,'E',1.0), 

(2000,1,'B',4.15), 
(2000,2,'C',1.35), 
(2000,3,'D',1.20), 
(2000,4,'F',12.10); 

DECLARE @ids TABLE(n int); 
INSERT INTO @ids (n) VALUES (1000),(2000); 

;WITH Lines AS (
    SELECT 
     CAST(text AS nvarchar) AS id, 
     position AS pos, 
     CASE WHEN rank_position = _rankMain THEN position ELSE NULL END AS position, 
     CASE WHEN rank_text = _rankMain THEN text ELSE NULL END AS text, 
     CASE WHEN rank_price = _rankMain THEN price ELSE NULL END AS price, 
     0 AS isRecordDeleted 
    FROM (
     SELECT 
      T4.position, RANK() OVER (PARTITION BY T4.text, T4.position ORDER BY id) AS rank_position, 
      T4.text, RANK() OVER (PARTITION BY T4.text ORDER BY id) AS rank_text, 
      T4.price, RANK() OVER (PARTITION BY T4.text, T4.price ORDER BY id) AS rank_price, 
      RANK() OVER (PARTITION BY T4.text ORDER BY id) AS _rankMain, 
      _cnt 
     FROM 
     (
      -- Filter lines 
      SELECT 
       text, 
       (
        SELECT COUNT(id) FROM @InvoiceLine WHERE invoiceId IN (SELECT * FROM @ids) AND text = T2.text 
       ) AS _cnt FROM 
      (
       -- add rank on text field (an invoice line is considered equal to another one if both text fields are equal) 
       SELECT RANK() OVER (PARTITION BY text ORDER BY invoiceId) AS rnk, text FROM 
       (
        -- distinct lines 
        SELECT DISTINCT invoiceId, text FROM @InvoiceLine WHERE invoiceId IN (SELECT n FROM @ids) 
       ) T1 
      ) T2 
      WHERE rnk = (SELECT COUNT(n) FROM @ids) 
     ) T3 INNER JOIN @InvoiceLine T4 ON T4.text = T3.text 
    ) T5 WHERE _cnt = _rankMain 
), 
LinesNoDuplicates AS (-- All lines of invoice from the selected invoices that aren't duplicated in any of those invoices 
    SELECT * FROM @InvoiceLine 
    WHERE 
     text NOT IN(-- Exclude texts that appear twice in an invoice 
      SELECT text FROM (-- texts that appear twice in an invoice 
       SELECT text, RANK() OVER (PARTITION BY text, invoiceId ORDER BY position) as rnk FROM @InvoiceLine WHERE invoiceId IN (SELECT n FROM @ids) 
      ) T1 WHERE rnk = 2 
     ) 
     AND 
     invoiceId IN (SELECT n FROM @ids) 

), 
Switchables AS (-- Which lines can be switched 
    SELECT 
     UpperLine.text UpperText, 
     LowerLine.text LowerText 
    FROM 
     LinesNoDuplicates UpperLine INNER JOIN 
     LinesNoDuplicates LowerLine ON UpperLine.invoiceId = LowerLine.invoiceId AND UpperLine.position = LowerLine.position - 1 
    GROUP BY 
     UpperLine.text, 
     LowerLine.text 
    HAVING COUNT(*) = (SELECT COUNT(*) FROM @ids) 
) 
SELECT id, position, text, price, isRecordDeleted, 
    CASE WHEN SwitchUp.LowerText IS NOT NULL THEN 'yes' ELSE 'no' END AS moveUp, 
    CASE WHEN SwitchDown.UpperText IS NOT NULL THEN 'yes' ELSE 'no' END AS moveDown 
FROM 
    Lines LEFT JOIN 
    Switchables SwitchUp ON text = SwitchUp.LowerText LEFT JOIN 
    Switchables SwitchDown ON text = SwitchDown.UpperText 
ORDER BY pos;