/*##################################################################################
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
#     https://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###################################################################################*/


/*
-- NOTE: This is designed for a single BigQuery region
--       This gets the past 3 months of data (from first day of prior 3 months)

-- WARNING!!!
-- This will return every SQL statement run in BigQuery which could be A LOT!
-- Also, if you have PII data in your SQL (Passport, Date of Birth), it can be shown in some of the SQL statements
-- WARNING!!!

-- SEARCH and REPLACE the below values (if downloading this single file from GitHub)
-- Replace Region          -> Search for: region-${bigquery_region}
-- Replace GCS bucket Path -> Search for: gs://${raw_bucket_name}

Use Cases:
    - Shows the queries that are most used in your organization
    - Shows the estimatated on-demand cost of the queries (retail pricing)

Description: 
    - Loops through each project and get your query data from the informational schema tables

Reference:
    - https://cloud.google.com/bigquery/docs/information-schema-jobs


Clean up / Reset script:
    DROP SCHEMA IF EXISTS `${project_id}.ondemand_query_usage` CASCADE;  
    DROP SCHEMA IF EXISTS `${project_id}.ondemand_query_analysis` CASCADE;  
*/


-- This is designed to help you understand where you should focus your efforts or costs
-- It is NOT an exact costs savings calculator
CREATE SCHEMA `${project_id}.ondemand_query_usage`
    OPTIONS (
    location = "us"
    );


-- Track which projects we are able to get data 
CREATE OR REPLACE TABLE `${project_id}.ondemand_query_usage.usage_export_project`
(
  project_id STRING,
  result STRING
) ;


-- Create a table to hold the results
CREATE OR REPLACE TABLE `${project_id}.ondemand_query_usage.usage_export_data` 
(
  creation_time TIMESTAMP,
  project_id STRING,
  project_number INT64,
  user_email STRING,
  job_id STRING,
  job_type STRING,
  statement_type STRING,
  priority STRING,
  start_time TIMESTAMP,
  end_time TIMESTAMP,
  query STRING,
  state STRING,
  reservation_id STRING,
  total_bytes_processed INT64,
  total_slot_ms INT64,
  error_result_reason STRING,
  error_result_location STRING,
  error_result_debug_info STRING,
  error_result_message STRING,
  cache_hit BOOLEAN,
  destination_table_project_id STRING,
  destination_table_dataset_id STRING,
  destination_table_table_id STRING,
  total_bytes_billed INT64,
  transaction_id STRING,
  parent_job_id STRING,
  session_info_session_id STRING,
  total_modified_partitions INT64,
  bi_engine_statistics_bi_engine_mode STRING,
  resource_warning STRING,
  normalized_literals STRING,
  transferred_bytes INT64,
  est_on_demand_cost FLOAT64,
  job_avg_slots FLOAT64,
  jobstage_max_slots FLOAT64,
  estimated_runnable_units INT64
);

SELECT DISTINCT project_id from `region-${bigquery_region}`.INFORMATION_SCHEMA.JOBS_BY_ORGANIZATION;

-- Loop through each project id
-- This is done since only the JOBS view has the "query" field
FOR record IN (SELECT DISTINCT project_id 
                          FROM `region-${bigquery_region}`.INFORMATION_SCHEMA.JOBS_BY_ORGANIZATION) 
                         -- You can inlcude the below WHERE statement to limit to just a few projects
                         --WHERE project_id IN ('data-analytics-demo-xxxxxxxxxx','data-analytics-demo-yyyyyyyyy'))
