2

Is it possible to store or cache values that are part of one select column and then be used for the next one? For example,

select 
       FirstColumn = (complex query returns a value based on ThirdColumn),
       SecondColumn = (uses the same value returned from above + does some additional calculations.)
from SomeTable

Is it possible to do something like that so I don't have to write the same complex query twice?

Dale K
  • 25,246
  • 15
  • 42
  • 71
User123
  • 549
  • 1
  • 11
  • 24

4 Answers4

7

You need CROSS APPLY here, it can refer to outer references, no annoying subqueries or CTEs needed:

select col1, col2
from table1 as outer_table

-- can also have multi-row values
cross apply (values (complex_expression_1) ) as v1 (col1)
cross apply (values (expression_referring_to_col1) ) as v2 (col2)

-- alternate syntax, select without from returns a single row
cross apply (select complex_expression_1 as col1 ) AS v1
cross apply (select expression_referring_to_col1 as col2 ) as v2

-- you can also do anything you like in there, can be one or multiple rows
cross apply (
    select complex_expression_1 as col1 
    from othercomplexjoin as o
    where o.join_column = outer_table.join_column
) AS v1

Some more tricks you can do with APPLY:

1. Top 1 per group of child table:

A classic solution to the "top 1 per group" is to use row_number(). This can often result in huge scans, especially when the number of distinct outer values is small relative to the child table.

select
    o.id,
    lastPayment.Date
from order_header as o
join
( select *, row_number() over (partition by order_id order by date desc) as rn
 from payments
) as lastPayment on ...
where lastPayment.rn = 1

Instead we can do:

select
    o.id,
    lastPayment.Date
from order_header as o
cross apply
( select top (1) *
 from payments as p
 where p.order_id = o.id
 order by date desc
) as lastPayment

Note: OUTER APPLY conceptually replaces a left join, i.e. returns nulls instead of no rows.


2. Unpivoting

select
    o.id,
    customer.*
from order_header as o
cross apply ( values    -- This returns two rows for every order_header
    ( 'DeliveryCustomer', o.deliveryCustomer ),
    ( 'billingCustomer', o.billingCustomer )
) as customer (type, name)

3. Exploding out a row a variable number of times:

Say we want to take an amount, and split it into different rows. If the amount <= 50 then one row of amount, if > 50 then two rows, one of 50 and one of the rest:

select t.id, v.amount
from table as t
cross apply (
    select case when amount > 50 then 50 else amount end as amount
    union all
    select amount - 50   -- note this row will not appear if amount < 50
    where amount > 50
) v
Charlieface
  • 52,284
  • 6
  • 19
  • 43
1

A sub-query is the simplest form. You can nest sub-queries to any level you like performing calculations along the way:

select 
    ComplexValue1 as FirstColumn
    , ComplexValue1 + ComplexValue2 as SecondColumn
from (
  select 
    {complex query returns a value} ComplexValue1
    , {complex query returns a value} ComplexValue2
    , {any other required columns}
  from SomeTable
) X;

Note: a CTE is essentially the same thing just prettier (maybe).

A lateral join is another option as it allows you to reference columns in the base table and perform calculations on them which are then available for use in the resultset:

select
    X.ComplexValue1 as FirstColumn
    , X.ComplexValue1 + ST.ComplexValue2 as SecondColumn
from SomeTable ST
cross apply (select {complex query returns a value referencing ST}) as X (ComplexValue1);
Dale K
  • 25,246
  • 15
  • 42
  • 71
  • Maybe it's just me, but I always felt (coming from procedural) that lateral made more sense than subquerying for complex expressions. I know some feel different, I guess it's just a matter of taste. – Charlieface Jan 20 '21 at 22:47
  • @Charlieface That depends on what you need it for. My rule is that if you can solve it with a join, then use the join since it is mostly faster. But sometimes a subquery can proof better, depends on what your doing in it – GuidoG Jan 20 '21 at 22:56
  • @GuidoG Obvs yes that is true. I tend to use it for things like: grouped up child tables; top 1 per group; complex calculations on previous tables. Also very useful if you want to double up rows (unpivot), you can even do this conditionally (`union all` with `where` inside the apply) and get different amounts of rows. – Charlieface Jan 20 '21 at 23:00
  • Given the engine has to perform the same operation either way, I would be surprised if there was ever a significant performance difference, although I am sure there is sometimes, but in others I expect identical execution plans. I use both forms, but I think that a well laid out sub-query is more straightforward to follow and understand, especially for a beginner. But as you say its totally a case of personal preference. – Dale K Jan 20 '21 at 23:09
  • 1
    @GuidoG and Dale K. See further edit on my answer – Charlieface Jan 20 '21 at 23:27
  • 1
    @Charlieface very clever :) just hope you haven't overloaded the OP. – Dale K Jan 20 '21 at 23:29
0

I think in that case, you should use CTE (common table expression). for example

;WITH CTE1 AS
(
 select col1 from table ---- your query 
),
CTE2 as 
(
 select col2 from table --- your query
)
select col1, Col2
from CTE1 join CTE2 on --- do anything you want
Jiacheng Gao
  • 365
  • 3
  • 9
0

One way to do it is select the resultset and work from there.

This works by selecting the firstcolumn with all its complex stuff and return it in a resultset called t.

select t.FirstColumn,
from   ( select FirstColumn --(complex query returns a value based on ThirdColumn)
         from SomeTable
       ) t

Then you can very simply select from t and it will return FirstColumn as if it was a normal simple column, so you can add to it without having to repeat the complex stuff.

select t.FirstColumn,
       secondcolum (that uses t.FirstColum as it where a normal field)
from   ( select FirstColumn
         from SomeTable
       ) t

An example:

select t.FirstColumn,
       (t.FirstColum / 123.0) * 50 as SecondColumn
from   ( select (s.A + s.B) * s.C as FirstColumn
         from   sometable s
       ) t

As you can see, I don't need to repeat the calculation for t.Firstcolumn to add stuff to it for my SecondColumn.

Another option is to use cross apply (there is an explanation in another answer here so I won't repeat that).
But do test which solution performs best for you, cross apply could have a performance penalty in some cases, and so can my solution. So just test what works best for you.

Dale K
  • 25,246
  • 15
  • 42
  • 71
GuidoG
  • 11,359
  • 6
  • 44
  • 79