Two questions about my SQL script. How to add subtotal/total lines, and a sorting issue - tsql

I have some T-SQL that generates a nice report giving a summary some stuff by month.
I have 2 questions, is there a way to get the to sort the months by calendar order, not by alpha? And, what i would like to do is add a total line for each year, and a total line for the whole report?
SELECT
CASE WHEN tmpActivity.Year IS NULL THEN
CASE WHEN tmpCreated.Year IS NULL THEN
CASE WHEN tmpContactsCreated.Year IS NULL THEN
null
ELSE tmpContactsCreated.Year END
ELSE tmpCreated.Year END
ELSE tmpActivity.Year END As Year,
CASE WHEN tmpActivity.Month IS NULL THEN
CASE WHEN tmpCreated.Month IS NULL THEN
CASE WHEN tmpContactsCreated.Month IS NULL THEN
null
ELSE DateName(month, DateAdd(month, tmpContactsCreated.Month - 1, '1900-01-01' )) END
ELSE DateName(month, DateAdd(month, tmpCreated.Month - 1, '1900-01-01' )) END
ELSE DateName(month, DateAdd(month, tmpActivity.Month - 1, '1900-01-01' )) END As Month,
CASE WHEN tmpActivity.ActiveAccounts IS NULL THEN 0 ELSE tmpActivity.ActiveAccounts END AS ActiveAccounts,
CASE WHEN tmpCreated.NewAccounts IS NULL THEN 0 ELSE tmpCreated.NewAccounts END AS NewAccounts,
CASE WHEN tmpContactsCreated.NewContacts IS NULL THEN 0 ELSE tmpContactsCreated.NewContacts END AS NewContacts
FROM
(
SELECT YEAR(LastLogon) As Year, MONTH(LastLogon) As Month, COUNT(*) As ActiveAccounts
FROM Users
WHERE LastLogon >= '1/1/1800'
GROUP BY YEAR(LastLogon), MONTH(LastLogon)
) as tmpActivity
FULL JOIN
(
SELECT YEAR(Created) As Year, MONTH(Created) As Month, COUNT(*) As NewAccounts
FROM Users
WHERE Created >= '1/1/1800'
GROUP BY YEAR(Created), MONTH(Created)
) as tmpCreated ON tmpCreated.Year = tmpActivity.Year AND tmpCreated.Month = tmpActivity.Month
FULL JOIN
(
SELECT YEAR(Created) As Year, MONTH(Created) As Month, COUNT(*) As NewContacts
FROM Contacts
WHERE Created >= '1/1/1800'
GROUP BY YEAR(Created), MONTH(Created)
) as tmpContactsCreated ON tmpContactsCreated.Year = tmpCreated.Year AND tmpContactsCreated.Month = tmpCreated.Month
Order By Year DESC, Month DESC

To order by the month use the following:
ORDER BY DATEPART(Month,Created) ASC
DatePart() returns an integer for the part specified 1 for January, 2 for Febuary etc.
You're SQL could be helped with the COALESCE() and ISNULL() functions. This is the same as your first select:
SELECT
COALESCE(tmpActivity.Year,tmpCreated.Year,tmpContactsCreated.Year) as Year,
COALESCE(tmpActivity.Month,tmpCreated.Month,tmpContactsCreated.Month) as Month,
ISNULL(tmpActivity.ActiveAccounts,0) AS ActiveAccounts,
ISNULL(tmpCreated.NewAccounts,0) AS NewAccounts,
ISNULL(tmpContactsCreated.NewContacts,0) AS NewContacts
I think there is a bug in your select, I believe your last line has to be this:
) as tmpContactsCreated ON (tmpContactsCreated.Year = tmpCreated.Year AND tmpContactsCreated.Month = tmpCreated.Month) OR
(tmpContactsCreated.Year = tmpActivity.Year AND tmpContactsCreated.Month = tmpActivity.Month)
But I would have to test this to be sure.
Adding in rollups is hard to do -- typically this is done externally to the SQL in the control or whatever displays the results. You could do something like this (contrived example):
SELECT 1 as reportOrder, date, amount, null as total
FROM invoices
UNION ALL
SELECT 2 , null, null, sum(amount)
FROM invoices
ORDER BY reportOrder, date
or you could not have the "extra" total column and put it in the amount column.

Related

Passing Bind Variable in case statements in Oracle query

