have a query relating to MS SQL Server.
I'm going to have to be vague with details and change or remove certain parts as i can't say whether the info is confidential or not, but I've written a query that searches for students with their training units spanning across semesters using report builder 2.0:
DS_spanning: (Main dataset)
SET DATEFORMAT dmy
SELECT
FIRST_NAME AS FirstName
,LAST_NAME AS LastName
,START_DATE AS StartDate
,END_DATE AS EndDate
,UNIT_TYPE AS UnitType
,TP_FULLNAME AS TrainingPost
,SEMESTER_YEAR AS SemesterYear
FROM AA_GPR_TU
WHERE TU_START_DATE < #checkdate //Checkdate returns the end date of the
AND TU_END_DATE > #checkdate // selected semester
ORDER BY TU_START_DATE
PM_checkdate: (Dataset that #checkdate is linked to)
SET DATEFORMAT dmy
SELECT
new_semesterenddate
,new_semesterstartdate
,new_semesternumber
,new_semesteryear
,new_name
FROM
FilteredNew_rtpsemester
WHERE new_semesteryear >= 2004
ORDER BY new_semesterenddate DESC
Now, this works fine and does the job i want it to, however i can only select one semester at a time. When i try to tick the 'Allow Multiple Values' box under the Report Parameter Properties for #checkdate, running the report with more than one semester selected gives me this error:
Incorrect syntax near ','.
Query execution failed for dataset 'DS_spanning'.
An error has occurred during report processing.
An error occurred during local report processing.**
(The problem with a ',' is because when i select multiple values for the report, it supplies them to #checkdate as data1, data2, data3... etc.)
Is there something wrong with the way my parameter has been written, or do i need to change my query to accomodate multiple values?
Easiest way i could think of doing this would be using an IN statement in the section
WHERE TU_START_DATE < #checkdate AND TU_END_DATE > #checkdate
but i'm not sure how to use an IN statement in conjunction with and operands.
Any ideas folks?
i have had select multiple value problem so many times and after searching the internet for numerous hours i found this and it works a treat.
First of all create this function call split into the database you are running your queries from.
USE [DatabaseName]
GO
/****** Object: UserDefinedFunction [dbo].[split] Script Date: 07/19/2013 10:49:08 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[split](
#delimited NVARCHAR(MAX),
#delimiter NVARCHAR(100)
) RETURNS #t TABLE (id INT IDENTITY(1,1), val NVARCHAR(MAX))
AS
BEGIN
DECLARE #xml XML
SET #xml = N'<t>' + REPLACE(#delimited,#delimiter,'</t><t>') + '</t>'
INSERT INTO #t(val)
SELECT r.value('.','varchar(MAX)') as item
FROM #xml.nodes('/t') as records(r)
RETURN
END
the way you then use this in a WHERE clause is by using this line of code be sure to change the word column and parameter leave the word val as this is needed for the function
WHERE [COLUMN] IN (SELECT val FROM dbo.split(#Parameter, ','))
There will be no restrictions on using the same dataset for more than one paramater.
You should use 2 paramaters for this report, say #checkdate1 and #checkdate2, as this enables more control over what is used and where.
Related
For a project, we are using a table (named txtTable) that contains all the texts. And each column contains a different language (for example column L9 is English, column L7 is German, etc..).
TextID L9 L7 L16 L10 L12
------------------------------------------------------
26 Archiving Archivierung NULL NULL NULL
27 Logging Protokollierung NULL NULL NULL
28 Comments Kommentar NULL NULL NULL
This table is located in a database on a Microsoft SQL Server 2005. The big problem is that this database name changes each time the program is restarted. This is a behavior typically for this third-party program and cannot be changed.
Next to this database and on the same server is our own database. In this database are several tables that point to the textID for generating data for reporting (SQL Server Reporting Services) in the correct language. This database contains also a table "ProjectSettings" with some properties like the name of the texttable database, and the stored procedures to generate the reporting data.
The way we now are requesting the right texts of the right language from this table with the changing database name is by creating a dynamic SQL query and execute it in a stored procedure.
Now we were wondering if there is a cleaner way to get the texts in the right language. We were thinking about creating a function with the textID and the language as a parameter, but we cannot find a good way to do this. We thought about a function so we just can use it in the select statement, but this doesn’t work:
CREATE FUNCTION [dbo].[GetTextFromLib]
(
#TextID int,
#LanguageColumn Varchar(5)
)
RETURNS varchar(255)
AS
BEGIN
-- return variables
DECLARE #ResultVar varchar(255)
-- Local variables
DECLARE #TextLibraryDatabaseName varchar(1000)
DECLARE #nvcSqlQuery varchar(1000)
-- get the report language database name
SELECT #TextLibraryDatabaseName = TextLibraryDatabaseName FROM ProjectSettings
SET #nvcSqlQuery = 'SELECT #ResultVar =' + #LanguageColumn + ' FROM [' + #TextLibraryDatabaseName + '].dbo.TXTTable WHERE TEXTID = ' + cast(#TextID as varchar(30))
EXEC(#nvcSqlQuery)
-- Return the result of the function
RETURN #ResultVar
END
Is there any way to work around this so we don’t have to use the dynamic sql in our stored procedures so it is only ‘contained’ in 1 function?
Thanks in advance & kind regards,
Kurt
Yes, it is possible with the help of synonym mechanism introduced with SQL Server 2005. So, you can create synonym during your setting up procedure based on data from ProjectSettings table and you can use it in your function. Your code will look something like this:
UPDATE: The code of function is commented here because it still contains dynamic SQL which does not work in function as Kurt said in his comment. New version of function is below this code.
-- Creating synonym for TXTTable table
-- somewhere in code when processing current settings
-- Suppose your synonym name is 'TextLibrary'
--
-- Drop previously created synonym
IF EXISTS (SELECT * FROM sys.synonyms WHERE name = N'TextLibrary')
DROP SYNONYM TextLibrary
-- Creating synonym using dynamic SQL
-- Local variables
DECLARE #TextLibraryDatabaseName varchar(1000)
DECLARE #nvcSqlQuery varchar(1000)
-- get the report language database name
SELECT #TextLibraryDatabaseName = TextLibraryDatabaseName FROM ProjectSettings
SET #nvcSqlQuery = 'CREATE SYNONYM TextLibrary FOR [' + #TextLibraryDatabaseName + '].dbo.TXTTable'
EXEC(#nvcSqlQuery)
-- Synonym created
/* UPDATE: This code is commented but left for discussion consistency
-- Function code
CREATE FUNCTION [dbo].[GetTextFromLib]
(
#TextID int,
#LanguageColumn Varchar(5)
)
RETURNS varchar(255)
AS
BEGIN
-- return variables
DECLARE #ResultVar varchar(255)
-- Local variables
DECLARE #nvcSqlQuery varchar(1000)
SET #nvcSqlQuery = 'SELECT #ResultVar =' + #LanguageColumn + ' FROM TextLibrary WHERE TEXTID = ' + cast(#TextID as varchar(30))
EXEC(#nvcSqlQuery)
-- Return the result of the function
RETURN #ResultVar
END
*/
UPDATE This is one more attempt to solve the problem. Now it uses some XML trick:
-- Function code
CREATE FUNCTION [dbo].[GetTextFromLib]
(
#TextID int,
#LanguageColumn Varchar(5)
)
RETURNS varchar(255)
AS
BEGIN
-- return variables
DECLARE #ResultVar varchar(255)
-- Local variables
DECLARE #XmlVar XML
-- Select required record into XML variable
-- XML has each table column value in element with corresponding name
SELECT #XmlVar = ( SELECT * FROM TextLibrary
WHERE TEXTID = #TextID
FOR XML RAW, ELEMENTS )
-- Select value of required element from XML
SELECT #ResultVar = Element.value('(.)[1]', 'varchar(255)')
FROM #XmlVar.nodes('/row/*') AS T(Element)
WHERE Element.value('local-name(.)', 'varchar(50)') = #LanguageColumn
-- Return the result of the function
RETURN #ResultVar
END
Hope this helps.
Credits to answerer of this question at Stackoverflow - How to get node name and values from an xml variable in t-sql
To me, it sounds like a total PITA... However, how large is this database of "words" you are dealing with. Especially if it is not changing much and remains pretty constant. Why not have on some normal cycle (such as morning), just have one dynamic query generated that queries the one that changes and synchronize it to a "standard" table name in YOUR database that won't change. Then, all your queries run against YOUR version and completely remove the constant dynamic queries every time. Yes there would need to be this synchronizing stored procedure to run, but if it can be run on a schedule, you should be fine, and again, how large is the table of "words" for proper language context.
I have to pass multiple values (eg: Year1, year2, year3, etc.) to the same query, but I cannot use the IN condition as I'm using less than or equal to in most of the cases. Can I do this by passing multiple values through the same parameter without changing the query?
Is it possible to get multiple values from an SSRS parameter and pass them on to the query to get the output as:
Year1 Year2 Year3
Value(output) Value(output) Value(output)
You can pass the multi-value parameter as a comma separated string but your SQL query is going to need updating to handle that CSV string. In order to pass the multi-value parameter as a CSV string you would open the dataset properties and go to the parameters tab. Then set the value of your parameter to this expresssion:
=JOIN(Parameters!MultiValueYearParameter.Value,",")
This will join all of the values in the multivalue parameter together and use a comma as the delimiter. You can then process this using the split function below (or just modify it to work inline in your SQL if you either cannot or don't need to create a separate function to do this).
This blog post on a Split Function for T-SQL using FOR XML shows how to do this without using string parsing or a while loop. String parsing is prone to error and isn't scalable and while loops should just be avoided in SQL whenever possible.
Below I've modified the split function to return a table of DATE values that you can then use in an INNER JOIN to filter your query using whatever operators you like.
--this is the parameter passed from the report
--(the date strings may not be formatted this way. do not try to rely on that)
DECLARE #YearParameter VARCHAR(MAX) = '2014-01-01,2011-12-02,2015-10-22';
--use this to do the xml parsing
declare #xml xml = N'<root><r>' + replace(#YearParameter,',','</r><r>') + '</r></root>'
--create a table variable to store the date values
DECLARE #dateValues TABLE (val DATE);
--parse the xml/csv string and cast the results to a DATE and insert into the table var
INSERT INTO #dateValues
select CAST(r.value('.','varchar(max)') AS DATE) AS val
from #xml.nodes('//root/r') as records(r);
You can then use that table variable to filter your SQL query. I've given an example of how to use it below.
In the example I create a table of rows with a start and end date. Then I filter that table to show only rows where a parameter value is between the start and end date.
DECLARE #testTable TABLE (Descript VARCHAR(25), startDate DATE, endDate DATE);
INSERT INTO #testTable (Descript, startDate, endDate)
VALUES ('row1', '2014-05-01','2014-08-01'), ('row2', '2013-10-01','2014-01-10'), ('row3', '2015-10-01','2015-12-15'),('row4','2013-01-01','2015-01-01'),
--these rows won't appear in the result set
('row5','2010-01-01','2010-06-01'), ('row6','2013-12-25','2014-05-20');
-- get all rows from the test table where a selected parameter value
-- is between the start and end dates.
SELECT *
FROM #testTable AS tbl
WHERE EXISTS (
SELECT *
FROM #dateValues
WHERE val BETWEEN tbl.startDate AND tbl.endDate
);
In SSRS you can build tables and complex solutions.
In the Text Query of report builder here is an example of splitting apart a parameter to get three dates.
BEGIN
/* suppose the inbound pram is a string with 10 places per date '01/01/2010,11/12/2012,05/06/2013' */
/* you could also nip the string apart by each comma... */
DECLARE #YEAR1 DATETIME
DECLARE #YEAR2 DATETIME
DECLARE #YEAR3 DATETIME
SET #YEAR1 = CAST(SUBSTRING(#INBOUNDPARAM, 1, 10) AS DATETIME)
SET #YEAR2 = CAST(SUBSTRING(#INBOUNDPARAM, 12, 10) AS DATETIME)
SET #YEAR3 = CAST(SUBSTRING(#INBOUNDPARAM, 23, 10) AS DATETIME)
SELECT #YEAR1 AS Year1, #YEAR2 AS Year2, #YEAR3 AS Year3
END
Of course the Year of the date is just YEAR(#Year1) = 2010 for example...
Okay, I'm getting this error when I try to execute this procedure. The thing is I'm not trying to convert to a bit at any time. At least not on purpose. I'm a bit stumped at the moment.
Declare #AValue varchar(max)
set #AValue = (SELECT Value
FROM dbo.Tbl
WHERE Name=#FILE
AND Value LIKE (CAST(#MODID as varchar(15))+'|%'))
set #AValue = PARSENAME(REPLACE(#AValue, '|', '.'), 1) -- Hack way to parse.
INSERT INTO dbo.Tbl
(
Name,
Value,
Type,
CDT,
UDT,
Active,
User
)
VALUES
(
'Agreement',
(CAST(#MODID AS varchar(15)) + '|' + #AValue),
'Download',
GETDATE(),
GETDATE(),
1,
#USER
)
Check the triggers on the tables, particularly the insert. Lots of times unexplainable errors can lurk there.
Post the error, the CREATE PROCEDURE statement, and - most importantly - the call to the procedure that is failing. Also, what version of SQL Server are you using?
Most likely, the stored proc has a parameter of type BIT, and you are calling the procedure with a value like 'F', '-1' or some other string that can't be converted to BIT.
Sorry, found the answer. I came back today and the error I was getting was completely different than the one I had when I first asked the question. Sorry I forgot to write that error.
Problem was my WHERE clause was getting multiple returns from the database cause I had been testing a lot, so there were duplicate rows.
GetDate needs to be assigned to something in T-SQL before it can be used in a insert.
Try this:
DECLARE #CDT DateTime
DECLARE #UDT DateTime
SELECT #CDT = GetDate()
SELECT #UDT = GetDate()
INSERT INTO dbo.Tbl
(
[Name],
[Value],
[Type],
CDT,
UDT,
Active,
User
)
VALUES
(
'Agreement',
(CAST(#MODID AS varchar(15)) + '|' + #AValue),
'Download',
#CDT,
#UDT,
1,
#USER
)
I apologize in advance if this question is too long but I wanted to make sure I included all the steps I followed to get to this point.
I have the following table in my SQL Server 2008 database:
CREATE TABLE [VSPRRecalc](
[VSPRDate] [datetimeoffset](7) NOT NULL,
[CalcType] [int] NOT NULL,
CONSTRAINT [PK_VSPRRecalc] PRIMARY KEY CLUSTERED ([VSPRDate] ASC)
It has some rows in it that look like this:
INSERT [vsprrecalc](VSPRDate,CalcType) VALUES('2010-12-15 10:17:49.5780000 -05:00','3')
INSERT [vsprrecalc](VSPRDate,CalcType) VALUES('2010-12-16 07:44:03.3750000 -05:00','1')
INSERT [vsprrecalc](VSPRDate,CalcType) VALUES('2010-12-17 07:40:40.1090000 -05:00','1')
INSERT [vsprrecalc](VSPRDate,CalcType) VALUES('2010-12-18 16:29:02.2203744 -05:00','2')
INSERT [vsprrecalc](VSPRDate,CalcType) VALUES('2010-12-20 09:58:50.1250000 -05:00','1')
INSERT [vsprrecalc](VSPRDate,CalcType) VALUES('2010-12-29 19:21:26.8120000 -05:00','1')
I'm using linq to check and see if a given date already exists in this table:
var recalc = (from re in VSPRRecalcs
where re.VSPRDate.Date == oDate.Value.Date
select re).SingleOrDefault();
Currently recalc returns null whenever the date is within 5 hours of midnight (like the 12-29 case in the insert statements above). I checked and the following sql is being executed:
exec sp_executesql N'SELECT [t0].[VSPRDate], [t0].[CalcType]
FROM [dbo].[VSPRRecalc] AS [t0]
WHERE
CONVERT(DATE, [t0].[VSPRDate]) = #p0',N'#p0 datetime',#p0='2010-12-29'
Which returns 0 records. I modified the query to make the test easier to play with and came up with the following:
declare #t as date
set #t = '2010-12-29'
select *,
case when CONVERT(DATE, [VSPRDate]) = #t then 'true' else 'false' end
from VSPRRecalc where
CONVERT(DATE, [VSPRDate]) = #t
That query works for any other date in the table but not for any date that is within 5 hours of midnight (again see 12-29 above). If I run the above query without the where clause the 12-29 row does have 'true' displayed so clearly the boolean is evaluating the way I expect in the select statement but not in the where clause. Why does that happen?
I would say that's a bug on SQL Server, regarding conversion between the DATETIMEOFFSET time and the more "standard" types DATETIME and DATE...
What I have find out is the following:
This works:
EXEC sp_executesql N'SELECT [t0].[VSPRDate], [t0].[CalcType]
FROM [dbo].[VSPRRecalc] AS [t0]
WHERE [t0].[VSPRDate] = #p0',
N'#p0 DATETIMEOFFSET(7)',
#p0 = '2010-12-29 19:21:26.8120000 -05:00'
Which means that when we keep using DATETIMEOFFSET, there is no problem whatsoever... Still, you seem to need to find all records in a given day, not search for an exact DATETIMEOFFSET, right?
So, probably a little bit more useful, this works also:
EXEC sp_executesql N'SELECT [t0].[VSPRDate], [t0].[CalcType]
FROM [dbo].[VSPRRecalc] AS [t0]
WHERE [t0].[VSPRDate] BETWEEN #p0 AND #p1',
N'#p0 DATETIMEOFFSET, #p1 DATETIMEOFFSET',
#p0 = '2010-12-29 00:00:00.0000000 -05:00',
#p1 = '2010-12-30 00:00:00.0000000 -05:00'
I guess the secret here is keep using the DATETIMEOFFSET data type (and it's CLR equivalent, System.DateTimeOffset). That way you will not get into this conversion issue...
(And, by the way, you should use a BETWEEN for searching records based on a date anyway. This allows the DBMS to use an index over that column, which is not possible when your WHERE clause is making a function call or a hard-coded conversion).
Edit I forgot there is no BETWEEN operator available for Linq for SQL - but that's easy to fix, just use something like WHERE [t0].[VSPRDate] >= #p0 AND [t0].[VSPRDate] <= #p1'... Also, this SO question is about declaring an extension method in order to implement it, but I don't know if it works...
I made the following function in SQL Server 2008 earlier this week that takes two parameters and uses them to select a column of "detail" records and returns them as a single varchar list of comma separated values. Now that I get to thinking about it, I would like to take this table and application-specific function and make it more generic.
I am not well-versed in defining SQL functions, as this is my first. How can I change this function to accept a single "column" worth of data, so that I can use it in a more generic way?
Instead of calling:
SELECT ejc_concatFormDetails(formuid, categoryName)
I would like to make it work like:
SELECT concatColumnValues(SELECT someColumn FROM SomeTable)
Here is my function definition:
FUNCTION [DNet].[ejc_concatFormDetails](#formuid AS int, #category as VARCHAR(75))
RETURNS VARCHAR(1000) AS
BEGIN
DECLARE #returnData VARCHAR(1000)
DECLARE #currentData VARCHAR(75)
DECLARE dataCursor CURSOR FAST_FORWARD FOR
SELECT data FROM DNet.ejc_FormDetails WHERE formuid = #formuid AND category = #category
SET #returnData = ''
OPEN dataCursor
FETCH NEXT FROM dataCursor INTO #currentData
WHILE (##FETCH_STATUS = 0)
BEGIN
SET #returnData = #returnData + ', ' + #currentData
FETCH NEXT FROM dataCursor INTO #currentData
END
CLOSE dataCursor
DEALLOCATE dataCursor
RETURN SUBSTRING(#returnData,3,1000)
END
As you can see, I am selecting the column data within my function and then looping over the results with a cursor to build my comma separated varchar.
How can I alter this to accept a single parameter that is a result set and then access that result set with a cursor?
Others have answered your main question - but let me point out another problem with your function - the terrible use of a CURSOR!
You can easily rewrite this function to use no cursor, no WHILE loop - nothing like that. It'll be tons faster, and a lot easier, too - much less code:
FUNCTION DNet.ejc_concatFormDetails
(#formuid AS int, #category as VARCHAR(75))
RETURNS VARCHAR(1000)
AS
RETURN
SUBSTRING(
(SELECT ', ' + data
FROM DNet.ejc_FormDetails
WHERE formuid = #formuid AND category = #category
FOR XML PATH('')
), 3, 1000)
The trick is to use the FOR XML PATH('') - this returns a concatenated list of your data columns and your fixed ', ' delimiters. Add a SUBSTRING() on that and you're done! As easy as that..... no dogged-slow CURSOR, no messie concatenation and all that gooey code - just one statement and that's all there is.
You can use table-valued parameters:
CREATE FUNCTION MyFunction(
#Data AS TABLE (
Column1 int,
Column2 nvarchar(50),
Column3 datetime
)
)
RETURNS NVARCHAR(MAX)
AS BEGIN
/* here you can do what you want */
END
You can use Table Valued Parameters as of SQL Server 2008, which would allow you to pass a TABLE variable in as a parameter. The limitations and examples for this are all in that linked article.
However, I'd also point out that using a cursor could well be painful for performance.
You don't need to use a cursor, as you can do it all in 1 SELECT statement:
SELECT #MyCSVString = COALESCE(#MyCSVString + ', ', '') + data
FROM DNet.ejc_FormDetails
WHERE formuid = #formuid AND category = #category
No need for a cursor
Your question is a bit unclear. In your first SQL statement it looks like you're trying to pass columns to the function, but there is no WHERE clause. In the second SQL statement you're passing a collection of rows (results from a SELECT). Can you supply some sample data and expected outcome?
Without fully understanding your goal, you could look into changing the parameter to be a table variable. Fill a table variable local to the calling code and pass that into the function. You could do that as a stored procedure though and wouldn't need a function.