📅 Why You Should Create a Permanent Calendar Table in SQL Server

Creating a permanent calendar table (also known as a date dimension) is a best practice for production systems that involve reporting, scheduling, or time-based analytics. It simplifies logic, improves performance, and makes queries more readable.


🧱 What Is a Calendar Table?

A calendar table is a user-defined table with one row per date and columns for various date-related attributes.

Example Schema

CREATE TABLE dbo.Calendar (
    CalendarDate     DATE PRIMARY KEY,
    Year             INT,
    Month            INT,
    Day              INT,
    Weekday          INT, -- 1=Sunday, 7=Saturday
    DayOfWeekName    VARCHAR(10),
    IsWeekend        BIT,
    IsHoliday        BIT,
    YearMonth        CHAR(6), -- e.g. '202504'
    Quarter          INT,
    DayOfYear        INT,
    WeekOfYear       INT,
    FiscalYear       INT,
    FiscalPeriod     INT
    -- Add more fields as needed
);

✅ Benefits

FeatureBenefit
🧠 ReadabilityEasier to understand and maintain date-related logic
🕵️‍♂️ SimplicityAvoids complex DATEPART or CASE expressions
🚀 PerformancePrecomputed attributes save CPU and memory during query time
📅 FlexibilityAdd columns like fiscal period, holiday flag, ISO week, etc.
📊 BI IntegrationWorks seamlessly with Power BI, SSAS, Tableau, and other tools

🧠 Example Use Case

SELECT 
    c.YearMonth,
    COUNT(*) AS WorkdayCount
FROM dbo.Calendar c
WHERE c.CalendarDate BETWEEN '2024-07-01' AND '2025-06-30'
  AND c.IsWeekend = 0
  AND c.IsHoliday = 0
GROUP BY c.YearMonth
ORDER BY c.YearMonth;

🔧 How to Populate It

Here’s how to generate dates and populate the calendar:

DECLARE @StartDate DATE = '2020-01-01';
DECLARE @EndDate   DATE = '2030-12-31';

WITH DateList AS (
    SELECT @StartDate AS CalendarDate
    UNION ALL
    SELECT DATEADD(DAY, 1, CalendarDate)
    FROM DateList
    WHERE CalendarDate < @EndDate
)
INSERT INTO dbo.Calendar (
    CalendarDate, Year, Month, Day, Weekday, DayOfWeekName,
    IsWeekend, YearMonth, Quarter, DayOfYear, WeekOfYear
)
SELECT 
    CalendarDate,
    YEAR(CalendarDate),
    MONTH(CalendarDate),
    DAY(CalendarDate),
    DATEPART(WEEKDAY, CalendarDate),
    DATENAME(WEEKDAY, CalendarDate),
    CASE WHEN DATENAME(WEEKDAY, CalendarDate) IN ('Saturday', 'Sunday') THEN 1 ELSE 0 END,
    FORMAT(CalendarDate, 'yyyyMM'),
    DATEPART(QUARTER, CalendarDate),
    DATEPART(DAYOFYEAR, CalendarDate),
    DATEPART(WEEK, CalendarDate)
FROM DateList
OPTION (MAXRECURSION 0);

🧩 Add Holiday Info

Assuming you maintain a dbo.Holidays table:

UPDATE c
SET c.IsHoliday = 1
FROM dbo.Calendar c
JOIN dbo.Holidays h ON c.CalendarDate = h.HolidayDate;

🏁 Final Thoughts

Creating and using a permanent calendar table ensures:

  • Better performance
  • Reusable logic
  • Cleaner, more maintainable SQL

💡 Tip: Build it once, use it everywhere!

MS-SQL查詢時產出日曆天暫存Table的方法

🗓️ Two Approaches to Generating Calendar Dates in SQL Server

When working with time-based reporting or scheduling systems, it’s often necessary to generate a list of calendar dates. Whether you’re counting workdays, building timelines, or joining with other datasets, having a calendar table—or generating dates on the fly—is crucial. Let’s explore two popular methods of generating calendar dates in SQL Server.