I want to pass Bind Variable (a Date) in a Case statement in order to achieve the following:
When User inputs a date and if that Date is falling on Monday, then the Case statement should fetch the value of Friday (meaning it should bypass the weekends and look for the values of a previous working day)
I tried to use the following query which works well when I use 'sysdate'
Select * from table_name
Where tradedate = trunc (sysdate - case to_char(sysdate, 'Dy')
when 'Mon' then 3 else 1 end);
but when I replace 'sysdate' with a Bind Variables, it gives me an error like:
tradedate = trunc (:sysdate1 - case to_char(:sysdate2, 'Dy')
when 'Mon' then 3 else 1 end);
ORA-00932: inconsistent datatypes: expected DATE got NUMBER
00932. 00000 - "inconsistent datatypes: expected %s got %s"
Can't we use Bind Variables in Case statement in oracle queries? If so, Can someone please give me any alternate solution to my problem stated above?
Any help would be really appreciated.
Below is the complete code:
select * from (
SELECT
S."TRADEDATE",S."ACCOUNT_NAME",S."BOOKING_AMOUNT",S."ACCOUNT_NUMBER",(CASE WHEN BOOKING_AMOUNT <0 THEN S."CREDIT" ELSE S."DEBIT" END) AS "DEBIT" , (CASE WHEN BOOKING_AMOUNT <0 THEN S."DEBIT" ELSE S."CREDIT" END) AS "CREDIT",
U.VALUE_DT , U.AC_NO , NVL(U.BOOKED_AMOUNT ,0) BOOKED_AMOUNT
FROM
SXB S
FULL OUTER JOIN UBS U ON
S.ACCOUNT_NUMBER = U.AC_NO
AND
S.TRADEDATE = U.VALUE_DT
UNION ALL
SELECT
BOOKING_DATE TRADEDATE,
'SAXO RECON' ACCOUNT_NAME,
SUM((Case when DR_CR_INDICATOR = 'D' then AMOUNT*-1 when DR_CR_INDICATOR = 'C' then AMOUNT end)) BOOKING_AMOUNT,
EXTERNAL_ACCOUNT ACCOUNT_NUMBER,
'Matched - ' ||A.MATCH_INDICATOR AS DEBIT,
NULL AS CREDIT,
VALUE_DATE VALUE_DT,
NULL AS AC_NO,
0 AS BOOKED_AMOUNT
FROM
FCUBS.RETB_EXTERNAL_ENTRY A
WHERE A.EXTERNAL_ENTITY = 'SAXODKKKXXX'
AND A.EXTERNAL_ACCOUNT = '78600/COMMEUR'
group by
BOOKING_DATE ,
EXTERNAL_ACCOUNT ,
VALUE_DATE,
MATCH_INDICATOR
order by tradedate, account_name)
where tradedate = trunc (:sysdate1 - case to_char(:sysdate2, 'Dy') when 'Mon' then 3 else 1 end);
SYSDATE is a date datatype so oracle will always treat it as a DATE datatype. For a bind variable I'd do an explicit conversion using TO_DATE(:bind_var, 'FORMAT_MASK'). For example:
select
case TO_CHAR(TO_DATE(:sysdate2,'DD-MON-YYYY'), 'Dy') when 'Mon'
then 3
else 1
end from dual

Postgresql, set order by desc or asc depending on variable parse into function

