在设计糟糕的架构中进行复杂查询的替代方法

时间:2010-02-11 12:19:24

标签: sql-server tsql optimization

我正在努力将来自第三方系统的一些数据集成到我的一个应用程序中(传统的基于ASP Classic的Web应用程序/ SQL 2000) - 当他们采用他们的方法时,他们做出了一些糟糕的决定(恕我直言)和数据结构,虽然也许我们可能有机会在某些时候进行重构......但在那之前,我必须使用在我面前的东西。

主表保存检查数据,其中一个字段用于记录是否观察到某些特征。这些特征存储在名为Categories的表中,但遗憾的是,主要检查表(Test)通过将相关CategoryID连接到单个字段(SelectedCategories来链接到类别})。因此,例如,如果观察到特征01和02,SelectedCategories中该行的Test列的值将为'01C02C'。

修剪DDL:

CREATE TABLE [dbo].[Test](
[ItemID] [varchar](255) NOT NULL,
[Result] [varchar](255) NULL,
[Comments] [varchar](255) NULL,
[ResultReason] [varchar](255) NULL,
[ImageLocation] [varchar](255) NULL,
[TestDateTime] [smalldatetime] NOT NULL,
[SelectedCategories] [varchar](255) NULL)

问题是,鉴于这种情况,我如何才能最好地从Test中提取数据,并对所观察到的特征进行细分?

我想要的客户端上的输出是一个包含以下列的表: Test.PK,Test.Field2 ... Test.Fieldn,Categories.ID1,Categories.ID2,Categories.IDn

这可能不够清楚 - 第一个字段是来自Test的常见嫌疑人,后面是Categories中每个类别的勾号或交叉(或其他视觉指示符)。

显然,如果这可以在一个查询中实现,那么在效率和性能方面就更好了。但是,我不确定如何实现这一目标 - 您将如何通过Categories加入SelectedCategories表格?

我显然可以简单地报告SelectedCategories值并让应用程序解析该值。这可能是硬编码的,或者我们更有可能为Test中的每一行重新查询Categories - 尽管这会产生性能影响。 TBH,在这种情况下表现可能不是问题,但仅仅因为你可以逃避某些事情,并不意味着你应该养成它的习惯。

同样,如果我有机会重构第三方应用程序,我会删除SelectedCategories列并添加TestCategories表吗?或者我会将每个类别硬编码为一系列Bit列。很有可能,Categories在系统的整个生命周期中都不会发生变化,但如果它们发生了变化,则意味着DB和应用程序都会发生变化(尽管很小)。

我希望我已经足够清楚地解释了它。从本质上讲,我说如果我坚持使用当前的系统,最好的方法是什么?如果我要重构,我可以采取什么不同的方法?

进度更新:

非常感谢列文,我到目前为止:

DML:

SELECT  c.ID, c.Category, t.FilterID, t.OperatorResult, t.SelectedCategories
FROM    dbo.Categories c
        inner JOIN dbo.Test t ON CHARINDEX(Cast(c.ID as varchar), t.SelectedCategories, 1) <> 0
order by FilterID, ID

输出:

ID   Category             FilterID   OperatorResult   SelectedCategories
4    Cracked Ceramic      137667     FAILED           04C
4    Cracked Ceramic      284821     FAILED           04C
4    Cracked Ceramic      287617     FAILED           04C05C
5    Damaged Case         287617     FAILED           04C05C
4    Cracked Ceramic      310112     FAILED           04C05C
5    Damaged Case         310112     FAILED           04C05C

这就足够了,除了为了达到我想要的屏幕输出......

Filter ID  Operator Result    Cat Matl   Crack    Damage   High Soot   
137667     FAILED             X          X        
178643     FAILED           
284821     FAILED                        X        
287617     FAILED                                 X        X     
310112     FAILED                        X        X    

...我要么需要进一步研究SQL(这样我只需要在一个查询中实现所需的输出),或者我需要在应用程序本身做一些额外的工作。

结论:

如果我们看一下Lieven的最新例子(如下),我们可以看到问题可以在TSQL中解决,但是类别是硬编码的。

