Search code examples
sql-serverazure-sql-databasedatabase-partitioning

Azure SQL table partitions Ignored when queries contain simple function


Using Azure SQL Server database. I have a few tables partitioned on a 90 day date boundary. We have a stored procedure to shift data to maintain the proper partition breakpoint/range. I'm using a small function to provide the proper date breakpoint for my queries so I don't have to constantly update all my views.

But just by virtue of using that function in my queries, partitioning is ignored. Do I have no choice but to put hard-coded values in my queries everywhere and constantly modify them?

Here is a sample that reproduces the problem.

Update: After changing the PartitionDate function below according to the marked answer, it was fine for a short time (partition elimination occurred). Then, queries started sucking again. When I ran simple queries filtered by the date function, partitions were no longer eliminated.

------------------------------- setup
-- Create functions PartitionDate and PartitionQueryDate
create function PartitionDate() returns date as
begin
  return GETDATE() - 91 -- returns 1/4/2019 today
end
go
create function PartitionQueryDate() returns date as
begin
  return GETDATE() - 90 -- returns 1/5/2019
end
go

-- Create partition func and scheme using above functions
CREATE PARTITION FUNCTION order_pf (smalldatetime) AS RANGE RIGHT FOR VALUES (dbo.PartitionDate())
CREATE PARTITION SCHEME order_ps AS PARTITION order_pf ALL TO ([PRIMARY])

-- Create Order (pk, OrderDate, Fk), Customer (pk) tables.  Order is partitioned
create table Customer
(
    id int primary key identity(1,1), 
    FirstName varchar(255) not null
)
create table [Order]
(
    id int identity(1,1), OrderDate smalldatetime not null, 
    CustomerId int not null,
    CONSTRAINT [FK_Orders_Customer] FOREIGN KEY ([CustomerId]) REFERENCES Customer([id])
) on order_ps(OrderDate);

-- Add in indexes to Order: only OrderDate on the partition func
CREATE CLUSTERED INDEX [Order_OrderDate] ON [Order]([OrderDate] ASC) ON [order_ps] ([OrderDate]);
CREATE NONCLUSTERED INDEX [FK_Order_Customer] ON [Order](CustomerId, OrderDate) ON [order_ps] ([OrderDate]) -- seems to work the same with or without the partition reference.
go

-- Add some data before and after the partition break
insert Customer values ('bob')
insert [Order] values('12-31-2018', SCOPE_IDENTITY())
insert Customer values ('hank')
insert [Order] values('1-6-2019', SCOPE_IDENTITY())

---------------------------- test
-- verify a row per partition:
SELECT $PARTITION.order_pf(OrderDate) as Partition_Number, COUNT(*) as Row_Count 
FROM [Order]
GROUP BY $PARTITION.order_pf(OrderDate)

-- Simple queries with actual execution plan turned on.  The queries are logically equivalent.
select COUNT(1) from [Order] where OrderDate > '1-5-2019'   -- Index seek Order_OrderDate; actual partition count 1
select COUNT(1) from [Order] where OrderDate > dbo.PartitionQueryDate() -- Index seek Order_OrderDate; actual partition count 2

-- Cleanup
drop table if exists [Order]
drop table if exists Customer
drop partition scheme order_ps
drop partition function order_pf
drop function if exists PartitionDate
drop function if exists PartitionQueryDate

Solution

  • One workaround would be to assign the function result to a variable first.

    declare @pqd smalldatetime = dbo.PartitionQueryDate();
    
    select COUNT(1) from [Order] where OrderDate > @pqd
    

    Another option would be to use an inline TVF

    CREATE FUNCTION dbo.PartitionQueryDateTVF ()
    RETURNS TABLE 
    AS
    RETURN 
    (
        SELECT CAST(CAST( GETDATE() - 90 AS  DATE) AS SMALLDATETIME) AS Date
    )
    
    GO
    
    SELECT COUNT(1) from [Order] where OrderDate > (SELECT Date FROM dbo.PartitionQueryDateTVF())
    

    This may be something that is improved with inline scalar UDFs but I'm not in a position to test this at the moment