SQL: datetime as a variable [closed] - sql

This question is unlikely to help any future visitors; it is only relevant to a small geographic area, a specific moment in time, or an extraordinarily narrow situation that is not generally applicable to the worldwide audience of the internet. For help making this question more broadly applicable, visit the help center.
Closed 10 years ago.
Trying to using parts of datetime as a variable in a procedure, so a parameter would be a month like 'June'. Here's what I wrote
/* 3. Create a stored procedure called sp_product_listing listing a specified product ordered during a specified month and year. The product and the month and year will be input parameters for the stored procedure. Display the product name, unit price, and quantity in stock from the products table, and the supplier name from the suppliers table. Run the stored procedure displaying a product name containing Jack and the month of the order date is June and the year is 2001. The stored procedure should produce the result set listed below.*/
CREATE PROCEDURE sp_product_listing
(
#product varchar(40),
#month datetime,
#year datetime
)
AS
SELECT
'product_name'=products.name,
'unit_price'=products.unit_price,
'quantity_in_stock'=products.quantity_in_stock,
'supplier_name'=suppliers.name
FROM
products
INNER JOIN suppliers ON suppliers.supplier_id=products.supplier_id
INNER JOIN order_details ON order_details.product_id=products.product_id
INNER JOIN orders ON orders.order_id=order_details.order_id
WHERE
products.name LIKE '%#product%' AND MONTH(orders.order_date) = #month AND YEAR(orders.order_date) = #year;
GO
/*Execute procedure*/
EXECUTE sp_product_listing 'Jack','June','2001'
Procedure is tested working fine until I add the variables, then it goes to H trying to convert varchar to datetime?
I've tried things like #month MONTH(datetime), etc. Not sure how to approach this? Maybe that's not even the problem?

You should not make "#month" datetime because, as its nametype implies, it expects data on the form "date and time" like YYYY/MM/DD hh:mm:ss, and "June" does not fits on the pattern I just presented. If you want just make one column for dates, let's call it [dateofsomething], then, you can pass a parameter "#date" (with type datetime) with a value like a normal date.
Of course, if you want the current date, just use getdate()
Good luck

This should work
CREATE PROCEDURE sp_product_listing
(
#product varchar(40),
#month int,
#year int
)
AS
SELECT
'product_name'=products.name,
'unit_price'=products.unit_price,
'quantity_in_stock'=products.quantity_in_stock,
'supplier_name'=suppliers.name
FROM
products
INNER JOIN suppliers ON suppliers.supplier_id=products.supplier_id
INNER JOIN order_details ON order_details.product_id=products.product_id
INNER JOIN orders ON orders.order_id=order_details.order_id
WHERE
products.name LIKE '%#product%' AND MONTH(orders.order_date) = #month AND YEAR(orders.order_date) = #year;
GO
/*Execute procedure*/
EXECUTE sp_product_listing 'Jack',6,2001

June is not a DateTime value, nor is 2001. Try passing in a complete Date as a single value, then adding a month to determine the search window:
EDIT: Updated sample code.
-- Pass the month and year as a string and an integer.
declare #Month as VarChar(16)
declare #Year as Int
set #Month = 'June'
set #Year = 2001
-- Combine them into a date representing the first day of the desired month and year.
declare #WindowStart as Date = Cast( '1 ' + #Month + Cast( #Year as VarChar(4) ) as Date )
-- Calculate the start of the following month.
declare #WindowEnd as Date = DateAdd( month, 1, #WindowStart )
-- Display the resulting window of dates.
select #WindowStart as WindowStart, #WindowEnd as WindowEnd
Compare using: #WindowStart <= orders.order_date and orders.order_date < #WindowEnd
That will allow the optimizer to use an index on order_date. The importance of this may become apparent later in the term.

Related

MS AdventureWorks dbo.ufnGetProductListPrice

