Adding CTE to SSRS / RDL


@jkane

Putting up the jkane-signal… can you show me how to do this?

I’m still working on my revamped Production Detail Report. Out-of-the-Box, the report just lists the Job Assemblies sequentially. I would like to sort them by parentage / lineage. I created a CTE BAQ to test this out and was successfully able to get them to present in an “indented” sequence. But… I can’t add my CTE BAQ as a datasource to a system table-based RDD.

I stumbled on a few threads where you mentioned the above, adding CTE’s directly into the SSRS/RDL Query. I’ve looked at a couple examples you’ve posted, but I’m struggling on exactly where/how I would inject my CTE query into the base RDL query.

I know I need to start with ;WITH

But I’m not sure how to reference the tables within my CTE. Would I use the same T1, T2, T3 designations they get in the standard query?

Do I include all the “from”, “where”, “union all”, “select” arguments (more or less copy/pasting from the BAQ designer?

Just not sure how to set this up.

3 Likes

Yes. The base RDL query is just SQL. If you can do it in SQL, you can do it in the RDL.

I’ll take a step back and provide some knowledge on how the reporting works in Epicor, forgive me if you already know any of this.

For whatever reason, Epicor created the RDD to compile all of the required data into one object for them to export. That is really all that the RDD is doing, creating its own query into its own tables and dumping all of the tables to the SQL db. That is why there is a separate SSRS Reports db when you install Epicor, it holds all of the tables that the RDD creates. So, if you think about it, the query in the RDL is just querying tables like any SQL query you write against a database. If you have access to the SSRS Reports db, you can open up SSMS and copy paste the RDL query in, replace the " + Parameters!TableGuid.Value + " with the actual GUID, and then run it and you will get the same results.

So, since it is just SQL, you can easily create a CTE with the tables that exist in the SSRS Reports db. It is a little trickier to do, but as long as you plan out what you need to do, it should not be bad. I would go into it more, but I am not sure what exactly you need to do and I don’t want to throw a bunch of stuff at you if you don’t need it.

Do you want to ask questions or throw the BAQ SQL at me?

5 Likes

Okay… I’m not sure where to start asking questions either, haha. Below is my CTE query (simply pasting out of BAQ designer).

with [AssySeq] as 
(select 
	[JobAsmbl].[Company] as [JobAsmbl_Company],
	[JobAsmbl].[JobNum] as [JobAsmbl_JobNum],
	[JobAsmbl].[AssemblySeq] as [JobAsmbl_AssemblySeq],
	[JobAsmbl].[Parent] as [JobAsmbl_Parent],
	(cast(FORMAT(JobAsmbl.AssemblySeq,'000') as nvarchar(1000))) as [Calculated_AssySeqPath],
	(1) as [Calculated_AssyLevel]
from Erp.JobAsmbl as JobAsmbl
where (JobAsmbl.AssemblySeq = 0)

union all
select 
	[JobAsmbl1].[Company] as [JobAsmbl1_Company],
	[JobAsmbl1].[JobNum] as [JobAsmbl1_JobNum],
	[JobAsmbl1].[AssemblySeq] as [JobAsmbl1_AssemblySeq],
	[JobAsmbl1].[Parent] as [JobAsmbl1_Parent],
	(CAST(AssySeq.Calculated_AssySeqPath + '.' + FORMAT(JobAsmbl1.AssemblySeq, '000') as nvarchar(1000))) as [Calculated_AssySeqPath2],
	(AssySeq.Calculated_AssyLevel+1) as [Calculated_AssyLevel2]
from Erp.JobAsmbl as JobAsmbl1
inner join  AssySeq  as AssySeq on 
	JobAsmbl1.Company = AssySeq.JobAsmbl_Company
	and JobAsmbl1.JobNum = AssySeq.JobAsmbl_JobNum
	and JobAsmbl1.Parent = AssySeq.JobAsmbl_AssemblySeq
where (JobAsmbl1.AssemblySeq <> 0))

select 
	[AssySeq1].[JobAsmbl_Company] as [JobAsmbl_Company],
	[AssySeq1].[JobAsmbl_JobNum] as [JobAsmbl_JobNum],
	[AssySeq1].[JobAsmbl_AssemblySeq] as [JobAsmbl_AssemblySeq],
	[AssySeq1].[JobAsmbl_Parent] as [JobAsmbl_Parent],
	[AssySeq1].[Calculated_AssySeqPath] as [Calculated_AssySeqPath],
	[AssySeq1].[Calculated_AssyLevel] as [Calculated_AssyLevel]
from  AssySeq  as AssySeq1

The below is the RDL Query for “JobHead” dataset (removing all the fields for easier reading):

="SELECT 
// a crap ton of fields //

FROM JobHead_" + Parameters!TableGuid.Value + " T1

LEFT OUTER JOIN JobAsmbl_" + Parameters!TableGuid.Value + " T2
  ON T1.Company = T2.Company AND T1.JobNum = T2.JobNum

LEFT OUTER JOIN JobOper_" + Parameters!TableGuid.Value + " T3
  ON T2.Company = T3.Company AND T2.JobNum = T3.JobNum AND T2.AssemblySeq = T3.AssemblySeq
 
LEFT OUTER JOIN JobOpDtl_" + Parameters!TableGuid.Value + " T4
  ON T3.Company = T4.Company AND T3.JobNum = T4.JobNum AND T3.AssemblySeq = T4.AssemblySeq AND T3.OprSeq = T4.OprSeq"

I guess, to start, the first part of my “joined” query would be selecting columns like JobAsmbl.Company, JobAsmbl.JobNum etc. I’m designating JobAsmbl as “T2” in the parent query. So, do I add these as T2.Company, T2.JobNum etc. in my CTE reference? Or should I just leave them as is, and they just act as aliases?

What about the portions of the CTE like the below?

from Erp.JobAsmbl as JobAsmbl

THIS, to me, makes sense to change to…

from T2 as JobAsmbl

I guess I’m just not sure how to tie the CTE query into the standard query and get all of the references correct.

Right now… the RDL uploads fine (no out-right rejection). But when I run the report, I get the follow error:

Query execution failed for dataset 'JobHead'. ---> System.Data.SqlClient.SqlException: "JobAsmbl" is not a recognized table hints option.
Incorrect syntax near 'AssySeq1'.

Since you are only using the JobAsmbl table, the easiest way to do this would be as follows.

=";WITH JACTE (*either copy paste all the T2 fields here or make up new ones*, *also add any new fields that appear in your CTE*)
AS
(
    SELECT *paste all the T2 fields here*, *add any additional fields you need to make*
    FROM JobAsmbl_" + Parameters!TableGuid.Value + " T2
    WHERE T2.AssemblySeq = 0
    UNION ALL
    SELECT *just put the fields you need for the recursive query*
    FROM JobAsmbl_" + Parameters!TableGuid.Value + " T22
    INNER JOIN T2 ON
    T22.Company = T2.Company AND
    T22.JobNum = T2.JobNum AND
    T22.Parent = T2.JobAsmbl_AssemblySeq
    WHERE T22.AssemblySeq <> 0
)

SELECT *add all of the other fields from the RDL query and your fields from the WITH clause*

*put all of the same FROM text from the original RDL query EXCEPT for T2*

LEFT OUTER JOIN JACTE
ON T1.Company = JACTE.Company AND T1.JobNum = JACTE.JobNum
2 Likes

Keyboard Head Smash GIFs - Find & Share on GIPHY

… I’ll try again tomorrow. :face_with_head_bandage:

1 Like

Word of caution… The query text box is not unlimited.

see here, a bit of an old post but I believe it still applies.

2 Likes

Thanks Simon, appreciate the insight. The ProdDtl query isn’t very long because there are 15 (I think was my last count) different subreports that slice & dice things. So the main query is relatively small considering everything that’s included in the main report.

What does the current query look like? Is there an error when running?

This was the error I was chasing most of yesterday afternoon:

Query execution failed for dataset 'JobHead'. ---> System.Data.SqlClient.SqlException: Incorrect syntax near '.'.

Hahah. Follow the bouncing “.”

I just wiped out my whole query so I could set it back to square one and test the base query again… then I’ll rebuild. I made so many tweaks yesterday I lost track of all my attempts. So, I’ll post once I get it back up and it fails again… which if history serves, it will :rofl:

Appreciate the offer for help!

1 Like

One thing I do with every report I touch is format the query to make it more readable. I put a return after each field and also group all of the table fields together. It sucks, but the format stays so you can easily copy and paste to Notepad++ to edit.

I have run into that error before, and it usually has to do with a space missing or something stupid like that.

3 Likes

Exactly… I’ve also learned it has little to nothing to do with an actual period. So, I was throwing everything I could think of at it yesterday. Gonna start clean and hope it doesn’t repeat.

I am sure we have all learnt that lesson at some stage…

Okay… Here is goes:

Currently getting this error:

Incorrect syntax near the keyword 'as'.

My thought is that it could be in the calculated fields I’m using in the CTE/Recursion. I’m not sure if the fields I use in the calculations need table prefixes. I did NOT use one in the first select statement but did in the second.

Also, my “Calculated_AssyLevel” field is just set to a constant value of (1)… do I call that out in my select statements?

I don’t want to alter the base “as” aliases on other fields because the RDL may be using them in all sorts of fields throughout the report (if statements, etc.). Don’t want to get the CTE working and have everything else (including subreports) break.

Query:

=";WITH JACTE 
(
    Description,
    PartNum as JobAsmbl_PartNum,
    RevisionNum as JobAsmbl_RevisionNum,
    TLABurdenCost,
    TLALaborCost,
    TLAMaterialBurCost,
    TLAMaterialCost,
    TLAMaterialLabCost,
    TLAMaterialMtlCost,
    TLAMaterialSubCost,
    TLAMtlBurCost,
    TLAProdHours,
    TLASetupHours,
    TLASubcontractCost,
    TLEBurdenCost,
    TLELaborCost,
    TLEMaterialCost,
    TLEMtlBurCost,
    TLEProdHours,
    TLESetupHours,
    TLESubcontractCost,
    Calc_SerialNoList,
    Calc_ProfitInvcAmt, 
    Calc_AttributeSetShortDescription, 

	Company,
	JobNum as JobAsmbl_JobNum,
	AssemblySeq,
	Parent,
	Calculated_AssySeqPath,
	Calculated_AssyLevel
    )

AS
(
    SELECT 
        T2.Description,
        T2.PartNum as JobAsmbl_PartNum,
        T2.RevisionNum as JobAsmbl_RevisionNum,
        T2.TLABurdenCost,
        T2.TLALaborCost,
        T2.TLAMaterialBurCost,
        T2.TLAMaterialCost,
        T2.TLAMaterialLabCost,
        T2.TLAMaterialMtlCost,
        T2.TLAMaterialSubCost,
        T2.TLAMtlBurCost,
        T2.TLAProdHours,
        T2.TLASetupHours,
        T2.TLASubcontractCost,
        T2.TLEBurdenCost,
        T2.TLELaborCost,
        T2.TLEMaterialCost,
        T2.TLEMtlBurCost,
        T2.TLEProdHours,
        T2.TLESetupHours,
        T2.TLESubcontractCost,
        T2.Calc_SerialNoList,
        T2.Calc_ProfitInvcAmt, 
        T2.Calc_AttributeSetShortDescription, 

        T2.Company as JobAsmbl_Company,
        T2.JobNum as JobAsmbl_JobNum,
        T2.AssemblySeq as JobAsmbl_AssemblySeq,
        T2.Parent as JobAsmbl_Parent,
        (cast(FORMAT(T2.AssemblySeq,'000') as nvarchar(1000))) as Calculated_AssySeqPath,
        (1) as Calculated_AssyLevel 
    
    FROM JobAsmbl_" + Parameters!TableGuid.Value + " T2 
    WHERE T2.AssemblySeq = 0 

    UNION ALL 
        SELECT 
            T22.Company as JobAsmbl1_Company,
            T22.JobNum as JobAsmbl1_JobNum,
            T22.AssemblySeq as JobAsmbl1_AssemblySeq,
            T22.Parent as JobAsmbl1_Parent,
            (CAST(T2.Calculated_AssySeqPath + '.' + FORMAT(T22.AssemblySeq, '000') as nvarchar(1000))) as Calculated_AssySeqPath2,
            (T2.Calculated_AssyLevel+1) as Calculated_AssyLevel2 

        FROM JobAsmbl_" + Parameters!TableGuid.Value + " T22 
        INNER JOIN T2 ON T22.Company = T2.Company AND T22.JobNum = T2.JobNum AND T22.Parent = T2.AssemblySeq 
        WHERE T22.AssemblySeq <> 0 
    )

SELECT 

    T1.ClosedDate,
    T1.IUM,
    T1.JobCompletionDate,
    T1.JobNum,
    T1.PartNum,
    T1.ProdCode,
    T1.ProdQty,
    T1.QtyCompleted,
    T1.ReqDueDate,
    T1.RevisionNum,
    T1.Calc_JobStatus,
    T1.Calc_MakeToJobQty,
    T1.Calc_MakeToOrderQty,
    T1.Calc_MakeToStockQty,
    T1.Calc_RecToJobQty,
    T1.Calc_RecToStockQty,
    T1.Calc_ShippedOrderQty,
    T1.ProdCode_Description, 

    T3.ActBurCost,
    T3.ActLabCost,
    T3.ActProdHours,
    T3.ActProdRwkHours,
    T3.ActSetupHours,
    T3.EstProdHours,
    T3.EstSetHours,
    T3.OpCode,
    T3.OprSeq,
    T3.PrimaryProdOpDtl,
    T3.PrimarySetupOpDtl,
    T3.ProdStandard,
    T3.QtyCompleted as JobOper_QtyCompleted,
    T3.RunQty,
    T3.SetupPctComplete,
    T3.StdFormat,
    T3.SetUpCrewSize,
    T3.Calc_ActRwkHrs,
    T3.Calc_AttainedStd,
    T3.Calc_EffPct, 
    T3.Calc_RptProdCrewSize,
    T3.Calc_RptSetUpCrewSize,
    T3.OpDesc,

    T4.CapabilityID,
    T4.DailyProdRate,
    T4.OpDtlSeq,
    T4.OprSeq as JobOpDtl_OprSeq,
    T4.ResourceGrpID,
    T4.ResourceID,
    T4.ResourceGrpID_DailyProdQty,
    T4.ResourceGrpID_Description,
    T4.CapabilityID_Description,
    T4.ResourceID_Description,
    T4.ResourceID_DailyProdQty 

FROM JobHead_" + Parameters!TableGuid.Value + " T1 
LEFT OUTER JOIN JACTE ON T1.Company = JACTE.Company AND T1.JobNum = JACTE.JobNum 
LEFT OUTER JOIN JobOper_" + Parameters!TableGuid.Value + " T3 ON JACTE.Company = T3.Company AND JACTE.JobNum = T3.JobNum AND JACTE.AssemblySeq = T3.AssemblySeq 
LEFT OUTER JOIN JobOpDtl_" + Parameters!TableGuid.Value + " T4 ON T3.Company = T4.Company AND T3.JobNum = T4.JobNum AND T3.AssemblySeq = T4.AssemblySeq AND T3.OprSeq = T4.OprSeq"

You don’t need the AS in the CTE declaration. What you type is what gets assigned to the field from the CTE query body.

2 Likes

So… would I use PartNum or JobAsmbl_PartNum?

I would assume JobAsmbl_PartNum, because that’s what other portions of the RDL may be referencing. Correct?

Removed all “as” from CTE declaration.

Now getting this one:

SqlException: Invalid object name 'T2'.

The CTE union needs to have the same number of columns as the top select.

Here is a quick try to fix up the query. I have other questions but don’t know your scope of this project to ask the right question.

;WITH JACTE AS
(
    SELECT 
        T2.Description,
        T2.PartNum AS JobAsmbl_PartNum,
        T2.RevisionNum AS JobAsmbl_RevisionNum,
        T2.TLABurdenCost,
        T2.TLALaborCost,
        T2.TLAMaterialBurCost,
        T2.TLAMaterialCost,
        T2.TLAMaterialLabCost,
        T2.TLAMaterialMtlCost,
        T2.TLAMaterialSubCost,
        T2.TLAMtlBurCost,
        T2.TLAProdHours,
        T2.TLASetupHours,
        T2.TLASubcontractCost,
        T2.TLEBurdenCost,
        T2.TLELaborCost,
        T2.TLEMaterialCost,
        T2.TLEMtlBurCost,
        T2.TLEProdHours,
        T2.TLESetupHours,
        T2.TLESubcontractCost,
        T2.Calc_SerialNoList,
        T2.Calc_ProfitInvcAmt, 
        T2.Calc_AttributeSetShortDescription, 
        T2.Company AS JobAsmbl_Company,
        T2.JobNum AS JobAsmbl_JobNum,
        T2.AssemblySeq AS JobAsmbl_AssemblySeq,
        T2.Parent AS JobAsmbl_Parent,
        CAST(FORMAT(T2.AssemblySeq, '000') AS NVARCHAR(1000)) AS Calculated_AssySeqPath,
        1 AS Calculated_AssyLevel 
    FROM JobAsmbl_" + Parameters!TableGuid.Value + " T2 
    WHERE T2.AssemblySeq = 0 

    UNION ALL 

    SELECT 
        T22.Description,
        T22.PartNum AS JobAsmbl_PartNum,
        T22.RevisionNum AS JobAsmbl_RevisionNum,
        T22.TLABurdenCost,
        T22.TLALaborCost,
        T22.TLAMaterialBurCost,
        T22.TLAMaterialCost,
        T22.TLAMaterialLabCost,
        T22.TLAMaterialMtlCost,
        T22.TLAMaterialSubCost,
        T22.TLAMtlBurCost,
        T22.TLAProdHours,
        T22.TLASetupHours,
        T22.TLASubcontractCost,
        T22.TLEBurdenCost,
        T22.TLELaborCost,
        T22.TLEMaterialCost,
        T22.TLEMtlBurCost,
        T22.TLEProdHours,
        T22.TLESetupHours,
        T22.TLESubcontractCost,
        T22.Calc_SerialNoList,
        T22.Calc_ProfitInvcAmt, 
        T22.Calc_AttributeSetShortDescription, 
        T22.Company AS JobAsmbl1_Company,
        T22.JobNum AS JobAsmbl1_JobNum,
        T22.AssemblySeq AS JobAsmbl1_AssemblySeq,
        T22.Parent AS JobAsmbl1_Parent,
        CAST(T2.Calculated_AssySeqPath + '.' + FORMAT(T22.AssemblySeq, '000') AS NVARCHAR(1000)) AS Calculated_AssySeqPath2,
        T2.Calculated_AssyLevel + 1 AS Calculated_AssyLevel2 
    FROM JobAsmbl_" + Parameters!TableGuid.Value + " T22 
    INNER JOIN JACTE T2 ON T22.Company = T2.JobAsmbl_Company 
        AND T22.JobNum = T2.JobAsmbl_JobNum 
        AND T22.Parent = T2.JobAsmbl_AssemblySeq 
    WHERE T22.AssemblySeq <> 0 
)

SELECT 
    T1.ClosedDate,
    T1.IUM,
    T1.JobCompletionDate,
    T1.JobNum,
    T1.PartNum,
    T1.ProdCode,
    T1.ProdQty,
    T1.QtyCompleted,
    T1.ReqDueDate,
    T1.RevisionNum,
    T1.Calc_JobStatus,
    T1.Calc_MakeToJobQty,
    T1.Calc_MakeToOrderQty,
    T1.Calc_MakeToStockQty,
    T1.Calc_RecToJobQty,
    T1.Calc_RecToStockQty,
    T1.Calc_ShippedOrderQty,
    T1.ProdCode_Description, 
    T3.ActBurCost,
    T3.ActLabCost,
    T3.ActProdHours,
    T3.ActProdRwkHours,
    T3.ActSetupHours,
    T3.EstProdHours,
    T3.EstSetHours,
    T3.OpCode,
    T3.OprSeq,
    T3.PrimaryProdOpDtl,
    T3.PrimarySetupOpDtl,
    T3.ProdStandard,
    T3.QtyCompleted AS JobOper_QtyCompleted,
    T3.RunQty,
    T3.SetupPctComplete,
    T3.StdFormat,
    T3.SetUpCrewSize,
    T3.Calc_ActRwkHrs,
    T3.Calc_AttainedStd,
    T3.Calc_EffPct, 
    T3.Calc_RptProdCrewSize,
    T3.Calc_RptSetUpCrewSize,
    T3.OpDesc,
    T4.CapabilityID,
    T4.DailyProdRate,
    T4.OpDtlSeq,
    T4.OprSeq AS JobOpDtl_OprSeq,
    T4.ResourceGrpID,
    T4.ResourceID,
    T4.ResourceGrpID_DailyProdQty,
    T4.ResourceGrpID_Description,
    T4.CapabilityID_Description,
    T4.ResourceID_Description,
    T4.ResourceID_DailyProdQty 
FROM JobHead_" + Parameters!TableGuid.Value + " T1 
LEFT OUTER JOIN JACTE ON T1.Company = JACTE.JobAsmbl_Company 
    AND T1.JobNum = JACTE.JobAsmbl_JobNum 
LEFT OUTER JOIN JobOper_" + Parameters!TableGuid.Value + " T3 ON JACTE.JobAsmbl_Company = T3.Company 
    AND JACTE.JobAsmbl_JobNum = T3.JobNum 
    AND JACTE.JobAsmbl_AssemblySeq = T3.AssemblySeq 
LEFT OUTER JOIN JobOpDtl_" + Parameters!TableGuid.Value + " T4 ON T3.Company = T4.Company 
    AND T3.JobNum = T4.JobNum 
    AND T3.AssemblySeq = T4.AssemblySeq 
    AND T3.OprSeq = T4.OprSeq;

Well… that approach seems to compile okay. The report ran, but my test report which was 15 pages (showing all job assemblies) is now only 2 pages (showing only Assembly 0). So, it ran, but I don’t think the recursion is working correctly.

All I’m trying to do is have the Production Detail Report re-order the assemblies in the printed report. Right now they come out in sequential order. Which is nice and clean.

But someone reviewing job cost wants to see assemblies in the order they’re on the job (in a nested/indented/lineage order). Otherwise we’re jumping all over the place because assembly sequences on large jobs are never “sequential”.

The purpose of the CTE is just to do the recursion and create an “AssySeqPath”. I can then sort the Assemblies in the report group by that calculated path.

Below is the CTE BAQ running on its own. Note the Assemblies are now sorting in an indented fashion. 0, 1, 3, 14, 18, etc.). Not 0,1,2,3,4,etc.

That’s my goal. To get ProdDtl Report to sort in that order.

wait that is not right let me review more

1 Like