DO
  BEGIN
  EXECUTE IMMEDIATE FORMAT("""
  INSERT INTO `${project_id}.ondemand_query_usage.usage_export_data`
    (
    creation_time,
    project_id,
    project_number,
    user_email,
    job_id,
    job_type,
    statement_type,
    priority,
    start_time,
    end_time,
    query,
    state,
    reservation_id,
    total_bytes_processed,
    total_slot_ms,
    error_result_reason,
    error_result_location,
    error_result_debug_info,
    error_result_message,
    cache_hit,
    destination_table_project_id,
    destination_table_dataset_id,
    destination_table_table_id,
    total_bytes_billed,
    transaction_id,
    parent_job_id,
    session_info_session_id,
    total_modified_partitions,
    bi_engine_statistics_bi_engine_mode,
    resource_warning,
    normalized_literals,
    transferred_bytes,
    est_on_demand_cost,
    job_avg_slots,
    jobstage_max_slots,
    estimated_runnable_units
  )
  SELECT job.creation_time,
        job.project_id,
        job.project_number,
        job.user_email,
        job.job_id,
        job.job_type,
        job.statement_type,
        job.priority,
        job.start_time,
        job.end_time,
        job.query,
        job.state,
        job.reservation_id,
        job.total_bytes_processed,
        job.total_slot_ms,
        job.error_result.reason     AS error_result_reason,
        job.error_result.location   AS error_result_location,
        job.error_result.debug_info AS error_result_debug_info,
        job.error_result.message    AS error_result_message,
        job.cache_hit,
        job.destination_table.project_id AS destination_table_project_id,
        job.destination_table.dataset_id AS destination_table_dataset_id,
        job.destination_table.table_id   AS destination_table_table_id,
        job.total_bytes_billed,
        job.transaction_id,
        job.parent_job_id,
        job.session_info.session_id AS session_info_session_id,
        job.total_modified_partitions,
        job.bi_engine_statistics.bi_engine_mode AS bi_engine_statistics_bi_engine_mode,
        job.query_info.resource_warning,
        job.query_info.query_hashes.normalized_literals,
        job.transferred_bytes,

        -- This is retail pricing (for estimating purposes)
        -- 6.25 / 1,099,511,627,776 = 0.00000000000568434188608080 ($6.25 per TB so cost per byte is 0.00000000000568434188608080)
        CASE WHEN job.reservation_id IS NULL 
             THEN CAST(CAST(job.total_bytes_billed AS BIGDECIMAL) * CAST(0.00000000000568434188608080 AS BIGDECIMAL) AS FLOAT64) 
             ELSE CAST (0 AS FLOAT64)
         END as est_on_demand_cost,

        -- Average slot utilization per job is calculated by dividing
        -- total_slot_ms by the millisecond duration of the job
        CAST(SAFE_DIVIDE(job.total_slot_ms,(TIMESTAMP_DIFF(job.end_time, job.start_time, MILLISECOND))) AS FLOAT64) AS job_avg_slots,

        MAX(SAFE_DIVIDE(unnest_job_stages.slot_ms,unnest_job_stages.end_ms - unnest_job_stages.start_ms)) AS jobstage_max_slots,
        MAX(unnest_timeline.estimated_runnable_units) AS estimated_runnable_units
    FROM `%s`.`region-${bigquery_region}`.INFORMATION_SCHEMA.JOBS AS job
          CROSS JOIN UNNEST(job.job_stages) as unnest_job_stages
          CROSS JOIN UNNEST(job.timeline) AS unnest_timeline
    WHERE DATE(job.creation_time) BETWEEN DATE_SUB(DATE_SUB(CURRENT_DATE(), INTERVAL 3 MONTH), INTERVAL (SELECT EXTRACT(DAY FROM CURRENT_DATE())-1) DAY) AND CURRENT_DATE()
  GROUP BY 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34;
  """,record.project_id);

  INSERT INTO `${project_id}.ondemand_query_usage.usage_export_project` (project_id, result) VALUES (record.project_id,'SUCCESS');

  EXCEPTION WHEN ERROR THEN
    -- do nothing we do not have access to the project
    INSERT INTO `${project_id}.ondemand_query_usage.usage_export_project` (project_id, result) VALUES (record.project_id,'FAILED');
  END;

END FOR;


-- Export the data (some people transfer this to have an analysis performed)
-- You should delete the data from this path before exporting
-- You can also share the data via Analytics Hub (Preferred Method of Sharing)
EXPORT DATA
OPTIONS (
   uri = 'gs://${raw_bucket_name}/ondemand_query_usage/*.parquet',
   format = 'PARQUET',
   overwrite = true
   )
AS (
   SELECT *
     FROM `${project_id}.ondemand_query_usage.usage_export_data`
);


-- Test loading the data

LOAD DATA OVERWRITE `${project_id}.ondemand_query_usage.usage_import_data`
FROM FILES (
  format = 'PARQUET',
  uris = ['gs://${raw_bucket_name}/ondemand_query_usage/*.parquet']
);


-- See which projects worked/failed
SELECT * FROM `${project_id}.ondemand_query_usage.usage_export_project` ORDER BY result DESC;


-- See some results
SELECT project_id, query, SUM(est_on_demand_cost) AS est_sum_on_demand_cost, COUNT(1) AS Cnt
  FROM `${project_id}.ondemand_query_usage.usage_export_data`
 GROUP BY 1, 2
 ORDER BY 3 DESC
 LIMIT 100;


------------------------------------------------------------------------------------------------------------
-- Do Analysis
------------------------------------------------------------------------------------------------------------