I'm studing the Ms SQL AdventureWorks 2014, to model an internal dbase for our company
I usually work on Postgres, and I'm trying to "understand" Ms SQL stored procedures... :-)) BUT ....
The store Procedure [dbo.ufnGetProductListPrice ] SEEMS STRANGE to me.
the SQL code is found here:
(https://dataedo.com/samples/html/AdventureWorks/doc/AdventureWorks_2/functions/dbo_ufnGetProductListPrice_116.html)
CREATE FUNCTION [dbo].[ufnGetProductListPrice](#ProductID [int], #OrderDate [datetime])
RETURNS [money]
AS
BEGIN
DECLARE #ListPrice money;
SELECT #ListPrice = plph.[ListPrice]
FROM [Production].[Product] p
INNER JOIN [Production].[ProductListPriceHistory] plph
ON p.[ProductID] = plph.[ProductID]
AND p.[ProductID] = #ProductID
AND #OrderDate BETWEEN plph.[StartDate] AND COALESCE(plph.[EndDate], CONVERT(datetime, '99991231', 112)); -- Make sure we get all the prices!
RETURN #ListPrice;
END;
The function is making use of the following two tables:
[Production.Product] (https://dataedo.com/samples/html/AdventureWorks/doc/AdventureWorks_2/tables/Production_Product_153.html)
[Production.ProductListPriceHistory] - (https://dataedo.com/samples/html/AdventureWorks/doc/AdventureWorks_2/tables/Production_ProductListPriceHistory_159.html)
In particular my doubts are:
the function is getting the parameter #ProductID.
The same #ProductID is used as the primary key of the Production.Product AND as part of the primary key for the table Production.ProductListPriceHistory
so is seems of no help making a join on Product.[ProductID] = ProductListPriceHistory.[ProductID]
when we can test the ProductID directly on the ProductListPriceHistory.[ProductID]
Why to create such a join ? Seems of no help...
The given #Orderdate datetime received as second parameter, is checked in the JOIN against the following condition
AND #OrderDate BETWEEN plph.[StartDate] AND COALESCE(plph.[EndDate], CONVERT(datetime, '99991231', 112)); -- Make sure we get all the prices!
BUT, if we are calling the store procedure for #Orderdate = 1/1/2022,
considering that [EndDate] could be NULL,
and we could have in ProductListPriceHistory.StartDate two records with our #ProductID, the first with StartDate=1/1/2020 and the second StartDate=1/1/2021,
such "BETWEEN" condition should match both of them, when obviously we would expect the last one ....
is it a bug ?
You are right, there are a number of serious flaws in this code, and I would recommend finding better tutorials instead.
I've noted comments on each flaw
CREATE FUNCTION [dbo].[ufnGetProductListPrice](#ProductID [int], #OrderDate [datetime])
RETURNS [money] -- money is a bad data type due to rounding problems, decimal should be used
-- scalar UDFs are very slow, this should be a inline Table Valued Function
AS
BEGIN
DECLARE #ListPrice money;
SELECT #ListPrice = plph.[ListPrice] -- as you point out, there should be some kind of aggregation
FROM [Production].[Product] p -- as you point out: join is unnecessary
INNER JOIN [Production].[ProductListPriceHistory] plph
ON p.[ProductID] = plph.[ProductID]
AND p.[ProductID] = #ProductID
-- BETWEEN should never be used on dates, use ">= AND <"
AND #OrderDate BETWEEN plph.[StartDate] AND COALESCE(plph.[EndDate], CONVERT(datetime, '99991231', 112)); -- ISNULL is better for performance than COALESCE
RETURN #ListPrice;
END;
A better function would be this
CREATE FUNCTION dbo.ufnGetProductListPrice (
#ProductID int,
#OrderDate datetime
)
RETURNS TABLE
AS RETURN
SELECT TOP (1)
plph.ListPrice
FROM Production.ProductListPriceHistory plph
WHERE plph.ProductID = #ProductID
AND #OrderDate >= plph.StartDate
AND #OrderDate < ISNULL(plph.EndDate, CONVERT(datetime, '99991231', 112))
ORDER BY
plph.StartDate DESC;
Assuming it's possible for there to be multiple active prices (I don't think it's possible in AdventureWorks), then you also need TOP (1) and ORDER BY plph.StartDate DESC. If this is not possible then you can leave that out.
Instead of doing this with a scalar UDF
SELECT p.*, dbo.ufnGetProductListPrice(p.ProductId, GETDATE())
FROM Production.Product p;
You use an APPLY for a TVF
SELECT p.*, plp.*
FROM Production.Product p
OUTER APPLY dbo.ufnGetProductListPrice(p.ProductId, GETDATE()) plp;
An OUTER APPLY works like a LEFT JOIN, which means you get NULL if there are no rows.
#Charlieface
Thanks for your reply. One more note on the function ufnGetProductStandardCost
AdventureWorks is storing historical Retailprices and CostPrices, to let you found the price (list, cost) of a certain product at a certain time. This is why I'm interested in this part of the database
It even offers a "dual" function for Cost prices ufnGetProductStandardCost
https://dataedo.com/samples/html/AdventureWorks/doc/AdventureWorks_2/functions/dbo_ufnGetProductStandardCost_117.html
with the same errors :-(
I think that the idea of ufnGetProductListPrice(ProductID, OrderDate), is to report a SINGLE price or Null if at that given time the price did not exist.
NOT a table or listrow.
Let's just recall the case I supposed before
search OrderDate 1/1/2022
one record with StartDate=1/1/2020 , EndDate=null
one record with StartDate=1/1/2021 , EndDate=null
One idea for such function would be to:
use a subquery to find the MAX() StardDate<#OrderDate for the given #ProductId
use such StartDate to get the correspondig ListPrice in the external query
or give back NULL if we have NO StartDate ListPrice before #OrderDate (e.g #OrderDate=1/1/2019)
What do you think about this query ? Any improvement ?
CREATE FUNCTION dbo.ufnGetProductListPrice (
#ProductID int,
#OrderDate datetime
)
AS
BEGIN
DECLARE #ListPrice money;
SELECT #ListPrice= plph.ListPrice
FROM Production.ProductListPriceHistory plph
WHERE plph.ProductID = #ProductID
AND plhp.StartDate = (
SELECT MAX(StardDate)
FROM Production.ProductListPriceHistory plph1
WHERE plph1.ProductID = #ProductID
AND plhp1.StartDate <= #OrderDate)
RETURN #ListPrice;
END;

'Summing' a date field in SQL - any ideas?

I'm creating an application that is essentially an integrity check between two databases - one is MSSQL and one is an old provider Btrieve. As part of the requirements all columns for every table need to be compared to ensure the data matches. Currently we loop through each table, get the basic count of the table in both DBs, and then delve into the columns. For numeric fields we do a simple SUM, and for text fields we sum up the length of the column for every row. If these match in both DBs, it's a good indicator the data has migrated across correctly.
This all works fine, but I need to develop something similar for datetime fields. Obviously we can't really SUM these fields, so I'm wondering if anyone has ideas on the best way to approach this. I was thinking maybe the seconds since a certain date but the number will be huge.
Any other ideas? Thanks!
The most straightforward answer to me would be to convert the date or datetime fields to integers with the same format. YYYYMMDD or YYYYMMDDHHmmss work just fine as long as your formats use leading zeroes. In SQL Server, you can do something like:
SELECT SUM(CAST(REPLACE(REPLACE(REPLACE(CONVERT(VARCHAR(20),DateTimeColumn,120),' ',''),':',''),'-','') AS BIGINT)) .....
Alternately, you can convert them to either the number of days from a given date ('1970-01-01'), or the number of seconds from a given date ('1970-01-01 00:00:00') if you use time.
SELECT SUM(DATEDIFF(DAY,'19700101',DateColumn)) ....
I'm not familiar enough with Btrieve to know what kinds of functions are available for formatting dates, however.
Using "Except" in SQL on the lines of Numeric fields you can compare the date counts in both the tables. For the Old source you may generate the select statement using excel or in the native database and bring to the SQL Server. For demonstration purpose I have used two tables and showing Except example below.
IF EXISTS (SELECT * FROM sys.objects
WHERE OBJECT_ID = OBJECT_ID(N'[dbo].[DateCompareOld]') AND
TYPE IN (N'U'))
DROP TABLE [dbo].[DateCompareOld]
GO
CREATE TABLE dbo.DateCompareOld
(
AsOf DATETIME
)
INSERT INTO DateCompareOld
SELECT '01/01/2016' UNION ALL
SELECT '01/01/2016' UNION ALL
SELECT '01/01/2016' UNION ALL
SELECT '01/02/2016' UNION ALL
SELECT '01/02/2016' UNION ALL
SELECT '01/02/2016'
IF EXISTS (SELECT * FROM sys.objects WHERE OBJECT_ID = OBJECT_ID(N'[dbo].[DateCompareNew]') AND TYPE IN (N'U'))
DROP TABLE [dbo].[DateCompareNew]
GO
CREATE TABLE dbo.DateCompareNew
(
AsOf DATETIME
)
INSERT INTO DateCompareNew
SELECT '01/01/2016' UNION ALL
SELECT '01/01/2016' UNION ALL
SELECT '01/01/2016' UNION ALL
SELECT '01/02/2016' UNION ALL
SELECT '01/02/2016' UNION ALL
SELECT '01/02/2016'
SELECT AsOf,COUNT(*) AsOfCount
FROM DateCompareOld
GROUP BY AsOf
Except
SELECT AsOf,COUNT(*) AsOfCount
FROM DateCompareNew
GROUP BY AsOf
Unless the date range used by rows in the database is extreme (like dates of astronomical stars being born and dying), it should be just as valid to convert the dates to an integer. This can be done any of several ways and is slightly database-specific, but converting 2016-01-04 to 20,160,104 is going to work fine.
Even SQL Server allows ORD(date_field) like expressions to obtain the internal representation. But this can also be done in a portable, system-agnostic means like
datediff(day, 'January 1, 1901', date_field)
if keeping track of days is sufficient, or
datediff(second, 'January 1, 1901', date_field)
if keeping track of seconds is needed.
Maybe it is not much help, maybe is something:
declare #d1 datetime; set #d1 = '2016-01-05 12:09'
declare #d2 datetime; set #d2 = '1970-04-05 07:09'
declare #d3 datetime; set #d3 = '1999-12-12 23:05'
declare #d4 datetime; set #d4 = '1999-12-12 23:06'
declare #i1 bigint
declare #i2 bigint
declare #i3 bigint
declare #i4 bigint
select #i1 = convert( bigint, convert( timestamp, #d1 ) )
select #i2 = convert( bigint, convert( timestamp, #d2 ) )
select #i3 = convert( bigint, convert( timestamp, #d3 ) )
select #i4 = convert( bigint, convert( timestamp, #d4 ) )
select #i1
select #i2
select #i3
select #i4
select #i1 ^ #i2 ^ #i3 ^ #i4
I think you could do something like this on the SQL Server side to find the center ("average") value of the column. Then use that value on the Btrieve side to avoid overflow issues where I'm guessing you're more constrained.
-- January 1, 2000 value pulled out of the air as a stab in the dark
select
dataadd(
second,
avg(cast(datediff(datediff(second, '20000101', <data>) as bigint)),
'20000101'
) /* find the center */
I wouldn't be surprised if you had to resort to a floating point type with Btrieve or partition your scans into smaller ranges to avoid intermediate sums that get too big. And you might want to use a cursor and randomize the ordering of the rows so you don't hit them in a sorted order that causes an overflow. At this point I'm just speculating since I haven't seen any of the data and my knowledge of Btrieve is so ancient and minimal to begin with.
I also have a feeling that some of this effort is all about satisfying some uneasiness on the part of non-technical stakeholders. I'm sure you could come up with checksums and hashes that would work better but this summing concept is the one they can grasp and will set their minds at ease on top of being somewhat easier to implement quickly.

How to compare smalldatetime in stored procedure

I'm writing stored procedure to compare dates but it's not working properly. How can I make it so it compares only the dates but not the time? What I'm trying to do is compare the times and if the Id is null than insert a new entry with the same name but new time. I'm keeping multiple entries with same name but different test time.
ALTER PROCEDURE [dbo].[UL_TestData]
(
#Name varchar(30),
#Test_Time smalldatetime,
#ID INT output
)
AS
Declare #UpdateTime smalldatetime
SELECT #ID=ID FROM Info_User WHERE Name=#Name AND UpdateTime= #Test_Time
IF(#ID IS NULL)
BEGIN
INSERT INTO Info_User (Name, UpdateTime) VALUES (#Name, #UpdateTime)
END
there are a lot of solutions to this depending on what type of DBMS, however here is one:
SELECT #ID=ID FROM Info_User WHERE Name=#Name AND floor(cast(#UpdateTime as float))= floor(cast(#Test_Time as float))
this works because smalldatetime's date is stored a whole numbers, where the time is stored as decimals.
I would cast the dates to a plain date which makes this solution independent of implementation details
select #ID=ID
from info_user
where Name = #Name
and cast (UpdateTime as Date) = Cast(#TestTime as Date)
However, I would either add the date part of the UpdateTime as an additional (calculated) column or split the information into a date and a time part. This makes it much easier to query entries by the plain date.
As a rule of thumb: The type of columns (in general: the table layout) greatly depends on the type of query you usually run against your data.
Edit: As attila pointed out, the date datatype only exists in version 2008 and up

SQL Server Include user function result into select query

I have this stored function
function GetPrevReading(
#utility int,
#asofdate datetime
) returns decimal(10,5)
This function returns the previous meter reading from the table with the following fieds:
utility - int
date - datetime
reading - numeric(18,4)
When I use select on this table I want to set a date as a parameter and get this from the table:
Utility Previous Reading
(distinct) GetPrevReading(utility from query, #date from parameter)
I want the function GetPrevReading to take parameter 'utility' from the current row.
Is it possible to accompish this with a query or should I make a stored procedure?
For example, this is the table:
Utility Date Reading
1 2013-10-1 105.6
1 2013-11-1 123.72
2 2013-10-1 226.1
2 2013-10-1 238.18
Now, if I set parameter #date to 2013-10-29 I should get this result:
Utility PreviousReading
1 105.6
2 226.1
Here, my function should get #utility=1 and #asofdate='2013-10-29' on the first row and #utility=2 and #asofdate='2013-10-29' on the second one.
Try this out. I fixed some inconsistencies in your data types, and assumed that your last line of sample data really should have had 2013-11-01 as the date. Also, the way that the function is written, it's not getting the previous reading, but the reading on that date.
CREATE TABLE MyTable (
Utility Int,
Date Date,
Reading Decimal(10,5)
);
INSERT INTO MyTable (Utility, Date, Reading)
VALUES
(1,'2013-10-01', 105.60),
(1,'2013-11-01', 123.72),
(2,'2013-10-01', 226.10),
(2,'2013-11-01', 238.18);
CREATE FUNCTION dbo.GetPrevReading(
#utility int,
#asofdate datetime
)
RETURNS Decimal(10,5)
AS
BEGIN
RETURN (
SELECT TOP 1 Reading
FROM MyTable
WHERE Utility = #Utility
AND Date = #asofdate
ORDER BY Date DESC
)
END;
SELECT
Utility
,Date
,dbo.GetPrevReading(Utility, Date)
FROM (
SELECT Utility, Max(Date) Date
FROM MyTable
WHERE Date < '2013-10-29'
GROUP BY Utility
) x;
Am I understanding the question; the function returns for this call
GetPrevReading( 1,2013-10-29)
Returns
1, 105.6
2, 226.1
And you want to join between the function and its results and the underlying table? You can do this in SQL 2005 + using the Apply join
Select
…
From tblUtilityReadings
Cross Apply GetPrevReading(tblUtilityReadings.utility, #date)

Convert Parameter Value Form DateTime To Int32

I am having problems with a Stored Procedure I am writing.
I am gathering data from a number of tables, most of which hold a date value, but one holds just a month(int)
I declare the following Parameters at the beginning of my SP.
#FromDate DateTime,
#ToDate DateTime
This works fine for most of my tables, but for the table where I am just requiring the Month from the #FromDate, I run into the following error:
"Failed to convert parameter value form a DateTime to a Int32."
Here is my Select statement for the Problem Table:
SELECT Branch, Discount
FROM MonthlyPromotions
WHERE (Month = DATEPART(mm,#FromDate))
Also, in the MonthlyPromotions Table the Month Field is an Int.
Can anyone help on this ASAP??
Thankyou
To troubleshoot your problem, can you do a
PRINT CAST(CAST(DATEPART(mm, #FromDate) AS INT) AS VARCHAR(50))
RETURN
At the beginning of your SP?
If that doesn't give an error, you can proceed to:
SELECT Branch, Discount
FROM MonthlyPromotions
WHERE Month = CAST(DATEPART(mm,#FromDate) AS INT)
select cast(getdate() as int)