规范化表格

时间:2012-03-20 20:59:26

标签: sql-server tsql

我有一张遗产表,我无法改变。 可以从遗留应用程序中修改其中的值(应用程序也无法更改)。 由于从新应用程序(新要求)中大量访问表,我想创建一个临时表,希望能加快查询速度。

实际要求是计算从X到Y的营业日数。例如,给我从2001年1月1日到2004年12月24日的所有工作日。该表用于标记哪些日期已关闭,因为不同的公司可能有不同的假期 - 它不仅仅是周六+周日)

临时表将从.NET程序创建,每次用户进入此查询的屏幕(用户可能多次运行查询,使用不同的值,表创建一次),所以我希望它是尽可能快地。下面的方法在不到一秒的时间内运行,但我只用一个小数据集测试它,但它仍然需要接近半秒,这对于UI来说并不是很好 - 尽管它只是第一次查询的开销。

遗留表如下所示:

CREATE TABLE [business_days](
    [country_code] [char](3) ,
    [state_code] [varchar](4) ,
    [calendar_year] [int] ,
    [calendar_month] [varchar](31) ,
    [calendar_month2] [varchar](31) ,
    [calendar_month3] [varchar](31) ,
    [calendar_month4] [varchar](31) ,
    [calendar_month5] [varchar](31) ,
    [calendar_month6] [varchar](31) ,
    [calendar_month7] [varchar](31) ,
    [calendar_month8] [varchar](31) ,
    [calendar_month9] [varchar](31) ,
    [calendar_month10] [varchar](31) ,
    [calendar_month11] [varchar](31) ,
    [calendar_month12] [varchar](31) ,
misc.
)

每个月有31个字符,任何休息日(星期六+星期日+节假日)都标有X.每半天标有“H”。例如,如果一个月在星期四开始,那么它将会是一个月(星期四+星期五工作日,星期六+星期日标有X):

'  XX     XX ..'

我希望新表看起来像这样:

create table #Temp (country varchar(3), state varchar(4), date datetime, hours int)

我想只关闭几天的行(从之前的查询标记为X或H)

我最终做了什么,到目前为止: 创建一个临时中间表,如下所示:

create table #Temp_2 (country_code varchar(3), state_code varchar(4), calendar_year int, calendar_month varchar(31), month_code int)

为了填充它,我有一个联盟,基本上联合calendar_month,calendar_month2,calendar_month3等。

我有一个循环遍历#Temp_2中的所有行,在处理完每一行后,它将从#Temp_2中删除。 要处理行,有一个从1到31的循环,并检查子字符串(calendar_month,counter,1)是否为X或H,在这种情况下,有一个插入#Temp表。 [编辑补充代码]