I have a function that takes product pricing data from today and yesterday and works out the difference, orders it by price_delta_percentage and then limits to 5. Now currently I order by price_delta_percentage DESC which returns the top 5 products that have increased in price since yesterday.
I would like to parse in a variable - sort - to change the function to either sort by DESC, or ASC. I have tried to use IF statements and get syntax errors and CASE statements which states that price_delta_percentage doesn't exist.
Script:
RETURNS TABLE(
product_id varchar,
name varchar,
price_today numeric,
price_yesterday numeric,
price_delta numeric,
price_delta_percentage numeric
)
LANGUAGE 'sql'
COST 100
STABLE STRICT PARALLEL SAFE
AS $BODY$
WITH cte AS (
SELECT
product_id,
name,
SUM(CASE WHEN rank = 1 THEN trend_price ELSE NULL END) price_today,
SUM(CASE WHEN rank = 2 THEN trend_price ELSE NULL END) price_yesterday,
SUM(CASE WHEN rank = 1 THEN trend_price ELSE 0 END) - SUM(CASE WHEN rank = 2 THEN trend_price ELSE 0 END) as price_delta,
ROUND(((SUM(CASE WHEN rank = 1 THEN trend_price ELSE NULL END) / SUM(CASE WHEN rank = 2 THEN trend_price ELSE NULL END) - 1) * 100), 2) as price_delta_percentage
FROM (
SELECT
magic_sets_cards.name,
pricing.product_id,
pricing.trend_price,
pricing.date,
RANK() OVER (PARTITION BY product_id ORDER BY date DESC) AS rank
FROM pricing
JOIN magic_sets_cards_identifiers ON magic_sets_cards_identifiers.mcm_id = pricing.product_id
JOIN magic_sets_cards ON magic_sets_cards.id = magic_sets_cards_identifiers.card_id
JOIN magic_sets ON magic_sets.id = magic_sets_cards.set_id
WHERE date BETWEEN CURRENT_DATE - days AND CURRENT_DATE
AND magic_sets.code = set_code
AND pricing.trend_price > 0.25) p
WHERE rank IN (1,2)
GROUP BY product_id, name
ORDER BY price_delta_percentage DESC)
SELECT * FROM cte WHERE (CASE WHEN price_today IS NULL OR price_yesterday IS NULL THEN 'NULL' ELSE 'VALID' END) !='NULL'
LIMIT 5;
$BODY$;sql
CASE Statement:
ORDER BY CASE WHEN sort = 'DESC' THEN price_delta_percentage END DESC, CASE WHEN sort = 'ASC' THEN price_delta_percentage END ASC)
Error:
ERROR: column "price_delta_percentage" does not exist
LINE 42: ORDER BY CASE WHEN sort = 'DESC' THEN price_delta_percenta...
You can't use CASE to decide between ASC and DESC like that. Those labels are not data, they are part of the SQL grammar. You would need to do it by combining the text into a string and then executing the string as a dynamic query, which means you would need to use pl/pgsql, not SQL
But since your column is numeric, you could just order by the product of the column and an indicator variable which is either 1 or -1.

PostgreSQL - SQL function to loop through all months of the year and pull 10 random records from each

