I Need to Know How to Create a Crosstab Query

I need to know how to create a crosstab query

This type of transformation is called a pivot. You did not specify what database you are using so I will provide a answers for SQL Server and MySQL.


SQL Server: If you are using SQL Server 2005+ you can implement the PIVOT function.

If you have a known number of values that you want to convert to columns then you can hard-code the query.

select typename, total, Deployed, Inventory, shipped
from
(
select count(*) over(partition by t.typename) total,
s.statusname,
t.typename
from assets a
inner join assettypes t
on a.assettype = t.id
inner join assetstatus s
on a.assetstatus = s.id
) d
pivot
(
count(statusname)
for statusname in (Deployed, Inventory, shipped)
) piv;

See SQL Fiddle with Demo.

But if you have an unknown number of status values, then you will need to use dynamic sql to generate the list of columns at run-time.

DECLARE @cols AS NVARCHAR(MAX),
@query AS NVARCHAR(MAX)

select @cols = STUFF((SELECT distinct ',' + QUOTENAME(statusname)
from assetstatus
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')

set @query = 'SELECT typename, total,' + @cols + ' from
(
select count(*) over(partition by t.typename) total,
s.statusname,
t.typename
from assets a
inner join assettypes t
on a.assettype = t.id
inner join assetstatus s
on a.assetstatus = s.id
) x
pivot
(
count(statusname)
for statusname in (' + @cols + ')
) p '

execute(@query)

See SQL Fiddle with Demo

This can also be written using an aggregate function with a case expression:

select typename,
total,
sum(case when statusname ='Deployed' then 1 else 0 end) Deployed,
sum(case when statusname ='Inventory' then 1 else 0 end) Inventory,
sum(case when statusname ='Shipped' then 1 else 0 end) Shipped
from
(
select count(*) over(partition by t.typename) total,
s.statusname,
t.typename
from assets a
inner join assettypes t
on a.assettype = t.id
inner join assetstatus s
on a.assetstatus = s.id
) d
group by typename, total

See SQL Fiddle with Demo


MySQL: This database does not have a pivot function so you will have to use the aggregate function and a CASE expression. It also does not have windowing functions, so you will have to alter the query slightly to the following:

select typename,
total,
sum(case when statusname ='Deployed' then 1 else 0 end) Deployed,
sum(case when statusname ='Inventory' then 1 else 0 end) Inventory,
sum(case when statusname ='Shipped' then 1 else 0 end) Shipped
from
(
select t.typename,
(select count(*)
from assets a1
where a1.assettype = t.id
group by a1.assettype) total,
s.statusname
from assets a
inner join assettypes t
on a.assettype = t.id
inner join assetstatus s
on a.assetstatus = s.id
) d
group by typename, total;

See SQL Fiddle with Demo

Then if you need a dynamic solution in MySQL, you will have to use a prepared statement to generate the sql string to execute:

SET @sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'sum(CASE WHEN statusname = ''',
statusname,
''' THEN 1 else 0 END) AS `',
statusname, '`'
)
) INTO @sql
FROM assetstatus;

SET @sql
= CONCAT('SELECT typename,
total, ', @sql, '
from
(
select t.typename,
(select count(*)
from assets a1
where a1.assettype = t.id
group by a1.assettype) total,
s.statusname
from assets a
inner join assettypes t
on a.assettype = t.id
inner join assetstatus s
on a.assetstatus = s.id
) d
group by typename, total');

PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

See SQL Fiddle with Demo.

The result is the same for all queries in both databases:

| TYPENAME | TOTAL | DEPLOYED | INVENTORY | SHIPPED |
-----------------------------------------------------
| Desktop | 2 | 1 | 1 | 0 |
| Laptop | 1 | 0 | 0 | 1 |
| Server | 1 | 1 | 0 | 0 |

To build a cross tab query with details

Incorrect first answer:
TRANSFORM First(tblTeamdata.Teammemebername) AS FirstMembername
SELECT tblTeamdata.DAT
FROM tblTeamdata
GROUP BY tblTeamdata.DAT
PIVOT tblTeamdata.Teamname;

Since each cell in the cross tab can have multiple MemberCodes you will have to use a function to return a list of those names. The function shall have date and teamcode as parameters.

Function names(dat As Variant, team As Variant)
Dim res$, sql$
Dim rs As DAO.Recordset
If IsNull(dat) Or IsNull(team) Then
names = Null
Else
sql = "SELECT * FROM Teamdata"
sql = sql & " Where ScheduleDate =#" & dat & "#"
sql = sql & " AND TeamCode=""" & team & """"
sql = sql & " Order by TeamMemberCode;"
Set rs = CurrentDb.OpenRecordset(sql)
Do Until rs.EOF
If res <> "" Then res = res & ","
res = res & rs!TeamMemberCode
rs.MoveNext
Loop
rs.Close
names = res
End If
End Function

TRANSFORM names([ScheduleDate],[Teamcode]) AS Result
SELECT TeamData.ScheduleDate
FROM TeamData
GROUP BY TeamData.ScheduleDate
PIVOT TeamData.TeamCode;

The Result of the query with the above dataset will be:

ScheduleDate  NT    USHRL   USHRT
2022-03-04 NW RN KI,KN,RF
2022-03-11 MF QD KI,KN,NW

How can I Create a Multi-Value CrossTab Query in Access 2013?

The answer was to use an inner join (similar to what I've read elsewhere). However, for those of you that don't know SQL, or couldn't figure it out -- I wrote my own SQL query from scratch that worked.

Here is a generic query for all of you that stumble upon this with a similar issue. Simply copy the code below, do a replace for the following varialbes... and you should be able to expand from there!

Variables

[myCrossTabQueryCOUNT] = name of your COUNT crosstab query

[myCrossTabQuerySUM] = name of your SUM crosstab query

[Month of Service] = field name that is the primary ROW HEADING for each crosstab query

[mySum] = field name of your 'Value' field in the [myCrossTabQuerySUM] query (in my example it held a Sum value)

[myCount] = field name of your 'Value' field in the [myCrossTabQueryCOUNT] query (in my example it held a Count value)

The SQL View Query

SELECT [myCrossTabQueryCOUNT].[Month of Service], [myCrossTabQueryCOUNT].[myCount] AS [Count], [myCrossTabQuerySUM].[mySum] AS [Total Claim Charges]
FROM (([myCrossTabQueryCOUNT]
INNER JOIN [myCrossTabQuerySUM]
ON [myCrossTabQueryCOUNT].[Month of Service]=[myCrossTabQuerySUM].[Month of Service])
ORDER BY [myCrossTabQueryCOUNT].[Month of Service];

Using A Crosstab On A Report

Simply change the IN clause to your columns. The linked article uses 1,2,3... due to their specific data. Therefore, simply open the SQL view of your query and add your needed 15 values.

TRANSFORM ...
SELECT ...
FROM ...
GROUP BY ...
PIVOT ...
IN ('A455AJ', 'A456AK', ...)

Do note, this IN() clause serves as both a filter and order of columns. Items can be quoted or not.


Alternatively, if you need to subset columns by form controls, use IN inside a WHERE clause condition of a derived table subquery -allowable in crosstab queries.

PARAMETERS [Forms]![frm_BulkComparison_Select]![Item 1] TEXT(255),
[Forms]![frm_BulkComparison_Select]![Item 2] TEXT(255),
...;
TRANSFORM ...
SELECT ...
FROM
(SELECT * FROM myTable
WHERE col_item IN ([Forms]![frm_BulkComparison_Select]![Item 1],
[Forms]![frm_BulkComparison_Select]![Item 2],
...)
) As sub
GROUP BY ...
PIVOT ...

How do you do a crosstab query in Access with a fixed amount of columns

Subquery? That's one of my weaknesses when it comes to Access so I can't help you there. I'd suggest posting the SQL of the query though so others can take a look. Also what happens when you run the query?

The following is a query that I'm using to give me costs for the last ten years for a given unit.
TRANSFORM Sum(ServiceRecords.srTotalCost) AS AnnualCost
SELECT ServiceRecords.srEquipmentID
FROM ServiceRecords
GROUP BY ServiceRecords.srEquipmentID
PIVOT "C" & DateDiff("yyyy",[srServiceDate],Date()) In ("C9","C8","C7","C6","C5","C4","C3","C2","C1","C0");

The trick is after the PIVOT. As I want the last ten years worth of data the "C" & DateDiff portion sets up a string variable call C0 to C9. The portion after the In tells which column to stuff things into.

Another query which pulls in data about the Equipment calls this query. The term we generally use for such is stacked queries.

If this isn't enough to get you going please indicate what type of data you are trying to create a cross tab.

Fellow Access MVP, Allen Browne has a good page on this topic. Crosstab query techniques

How to create an Access crosstab query with totals for the columns AND the rows?

You'd basically have to run your query twice - once to get the data and then a second time to provide the aggregates. If you're set on doing this, make the first query to return data its own object. Then make another query to aggregate the first one another object. Create a final third query object to combine the two using a UNION as you mentioned.

Although I have to say I don't really recommend this. It sounds like you're trying to force the SQL to generate something that's really presentational information (i.e. it doesn't belong in the same dataset).

Sql Cross Tab (Query)

I tested the below with your provided sample data and it returns accordingly. Basically for each column you want to conditionally pass in the TotalPatients tot he SUM aggregator based on the PatientType value.

select
Dept_Name
,sum(case when Patient_Type = 'Entitled' then TotalPatients ELSE 0 end) Entitled
,sum(case when Patient_Type = 'General' then TotalPatients ELSE 0 end) General
,sum(case when Patient_Type = 'Staff' then TotalPatients ELSE 0 end) Staff
,sum(case when Patient_Type = 'Referred' then TotalPatients ELSE 0 end) Referred
from @t
group by Dept_Name

In the cases where you need dynamic columns...

IF EXISTS (SELECT * 
FROM tempdb.dbo.sysobjects o
WHERE o.xtype IN ( 'U' )
AND o.id = Object_id(N'tempdb..#t'))
DROP TABLE #t;

CREATE TABLE #t
(
dept_name NVARCHAR(255),
patient_type NVARCHAR(255),
totalpatients INT
)

INSERT INTO #t
(dept_name,
patient_type,
totalpatients)
Insert Into #T (Dept_Name, Patient_Type , TotalPatients)
SELECT department.dept_name,
patient.patient_type,
Count(*) AS TotalPatients
FROM patient
INNER JOIN payment
ON patient.regno = payment.regno
INNER JOIN department
ON payment.deptid = department.dept_id
WHERE ( CONVERT(VARCHAR, patient.regdatetime, 112) =
CONVERT(VARCHAR, Getdate() - 30, 112) )
GROUP BY department.dept_name,
patient.patient_type

DECLARE @cols AS NVARCHAR(max),
@query AS NVARCHAR(max)

SELECT @cols = Stuff((SELECT DISTINCT ',' + Quotename(patient_type)
FROM #t
GROUP BY dept_name,
patient_type
ORDER BY 1
FOR xml path(''), type).value('.', 'NVARCHAR(MAX)'), 1, 1,
'')

SET @query = N'SELECT Dept_Name, ' + @cols + N'
from
#T x
pivot
(
SUM(TotalPatients)
for Patient_Type in ('
+ @cols + N')
) p '

EXEC Sp_executesql @query;

PostgreSQL Crosstab Query

Install the additional module tablefunc once per database, which provides the function crosstab(). Since Postgres 9.1 you can use CREATE EXTENSION for that:

CREATE EXTENSION IF NOT EXISTS tablefunc;

Improved test case

CREATE TABLE tbl (
section text
, status text
, ct integer -- "count" is a reserved word in standard SQL
);

INSERT INTO tbl VALUES
('A', 'Active', 1), ('A', 'Inactive', 2)
, ('B', 'Active', 4), ('B', 'Inactive', 5)
, ('C', 'Inactive', 7); -- ('C', 'Active') is missing

Simple form - not fit for missing attributes

crosstab(text) with 1 input parameter:

SELECT *
FROM crosstab(
'SELECT section, status, ct
FROM tbl
ORDER BY 1,2' -- needs to be "ORDER BY 1,2" here
) AS ct ("Section" text, "Active" int, "Inactive" int);

Returns:


Section | Active | Inactive
---------+--------+----------
A | 1 | 2
B | 4 | 5
C | 7 | -- !!
  • No need for casting and renaming.
  • Note the incorrect result for C: the value 7 is filled in for the first column. Sometimes, this behavior is desirable, but not for this use case.
  • The simple form is also limited to exactly three columns in the provided input query: row_name, category, value. There is no room for extra columns like in the 2-parameter alternative below.

Safe form

crosstab(text, text) with 2 input parameters:

SELECT *
FROM crosstab(
'SELECT section, status, ct
FROM tbl
ORDER BY 1,2' -- could also just be "ORDER BY 1" here

, $$VALUES ('Active'::text), ('Inactive')$$
) AS ct ("Section" text, "Active" int, "Inactive" int);

Returns:


Section | Active | Inactive
---------+--------+----------
A | 1 | 2
B | 4 | 5
C | | 7 -- !!
  • Note the correct result for C.

  • The second parameter can be any query that returns one row per attribute matching the order of the column definition at the end. Often you will want to query distinct attributes from the underlying table like this:

      'SELECT DISTINCT attribute FROM tbl ORDER BY 1'

That's in the manual.

Since you have to spell out all columns in a column definition list anyway (except for pre-defined crosstabN() variants), it is typically more efficient to provide a short list in a VALUES expression like demonstrated:

    $$VALUES ('Active'::text), ('Inactive')$$)

Or (not in the manual):

    $$SELECT unnest('{Active,Inactive}'::text[])$$  -- short syntax for long lists
  • I used dollar quoting to make quoting easier.

  • You can even output columns with different data types with crosstab(text, text) - as long as the text representation of the value column is valid input for the target type. This way you might have attributes of different kind and output text, date, numeric etc. for respective attributes. There is a code example at the end of the chapter crosstab(text, text) in the manual.

db<>fiddle here

Effect of excess input rows

Excess input rows are handled differently - duplicate rows for the same ("row_name", "category") combination - (section, status) in the above example.

The 1-parameter form fills in available value columns from left to right. Excess values are discarded.

Earlier input rows win.

The 2-parameter form assigns each input value to its dedicated column, overwriting any previous assignment.

Later input rows win.

Typically, you don't have duplicates to begin with. But if you do, carefully adjust the sort order to your requirements - and document what's happening.

Or get fast arbitrary results if you don't care. Just be aware of the effect.

Advanced examples

  • Pivot on Multiple Columns using Tablefunc - also demonstrating mentioned "extra columns"

  • Dynamic alternative to pivot with CASE and GROUP BY



\crosstabview in psql

Postgres 9.6 added this meta-command to its default interactive terminal psql. You can run the query you would use as first crosstab() parameter and feed it to \crosstabview (immediately or in the next step). Like:

db=> SELECT section, status, ct FROM tbl \crosstabview

Similar result as above, but it's a representation feature on the client side exclusively. Input rows are treated slightly differently, hence ORDER BY is not required. Details for \crosstabview in the manual. There are more code examples at the bottom of that page.

Related answer on dba.SE by Daniel Vérité (the author of the psql feature):

  • How do I generate a pivoted CROSS JOIN where the resulting table definition is unknown?

How can I sort the columns in a crosstab query, when the column data is dynamic?

Having encountered the same scenario various times, I prepared a repeatable way to add an In list to the end of the PIVOT clause. Doing so will sort the columns in the crosstab query by order of the elements in the pivotfield In list. Documentation for this construct is available from MSDN. The solution is a procedure that needs triggered by a command button on a form or another event. Please see the screen shots below the Sub.

Public Sub SortPivotColumns(querynameSource As String, queryname As String, SortName As String, SortColumnNameField As String, SortIndexName As String, NonPivotFieldCount As Integer, ParamArray ParamArr() As Variant)

' This sub goes through several steps to effectively adds an In list that sorts the 'Columns' of a crosstab query in MS Access
' 13 November 2012
' E Easterly
'
' This technique uses several components.
' 1) The original unmodified cross tab query (querynameSource)
' 2) The resulting, columns-have-been-sorted query (query)
' 3) An index table which has two columns, a numeric index used for sorting and the column name
' 4) A table or query that can be joined on the column names of the cross tab query to update the index table
' The name of the table or query would be 'SortName'
' The field in 'SortName' that the crosstab query columns are joined against is the 'SortColumnNameField'
' The field in 'SortName' that has the desired order is the SortIndexName
' 5) A number which specifies the count of non-pivot/row heading columns (NonPivotFieldCount)
' 6) An optional array that contains any parameters needed for the query
'
'
' USE:
'
' SortPivotColumns "qryCrosstab_Initial", _
' "qryCrosstab_Sorted", _
' "tblKeyDescriptions", _
' "Descriptions", _
' "NumericIndexForSorting", _
' 1
'
'
'
'
Dim rs As DAO.Recordset
Dim db As Database
Dim fld As DAO.Field
Dim sql As String
Dim ColumnHeading As Variant
Dim qdf As QueryDef
Dim qdfSRC As QueryDef
Dim UpdateIndexSQL As Variant

DoCmd.SetWarnings False 'Turn off warnings

Set db = CurrentDb

Set qdfSRC = db.QueryDefs(querynameSource)
Set qdf = db.QueryDefs(queryname)
qdf.sql = qdfSRC.sql

If Not (IsEmpty(ParamArr)) Then
Dim i As Integer
For i = 0 To UBound(ParamArr)
qdf.Parameters(i) = ParamArr(i)
Next
End If

' First, get the list of fields from the query

Set rs = qdf.OpenRecordset

' Then, create a temporary indexing table
If Not IsNull(DLookup("Name", "MSysObjects", "Name='ttblSortCrosstabColumns' And Type In (1,4,6)")) Then
db.Execute "DROP TABLE ttblSortCrosstabColumns"
End If

db.Execute "CREATE TABLE ttblSortCrosstabColumns (FieldIndex INTEGER , ColumnName TEXT(250))"

' And populate it with the current index and column names from queryname
For Each fld In rs.Fields
If fld.OrdinalPosition > (NonPivotFieldCount - 1) Then
DoCmd.RunSQL "Insert into ttblSortCrosstabColumns VALUES(" & fld.OrdinalPosition & ", """ & fld.Name & """)"
End If
Next fld
Set fld = Nothing
rs.Close
Set rs = Nothing

' Now, the temporary table is joined with the sort table/query and the indexes are updated
UpdateIndexSQL = (" UPDATE ttblSortCrosstabColumns " & _
" INNER JOIN " & SortName & " ON ttblSortCrosstabColumns.ColumnName=" & SortName & "." & SortColumnNameField & _
" Set ttblSortCrosstabColumns.FieldIndex = [" & SortIndexName & "]")
DoCmd.RunSQL (UpdateIndexSQL)

' Then, the column headings are added to a string to prepare the In list
sql = "SELECT ttblSortCrosstabColumns.ColumnName FROM ttblSortCrosstabColumns ORDER BY ttblSortCrosstabColumns.FieldIndex"
Set rs = db.OpenRecordset(sql)
rs.MoveFirst
ColumnHeading = "'" & rs.Fields(0).Value & "'"
rs.MoveNext

Do While Not rs.EOF
ColumnHeading = ColumnHeading & ", '" & rs.Fields(0).Value & "'"
rs.MoveNext
Loop

rs.Close
Set rs = Nothing
' db.Execute "DROP TABLE ttblSortCrosstabColumns"

Dim cs As Variant

' Set qdf = db.QueryDefs(queryname) ' may not need this

' The query is updated with the In list
cs = Left$(qdf.sql, Len(qdf.sql) - 3) & " In(" & ColumnHeading & ");"

qdf.sql = cs

' Take a look at the resulting query sql by uncommenting the below section
' Debug.Print cs

DoCmd.SetWarnings True 'Turn warnings back on

End Sub

In the below screen shot, note the tblKeyDescriptions and the tblPFValues. These are from the question. qryCrosstab_Initial is analogous to the query provided in the above question. The form is used to run the procedure and open the before and after queries.

Screen Shot Crosstab Sorting

An integer field (NumericIndexForSorting) was added to tblKeyDescriptions because the sub requires a numeric index for sorting the column names.

Key Descriptions table with numeric index

Now, inspect the In list highlighted in the SQL view of the initial and sorted queries.

SQL differences showing in PIVOT clause

This is all that is needed to order the columns in a crosstab query. Dynamically generating the In list is the purpose of the sub.

Note: the sub needs to be run each time the query is run, so using an event such as a command button On Click event to tie the sequence together is helpful.



Related Topics



Leave a reply



Submit