How to write a conditional SELECT query in TSQL using arguments in the WHERE/AND clause? - tsql

I've got a stored procedure that returns postal codes within a specified radius. The arguments are
ALTER PROCEDURE [dbo].[proximitySearch]
#proximity int = 0,
#country varchar (2) = NULL,
#city varchar (180) = NULL,
#state varchar (100) = NULL,
#stateAbr varchar (2) = NULL,
#postalCode varchar(20) = NULL
AS...
In the proc, the first query needs to select a single record (or no record) that matches whatever was passed in and assign the lat/long to local variables, as I started to write below:
SELECT TOP 1 #Longitude = Longitude, #Latitude = Latitude
FROM PostalCodes
WHERE ...
This is where I get stumped... the WHERE clause needs to be conditional based on what was passed in. Some arguments (or all of them) can be NULL, and I don't want use them in the query if they are.
I was thinking along the lines of:
SELECT TOP 1 #Longitude = Longitude, #Latitude = Latitude
FROM PostalCodes
WHERE Longitude IS NOT NULL
AND CASE WHEN #postalCode IS NOT NULL THEN PostalCode = #postalCode ELSE 1 END
...but this doesn't work. How is something like this typically done? (I'm definitely not a seasoned TSQL guy!!!) Thanks in advance!

There is more than one way of implementing that kind of logic:
1)
SELECT TOP 1 #Longitude = Longitude, #Latitude = Latitude
FROM PostalCodes
WHERE Longitude IS NOT NULL
AND (#postalCode IS NULL OR PostalCode = #postalCode)
2)
SELECT TOP 1 #Longitude = Longitude, #Latitude = Latitude
FROM PostalCodes
WHERE Longitude IS NOT NULL
AND PostalCode = COALESCE(#postalCode, PostalCode)

SELECT TOP 1 #Longitude = Longitude, #Latitude = Latitude
FROM PostalCodes
WHERE
((#postalCode IS NULL) OR (PostalCode = #postalCode))
AND ((#someotherparam IS NULL) OR (someothercolumn = #someotherparam)) etc...
But be aware that this technique can suffer from 'parameter sniffing'

Related

How do I select a subset of columns with diesel-rs?

I am struggling for several hours now to query a subset of the available columns of a table as well as including a calculation in it. I know that it is not the best way to execute the calculation in the select query, but for now, I am just working on a prototype and it should be feasible for that.
I am using diesel-rs as a ORM for all my database actions in my back-end implementation. The data will be stored in a PostgresSQL server. The full table - as stored in the database - is created using the following query:
CREATE TABLE airports
(
id SERIAL PRIMARY KEY,
icao_code VARCHAR(4) NOT NULL UNIQUE, -- the official ICAO code of the airport
last_update TIMESTAMP NOT NULL, -- when were the information updated the last time?
country VARCHAR(2) NOT NULL, -- two letter country code
longitude REAL NOT NULL, -- with 6 decimal places
latitude REAL NOT NULL, -- with 6 decimal places
name VARCHAR NOT NULL -- just a human readable name of the airport
);
Running diesel migrations run generates the airports table definition and querying the database works without any issue.
Now I am trying to query a list of all airports (their ICAO code) with the corresponding coordinates as well as the distance to a supplied coordinate. Therefore, I created the following diesel-rs table! macro myself
table! {
airport_by_distance (icao_code) {
icao_code -> Varchar,
longitude -> Float8,
latitude -> Float8,
distance -> Float8,
}
}
as well as the struct corresponding to the diesel-rs definition:
#[derive(QueryableByName)]
#[table_name = "airport_by_distance"]
struct AirportByDistance {
icao_code: String,
longitude: f64,
latitude: f64,
distance: f64,
}
The following snipped - as of my understanding - should query the required information:
use diesel::dsl::sql_query;
let latitude = 4.000001;
let longitude = 47.000001;
let query_sql = format!("SELECT icao_code, longitude, latitude, (3959.0 * acos(cos(radians({lat})) * cos(radians(latitude)) * cos(radians(longitude) - radians({long})) + sin(radians({lat})) * sin(radians(latitude)))) AS distance FROM airports ORDER BY distance;", lat=latitude, long=longitude);
let result = match sql_query(query_sql).load::<AirportByDistance>(database_connection) {
Ok(result) => result,
Err(error) => {
error!("{:?}", error);
return Err(());
}
};
Unfortunately, executing the load method, results in a DeserializationError(Custom { kind: UnexpectedEof, error: "failed to fill whole buffer" }) error.
The query which was executed was:
SELECT icao_code,
longitude,
latitude,
(3959.0 * acos(cos(radians(4.000001)) * cos(radians(latitude)) * cos(radians(longitude) - radians(47.000001)) +
sin(radians(4.000001)) * sin(radians(latitude)))) AS distance
FROM airports
ORDER BY distance;
I took it and executed it manually, but it worked flawlessly. I even tried removing the calculation and just selecting a subset of columns, but with no luck either.
Right now I am not sure what I am doing wrong. How can I fix this issue?
EDIT with the fixed code: For the ones who are interested how to code would look like after using the helpful advice of Rasmus:
The table! macro is completely gone and the data definition struct looks like this:
#[derive(Queryable)]
struct AirportByDistance {
icao_code: String,
longitude: f32,
latitude: f32,
distance: f64,
}
The code for the querying the database is as follows:
let result = match airports.select(
(
icao_code,
longitude,
latitude,
sql::<Double>(
&format!("(3959.0 * acos(cos(radians({lat})) * cos(radians(latitude)) * cos(radians(longitude) - radians({long})) + sin(radians({lat})) * sin(radians(latitude)))) AS distance", lat=latitude_reference, long=longitude_reference)
)
)
).load::<AirportByDistance>(database_connection)
{
Ok(result) => result,
Err(error) => {
error!("{:?}", error);
return Err(());
}
};
for airport in result {
info!(
"AIRPORT: {} has {}nm distance",
airport.icao_code, airport.distance
);
}
I think the problem is that the deserializer don't know the raw types of the queried columns.
Try to use typed diesel names/values as much as possible, and explicit sql strings only where needed. And I don't think the "fake" table declaration airports_by_distance helps. Maybe something like this:
use diesel::sql_types::Double;
let result = a::airports
.select((
a::icao_code,
a::longitude,
a::latitude,
sql::<Double>(&format!("(3959.0 * acos(cos(radians({lat})) * cos(radians(latitude)) * cos(radians(longitude) - radians({long})) + sin(radians({lat})) * sin(radians(latitude)))) AS distance", lat=latitude, long=longitue)
))
.load::<AirportByDistance>(&db)?
(Using the table! macro manually is basically just telling diesel that such a table will exist in the actual database when running the program. If that is not true, you will get runtime errors.)

SQL Return a Value depending on 3 ranges of dates

I am having issues sorting some dates in 3 different ranges of dates and return a values according to the correct range. I am hoping you can give me a efficent and clean way of doing it.
I have 6 different dates that I get from a SQL Table. Those dates are then stored in variables. All the dates can also be a Null value. My dates are seperated in 3 date ranges. I want to return an indication of what ranges I am in by using the earliest start Date in all of my ranges. The date of the correct range must also be smaller than the current Date. A date Range can also consist of only an End Date. In that case, we considered that the range end at the end date and is active before that. We select the earliest end date that is close to the current Date in that case.
Return 0 if all the date are null
Range #1(Category #1) X Start Date and X end Date Return 1
Range #2(Category #2) Y Start Date and Y end Date Return 2
Range #3(Category #3) Z Start Date and Z end Date Return 3
EDIT
Ex#1 XStart = December 10 , XEnd = December 15
YStart = December 12 , Yend = December 13
ZStart = December 9 , ZEnd = Null
Expected result would be Z Category
Ex#2 XStart = December 8 , XEnd = December 15
YStart = NULL , Yend = NULL
ZStart = December 9 , ZEnd = Null
Expected result would be X Category
Ex#3XStart = NULL , XEnd = December 15
YStart = NULL , Yend = NULL
ZStart = December 9 , ZEnd = Null
Expected result would be X Category
Ex#4 XStart = December 10 , XEnd = December 15
YStart = NULL , Yend = NULL
ZStart = December 9 , ZEnd = Null
Expected result would be Z Category
Is there a more efficent way than doing a lot of IF statements ? I am having difficulty handling all of those conditions and checks. Here is a snippet of what I have so far.
--Return 0 is not Condition is Applicable
ALTER PROCEDURE [dbo].[HO_GetReason]
#HOID INT
AS
BEGIN
Declare #IsHOIDReal INT = (SELECT ID from T_HO where id = #HOID)
Declare #XStartDate Datetime
Declare #XEndDate Datetime
Declare #YStartDate Datetime
Declare #YEndDate Datetime
Declare #ZStartDate Datetime
Declare #ZEndDate Datetime
CREATE TABLE #tmpT_HO_Withhold (
ID INT NOT NULL,
XStartDate Datetime null,
XEndDate Datetime null,
YStartDate Datetime null,
YEndDate Datetime null,
ZStartDate Datetime null,
ZEndDate Datetime null,
PRIMARY KEY CLUSTERED (ID)
)
IF (#IsHOIDReal IS NOT NULL)
BEGIN
INSERT INTO #tmpT_HO_Withhold
SELECT T_HO.ID,
XStartDate ,
XEndDate ,
YStartDate ,
YEndDate ,
ZStartDate ,
ZEndDate
FROM dbo.T_HO
WHERE ID = #HOID
SET #XStartDate = (Select TOP 1 XStartDate from #tmpT_HO_Withhold)
SET #XEndDate = (Select TOP 1 XEndDate from #tmpT_HO_Withhold)
SET #YStartDate = (Select TOP 1 YStartDate from #tmpT_HO_Withhold)
SET #YEndDate = (Select TOP 1 YEndDate from #tmpT_HO_Withhold)
SET #ZStartDate = (Select TOP 1 ZStartDate from #tmpT_HO_Withhold)
SET #ZEndDate = (Select TOP 1 ZEndDate from #tmpT_HO_Withhold)
IF(#XStartDate IS NULL AND #YStartDate IS NULL AND #ZStartDate IS NULL)
BEGIN print 'NO CONDITION' Select 0 as 'HO_GetReason' END
ELSE IF (#XStartDate IS NOT NULL AND #YStartDate IS NULL AND #ZStartDate IS NULL) BEGIN print '1' Select 1 as 'HO_GetReason'END
ELSE IF (#XStartDate IS NOT NULL AND #YStartDate IS NULL AND #ZStartDate IS NULL) BEGIN print '2' Select 2 as 'HO_GetReason'END
ELSE IF (#XStartDate IS NULL AND #YStartDate IS NULL AND #ZStartDate IS NOT NULL) BEGIN print '3' Select 3 as 'HO_GetReason'END
END
DROP TABLE #tmpT_HO_Withhold END
Notes regarding efficient and clean:
Complex conditional are not in the inefficient category. It can fall into the hard to read category and maintain, but they are a pretty quick operation.
Example: That second "else if" looks strangely like the first "else if". Code will not be reached.
Creating and destroying the temp table will be the slowest part of your stored procedure.
Temp tables using #tablename are not concurrency safe in stored procedure, you can end up with odd schema altered errors in some cases.
You can get to the same results by swapping most of that with:
SELECT
#XStartDate = XStartDate ,
#XEndDate = XEndDate ,
#YStartDate = YStartDate ,
#YEndDate = YEndDate ,
#ZStartDate = ZStartDate ,
#ZEndDate = ZEndDate
FROM dbo.T_HO
WHERE ID = #HOID
Id is unique based on the primary key spotted in your create table, so TOP isn't necessary in this format, no rows will leave the values as null.
Personally, once I get that conditional working (absolute final form), I would be tempted to directly adjust it to a CASE statement and set that as a PERSISTENT computed COLUMN in the base table.
ALTER TABLE dbo.T_HO ADD Reason AS (CASE WHEN XStartDate IS NOT NULL AND ... THEN ... WHEN ... THEN ... ELSE 0 END) PERSISTED

How to split a field that has carriage return

I have a field in my database table called ADDRESSFORMAT
1,The Lodge
Street
Town
Postcode
Where the contents are separated by a CHAR (13) and CHAR (10)
How would I go about creating fields in a query that would only pull back either the first line, second line...and so on?
The following is an in-line approach.
The Cross Apply B generates a "clean string". It will eliminate any number of repeating CRLFs and create a pipe delimited string to be processed by Cross Appy C.
I should note that this method of eliminating repeating strings was demonstrated by Gordon Linoff several weeks back. Sorry I can't find the original post.
Example
Declare #YourTable table (ID int,ADDRESSFORMAT varchar(max))
Insert Into #YourTable values
(1,'The Lodge
Street
Town
Postcode')
Select A.ID
,C.*
From #YourTable A
Cross Apply (
Select CleanString = replace(replace(replace(replace(replace(ADDRESSFORMAT,char(13),'|'),char(10),'|'),'|','><'),'<>',''),'><','|')
) B
Cross Apply (
Select Pos1 = ltrim(rtrim(xDim.value('/x[1]','varchar(max)')))
,Pos2 = ltrim(rtrim(xDim.value('/x[2]','varchar(max)')))
,Pos3 = ltrim(rtrim(xDim.value('/x[3]','varchar(max)')))
,Pos4 = ltrim(rtrim(xDim.value('/x[4]','varchar(max)')))
,Pos5 = ltrim(rtrim(xDim.value('/x[5]','varchar(max)')))
,Pos6 = ltrim(rtrim(xDim.value('/x[6]','varchar(max)')))
,Pos7 = ltrim(rtrim(xDim.value('/x[7]','varchar(max)')))
,Pos8 = ltrim(rtrim(xDim.value('/x[8]','varchar(max)')))
,Pos9 = ltrim(rtrim(xDim.value('/x[9]','varchar(max)')))
From (Select Cast('<x>' + replace((Select replace(B.CleanString,'|','§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml) as xDim) as A
) C
Returns
ID Pos1 Pos2 Pos3 Pos4 Pos5 Pos6 Pos7 Pos8 Pos9
1 The Lodge Street Town Postcode NULL NULL NULL NULL NULL

Why selectrow_array does not work with null values in where clause

I am trying to fetch the count from SQL server database and it gives 0 for fields with null values. Below is what I am using.
my $sql = q{SELECT count(*) from customer where first_name = ? and last_name = ?};
my #bind_values = ($first_name, $last_name);
my $count = $dbh->selectrow_array($sql, undef, #bind_values);
This returns 0 if either value is null in the database. I know prepare automatically makes it is null if the passed parameter is undef, but I don't know why it's not working.
So here is weird observation. When I type the SQL with values in Toda for SQL server, it works :
SELECT count(*) from customer where first_name = 'bob' and last_name is null
but when I try the same query and pass values in the parameter for the first_name = bob and the last_name {null} . it does not work.
SELECT count(*) from customer where first_name = ? and last_name = ?
For NULL in the WHERE clause you simply need a different query. I write them below each other, so you can spot the difference:
...("select * from test where col2 = ?", undef, 1);
...("select * from test where col2 is ?", undef, undef);
...("select * from test where col2 is ?", undef, 1);
...("select * from test where col2 = ?", undef, undef);
The first two commands work, stick to those. The third is a syntax error, the fourth is what you tried and which indeed does not return anything.
The DBI manpage has a section of NULL values that talks about this case a bit more.
So, here it is what I did. I added or field is null statement with each field if the value is undef.
my $sql = q{SELECT count(*) from customer where (first_name = ? or (first_name is null and ? = 1)) and (last_name = ? or (last_name is null and ? = 1))};
my #bind_values = ($first_name, defined($first_name)?0:1, $last_name, defined($last_name)?0:1);
my $count = $dbh->selectrow_array($sql, undef, #bind_values);
If anyone has better solution please post it.

INSERT INTO not working in IF block - T-SQL

im working on procedure which should transfer number of items (value #p_count) from old store to new store
SET #countOnOldStore = (SELECT "count" FROM ProductStore WHERE StoreId = #p_oldStoreId AND ProductId = #p_productID)
SET #countOnNewStore = (SELECT "count" FROM ProductStore WHERE StoreId = #p_newStoreID AND ProductId = #p_productID)
SET #ShiftedCount = #countOnOldStore - #p_count
SET #newStoreAfterShift = #countOnNewStore + #p_count
IF #ShiftedCount > 0
BEGIN
DELETE FROM ProductStore WHERE storeId = #p_oldStoreId and productID = #p_productID
INSERT INTO ProductStore (storeId,productId,"count") VALUES (#p_oldStoreId,#p_productID,#ShiftedCount)
DELETE FROM ProductStore WHERE storeId = #p_newStoreID and productID = #p_productID
INSERT INTO ProductStore (storeId,productId,"count") VALUES (#p_newStoreID,#p_productID,#newStoreAfterShift)
END
ELSE
PRINT 'ERROR'
well ... second insert is not working. I cant figure it out. It says
Cannot insert the value NULL into column 'count', table 'dbo.ProductStore'; column does not allow nulls. INSERT fails.
Can anyone see problem and explain it to me ? Its school project
It looks like your entire query should just be:
UPDATE ProductStore
SET [count] = [count] + CASE
WHEN storeId = #p_NewStoreID THEN #p_Count
ELSE -#p_Count END
WHERE
productID = #p_ProductID and
storeID in (#p_NewStoreID,#p_OldStoreID)
If either value in the following is NULL, the total will be NULL:
SET #newStoreAfterShift = #countOnNewStore + #p_count
Check both values (#countOnNewStore, #p_count) for NULL.
Looks like you are not assigning any value to #p_count, so it is NULL and so are #ShiftedCount and #newStoreAfterShift.