SSRS monthly/daily report - sql-server

I have a variable in my report which holds 2 possible values: 'monthly' and 'daily'. How can I put this variable (lets call it #reportModel). I think it should be somewhere in GROUP BY clause, but don't know how should it look like.
DECLARE #reportModel VARCHAR(10)
SET #reportModel = 'monthly'
SELECT P.product, SUM(O.price * O.quantity), O.orderDate
FROM Products AS P
INNER JOIN Orders AS O ON P.ID = O.ID
And what now?

How about a stored procedure to handle this, something like.....
CREATE PROCEDURE rpt_GetData
#reportModel VARCHAR(10)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #Sql NVARCHAR(MAX);
IF (#reportModel = 'daily')
BEGIN
SET #Sql = N' SELECT P.product
, SUM(O.price * O.quantity) AS Total
, O.orderDate
FROM Products AS P
INNER JOIN Orders AS O ON P.ID = O.ID
GROUP BY P.product , O.orderDate'
Exec sp_executesql #Sql
END
ELSE IF (#reportModel = 'monthly')
BEGIN
SET #Sql = N' SELECT P.product
, SUM(O.price * O.quantity) AS Total
, MONTH(O.orderDate) AS [Month]
FROM Products AS P
INNER JOIN Orders AS O ON P.ID = O.ID
GROUP BY P.product, MONTH(O.orderDate)'
Exec sp_executesql #Sql
END
END

Adding this as an answer because I mentioned it in the comments in #M.Ali's answer.
So I would suggest you change thinking slightly with one of these options.
Two reports - Simply make a report for daily and another for monthly. Now you have no worries with complex SQL etc.
Make 2 stored procedures, one with the GROUP BY daily and one monthly. Then in your SSRS dataset, create an expression for you SQL that chooses the procedure based on parameter:
=IIf(Parameters!reportModel.Value = "monthly", "GetMonthlyData", "GetDailyData")

I would put it in the Group On Expression of the table or chart rather than doing it in the query.
=IIF(Parameters!reportModel.Value = "monthly", Month(Fields!orderDate.Value), Fields!orderDate.Value)
If you'd rather do it in the query and don't want to wait for DBAs to get around to deploying a Stored Procedure (not to mention maintaining it whenever there's a change), you could use your parameter in a CASE like:
SELECT P.product, SUM(O.price * O.quantity),
CASE WHEN #reportModel = 'monthly' THEN CAST(MONTH(O.orderDate) AS VARCHAR(12))
ELSE CAST(O.orderDateAS VARCHAR(12)) END AS DT
FROM WORKFLOW_SHARED.MAIN.VW_CLAIMSOVERPAYMENT
WHERE DATECOMPLETED > '7/1/2015'
GROUP BY P.product,
CASE WHEN #reportModel = 'monthly' THEN CAST(MONTH(O.orderDate) AS VARCHAR(12))
ELSE CAST(O.orderDateAS VARCHAR(12)) END
This way you don't have to maintain two separate reports.

How about something simple like this
select
P.product
,Total = sum(O.price * O.quantity)
, O.orderDate
from
Products as P
inner join Orders as O on P.ID = O.ID
where
#reportModel = 'daily'
union all
select
P.product
,Total = sum(O.price * O.quantity)
,[Month] = MONTH(O.orderDate)
from
Products as P
inner JOIN Orders as O on P.ID = O.ID
group by
P.product
,[Month] = MONTH(O.orderDate)
where
#reportModel = 'monthly'

Related

Creating result sets with multiple values in a column

I am using the AdventureWorks 2008 schema.
I am writing a procedure that returns the total sales by Sales Territory for sales people. There will be multiple sales people, so they need to be concatenated together in a comma separated list to show all sales people per record.
I have a solution below using cursors but I am wondering if there are any other ways of doing this (ie: cross apply, CTE's, etc).
if OBJECT_ID('tempdb..#tmpTerritorySalesPeople') is not null
drop table #tmpTerritorySalesPeople
create table #tmpTerritorySalesPeople (TerritoryID int, TerritoryName varchar(50), TerritorySalesPeople varchar(2000))
GO
declare #territoryID int,
#territoryName varchar(50),
#salesPersonName varchar(100),
#salesPersonList varchar(2000)
--cursor for territy and tname
declare c_territory cursor for
select st.territoryid, st.name
from sales.SalesTerritory st
order by st.territoryid
open c_territory
FETCH c_territory
INTO #territoryid, #territoryname
while ##FETCH_STATUS = 0
BEGIN
declare c_territorySalesPeople cursor for
-- cursor to hold list of sales people per territory
select [SalesPersonName] = p.FirstName + ' ' + p.LastName
FROM
sales.SalesPerson SP
JOIN Person.person P
ON SP.BusinessEntityID = P.BusinessEntityID
JOIN Sales.SalesTerritory ST
ON SP.territoryID = ST.TerritoryID
where sp.territoryid = #territoryid
order by st.territoryid
set #salesPersonList = ''
open c_territorySalesPeople
fetch next from c_territorySalesPeople into #salesPersonName;
while ##FETCH_STATUS = 0
begin
if #salesPersonList = ''
set #salesPersonList = #salesPersonName
else
set #salesPersonList = #salesPersonList + ', ' + #salesPersonName
fetch next from c_territorySalesPeople into #salesPersonName;
end
close c_territorySalesPeople
deallocate c_territorySalesPeople
insert into #tmpTerritorySalesPeople
select #territoryid, #territoryname, #salesPersonList
fetch next from c_territory
into #territoryid, #territoryname
end
close c_territory
deallocate c_territory
go
select
[Sales Territory] = tsp.TerritoryName,
[Sales Person] = tsp.TerritorySalesPeople,
[TotalSalesAmount] = '$'+CONVERT(VARCHAR,sum(SOH.TotalDue),1)
FROM
sales.SalesOrderHeader SOH
JOIN #tmpTerritorySalesPeople tsp
ON SOH.TerritoryID = tsp.TerritoryID
where soh.TerritoryID is not null
group by tsp.TerritoryName, tsp.TerritorySalesPeople
order by tsp.TerritoryName
go
Source of data is Adventureworks.
Results of data:
Sales Territory,Sales Person,TotalSalesAmount
Australia,Lynn Tsoflias,"$11,814,376.10"
Canada,"Garrett Vargas, José Saraiva","$18,398,929.19"
Central,Jillian Carson,"$8,913,299.25"
France,Ranjit Varkey Chudukatil,"$8,119,749.35"
Germany,Rachel Valdez,"$5,479,819.58"
Northeast,Michael Blythe,"$7,820,209.63"
Northwest,"Pamela Ansman-Wolfe, David Campbell, Tete Mensa-Annan","$18,061,660.37"
Southeast,Tsvi Reiter,"$8,884,099.37"
Southwest,"Linda Mitchell, Shu Ito","$27,150,594.59"
United Kingdom,Jae Pak,"$8,574,048.71"
You need to use STUFF() string function and FOR XML PATH to get CSV value column of territory wise sales persons.
You need to use CROSS apply to get territory wise total sales amount.
You need to use ISNULL() also for NULL value handle in any field.
Please check below query.
SELECT
ST.Name AS [Sales Territory],
STUFF(
(
SELECT
',' + ISNULL(PER.FirstName,'') + ' ' + ISNULL(PER.LastName,'')
---CONCAT(',',PER.FirstName,' ',PER.LastName) /*SQL SERVER 2012 Onwards*/
FROM sales.SalesPerson SSP
JOIN Person.person PER ON SSP.BusinessEntityID = PER.BusinessEntityID
AND SSP.territoryid = SP.territoryid
FOR XML PATH('')
),
1,1,'') AS [Sales Person],
TSalesAmt.[TotalSalesAmount]
FROM sales.SalesPerson SP
JOIN Person.person P ON SP.BusinessEntityID = P.BusinessEntityID
JOIN Sales.SalesTerritory ST ON SP.territoryID = ST.TerritoryID
CROSS APPLY
(
SELECT
SOH.TerritoryID,
'$'+CONVERT(VARCHAR,sum(SOH.TotalDue)) AS TotalSalesAmount
FROM sales.SalesOrderHeader SOH
Where SOH.TerritoryID = ST.TerritoryID
GROUP BY SOH.TerritoryID
) TSalesAmt
order by st.name

Is it always possible to transform multiple spatial selects with while loop and variables into a single query without using temp tables in sql?

This problem can be solved with temp table, however, I don't want to use Temp table or var table, this question is mostly for my personal educational purposes.
I inherited the following SQL:
DECLARE #i int = 993
while #i <=1000
begin
declare #lat nvarchar(20)
select top 1 #lat = SUBSTRING(Address,0,CHARINDEX(',',Address,0)) from dbo.rent
where id = #i;
declare #lon nvarchar(20)
select top 1 #lon = SUBSTRING(Address,CHARINDEX(',',Address)+1,LEN(Address)) from dbo.rent
where id = #i
declare #p GEOGRAPHY = GEOGRAPHY::STGeomFromText('POINT('+ #lat +' '+#lon+')', 4326)
select price/LivingArea sq_m, (price/LivingArea)/avg_sq_m, * from
(select (sum(price)/sum(LivingArea)) avg_sq_m, count(1) cnt, #i id from
(select *, GEOGRAPHY::STGeomFromText('POINT('+
convert(nvarchar(20), SUBSTRING(Address,0,CHARINDEX(',',Address,0)))+' '+
convert( nvarchar(20), SUBSTRING(Address,CHARINDEX(',',Address)+1,LEN(Address)))+')', 4326)
.STBuffer(500).STIntersects(#p) as [Intersects]
from dbo.rent
where Address is not null
) s
where [Intersects] = 1) prox
inner join dbo.rent r on prox.id = r.id
set #i = #i+1
end
it is used to analyze property prices per square meter that are in proximity and compare them to see which ones are cheaper...
Problem: a mechanism for calling has to be moved from C# to SQL and all queries have to be combined into a single result (now you get one row per one while run), i.e #i and #p has to go and become while id < x and id > y or somehow magically joined,
the procedure is a cut down version of actual thing but having a solution to the above I will have no problem making the whole thing work...
I am of the opinion that any SQL mechanism with variables and loops can be transformed to a single SQL statement, hence the question.
SqlFiddle
If I understand your question properly (Remove the need for loops and return one data set) then you can use CTE (Common Table Expressions) for the Lats, Lons and Geog variables.
You;re SQLFIddle was referencing a database called "webanalyser" so I removed that from the query below
However, the query will not return anything as the sample data has wrong data for address column.
;WITH cteLatsLongs
AS(
SELECT
lat = SUBSTRING(Address, 0, CHARINDEX(',', Address, 0))
,lon = SUBSTRING(Address, CHARINDEX(',', Address) + 1, LEN(Address))
FROM dbo.rent
)
,cteGeogs
AS(
SELECT
Geog = GEOGRAPHY ::STGeomFromText('POINT(' + LL.lat + ' ' + LL.lon + ')', 4326)
FROM cteLatsLongs LL
),cteIntersects
AS(
SELECT *,
GEOGRAPHY::STGeomFromText('POINT(' + CONVERT(NVARCHAR(20), SUBSTRING(Address, 0, CHARINDEX(',', Address, 0))) + ' ' + CONVERT(NVARCHAR(20), SUBSTRING(Address, CHARINDEX(',', Address) + 1, LEN(Address))) + ')', 4326).STBuffer(500).STIntersects(G.Geog) AS [Intersects]
FROM dbo.rent
CROSS APPLY cteGeogs G
)
SELECT avg_sq_m = (SUM(price) / SUM(LivingArea)), COUNT(1) cnt
FROM
cteIntersects I
WHERE I.[Intersects] = 1
It can be done, in this specific case 'discovery' that was necessary was the ability to perform JOINs on Point e.g ability to join tables on proximity (another a small cheat was to aggregate point-strings to actual points, but it's just an optimization). Once this is done, a query could be rewritten as follows:
SELECT adds.Url,
adds.Price/adds.LivingArea Sqm,
(adds.Price/adds.LivingArea)/k1.sale1Avg ratio,
*
FROM
(SELECT baseid,
count(k1Rent.rentid) rent1kCount,
sum(k1Rent.RperSqM)/(count(k1Rent.rentid)) AS rent1kAvgSqM,
count(around1k.SaleId) sale1kCount,
(sum(k1sale.price)/sum(k1Sale.LivingArea)) sale1Avg,
(sum(k1sale.price)/sum(k1Sale.LivingArea))/((sum(k1Rent.RperSqM)/(count(k1Rent.rentid)))*12) years --*
FROM
(SELECT sa.id baseid,
s.id saleid,
s.RoomCount,
POINT
FROM SpatialAnalysis sa
INNER JOIN Sale s ON s.Id = SaleId
WHERE sa.SalesIn1kRadiusCount IS NULL) AS base
JOIN SpatialAnalysis around1k ON base.Point.STBuffer(1000).STIntersects(around1k.Point) = 1
LEFT OUTER JOIN
(SELECT id rentid,
rc,
Price/avgRoomSize RperSqM
FROM
(SELECT *
FROM
(SELECT rc,
sum(avgArea*c)/sum(c) avgRoomSize
FROM
(SELECT roomcount rc,
avg(livingarea) avgArea,
count(1) c
FROM Rent
WHERE url LIKE '%systemname%'
AND LivingArea IS NOT NULL
GROUP BY RoomCount
UNION
(SELECT roomcount rc,
avg(livingarea) avgArea,
count(1) c
FROM sale
WHERE url LIKE '%systemname%'
AND LivingArea IS NOT NULL
GROUP BY RoomCount))uni
GROUP BY rc) avgRoom) avgrents
JOIN rent r ON r.RoomCount = avgrents.rc) k1Rent ON k1Rent.rentid =around1k.RentId
AND base.RoomCount = k1Rent.rc
LEFT OUTER JOIN Sale k1Sale ON k1Sale.Id = around1k.SaleId
AND base.RoomCount = k1Sale.RoomCount
GROUP BY baseid) k1
left outer join SpatialAnalysis sp on sp.Id = baseid
left outer join Sale adds on adds.Id = sp.SaleId
where adds.Price < 250000
order by years, ratio

SQL Server: in-query error catching

In my query I have to join tables from db that is not under my control. It is driving me mad as sometimes this db is not accessible (please don't ask me why) and this breaks my query. Fields I'm joining are not fundamental for my operations and I want my app to work normally even if these fields are not accessible at a time.
Here's the data structure that I do not own:
[DBOutOfControl].[dbo].[Table1]:
[Field1]
[Field2]
[DBOutOfControl].[dbo].[Table2]:
[Field1]
[Field2]
[Field3]
And here is my table:
[DBInMyControl].[dbo].[Table3]:
[Field1]
My original query looks something like that:
SELECT [Table3].[MyID],
[ForeignDataQry].[A],
[ForeignDataQry].[B]
FROM [DBInMyControl].[dbo].[Table3]
LEFT JOIN
(SELECT [Table1].[Field1] AS [MyID],
[Table1].[Field2] AS [A],
[SubQry].[Field2] AS [B]
FROM [DBOutOfControl].[dbo].[Table1]
LEFT JOIN
(SELECT [Table2].[Field1],
[Table2].[Field2]
FROM [DBOutOfControl].[dbo].[Table2]
WHERE [Table2].[Field3] = 'Where') AS [SubQry] ON [Table1].[Field1] = [SubQry].[Field1]) AS [ForeignDataQry] ON [Table3].[MyID]=[ForeignDataQry].[MyID]
How can i bullet-proof this query so when [ForeignDataQry] generates an error the result would be:
[MyID] [A] [B]
1 NULL NULL
Otherwise
[MyID] [A] [B]
1 Va1 Val2
Is there something that could be done server side?
Just specify the expected result of COUNT, the three names, and you can check tables beforehand. A minor rewrite can allow this to check for objects other than tables, utilize EXISTS if desired, skip or add more checks, etc.:
IF 0 = ( -- Specify how many records you expect to come.
SELECT COUNT(C.[name]) AS [COUNT]
FROM sys.objects AS O
LEFT JOIN sys.schemas AS S ON S.schema_id = O.schema_id
LEFT JOIN sys.columns AS C ON C.object_id = O.object_id
WHERE O.[name] = 'tablename'
AND S.[name] = 'schemaname'
AND C.[name] = 'columnname'
)
SELECT 1 AS A -- Do some code.
ELSE
SELECT 2 AS B -- Do some other code.
I'd wrap the problematic query in dynamic code in order to be able to catch the compilation error (that we cannot catch in the same scope) like this:
begin try
declare #sql varchar(4000) =
'SELECT [Table3].[MyID],
[ForeignDataQry].[A],
[ForeignDataQry].[B]
FROM [DBInMyControl].[dbo].[Table3]
LEFT JOIN
(SELECT [Table1].[Field1] AS [MyID],
[Table1].[Field2] AS [A],
[SubQry].[Field2] AS [B]
FROM [DBOutOfControl].[dbo].[Table1]
LEFT JOIN
(SELECT [Table2].[Field1],
[Table2].[Field2]
FROM [DBOutOfControl].[dbo].[Table2]
WHERE [Table2].[Field3] = ''Where'') AS [SubQry] ON [Table1].[Field1] = [SubQry].[Field1]) AS [ForeignDataQry] ON [Table3].[MyID]=[ForeignDataQry].[MyID]'
exec(#sql)
end try
begin catch
SELECT [Table3].[MyID],
cast(null as ... )as [A],
cast(null as ...) as [B]
FROM [DBInMyControl].[dbo].[Table3]
end catch
Here I use cast(null as ... )as [A] to get the same type as [ForeignDataQry].[A] has, for example if [ForeignDataQry].[A] is int there should be int: cast(null as int )as [A]
I dealt with this problem in a different way that I originally wanted but anyway:
I created a [Table4] that keeps copy of the records from foreign tables - fields matches [ForeignDataQry] + timestamp. I created procedure:
CREATE PROCEDURE [dbo].[CopyForeignData]
AS
DECLARE #Timestamp datetime
SET #Timestamp = getdate()
BEGIN
INSERT INTO [DBInMyControl].[dbo].[Table4] ([MyID], [A], [B], [Timestamp])
SELECT [Table1].[Field1] AS [MyID],
[Table1].[Field2] AS [A],
[SubQry].[Field2] AS [B],
#Timestamp
FROM [DBOutOfControl].[dbo].[Table1]
LEFT JOIN
(SELECT [Table2].[Field1],
[Table2].[Field2]
FROM [DBOutOfControl].[dbo].[Table2]
WHERE [Table2].[Field3] = 'Where') AS [SubQry] ON [Table1].[Field1] = [SubQry].[Field1]
DELETE FROM [DBInMyControl].[dbo].[Table4] WHERE [Timestamp] <> #Timestamp
END
I will call this every time when I start my app and handle error there and modify my main LEFT JOIN to refer [Table4]

Why UNION used in a stored procedure in derived table?

I am not author of the stored procedure and I am wondering why they used UNION in SELECT statement when selecting from the derived table ...
If I comment out the whole UNION ALL SELECT statement, I get the same result with the same basically performance.
So I am just wonder why it is there? What kind of trick does it makes?
Below is the whole stored procedure in case I am missing something
ALTER PROCEDURE [dbo].[rptActivityLog] --'1/1/2016', '2/3/2016'
(#DateFrom datetime = null,
#DateTo datetime = null,
#UserGuid uniqueidentifier = null,
#CurrentUserGuid uniqueidentifier = NULL)
AS
SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
DECLARE #UserID SMALLINT
SELECT #UserID = UserID
FROM tblUsers
WHERE (UserGUID = #UserGuid)
DECLARE #ValidOfficeGuids TABLE (OfficeGuid uniqueidentifier primary key)
--if user is in tblUserQuotingOffice then use only that Office
--otherwise they will have access to all offices
IF EXISTS (SELECT OfficeGuid
FROM tblUserQuotingOffice
WHERE UserGuid = #CurrentUserGuid)
BEGIN
INSERT INTO #ValidOfficeGuids
SELECT OfficeGuid
FROM tblUserQuotingOffice
WHERE UserGuid = #CurrentUserGuid
END
ELSE
BEGIN
INSERT INTO #ValidOfficeGuids
SELECT OfficeGUID
FROM tblClientOffices
END
DECLARE #compareDateFrom DATETIME
set #compareDateFrom = CAST(CONVERT(VARCHAR(50), #DateFrom, 101) AS DATETIME)
declare #compareDateTo datetime
set #compareDateTo = DateAdd(ms, -2, DateAdd(d, 1, CAST(CONVERT(VARCHAR(50), DATEADD(day, 7, #DateTo), 101) AS DATETIME)))
--First get the log entries
declare #logResults table
(
ID int primary key not null
, IdentifierGuid uniqueidentifier
)
insert into #logResults
select
l.ID
, l.IndentifierGuid
from
tblLog l
where
l.ActionDate between #compareDateFrom and #compareDateTo
and l.IndentifierGuid is not null
select
distinct
T.UserName
, T.ControlNo
, T.InsuredPolicyName
, Replace(Replace(T.[Action],Char(10),''),Char(13),'') as [Action]
, T.ActionDate
, T.LineName as LOB
from
(
select
u.UserName
, q.ControlNo
, q.InsuredPolicyName
, l.[Action]
, l.ActionDate
, ll.LineName
, l.UserID
from
#logResults r
inner join tblLog l on r.ID = l.ID
inner join tblUsers u on l.UserID = u.UserID
inner join tblQuotes q on r.IdentifierGuid = q.QuoteGUID
inner join lstLines ll on q.LineGUID = ll.LineGUID
-- WHY DO WE USE BELOW UNION STATEMENT??????????????????????????????????
union
select
u.UserName
, q.ControlNo
, q.InsuredPolicyName
, l.[Action]
, l.ActionDate
, ll.LineName
, l.UserID
from
#logResults r
inner join tblLog l on r.ID = l.ID
inner join tblUsers u on l.UserID = u.UserID
inner join tblQuotes q on r.IdentifierGuid = q.ControlGUID
inner join lstLines ll on q.LineGUID = ll.LineGUID
) T
WHERE IsNull(#UserID, T.UserID) = T.UserID
order by
T.ActionDate
There is a difference in the join with tblQuotes, looks like the union is meant to union two different datasets (one for QuoteGUIDs and one for ControlGUIDs)

SQL Server : I need to pass a row value as a parameter to table function in a stored procedure

I want to create a stored procedure with a table-valued function as a column.
I want to use one of the other column values as the parameter for the function.
ALTER PROCEDURE [dbo].[AuditReportLeaseID]
#leaseID int
AS
BEGIN
SET NOCOUNT ON;
SELECT
a.assetID as Asset, a.Location,
CONVERT(VARCHAR, a.auditdate, 101) AS Date,
a.qtyaudit AS Qty,
c.classname AS Class, a.grade AS Grade,
a.serialnumber AS [S/N],
a.materialdescription AS Description, a.Notes,
(SELECT tf.AD
FROM fGetAuditDescrConcat(a.assetId) tf) AS AuditDescription
FROM
audit a
LEFT OUTER JOIN
ORDER_DETAILS od ON a.assetID = od.assetId
INNER JOIN
class c ON a.classid = c.classid
WHERE
a.classID = c.classID
AND a.leaseID = #leaseID
ORDER BY
class, grade, a.materialDescription
END
This procedure will return multiple rows and I want to use the value of the first column a.assetID (which is a varchar(64)) as the parameter to the fGetAuditDescrConcat function.
Is this possible?
Your question is unclear, but I take it you might be looking for something like this:
ALTER PROCEDURE [dbo].[AuditReportLeaseID]
#leaseID int
AS
BEGIN
SET NOCOUNT ON;
SELECT * FROM
(Select a.assetID
from audit a
left outer join ORDER_DETAILS od on a.assetID=od.assetId
inner join class c on a.classid=c.classid where a.classID = c.classID and a.leaseID = #leaseID
Order by class,grade,a.materialDescription
) a
CROSS APPLY fGetAuditDescrConcat(a.assetID)
END
If your given query returns error like Subquery returns more than one row;
Try the modified on as below:
ALTER PROCEDURE [dbo].[AuditReportLeaseID]
#leaseID int
AS
BEGIN
SET NOCOUNT ON;
SELECT
a.assetID as Asset, a.Location,
CONVERT(VARCHAR, a.auditdate, 101) AS Date,
a.qtyaudit AS Qty,
c.classname AS Class, a.grade AS Grade,
a.serialnumber AS [S/N],
a.materialdescription AS Description, a.Notes,
(SELECT TOP 1 tf.AD
FROM fGetAuditDescrConcat(a.assetId) tf) AS AuditDescription
FROM
audit a
LEFT OUTER JOIN
ORDER_DETAILS od ON a.assetID = od.assetId
INNER JOIN
class c ON a.classid = c.classid
WHERE
a.classID = c.classID
AND a.leaseID = #leaseID
ORDER BY
class, grade, a.materialDescription
END

Resources