替代方案是坚持原始数据并使IIS / ASP做更多工作。它会使源代码复杂化,但如果添加或删除类别,将消除更新TSQL的潜在开销。我当然可以忍受更新TSQL这种非常偶然的需求,但我正在考虑类别表将定期主动更改的不同问题。

1 个答案:

答案 0 :(得分:3)

通过Categories加入SelectedCategories表可以像这样工作

修改

要考虑的一些事情

  • 虽然可以通过group by和all使用交叉表功能,但在客户端应用程序中处理此功能可能会更好。
  • 您在问题中提供的输入与您提供的输出不符。我已经使用了你的输入并假设了一些关于请求输出的东西。我假设 Cat Matl High Soot 只是类别的其他可能值。

让我们知道它是否适合您。

BEGIN TRAN

CREATE TABLE [dbo].[Categories](
[CategorieID] INTEGER NOT NULL)

CREATE TABLE [dbo].[Test](
[ItemID] [varchar](255) NOT NULL,
[Result] [varchar](255) NULL,
[Comments] [varchar](255) NULL,
[ResultReason] [varchar](255) NULL,
[ImageLocation] [varchar](255) NULL,
[TestDateTime] [smalldatetime] NOT NULL,
[SelectedCategories] [varchar](255) NULL)

INSERT INTO dbo.Categories VALUES (4)
INSERT INTO dbo.Categories VALUES (5)


INSERT INTO dbo.Test VALUES (137667, 'FAILED', NULL, 'Cracked Ceramic', NULL, GetDate(), '04C')
INSERT INTO dbo.Test VALUES (284821, 'FAILED', NULL, 'Cracked Ceramic', NULL, GetDate(), '04C')
INSERT INTO dbo.Test VALUES (287617, 'FAILED', NULL, 'Cracked Ceramic', NULL, GetDate(), '04C05C')
INSERT INTO dbo.Test VALUES (287617, 'FAILED', NULL, 'Damaged Case'   , NULL, GetDate(), '04C05C')
INSERT INTO dbo.Test VALUES (310112, 'FAILED', NULL, 'Cracked Ceramic', NULL, GetDate(), '04C05C')
INSERT INTO dbo.Test VALUES (310112, 'FAILED', NULL, 'Damaged Case'   , NULL, GetDate(), '04C05C')

SELECT  [Filter ID] = t.ItemID
        , [Operator Result] = t.Result
        , [Reason] = t.ResultReason
INTO    #Output
FROM    dbo.Categories c                  
        LEFT OUTER JOIN dbo.Test t ON 
          /* Search for "C<{00}CategorieID>C" */
          CHARINDEX('C'                                                       -- Prefix CategorieID & SelectedCategories with 'C'
                      + REPLICATE('0', 2 - LEN(CAST(CategorieID AS VARCHAR))) -- Left Pad CategorieID with '0'
                      + CAST(CategorieID AS VARCHAR)                          -- Add CategorieID itself
                      + 'C'                                                   -- Suffix search string with 'C'.
                    , 'C' + t.SelectedCategories                              -- Prefix CategorieID & SelectedCategories with 'C'
                    , 1) <> 0

SELECT    [Filter ID]
          , [Operator Result]
          , [Cat Matl] = CASE WHEN [Cat Matl] = 1 THEN 'X' ELSE '' END
          , [Crack] = CASE WHEN [Crack] = 1 THEN 'X' ELSE '' END
          , [Damage] = CASE WHEN [Damage] = 1 THEN 'X' ELSE '' END
          , [High Soot] = CASE WHEN [High Soot] = 1 THEN 'X' ELSE '' END
FROM      (
            SELECT    [Filter ID]
                      , [Operator Result]
                      , [Cat Matl] = MAX(CASE WHEN Reason = 'Cat Matl' THEN 1 ELSE 0 END) 
                      , [Crack] = MAX(CASE WHEN Reason = 'Cracked Ceramic' THEN 1 ELSE 0 END) 
                      , [Damage] = MAX(CASE WHEN Reason = 'Damaged Case' THEN 1 ELSE 0 END) 
                      , [High Soot] = MAX(CASE WHEN Reason = 'High Soot' THEN 1 ELSE 0 END) 
            FROM      #Output
            GROUP BY  [Filter ID]
                      , [Operator Result]
          ) o

DROP TABLE #Output

ROLLBACK TRAN