Declare @country_code char(3)
Declare @state_code varchar(4)
Declare @calendar_year int
Declare @calendar_month varchar(31)
Declare @month_code int
Declare @calendar_date datetime
Declare @day_code int
WHILE EXISTS(SELECT * From #Temp_2) -- where processed = 0)
BEGIN
    Select Top 1 @country_code = t2.country_code, @state_code = t2.state_code, @calendar_year = t2.calendar_year, @calendar_month = t2.calendar_month, @month_code = t2.month_code From #Temp_2 t2 -- where processed = 0

    set @day_code = 1
    while @day_code <= 31
    begin
        if substring(@calendar_month, @day_code, 1) = 'X'
        begin
            set @calendar_date = convert(datetime, (cast(@month_code as varchar) + '/' + cast(@day_code as varchar) + '/' + cast(@calendar_year as varchar)))
            insert into #Temp (country, state, date, hours) values (@country_code, @state_code, @calendar_date, 8)
        end
        if substring(@calendar_month, @day_code, 1) = 'H'
        begin
            set @calendar_date = convert(datetime, (cast(@month_code as varchar) + '/' + cast(@day_code as varchar) + '/' + cast(@calendar_year as varchar)))
            insert into #Temp (country, state, date, hours) values (@country_code, @state_code, @calendar_date, 4)
        end

        set @day_code = @day_code + 1
    end
    delete from #Temp_2 where @country_code = country_code AND @state_code = state_code AND @calendar_year = calendar_year AND @calendar_month = calendar_month AND @month_code = month_code
    --update #Temp_2 set processed = 1 where @country_code = country_code AND @state_code = state_code AND @calendar_year = calendar_year AND @calendar_month = calendar_month AND @month_code = month_code
END

我不是SQL的专家,所以我想对我的方法有所了解,甚至可能是一个更好的方法建议。

拥有临时表后,我打算这样做(日期将来自一张表):

select cast(convert(datetime, ('01/31/2012'), 101) -convert(datetime, ('01/17/2012'), 101) as int) -  ((select sum(hours) from #Temp where date between convert(datetime, ('01/17/2012'), 101) and convert(datetime, ('01/31/2012'), 101)) / 8)

除了对表进行规范化的解决方案之外,我现在实现的另一个解决方案是通过扫描当前表来完成所有这些逻辑以获取工作日的功能。它的运行速度非常快,但是如果我可以添加一个更简单的查询来获得结果,我会对调用函数犹豫不决。

(我目前正在MSSQL上尝试这个,但我需要为Sybase ASE和Oracle做同样的事情)

2 个答案:

答案 0 :(得分:2)

这应符合要求,“......计算从X到Y的营业日数。”

它将每个空间计为一个工作日,将X或空间以外的任何空间计为半天(根据OP,应该只是H)。

我在SQL Server 2008 R2中删除了它:

-- Calculate number of business days from X to Y
declare @start date = '20120101' -- X
declare @end date = '20120101' -- Y
-- Outer query sums the length of the full_year text minus non-work days
-- Spaces are doubled to help account for half-days...then divide by two
select sum(datalength(replace(replace(substring(full_year, first_day, last_day - first_day + 1), ' ', '  '), 'X', '')) / 2.0) as number_of_business_days
from (
    select
        -- Get substring start value for each year
        case
                when calendar_year = datepart(yyyy, @start) then datepart(dayofyear, @start)
                else 1
            end as first_day
        -- Get substring end value for each year
        , case
                when calendar_year = datepart(yyyy, @end) then datepart(dayofyear, @end)
                when calendar_year > datepart(yyyy, @end) then 0
                when calendar_year < datepart(yyyy, @start) then 0
                else datalength(full_year)
            end as last_day
        , full_year
    from (
        select calendar_year
            -- Get text representation of full year
            , calendar_month
            + calendar_month2
            + calendar_month3
            + calendar_month4
            + calendar_month5
            + calendar_month6
            + calendar_month7
            + calendar_month8
            + calendar_month9
            + calendar_month10
            + calendar_month11
            + calendar_month12 as full_year
        from business_days
        -- where country_code = 'USA' etc.
    ) as get_year
) as get_days

where子句可以在最内部的查询中进行。

这不是传统格式的非枢纽,OP花费了大量时间,并且可能需要更多(可能不必要的)计算周期。我假设这样的事情“很高兴看到”而不是部分要求。关于tally table在这种情况下如何帮助,Jeff Moden有很多文章(对于SQL Server,无论如何)。

根据特定DBMS的设置方式,可能需要观察尾随空格(注意我使用的是数据长度而不是len)。

更新:添加了OP请求的临时表:

select country_code
    , state_code
    , dateadd(d, t.N - 1, cast(cast(a.calendar_year as varchar(8)) as date)) as calendar_date
    , case substring(full_year, t.N, 1) when 'X' then 0 when 'H' then 4 else 8 end as business_hours
from (
    select country_code
        , state_code
        , calendar_year
        , calendar_month
            + calendar_month2
            + calendar_month3
            + calendar_month4
            + calendar_month5
            + calendar_month6
            + calendar_month7
            + calendar_month8
            + calendar_month9
            + calendar_month10
            + calendar_month11
            + calendar_month12
            as full_year
    from business_days
) as a, (
        select a.N + b.N * 10 + c.N * 100 + 1 as N
        from (select 0 as N union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) a
            , (select 0 as N union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) b
            , (select 0 as N union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) c
    ) as t -- cross join with Tally table built on the fly
where t.N <= datalength(a.full_year)

答案 1 :(得分:0)

鉴于您的临时表创建速度很慢,您是否可以预先计算它?

如果你能够在现有表上放置一个触发器,也许你可以触发一个将丢弃并创建临时表的proc。或者有一个代理程序作业,它检查现有表是否已更新(在某处引发标记),然后重新计算临时表。

现有表的结构非常糟糕,如果将其标准化总是很昂贵,我也不会感到惊讶。预先计算是解决该问题的简单方法。