Files
payg-report-dev/mod/compute_payg.cfm
T
2026-07-26 23:04:05 +03:00

546 lines
29 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<cfimport prefix="m" taglib="../lib"/>
<cfimport prefix="d" taglib="../lib/data"/>
<cfparam name="ATTRIBUTES.dt_finish" type="date" default=#createDateTime(year(Now()),month(Now()),1,0,0,0)#/>
<cfparam name="ATTRIBUTES.dt_start" type="date" default=#dateAdd('m',-1,ATTRIBUTES.dt_finish)#/>
<cfparam name="ATTRIBUTES.output" type="string" default="report"/>
<cfparam name="ATTRIBUTES.debug" type="boolean" default=false/>
<cfparam name="ATTRIBUTES.tenant_wz_index" type="string"/>
<cfparam name="ATTRIBUTES.restrict_to_own_instances" type="boolean" default=true/>
<cfset hours=dateDiff('h', ATTRIBUTES.dt_start, ATTRIBUTES.dt_finish)/> <!--- *** нужно для каждого фрагмента --->
<cfif hours LE 0>
<cfthrow message="Некорректный период" detail="Период должен быть больше 0"/>
</cfif>
<!---
*** ВНИМАНИЕ! Вычитать минимальный бесплатный лимит (free tier) нужно после агрегации!
Исходная последовательность расчетов сначала делает агрегацию по месяцу, при расчете по дням нужно менять порядок
--->
<!---
Скрытое противоречие, которое требует разрешения: если у нас несколько периодов тарификации в одном месяце (по разной цене), ИЗ КАКОГО вычитать минимальный лимит?
Еще сложность, если лимита хватает на несколько периодов тарификации, как его распределять по периодам. Если тарификация строго по месяцам, этой проблемы нет.
Это можно сделать процедурно, но сложность расчета все возрастает
--->
<!--- *** Проверить, что у нас не биллится закрытый период (закрытый следующим) - возможно, с учетом line_key
- насколько вижу, прошлый период закрывается, причем концы отрезки закрытые (конец включается). Считаем, что этой проблемы нет
--->
<!--- QoQ does not support join syntax, and of course outer joins, at least of Lucee 5.4--->
<!--- а внешние джойны хорошо подсвечивают ошибки в данных, особенно помогает full --->
<cfmodule template="crm_contract.cfm" wz="WZ#numberFormat(ATTRIBUTES.tenant_wz_index,'00000')#" out="qSpecification"/>
<cfif (ATTRIBUTES.debug)>
qSpecification (from CRM)
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qSpecification#/>
<cfflush/>
</cfif>
<cfquery name="qComputingAge">
select max(capacity_resource.timestamp) as dt_load from ngcloud_ru.capacity_resource
</cfquery>
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qComputingAge#/>
<cfflush/>
</cfif>
<cfquery name="qStorageAge" datasource="rpt">
select max(capacity_storage.timestamp) as dt_load from ngcloud_ru.capacity_storage
</cfquery>
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qStorageAge#/>
<cfflush/>
</cfif>
<cfquery name="qS3VolAge">
select max(timestamp_addition) as dt_load from s3billing.bucket_stat
</cfquery>
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qS3VolAge#/>
<cfflush/>
</cfif>
<cfquery name="qS3OpsTrfAge">
select max(time) as dt_load from s3billing.usage_bucket_by_user
</cfquery>
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qS3OpsTrfAge#/>
<cfflush/>
</cfif>
<cfset qFreeTier=queryNew("code,free_tier","CF_SQL_VARCHAR,CF_SQL_NUMERIC",[['paas.s3.ceph.vol-m',1],['paas.s3.ceph.get-m',10],['paas.s3.ceph.put-m',1],['paas.s3.ceph.trf-m',100]])/>
<!--- -- все-таки пришлось прибить гвоздями - в каталоге нет даже бесплатного лимита, не то что скидочной политики
-- придется добавить в каталог
-- бесплатный лимит есть только для S3 HOT (в месяц в единицах измерения) --->
<!--- ******************************** здесь нужно организовать пересечение версионной спецификации с анализируемым диапазоном дат --->
<!--- Проверка: включаем дату смены цены в диапазон и смотрим --->
<!--- Важно: если в периоде было 2+ цены, то строки должны быть замножены, а цена за период перестает иметь смысл (не показывать же среднее арифметическое, это неправильно) --->
<!--- Left Outer Join with Free Tier --->
<cfquery name="qSpec" dbtype="query">
select s.catalogName as svc, s.component, s.serviceCode as service_code, s.fullCode as code, s.measureShort as unit
, s.priceWVat as price, (s.discount + s.serviceDiscount - s.discount*s.serviceDiscount/100) as discount
,s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, f.free_tier
from qSpecification s, qFreeTier f
WHERE s.fullCode=f.code
AND s.serviceType = 3
UNION
select s.catalogName as svc, s.component, s.serviceCode as service_code, s.fullCode as code, s.measureShort as unit
, s.priceWVat as price, (s.discount + s.serviceDiscount - s.discount*s.serviceDiscount/100) as discount
,s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, 0
from qSpecification s
WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsqltype="cf_sql_varchar" list="true">)
AND s.serviceType = 3
</cfquery>
<cfif (ATTRIBUTES.debug)>
qSpec (Left Outer Join with Free Tier)
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qSpec#/>
<cfflush/>
</cfif>
<!--- *** Нужно еще реализовать в модели данных (каталоге) политики бесплатного минимума (free tier), сейчас прибито гвоздями --->
<cfquery name="qComputing">
select 'WZ'||to_char(tenant.wzcode,'FM00000') as wz, vdc.name as vdc_name,
case
WHEN vdc.name LIKE '%-v1cl1%' THEN 'iaas.ngc.i29'
WHEN vdc.name LIKE '%-v1cl2%' THEN 'iaas.ngc.i31'
WHEN vdc.name LIKE '%-v1cl4%' THEN 'iaas.ngc.a28'
WHEN vdc.name LIKE '%-v1cl5%' THEN 'iaas.ngc.a45'
WHEN vdc.name LIKE '%-v1cl6%' THEN 'iaas.ngc.i28'
WHEN vdc.name LIKE '%-v1cl7%' THEN 'iaas.ngc.a40' -- *** смапить все кластера
ELSE '['||vdc.name||']' end as code,
sum(cpu_used/cpu_speed)/4. as core_h,
sum(mem_used)/1024/4. as gb_h,
capacity_resource.timestamp::date as dt,
vdc.name as dimension_1,
null as dimension_2
from ngcloud_ru.capacity_resource
join ngcloud_ru.vdc on capacity_resource.vdc_id=vdc.id
join ngcloud_ru.tenant on tenant.id=vdc.tenant_id
where tenant.wzcode = <cfqueryparam cfsqltype="cf_sql_integer" value=#ATTRIBUTES.tenant_wz_index#/>
AND capacity_resource.timestamp::date >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_start#/>
AND capacity_resource.timestamp::date <= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_finish#/>
group by tenant.wzcode, vdc.name, capacity_resource.timestamp::date
order by tenant.wzcode, vdc.name, capacity_resource.timestamp::date;
</cfquery>
<cfif (ATTRIBUTES.debug)>
qComputing
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qComputing#/>
<cfflush/>
</cfif>
<!--- если захочется сгенерить литерал таблицы соответствия
with a as (
select * from ngcloud_ru.storage_profile_types
)
select '('||id||', '''|| name||''', '''||
case
when name ilike '%fastssd%' then 'fstssd'
when name ilike '%ssd%' then 'ssd'
when name ilike '%sata%' then 'sata'
else '' end
||''''||')'
from a
--->
<!--- переделать storage_profile_types с расшифровкой ssd, sata, можно прямо до каталожного кода --->
<!--- очень долго исполняется --->
<cfquery name="qStorage">
select 'WZ'||to_char(tenant.wzcode,'FM00000') as wz, vdc.name as vdc_name,
case
WHEN vdc.name LIKE '%-v1cl1%' THEN 'iaas.ngc.i29'
WHEN vdc.name LIKE '%-v1cl2%' THEN 'iaas.ngc.i31'
WHEN vdc.name LIKE '%-v1cl4%' THEN 'iaas.ngc.a28'
WHEN vdc.name LIKE '%-v1cl5%' THEN 'iaas.ngc.a45'
WHEN vdc.name LIKE '%-v1cl6%' THEN 'iaas.ngc.i28'
WHEN vdc.name LIKE '%-v1cl7%' THEN 'iaas.ngc.a40'
ELSE '['||vdc.name||']' END
|| CASE
WHEN storage_profile_types.name ILIKE '%FASTSSD%' THEN '.fstssd-m'
WHEN storage_profile_types.name ILIKE '%SSD%' THEN '.ssd-m'
WHEN storage_profile_types.name ILIKE '%SAS%' THEN '.sas-m'
WHEN storage_profile_types.name ILIKE '%SATA%' THEN '.sata-m'
ELSE '.['|| storage_profile_types.name ||']' END
as code,
storage_profile_types.name, -- *** это не тип, а профайл
sum("limit")/1024./4./720. as gb_m_limit, -- 4 потому, что сбор раз в 15 минут, а цена за УСЛОВНЫЙ ГБ-месяц (720 ГБ-часов)
sum(used)/1024./4./720. as gb_m_used,
capacity_storage.timestamp::date as dt,
vdc.name as dimension_1,
storage_profile_types.name as dimension_2
from ngcloud_ru.capacity_storage
join ngcloud_ru.vdc on capacity_storage.vdc_id=vdc.id
join ngcloud_ru.tenant on tenant.id=vdc.tenant_id
join ngcloud_ru.storage_profile_types on capacity_storage.storage_profile_types_id=storage_profile_types.id
where tenant.wzcode = <cfqueryparam cfsqltype="cf_sql_integer" value=#ATTRIBUTES.tenant_wz_index#/>
AND capacity_storage.timestamp::date >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_start#/>
AND capacity_storage.timestamp::date <= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_finish#/>
group by tenant.wzcode, vdc.name, storage_profile_types.name, capacity_storage.timestamp::date
order by tenant.wzcode, vdc.name, storage_profile_types.name, capacity_storage.timestamp::date;
</cfquery>
<!---
"placement" "size_gb" "get_10k" "put_10k" "bytes_sent_gb"
HOT_FREE_LIMIT 1 10 1 100
--->
<!--- надо дисконтировать не ГБ-часы а ГБ-месяцы --->
<cfquery name="qS3Vol">
select /* *** ниже дублирование кода агрегации - round ceil etc, обратить внимание при правке */
(sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>) as vol_B
,ceil(sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>/1024/1024/1024) as vol_GB
<!--- ,greatest(ceil(sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>
/1024/1024/1024)-
case when bucket_stat.placement_id = 1 then 1 else 0 end, 0) as vol_GB_free_tier_subtracted /*считается не в точности так, как в функции*/ --->
,case
when bucket_stat.placement_id = 1 then 'paas.s3.ceph'
when bucket_stat.placement_id = 2 then 'paas.s3.cphc'
else '['|| TO_CHAR(bucket_stat.placement_id,'FM9') ||']' end as code
--,bucket_stat.placement_id
,'WZ'||lpad(bucket_stat.owner, 5, '0') as wz -- *** здесь не число, а строка, поэтому другая формула обратной сборки WZ
,bucket_stat.timestamp_addition::date as dt
,bucket_info.name || ' - ' || case
when bucket_stat.placement_id = 1 then 'paas.s3.ceph'
when bucket_stat.placement_id = 2 then 'paas.s3.cphc'
else '['|| TO_CHAR(bucket_stat.placement_id,'FM9') ||']' end
as dimension_1
,null as dimension_2
from s3billing.bucket_stat
join s3billing.placement ON s3billing.placement.id=bucket_stat.placement_id
join s3billing.bucket_info ON bucket_info.id=bucket_stat.bucket_id
where bucket_stat.placement_id in (1,2)
AND bucket_stat.owner = <cfqueryparam cfsqltype="cf_sql_varchar" value=#ATTRIBUTES.tenant_wz_index#/>
AND bucket_stat.timestamp_addition::date >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_start#/>
AND bucket_stat.timestamp_addition::date <= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_finish#/>
group by bucket_stat.owner, bucket_stat.placement_id, bucket_info.name, bucket_stat.timestamp_addition::date
order by bucket_stat.owner, bucket_stat.placement_id, bucket_info.name, bucket_stat.timestamp_addition::date;
</cfquery>
<cfif (ATTRIBUTES.debug)>
qS3Vol
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qS3Vol#/>
<cfflush/>
</cfif>
<cfquery name="qS3OpsTrf">
select
sum(
CASE WHEN
(category.name LIKE 'get%'
OR category.name LIKE 'head%'
OR category.name LIKE 'options%'
) THEN ops ELSE 0 END
) as get
,ceil(sum(
CASE WHEN
(category.name LIKE 'get%'
OR category.name LIKE 'head%'
OR category.name LIKE 'options%'
) THEN ops ELSE 0 END
)/10000) as get_10k
/*,greatest(ceil(sum(
CASE WHEN
(category.name LIKE 'get%'
OR category.name LIKE 'head%'
OR category.name LIKE 'options%'
) THEN ops ELSE 0 END
)/10000) - CASE WHEN bucket_info.placement_id = 1 THEN 10 ELSE 0 END, 0
) as get_10k_free_tier_subtracted*/
,sum(
CASE WHEN
(category.name LIKE 'put%'
OR category.name LIKE 'post%'
OR category.name LIKE 'patch%'
OR category.name LIKE 'list%'
) THEN ops ELSE 0 END
) as put
,ceil(sum(
CASE WHEN
(category.name LIKE 'put%'
OR category.name LIKE 'post%'
OR category.name LIKE 'patch%'
OR category.name LIKE 'list%'
) THEN ops ELSE 0 END
)/10000) as put_10k
/*,greatest(ceil(sum(
CASE WHEN
(category.name LIKE 'put%'
OR category.name LIKE 'post%'
OR category.name LIKE 'patch%'
OR category.name LIKE 'list%'
) THEN ops ELSE 0 END
)/10000) - CASE WHEN bucket_info.placement_id = 1 THEN 1 ELSE 0 END, 0
) as put_10k_free_tier_subtracted*/
,(sum(bytes_sent)) as bytes_sent
,ceil(sum(bytes_sent)/1024/1024/1024) as bytes_sent_GB
/*,greatest(ceil(sum(bytes_sent)/1024/1024/1024) - CASE WHEN bucket_info.placement_id = 1 THEN 100 ELSE 0 END, 0
) as bytes_sent_GB_free_tier_subtracted */
,case
when bucket_info.placement_id = 1 then 'paas.s3.ceph'
when bucket_info.placement_id = 2 then 'paas.s3.cphc'
else '['|| TO_CHAR(bucket_info.placement_id,'FM9') ||']' end as code --*** зачем делать там ключ bigint
,bucket_info.placement_id
--category.name, bucket_info.name
,'WZ'||lpad(user_info.name, 5, '0') as wz
,usage_bucket_by_user.time::date as dt
,bucket_info.name || ' - ' || case
when bucket_info.placement_id = 1 then 'paas.s3.ceph'
when bucket_info.placement_id = 2 then 'paas.s3.cphc'
else '['|| TO_CHAR(bucket_info.placement_id,'FM9') ||']' end
as dimension_1
,null as dimension_2
from s3billing.usage_bucket_by_user
join s3billing.user_info on usage_bucket_by_user.user_id=user_info.id
join s3billing.category on usage_bucket_by_user.category_id=category.id
join s3billing.bucket_info on usage_bucket_by_user.bucketid=bucket_info.id
join s3billing.placement on bucket_info.placement_id=placement.id
where 1=1
AND user_info.name = <cfqueryparam cfsqltype="cf_sql_varchar" value=#ATTRIBUTES.tenant_wz_index#/>
AND usage_bucket_by_user.time::date >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_start#/>
AND usage_bucket_by_user.time::date <= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_finish#/>
group by user_info.name, bucket_info.placement_id, bucket_info.name, usage_bucket_by_user.time::date
order by user_info.name, bucket_info.placement_id, bucket_info.name, usage_bucket_by_user.time::date;
</cfquery>
<!--- здесь метрики уже сгруппированы по дням. Умножать на цену еще рано, надо сильно позже --->
<cfif (ATTRIBUTES.debug)>
qS3OpsTrf
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qS3OpsTrf#/>
<cfflush/>
</cfif>
<!--- собрали все метрики в один резалтсет --->
<!--- Здесь мы знаем единицы измерения, причем надежнее, чем в спецификации --->
<cfquery name="qUnifiedMetric" dbType="query">
select dt, wz, code + '.vcpu-m' as code, core_h as raw_metric, core_h as metric,
dimension_1, 'vdc' as dimension_1_name, dimension_2, NULL as dimension_2_name, 'ядро-ч' as unit
from qComputing
union all
select dt, wz, code + '.ram-m' as code, gb_h as raw_metric, gb_h as metric,
dimension_1, 'vdc' as dimension_1_name, dimension_2, NULL as dimension_2_name, 'ГБайт-ч' as unit
from qComputing
union all
select dt, wz, code, gb_m_used as raw_metric, gb_m_used as metric,
dimension_1, 'vdc' as dimension_1_name, dimension_2, 'storage policy' as dimension_2_name, 'ГБайт-м' as unit
from qStorage
union all
select dt, wz, code + '.vol-m', vol_b as raw_metric, vol_gb as metric,
dimension_1, 'bucket' as dimension_1_name, dimension_2, NULL as dimension_2_name, 'ГБайт-м' as unit
from qS3Vol
union all
select dt, wz, code + '.get-m', get as raw_metric, get_10k as metric,
dimension_1, 'bucket' as dimension_1_name, dimension_2, NULL as dimension_2_name, '10тыс.шт.' as unit
from qS3OpsTrf
union all
select dt, wz, code + '.put-m', put as raw_metric, put_10k as metric,
dimension_1, 'bucket' as dimension_1_name, dimension_2, NULL as dimension_2_name, '10тыс.шт.' as unit
from qS3OpsTrf
union all
select dt, wz, code + '.trf-m', bytes_sent as raw_metric, bytes_sent_gb as metric,
dimension_1, 'bucket' as dimension_1_name, dimension_2, NULL as dimension_2_name, 'ГБайт' as unit
from qS3OpsTrf
order by dt, code, dimension_1, dimension_2 <!--- /*dimension_*_name нужно только в качестве заголовка *** потом оптимизировать */ --->
</cfquery>
<!--- clean extra spaces to work around QoQ bug --->
<cfloop query="qUnifiedMetric">
<cfset qUnifiedMetric.dimension_1_name=trim(qUnifiedMetric.dimension_1_name)/>
<cfset qUnifiedMetric.dimension_2_name=trim(qUnifiedMetric.dimension_2_name)/>
<!--- *** возможно, и с другими полями придется сделать то же самое --->
</cfloop>
<!--- This is a known, long-standing quirk in ColdFusion's Query of Queries (QoQ) engine.When you perform a UNION ALL across queries where a character column has a dynamic or varying string length (like your concatenated m.dimension_1_name + 'x'), QoQ identifies the longest string in that column across all result sets. It then treats the column like a fixed-length CHAR database field and pads all shorter values with trailing spaces to match that maximum length. --->
<!--- лучше никогда не связываться с QoQ, но если все исполнять в исходной бд, это скорее мегаселект с большим числом CTE --->
<!--- надо было сразу делать в БД. Причем сводить в одну, чтобы избежать нескольких источников данных... можно было бы и так добиться, но все равно сводим в буферную базу --->
<!--- <cfdump var=#qUnifiedMetric#/> --->
<cfif (ATTRIBUTES.debug)>
qUnifiedMetric
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qUnifiedMetric#/>
<cfflush/>
</cfif>
<!--- умножаем на цену, вычисляем дисконт (довольно поздно) и форматируем
*** здесь нужно учесть версионность. Самым простым кажется декомпозировать по дням и снова собрать
*** только как нам это сделать без нормального SQL движка
Может быть, процедурная обработка не будет выглядеть ужасно, и не хуже SQL
В конце концов, можно просто В ЦИКЛЕ обсчитать диапазон по дням, выполнив запрос 30 раз, или сколько надо
Если будет тупить - оптимизируем
--->
<!--- Для декомпозиции на более мелкие объекты мы можем как полумеру - профильтровать по конкретному бакету или ВМ-ВДЦ
Что не нравится - для ВМ у нас может быть целое дерево нижележащих объектов вместо линейного списка бакетов --->
<!--- Вычитание Free Tier можно начинать с начала месяца - для этого можно сгруппировать по месяцам --->
<!--- Кажется, здесь еще рано агрегировать, если мы хотим OUTER JOIN --->
<!--- агрегировать вообще не надо в этом компоненте, а стоимость вычислять после агрегации --->
<cfquery name="qChargeDaily" dbType="query">
SELECT
<d:field_set titleMapOut="titleMap" lengthOut="fieldCount">
<d:field title="Код компонента">s.code as s_code</d:field>
<d:field title="Код метрики">m.code as m_code</d:field>
<d:field title="Код услуги">s.service_code as service_code</d:field>
<d:field title="Услуга">s.svc</d:field>
<d:field title="Компонент">s.component</d:field>
<d:field title="Ед.изм.">m.unit</d:field>
<d:field title="Цена &##8381; с НДС" cfSqlType="CF_SQL_NUMERIC">
s.price*(100-s.discount)/100 as discounted_price
</d:field>
<d:field title="Цена с НДС" cfSqlType="CF_SQL_NUMERIC">s.price</d:field>
<d:field title="Скидка%" cfSqlType="CF_SQL_NUMERIC">s.discount</d:field>
<d:field title="Сырая метрика" cfSqlType="CF_SQL_NUMERIC">m.raw_metric</d:field>
<d:field title="Приведенная метрика" cfSqlType="CF_SQL_NUMERIC">m.metric</d:field>
<d:field title="serviceStart" cfSqlType="CF_SQL_TIMESTAMP">s.serviceStart</d:field>
<d:field title="serviceEnd" cfSqlType="CF_SQL_TIMESTAMP">s.serviceEnd</d:field>
<d:field title="serviceHash">s.serviceHash</d:field>
<d:field title="serviceCustomName">s.serviceCustomName</d:field>
<d:field title="dt" cfSqlType="CF_SQL_TIMESTAMP">m.dt</d:field>
<d:field title="free tier" cfSqlType="CF_SQL_NUMERIC">s.free_tier</d:field>
<d:field>m.dimension_1</d:field>
<d:field>m.dimension_1_name</d:field>
<d:field>m.dimension_2</d:field>
<d:field>m.dimension_2_name</d:field>
</d:field_set>
FROM qSpec as s, qUnifiedMetric as m
WHERE s.code=m.code
AND s.serviceStart <= m.dt
AND (m.dt <= s.serviceEnd OR s.serviceEnd IS NULL)
<cfif #attributes.restrict_to_own_instances#>
AND m.wz=<cfqueryparam cfsqltype="cf_sql_varchar" value=#request.auth.wz#/>
</cfif>
<!--- таким замысловатым способом мы реализуем FULL JOIN --->
UNION ALL
SELECT NULL, m.code, NULL, NULL, NULL, NULL, 0,
NULL, NULL, m.raw_metric, m.metric, NULL, NULL, NULL, NULL, m.dt, 0, m.dimension_1, m.dimension_1_name, m.dimension_2, m.dimension_2_name
FROM qUnifiedMetric as m
WHERE m.code NOT IN (<cfqueryparam value="#ValueList(qSpec.code)#" cfsqltype="cf_sql_varchar" list="true">)
UNION ALL
SELECT s.code, NULL, s.service_code, s.svc, s.component, s.unit, s.price*(100-s.discount)/100 as discounted_price,
s.price, s.discount, NULL, NULL, s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, NULL, 0, NULL, NULL, NULL, NULL
FROM qSpec as s
WHERE s.code NOT IN (<cfqueryparam value="#listRemoveDuplicates(ValueList(qUnifiedMetric.code))#" cfsqltype="cf_sql_varchar" list="true">)
ORDER by dt, s_code, m_code, dimension_1, dimension_2
</cfquery>
<!--- диагностика ошибок в QoQ крайне невнятна и вводит в заблуждение --->
<!---<cfdump var=#qChargeDaily# abort=true/> --->
<cfif (ATTRIBUTES.debug)>
qChargeDaily
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qChargeDaily#/>
<cfflush/>
</cfif>
<!--- Из-за детализации придется вынести вычитание free_tier на более поздний этап --->
<!--- Здесь агрегировано за месяц, но детализировано по измерениям --->
<cfquery name="qChargeMonthly" dbType="query">
SELECT
<d:field_set titleMapOut="titleMap" lengthOut="fieldCount">
<d:field title="Артикул по договору">s_code</d:field>
<d:field title="Артикул по системам">m_code</d:field>
<d:field title="Артикул">s_code as code</d:field>
<d:field title="Код услуги">service_code</d:field>
<d:field title="Услуга по каталогу">svc</d:field>
<d:field title="serviceCustomName">serviceCustomName</d:field>
<d:field title="Компонент">component</d:field>
<d:field title="Ед.изм.">unit</d:field>
<d:field title="Цена &##8381; с НДС" cfSqlType="CF_SQL_NUMERIC">discounted_price</d:field>
<d:field title="Сырая метрика" cfSqlType="CF_SQL_NUMERIC">sum(raw_metric) as raw_metric</d:field>
<d:field title="Приведенный объем" cfSqlType="CF_SQL_NUMERIC">sum(metric) as metric</d:field>
<d:field title="serviceStart" cfSqlType="CF_SQL_TIMESTAMP">serviceStart</d:field>
<d:field title="serviceEnd" cfSqlType="CF_SQL_TIMESTAMP">serviceEnd</d:field>
<d:field title="dt_billed_from" cfSqlType="CF_SQL_TIMESTAMP">min(dt) as dt_billed_from</d:field>
<d:field title="dt_billed_to" cfSqlType="CF_SQL_TIMESTAMP">max(dt) as dt_billed_to</d:field>
<d:field title="serviceHash">serviceHash</d:field>
<d:field title="free tier" cfSqlType="CF_SQL_NUMERIC">free_tier</d:field>
<d:field>dimension_1</d:field>
<d:field>dimension_1_name</d:field>
<d:field>dimension_2</d:field>
<d:field>dimension_2_name</d:field>
<d:field title="Год" cfSqlType="CF_SQL_INTEGER">year(dt) as year</d:field>
<d:field title="Месяц" cfSqlType="CF_SQL_INTEGER">month(dt) as month</d:field>
</d:field_set>
from qChargeDaily
group by s_code, m_code, svc, service_code, serviceCustomName, component, unit, discounted_price, serviceStart, ServiceEnd, serviceHash, free_tier, year(dt), month(dt),
dimension_1_name, dimension_1, dimension_2_name, dimension_2
order by year(dt), month(dt), m_code, code, serviceStart, dimension_1, dimension_2
</cfquery>
<!--- в связи с бедностью языка QoQ (нет CASE) грубо патчим резалтсет --->
<cfloop query="qChargeMonthly">
<cfset qChargeMonthly.code = (len(code) GT 0) ? code : m_code/><!--- имя в левой части должно быть квалифицировано запросом (резалтсетом) --->
</cfloop>
<!--- тут могут быть подводные камни с копированием и переиспользованием объектов --->
<!--- <cfif isQuery(qTotal)>
<cfquery name="qTotalNew" dbType="query">
select * from qTotal
union all
select * from qCharge
</cfquery>
<cfset qTotal = qTotalNew/>
<cfelse>
<cfset qTotal = qCharge/>
</cfif>
<cfdump var=#qTotal#/> --->
<!--- <cfdump var=#qChargeMonthly#/> --->
<!--- *** проверить краевые даты --->
<cfif (ATTRIBUTES.debug)>
qChargeMonthly
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qChargeMonthly#/>
<cfflush/>
</cfif>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<!--- <cfflush/> --->
<cfset var report={}/>
<cfset report.qComputingAge = qComputingAge/>
<cfset report.qStorageAge = qStorageAge/>
<cfset report.qS3VolAge = qS3VolAge/>
<cfset report.qS3OpsTrfAge = qS3OpsTrfAge/>
<cfset report.qSpecification = qSpecification/>
<cfset report.qCharge = qChargeMonthly/>
<cfset report.qFreeTier = qFreeTier/><!--- мы не применяем скидку на уровне детализации, а только после агрегации. На самом деле правильно было бы агрегировать натуральные метрики, а умножать на цену уже агрегаты --->
<!--- <cfset report.chargeTitleMap = titleMap/>
<cfset report.chargeFieldCount = fieldCount/> --->
<cfset "CALLER.#ATTRIBUTES.output#" = report/>
<cfexit method="exittag"/>