-- This is designed to help you understand where you should focus your efforts or costs
-- It is NOT an exact costs savings calculator
CREATE SCHEMA `${project_id}.ondemand_query_analysis`
    OPTIONS (
    location = "us"
    );


-- In case we have duplicates (the tables were not dropped between runs)
CREATE OR REPLACE TABLE `${project_id}.ondemand_query_analysis.usage_data` AS
SELECT DISTINCT *
  FROM `${project_id}.ondemand_query_usage.usage_import_data`
 WHERE error_result_reason IS NULL  
   AND reservation_id IS NULL;


-- Cost by month for top 1000 most expensive queries
CREATE OR REPLACE TABLE `${project_id}.ondemand_query_analysis.usage_cost_by_query_top_1000` AS
SELECT project_id, 
       EXTRACT(YEAR  FROM start_time) AS Year,
       EXTRACT(MONTH FROM start_time) AS Month,
       query, 
       SUM(est_on_demand_cost) AS est_sum_on_demand_cost, 
       COUNT(1) AS Cnt,
       AVG(total_bytes_processed) / 1000000000 AS average_gb_processed,
       AVG(job_avg_slots) AS average_job_avg_slots,
       AVG(estimated_runnable_units) AS average_estimated_runnable_units,
  FROM `${project_id}.ondemand_query_analysis.usage_data`
 WHERE error_result_reason IS NULL
   AND reservation_id IS NULL
 GROUP BY 1, 2, 3, 4
 LIMIT 1000;

SELECT project_id,
       Year,
       Month,
       query,
       CAST(est_sum_on_demand_cost AS INT64) AS est_sum_on_demand_cost,
       Cnt,
       CAST(average_gb_processed AS INT64)   AS average_gb_processed,
       CAST(average_job_avg_slots AS INT64)  AS average_job_avg_slots,
       CAST(average_estimated_runnable_units AS INT64) AS average_estimated_runnable_units  
  FROM `${project_id}.ondemand_query_analysis.usage_cost_by_query_top_1000`
 ORDER BY est_sum_on_demand_cost DESC;


 -- Usage Costs by Project
CREATE OR REPLACE TABLE `${project_id}.ondemand_query_analysis.usage_cost_by_project` AS
SELECT project_id, 
       EXTRACT(YEAR  FROM start_time) AS Year,
       EXTRACT(MONTH FROM start_time) AS Month,
       SUM(est_on_demand_cost) AS est_sum_on_demand_cost, 
       AVG(total_bytes_processed) / 1000000000 AS average_gb_processed,
       AVG(job_avg_slots) AS average_job_avg_slots,
       AVG(estimated_runnable_units) AS average_estimated_runnable_units,
  FROM `${project_id}.ondemand_query_analysis.usage_data`
 GROUP BY 1, 2, 3;

SELECT project_id,
       Year,
       Month,
       CAST(est_sum_on_demand_cost AS INT64) AS est_sum_on_demand_cost,
       CAST(average_gb_processed AS INT64)   AS average_gb_processed,
       CAST(average_job_avg_slots AS INT64)  AS average_job_avg_slots,
       CAST(average_estimated_runnable_units AS INT64) AS average_estimated_runnable_units  
  FROM `${project_id}.ondemand_query_analysis.usage_cost_by_project`
 ORDER BY est_sum_on_demand_cost DESC;


-- Total costs for each month single month
-- This is based off of Retail Pricing (no discounts)
-- If you have a 10% discount then you can change the below to (1 minus .10 = .90) SUM(est_sum_on_demand_cost * .90)
SELECT Year,
       Month,
       CAST(SUM(est_sum_on_demand_cost) AS INT64)  AS total
  FROM `${project_id}.ondemand_query_analysis.usage_cost_by_project`
GROUP BY 1,2
ORDER BY Year, Month;


-- For each minute, for each job, get the maximum number of slots
CREATE OR REPLACE TABLE `${project_id}.ondemand_query_analysis.usage_slots_by_job` AS
SELECT project_id, 
       job_id,
       EXTRACT(YEAR FROM start_time)   AS Year,
       EXTRACT(MONTH FROM start_time)  AS Month,

       ((EXTRACT(DAY    FROM start_time) - 1) * (24*60)) +
       (EXTRACT(HOUR   FROM start_time) * 60) +
       (EXTRACT(MINUTE FROM start_time)) AS start_minute_of_job,

       ((EXTRACT(DAY    FROM end_time) - 1) * (24*60)) +
       (EXTRACT(HOUR   FROM end_time) * 60) +
       (EXTRACT(MINUTE FROM end_time)) AS end_minute_of_job,

       MAX(job_avg_slots) AS average_job_max_slots

  FROM `${project_id}.ondemand_query_analysis.usage_data`
 GROUP BY 1,2,3,4,5,6;