I am attempting to pull 10 random records from each month of this year using this query here but I get an error "ERROR: relation "c1" does not exist
"
Not sure where I'm going wrong - I think it may be I'm using Mysql syntax instead, but how do I resolve this?
My desired output is like this
Month
Another header
2021-01
random email 1
2021-01
random email 2
total of ten random emails from January, then ten more for each month this year (til November of course as Dec yet to happen)..
With CTE AS
(
Select month,
email,
Row_Number() Over (Partition By month Order By FLOOR(RANDOM()*(1-1000000+1))) AS RN
From (
SELECT
DISTINCT(TO_CHAR(DATE_TRUNC('month', timestamp ), 'YYYY-MM')) AS month
,CASE
WHEN
JSON_EXTRACT_PATH_TEXT(json_extract_array_element_text (form_data,0),'name') = 'email'
THEN
JSON_EXTRACT_PATH_TEXT(json_extract_array_element_text (form_data,0),'value')
END AS email
FROM form_submits_y2 fs
WHERE fs.website_id IN (791)
AND month LIKE '2021%'
GROUP BY 1,2
ORDER BY 1 ASC
)
)
SELECT *
FROM CTE C1
LEFT JOIN
(SELECT RN
,month
,email
FROM CTE C2
WHERE C2.month = C1.month
ORDER BY RANDOM() LIMIT 10) C3
ON C1.RN = C3.RN
ORDER By month ASC```
You can't reference an outer table inside a derived table with a regular join. You need to use left join lateral to make that work
I did end up finding a more elegant solution to my query here via this source from github :
SELECT
month
,email
FROM
(
Select month,
email,
Row_Number() Over (Partition By month Order By FLOOR(RANDOM()*(1-1000000+1))) AS RN
From (
SELECT
TO_CHAR(DATE_TRUNC('month', timestamp ), 'YYYY-MM') AS month
,CASE
WHEN JSON_EXTRACT_PATH_TEXT(json_extract_array_element_text (form_data,0),'name') = 'email'
THEN JSON_EXTRACT_PATH_TEXT(json_extract_array_element_text (form_data,0),'value')
END AS email
FROM form_submits_y2 fs
WHERE fs.website_id IN (791)
AND month LIKE '2021%'
GROUP BY 1,2
ORDER BY 1 ASC
)
) q
WHERE
RN <=10
ORDER BY month ASC

T-SQL Union returns duplicated row (date)

I was trying to solve the issue of returning sum for each date in a specific date range, no matter if data is present for a day or not.
I have found that the best way would be to use a table pre-populated with all dates, select date range and union it with my data.
For some reason couldn't get left join to work, but union looks like working almost perfectly. The only issue is that it returns duplicates for dates where data is present in my data table.
SELECT NULL AS Visitors
,D.DATE AS Day
FROM Database.support.dates D
WHERE D.DATE BETWEEN #Start_date
AND #End_date
UNION
SELECT count(V.id) AS Visitors
,DATEADD(day, 0, DATEDIFF(day, 0, V.CreateTime)) AS Day
FROM Database.Clients.Clients V
WHERE V.CreateTime BETWEEN #Start_date
AND #End_date
AND V.WADID = #WADID
AND (
V.WAPID = #WAPID
OR #WAPID IS NULL
)
GROUP BY DATEADD(day, 0, DATEDIFF(day, 0, V.CreateTime))
ORDER BY Day DESC
Was researching it whole last day and still can't get it working :/
I think that left joining your calendar table to your current query actually was the right thing to do. That being said, you can remedy your current situation by simply aggregating on the day, e.g. using MAX. By default, NULL values for each day would be ignored, so long as there is a non null visitor count present:
WITH cte AS (
SELECT NULL AS Visitors, D.DATE AS Day
FROM Database.support.dates D
WHERE D.DATE BETWEEN #Start_date AND #End_date
UNION
SELECT COUNT(V.id), DATEADD(day, 0, DATEDIFF(day, 0, V.CreateTime))
FROM Database.Clients.Clients V
WHERE V.CreateTime BETWEEN #Start_date AND #End_date AND
V.WADID = #WADID AND (V.WAPID = #WAPID OR #WAPID IS NULL)
GROUP BY DATEADD(day, 0, DATEDIFF(day, 0, V.CreateTime))
)
SELECT Day, MAX(Visitors) AS Visitors -- filter off unwanted NULL values
FROM cte
GROUP BY Day
ORDER BY Day DESC;

Aggregated values depending on an other field

I have a table with a date-time and multiples propertied some on which I group by and some on which I aggregate, the query will be like get me revenue per customer last week.
Now I want to see the change between the requested period and the previous one so I will have 2 columns revenue and previous_revenue.
Right now I'm requesting the rows of the requested period plus the rows of the previous period and for each aggregated field I add a case statement inside which return the value or 0 if not in the period that I want.
That lead to as many CASE as aggregate fields but always with the same conditional statement.
I'm wondering if there is a better design for this use case...
SELECT
customer,
SUM(
CASE TIMESTAMP_CMP('2016-07-01 00:00:00', ft.date) > 0 WHEN true THEN
REVENUE
ELSE 0 END
) AS revenue,
SUM(
CASE TIMESTAMP_CMP('2016-07-01 00:00:00', ft.date) < 0 WHEN true THEN
REVENUE
ELSE 0 END
) AS previous_revenue
WHERE date_hour >= '2016-06-01 00:00:00'
AND date_hour <= '2016-07-31 23:59:59'
GROUP BY customer
(In my real use case I have many columns which make it even more ugly)
First, I'd suggest to refactor out the timestamps and precalculate the current and previous period for later use. This is not strictly necessary to solve your problem, though:
create temporary table _period as
select
'2016-07-01 00:00:00'::timestamp as curr_period_start
, '2016-07-31 23:59:59'::timestamp as curr_period_end
, '2016-06-01 00:00:00'::timestamp as prev_period_start
, '2016-06-30 23:59:59'::timestamp as prev_period_end
;
Now a possible design to avoid repetition of timestamps and CASE statements is to group by the periods first and then doing a FULL OUTER JOIN for that table on itself:
with _aggregate as (
select
case
when date_hour between prev_period_start and prev_period_end then 'previous'
when date_hour between curr_period_start and curr_period_end then 'current'
end::varchar(20) as period
, customer
-- < other columns to group by go here >
, sum(revenue) as revenue
-- < other aggregates go here >
from
_revenue, _period
where
date_hour between prev_period_start and curr_period_end
group by 1, 2
)
select
customer
, current_period.revenue as revenue
, previous_period.revenue as previous_revenue
from
(select * from _aggregate where period = 'previous') previous_period
full outer join (select * from _aggregate where period = 'current') current_period
using(customer) -- All columns which have been group by must go into the using() clause:
-- e.g. using(customer, some_column, another_column)
;