✅ 1. Using a Recursive CTE (Common Table Expression)

A recursive CTE can dynamically generate a list of dates without relying on any external table:

WITH Calendar AS (
    SELECT CAST('2024-07-01' AS DATE) AS DateValue
    UNION ALL
    SELECT DATEADD(DAY, 1, DateValue)
    FROM Calendar
    WHERE DateValue < '2025-06-30'
)
SELECT * FROM Calendar
OPTION (MAXRECURSION 0);

🔍 Pros:

  • No dependency on system tables.
  • Simple and self-contained.

⚠️ Cons:

  • Slightly less performant for very large ranges.
  • MAXRECURSION default is 100; you must override it for larger date ranges.

✅ 2. Using master.dbo.spt_values (or a numbers/tally table)

This system table contains thousands of rows and can be repurposed to generate sequential numbers (and thus, dates):

SELECT DATEADD(DAY, number, '2024-07-01') AS DateValue
FROM master.dbo.spt_values
WHERE type = 'P'
  AND number BETWEEN 0 AND DATEDIFF(DAY, '2024-07-01', '2025-06-30');

🔍 Pros:

  • Faster performance for large sets.
  • No recursion required.

⚠️ Cons:

  • Relies on an undocumented system table (spt_values), which may be unsupported or unavailable in some SQL Server environments (especially Azure SQL DB).
  • Less self-explanatory for beginners.

🧠 When to Use Which?

Use CaseRecommended Method
Small date ranges (e.g. 1 year)Recursive CTE
Large date ranges (many years)Tally table or spt_values
Need portability and readabilityRecursive CTE
Need performanceTally/spt_values

🏁 Conclusion

Both approaches have their place. The recursive CTE is elegant and portable, great for small to medium needs. If you’re working with large datasets or care about performance, using spt_values (or better, your own numbers table) is a strong alternative.

-- Suggested: Create your own Numbers table for safety
CREATE TABLE dbo.Numbers (n INT PRIMARY KEY);
WITH E1(n) AS (
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
),
E2(n) AS (
    SELECT 1 FROM E1 a CROSS JOIN E1 b  -- 16 rows
),
E4(n) AS (
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 AS n FROM E2 a CROSS JOIN E2 b
)
INSERT INTO dbo.Numbers(n)
SELECT n FROM E4 WHERE n < 10000;

👉 Tip: For production systems, consider creating a permanent Calendar table with pre-generated dates and useful attributes (week number, holiday flag, etc.) to simplify your queries.

ms-sql–lead抓取與下筆資料日期差異天數,依群組條件(每月)重置,該月最後一筆天期計算至下月初

SELECT 
    t1.TransactionType,
    t1.TransactionDate,
    YEAR(t1.TransactionDate) AS TransactionYear,
    MONTH(t1.TransactionDate) AS TransactionMonth,
    LEAD(t1.TransactionDate, 1, DATEADD(day, 1, EOMONTH(t1.TransactionDate)))
            OVER (PARTITION BY t1.TransactionType,YEAR(t1.TransactionDate),MONTH(t1.TransactionDate) ORDER BY t1.TransactionDate) AS NextTransactionDate,
    DATEDIFF(day, t1.TransactionDate, 
        LEAD(t1.TransactionDate, 1, DATEADD(day, 1, EOMONTH(t1.TransactionDate)))
            OVER (PARTITION BY t1.TransactionType,YEAR(t1.TransactionDate),MONTH(t1.TransactionDate) ORDER BY t1.TransactionDate)) AS DifferenceInDays
FROM Transactions t1
GROUP BY 
    t1.TransactionType,
    t1.TransactionDate,
    YEAR(t1.TransactionDate),
    MONTH(t1.TransactionDate)
ORDER BY 
    t1.TransactionType,
    t1.TransactionDate;

LEAD (scalar_expression [,offset] [,default])
OVER ( [ partition_by_clause ] order_by_clause )  provides access to a row at a specified physical offset which follows the current row. It allows you to query more than one row in a SQL statement without having to join the table to itself