SELECT *
  FROM `${project_id}.ondemand_query_analysis.usage_slots_by_job`
 LIMIT 100;


-- For each minute in the month sum the max slots used by the jobs
-- For each minute determine the number of slots to buy (in increments of 100)
CREATE OR REPLACE TABLE `${project_id}.ondemand_query_analysis.usage_slots_data_per_minute` AS
WITH minutes AS
(
  -- every minute in the month
  SELECT element as minute_number
    FROM UNNEST(GENERATE_ARRAY(1, 44640)) AS element
)
 SELECT project_id, 
        Year,
        Month,
        minute_number,
        CAST(SUM(average_job_max_slots) AS INT64) AS avg_slots,
        CAST(FLOOR((CAST(SUM(average_job_max_slots) AS INT64) + 99) / 100) * 100  AS INT64) AS avg_slots_rounded_up_100_slots
   FROM `${project_id}.ondemand_query_analysis.usage_slots_by_job` AS usage_slots_by_job
        INNER JOIN minutes
                ON minute_number BETWEEN start_minute_of_job AND end_minute_of_job
GROUP BY 1,2,3,4;


SELECT *
 FROM `${project_id}.ondemand_query_analysis.usage_slots_data_per_minute`
ORDER BY project_id, 
         Year,
         Month,
         minute_number
LIMIT 10000;


-- For each minute determine the number of slots to buy (in increments of 100)
-- 0.060 = US PAYG Slot price per 100 per minute at Retail price
CREATE OR REPLACE TABLE `${project_id}.ondemand_query_analysis.usage_slots_payg_slot_cost` AS
 SELECT project_id, 
        Year,
        Month,
        CAST(0.060 * FLOOR((SUM(avg_slots_rounded_up_100_slots) + 99) / 100) AS INT64) AS payg_slot_cost
   FROM `${project_id}.ondemand_query_analysis.usage_slots_data_per_minute` AS usage_slots_data_per_minute
GROUP BY 1,2,3;



-- Compare On-Demand versus Slots (HIGH LEVEL ESTIMATE FOR SEEING IF SLOTS SHOULD BE EVAULATED)
-- The above pricing is a ROUGH estimate so we say payg_slot_cost * 2 to account for any inefficiencies in auto-scaling
SELECT usage_cost_by_project.project_id,
       usage_cost_by_project.Year,
       usage_cost_by_project.Month,
       CAST(AVG(CAST(usage_cost_by_project.est_sum_on_demand_cost AS INT64)) AS INT64) AS est_sum_on_demand_cost,
       CAST(AVG(usage_slots_payg_slot_cost.payg_slot_cost) AS INT64)                   AS est_payg_slot_cost
  FROM `${project_id}.ondemand_query_analysis.usage_cost_by_project` AS usage_cost_by_project
        INNER JOIN `${project_id}.ondemand_query_analysis.usage_slots_payg_slot_cost` AS usage_slots_payg_slot_cost
                ON usage_cost_by_project.project_id = usage_slots_payg_slot_cost.project_id
               AND usage_cost_by_project.Year       = usage_slots_payg_slot_cost.Year
               AND usage_cost_by_project.Month      = usage_slots_payg_slot_cost.Month
  WHERE est_sum_on_demand_cost > payg_slot_cost * 2
 GROUP BY 1,2,3
 ORDER BY 4 DESC;

-- $2000 is used as a high level estimate of 100 slots
 SELECT usage_cost_by_project.project_id,
        usage_cost_by_project.Year,
        usage_cost_by_project.Month,
        CAST(AVG(CAST(usage_cost_by_project.est_sum_on_demand_cost AS INT64)) AS INT64) -
        CAST(AVG(usage_slots_payg_slot_cost.payg_slot_cost) AS INT64)                   AS rough_savings,
        REPEAT('$', CAST((CAST(AVG(CAST(usage_cost_by_project.est_sum_on_demand_cost AS INT64)) AS INT64) -
        CAST(AVG(usage_slots_payg_slot_cost.payg_slot_cost) AS INT64))/2000 AS INT))
  FROM `${project_id}.ondemand_query_analysis.usage_cost_by_project` AS usage_cost_by_project
        INNER JOIN `${project_id}.ondemand_query_analysis.usage_slots_payg_slot_cost` AS usage_slots_payg_slot_cost
                ON usage_cost_by_project.project_id = usage_slots_payg_slot_cost.project_id
               AND usage_cost_by_project.Year       = usage_slots_payg_slot_cost.Year
               AND usage_cost_by_project.Month      = usage_slots_payg_slot_cost.Month
  WHERE est_sum_on_demand_cost > payg_slot_cost * 2
 GROUP BY 1,2,3
ORDER BY 4 DESC;


-- Create Looker Views
CREATE OR REPLACE VIEW `${project_id}.ondemand_query_analysis.looker_cost_per_month` AS
SELECT Year,
       Month,
       CAST(CONCAT(CAST(Year AS STRING),'-',CAST(Month AS STRING),'-01') AS DATE) AS SortDate,
       CAST(SUM(est_sum_on_demand_cost) AS INT64)  AS Total
  FROM `${project_id}.ondemand_query_analysis.usage_cost_by_project`
GROUP BY 1,2;


CREATE OR REPLACE VIEW `${project_id}.ondemand_query_analysis.looker_cost_per_month_per_project` AS
SELECT project_id,
       Year,
       Month,
       CAST(CONCAT(CAST(Year AS STRING),'-',CAST(Month AS STRING),'-01') AS DATE) AS SortDate,
       CAST(SUM(est_sum_on_demand_cost) AS INT64)  AS Total
  FROM `${project_id}.ondemand_query_analysis.usage_cost_by_project`
GROUP BY 1,2,3;


CREATE OR REPLACE VIEW `${project_id}.ondemand_query_analysis.looker_most_expensive_queries` AS
SELECT project_id,
       Year,
       Month,
       SUBSTRING(query, 1, 1000) AS query,
       CAST(est_sum_on_demand_cost AS INT64) AS est_sum_on_demand_cost,
       Cnt AS execution_count,
       CAST(average_gb_processed AS INT64)   AS average_gb_processed,
       CAST(average_job_avg_slots AS INT64)  AS average_job_avg_slots,
       CAST(average_estimated_runnable_units AS INT64) AS average_estimated_runnable_units  
  FROM `${project_id}.ondemand_query_analysis.usage_cost_by_query_top_1000`;


-- $2000 is used as a high level estimate of 100 slots
CREATE OR REPLACE VIEW `${project_id}.ondemand_query_analysis.looker_ondemand_vs_slots` AS
WITH data AS
(
 SELECT usage_cost_by_project.project_id,
        usage_cost_by_project.Year,
        usage_cost_by_project.Month,
        CAST(SUM(usage_cost_by_project.est_sum_on_demand_cost) AS INT64) AS est_sum_on_demand_cost,
        CAST(AVG(CAST(usage_cost_by_project.est_sum_on_demand_cost AS INT64)) AS INT64) -
        CAST(AVG(usage_slots_payg_slot_cost.payg_slot_cost) AS INT64)                   AS rough_savings,
        REPEAT('$', CAST((CAST(AVG(CAST(usage_cost_by_project.est_sum_on_demand_cost AS INT64)) AS INT64) -
        CAST(AVG(usage_slots_payg_slot_cost.payg_slot_cost) AS INT64))/2000 AS INT)) AS stars
  FROM `${project_id}.ondemand_query_analysis.usage_cost_by_project` AS usage_cost_by_project
        INNER JOIN `${project_id}.ondemand_query_analysis.usage_slots_payg_slot_cost` AS usage_slots_payg_slot_cost
                ON usage_cost_by_project.project_id = usage_slots_payg_slot_cost.project_id
               AND usage_cost_by_project.Year       = usage_slots_payg_slot_cost.Year
               AND usage_cost_by_project.Month      = usage_slots_payg_slot_cost.Month
  WHERE est_sum_on_demand_cost > payg_slot_cost * 2
 GROUP BY 1,2,3
)
SELECT *
  FROM data;


-- Show the Looker report:
/*
Clone this report: https://lookerstudio.google.com/reporting/32c72a9a-1172-44d3-8f92-3eebdb042a02
Click the 3 dots in the top right and select "Make a copy"
Click "Copy Report"
Click "Resouce" menu then "Manage added data sources"
Click "Edit" under Actions title
Click "${project_id}" (or enter the Project Id) under Project title
Click "${bigquery_taxi_dataset}" under Dataset title
Click "looker_most_expensive_queries" under Table title
Click "Reconnect"
Click "Apply" - there should be no field changes
Click "Done" - in top right
Repeat the above for each data source (3 additional ones)
Click "Close" - in top right
You can now see the data
*/
SELECT * FROM `${project_id}.ondemand_query_analysis.looker_ondemand_vs_slots` ORDER BY rough_savings DESC;