The  EOMONTH function in SQL is used to get the last day of the month for a specified date

DATEADD(day, 1, EOMONTH(t1.TransactionDate)) 找出月底再+1得到下月初

Excel VBA用SQL語法找出生產報工資料員工每日最早一筆報工開始與最晚一筆報工結束做工時統計,資料來源為自己的寫法.

chatgpt提醒Make sure to add a reference to the “Microsoft ActiveX Data Objects” library in your VBA project by going to “Tools” > “References” and checking the appropriate version (e.g., “Microsoft ActiveX Data Objects x.x Library”). 實測office2010 選用 2.8 library是可行。

Sub GetGroupedData()
    ' Define variables
    Dim wsResult As Worksheet
    Dim wsSource As Worksheet
    Dim rngSource As Range
    Dim rngResult As Range
    Dim strSQL As String
    Dim connString As String
    Dim cn As ADODB.Connection
    Dim rs As ADODB.Recordset
    Dim rowIndex As Long
    Dim colIndex As Long
    Dim strSourceRng As String
    ' Set the source worksheet
    Set wsSource = ThisWorkbook.Worksheets("Sheet1")
    
     Dim lastRow As Long
    lastRow = wsSource.Cells(wsSource.Rows.Count, 1).End(xlUp).Row
    
    strSourceRng = Trim("[Sheet1$A1:F" + Trim(Str(lastRow)) + "]")
        
    ' Check if the result sheet already exists
    On Error Resume Next
    Set wsResult = ThisWorkbook.Worksheets("Result")
    On Error GoTo 0
    
    ' If the result sheet exists, clear its contents
    If Not wsResult Is Nothing Then
        Application.DisplayAlerts = False
        wsResult.Cells.Clear
        Application.DisplayAlerts = True
    Else
        ' If the result sheet doesn't exist, create it
        Set wsResult = ThisWorkbook.Worksheets.Add
        wsResult.Name = "Result"
    End If
    
    ' Define the range where the result will be displayed
    Set rngResult = wsResult.Range("A1")
    
    ' Connection string (Excel version-dependent)
    connString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & ThisWorkbook.FullName & ";" & _
                 "Extended Properties=""Excel 12.0 Xml;HDR=YES"";"
    
    ' SQL query to retrieve the grouped data
    'strSQL = "SELECT col1, col2, col3, col4, MIN(col5) AS MinValue, MAX(col6) AS MaxValue FROM [Sheet1$A1:N274] " & _
             "GROUP BY col1, col2, col3, col4"
    
    strSQL = "SELECT col1, col2, col3, col4, MIN(col5) AS MinValue, MAX(col6) AS MaxValue FROM " & strSourceRng & _
             "GROUP BY col1, col2, col3, col4"
    'wsSource.Range("N3").Value = strSQL
    
    ' Create a connection object
    Set cn = New ADODB.Connection
    
    ' Open the connection
    cn.Open connString
    
    ' Create a recordset object
    Set rs = New ADODB.Recordset
    
    ' Execute the query and populate the recordset
    rs.Open strSQL, cn
    
    ' Write the column headers to the result range
    For colIndex = 1 To rs.Fields.Count
        rngResult.Offset(0, colIndex - 1).Value = rs.Fields(colIndex - 1).Name
    Next colIndex
        rngResult.Offset(0, colIndex - 1).Value = "時數(HR)"
    ' Write the data to the result range
    rowIndex = 1
    Do Until rs.EOF
        For colIndex = 1 To rs.Fields.Count
            rngResult.Offset(rowIndex, colIndex - 1).Value = rs.Fields(colIndex - 1).Value
        Next colIndex
               ' Convert col5 and col6 to DateTime values
        Dim dt1 As Date
        Dim dt2 As Date
        dt1 = CDate(rs.Fields("MinValue").Value)
        dt2 = CDate(rs.Fields("MaxValue").Value)
        
        ' Calculate the difference and add as col7
        Dim diff As Double
        diff = dt2 - dt1
        rngResult.Offset(rowIndex, rs.Fields.Count).Value = Round(diff * 24, 2)
        
        rs.MoveNext
        rowIndex = rowIndex + 1
    Loop
    
    ' Clean up
    rs.Close
    cn.Close
    Set rs = Nothing
    Set cn = Nothing
    
    ' Format the result range as desired
    
    ' Optional: Autofit columns in the result range
    wsResult.Columns.AutoFit
End Sub
其中組合SQL語法from 片段時原本程式碼為
 ' Set the name of the sheet containing the data
    sheetName = "Sheet1"
    
    ' Set the range of data in Sheet1
    Set rngData = ThisWorkbook.Worksheets(sheetName).UsedRange
  
    ' SQL query to retrieve data from Sheet1
    strSQL = "SELECT * FROM [" & sheetName & "$" & rngData.Address & "]"
會有錯誤才改成自行抓取最後一筆,手動組成範圍。
原本不使用VBA與SQL語法,單純用excel Function來做
1.手動找出distinct的 上班日、員工代號、姓名、報工別
2.將上下工時間由字串改成日期時間 =DATEVALUE(上工時間) + TIMEVALUE(上工時間),這樣才能做運算,算出時數。
3.=MAX(IF((A:A=L2)*(B:B=M2)*(D:D=O2),I:I)) 找出MAX值
  =MIN(IF((A:A=L2)*(B:B=M2)*(D:D=O2),H:H)) 找出MIN值
  其中L、M、O欄位第1.步驟抓出之distinct值 A、B、D為原始資料
4.Instead of pressing Enter, use Ctrl+Shift+Enter to enter the formula as an array formula. This will enclose the formula in curly braces {} and calculate the distinct values based on the three columns
5.差異為=(MAX-MIN)*24 

CROSS join 常數table產生多筆資料 如月份

select
m.site,
m.cal_year,
m.currency_id,
m.main_account,
m.sub_account,
m.project,
m.dept_no,
X.a,
0,
0,
0,
0,
‘XYZ’,
‘2023-02-21 12:00:00’
from
sybase64.erpdb.dbo.acc_balm m
left outer join sybase64.erpdb.dbo.acc_bald d on m.site = d.site
and m.cal_year = d.cal_year
and m.main_account = d.main_account
and m.currency_id = d.currency_id
and m.sub_account = d.sub_account
and m.project = d.project
and m.dept_no = d.dept_no cross
join (
VALUES
(’01’),
(’02’),
(’03’),
(’04’),
(’05’),
(’06’),
(’07’),
(’08’),
(’09’),
(’10’),
(’11’),
(’12’),
(’13’)
) AS X(a)
where
m.site = ‘CF’
and m.cal_year = ‘2023’
and m.main_account = ‘1122’
and d.site is null;

Setup A Excel file as A linked server in ms-sql

USE [master]
GO
sp_configure ‘show advanced options’, 1
GO
RECONFIGURE WITH OverRide
GO
sp_configure ‘Ad Hoc Distributed Queries’, 1
GO
RECONFIGURE WITH OverRide
GO

EXEC master . dbo. sp_MSset_oledb_prop N’Microsoft.ACE.OLEDB.12.0′ , N’AllowInProcess’ , 1
GO

EXEC master . dbo. sp_MSset_oledb_prop N’Microsoft.ACE.OLEDB.12.0′ , N’DynamicParameters’ , 1

EXEC sp_addLinkedServer
@server= ‘XLSX_MATRIX’,
@srvproduct = ‘ACE 12.0’,
@provider = ‘Microsoft.ACE.OLEDB.12.0’,
@datasrc =’D:\SQL\vn_item_no.xlsx’,
@provstr = ‘Excel 12.0; HDR=Yes’

SELECT * FROM OPENQUERY (XLSX_MATRIX, ‘Select * from [Application$]’)
SELECT * FROM OPENQUERY (XLSX_MATRIX, ‘Select * from [Device$]’)
SELECT * FROM OPENQUERY (XLSX_MATRIX, ‘Select * from [User$]’)

SELECT * FROM XLSX_MATRIX…[Application$]
SELECT * FROM XLSX_MATRIX…[Device$]
SELECT * FROM XLSX_MATRIX…[User$]

select b.*,a.item_no,a.ref_no from [XLSX_MATRIX]…[工作表1$] a right outter join [XLSX_MATRIX]…[‘原料(B倉)$’] b on a.f_2=b.f_2 where (b.ref_no like ‘%’+a.ref_no+’%’ or (a.ref_no) is null)
order by 1

select b.*,a.item_no,a.ref_no from [XLSX_MATRIX]…[‘原料(B倉)$’] b left outer join [XLSX_MATRIX]…[工作表1$] a on a.f_2=b.f_2
where (b.ref_no like ‘%’+a.ref_no+’%’ or (a.ref_no) is null) and sort_id is not null
order by sort_id

final**
select distinct b.*,a.item_no,a.ref_no from [XLSX_MATRIX]…[‘原料(B倉)$’] b left outer join [XLSX_MATRIX]…[工作表1$] a on a.f_2=b.f_2
and (b.ref_no like ‘%’+a.ref_no+’%’ or (a.ref_no) is null) where sort_id is not null
order by sort_id

select distinct b.*,a.item_no,a.ref_no,isnull(a.v_no,a.t_no) from [XLSX_MATRIX]…[‘原料(B倉)$’] b left outer join [XLSX_MATRIX]…[工作表1$] a on isnull(a.v_no,a.t_no)=b.f_2
and (b.ref_no like ‘%’+a.ref_no+’%’ or (a.ref_no) is null) where sort_id is not null
order by sort_id

AccessDatabaseEngine_X64 maybe needed for Microsoft.ACE.OLEDB.12.0 driver Microsoft Access Database Engine 2010 可轉散發套件

p.s.使用openrowset 可直接讀excel檔案,不用設置linked server
select a.* from
(SELECT * FROM OpenRowSet(‘Microsoft.ACE.OLEDB.12.0’,
‘Excel 12.0;HDR=Yes;IMEX=2;Database=c:\temp\Employee.xls’,[工作表1$])) a,
(SELECT * FROM OpenRowSet(‘Microsoft.ACE.OLEDB.12.0’,
‘Excel 12.0;HDR=Yes;IMEX=2;Database=c:\temp\Employee.xls’,[工作表1$])) b
where a.col1 = b.col1

select distinct b.*,a.item_no,a.ref_no,isnull(a.v_no,a.t_no) from
(SELECT * FROM OpenRowSet(‘Microsoft.ACE.OLEDB.12.0’,
‘Excel 12.0;HDR=Yes;IMEX=1;Database=D:\SQL\vn_item_no.xlsx’,[‘原料(B倉)$’])) b left outer join
(SELECT * FROM OpenRowSet(‘Microsoft.ACE.OLEDB.12.0’,
‘Excel 12.0;HDR=Yes;IMEX=1;Database=D:\SQL\vn_item_no.xlsx’,[工作表1$])) a on isnull(a.v_no,a.t_no)=b.f_2
and (b.ref_no like ‘%’+a.ref_no+’%’ or (a.ref_no) is null) where sort_id is not null
order by sort_id

(註1)SQL Server預設不允許使用 OPENROWSET 和 OPENDATASOURCE 進行特定分散式查詢。使用 OpenRowSet前,需使用sp_configure 來啟用 ‘Ad Hoc Distributed Queries’

EXEC sp_configure 'show advanced options','1'
RECONFIGURE 
EXEC sp_configure 'Ad Hoc Distributed Queries',1
RECONFIGURE 

參照

Import data from Excel to SQL Server or Azure SQL Database

使用 OpenRowset、OpenDataSource 查詢EXCE

設定 IMEX=1 透過 OleDb 讀取 Excel 仍然出現 null 值的問題