具有聚合函数的 Oracle Dynamic SQL 过程通过 ORDS 调用需要很长时间,但在 SQL Developer 中运行速度很快

Oracle Dynamic SQL Procedure with Aggregate Function takes long time to call via ORDS, but runs quickly in SQL Developer

我一直在努力改进我的程序,所以我开始将它们从使用 || 字符串连接更改为使用绑定变量。当我将绑定变量添加到以下过程时,我发现我需要将聚合函数添加到 where 子句,以便搜索数字在逗号分隔的数字列表中的位置。

添加聚合函数 string_to_table_numstring_to_table_varchar2 并更新过程的其他部分以使用绑定变量后,我尝试向我的 ORDS 端点发出请求。该请求用了 10 多分钟才完成。我认为这是由于聚合函数和 APEX 的组合造成的,因为原始查询 运行s 不到 500 毫秒。为了提高速度,我尝试了几种不同的功能,但我没有任何运气。

如果您有建议,我也会采纳有关如何改进整体查询的建议。我对 Oracle 比较陌生。

ORDS 端点

BEGIN
  get_categoryprods2(:commasepproductids, :commasepproductskus, :pcategoryid, :sortby);
END;

参数

commasepproductids IN STRING from URI
commasepproductskus IN STRING from URI
pcategoryid IN STRING from URI
sortby IN STRING from URI
ptopcount in INT

原始查询

create or replace PROCEDURE get_categoryprods (
    commasepproductids     IN   VARCHAR2,
    commasepproductskus     in  varchar2,
    pcategoryid   IN   NUMBER,
    sortby        IN   VARCHAR2
) AS

    l_cursor               SYS_REFCURSOR;
    v_stmt_str             STRING(5000);
    s_counter              NUMBER;
    v_productid            productnew.productid%TYPE;
    v_manufacturerid       productmanufacturer.manufacturerid%TYPE;
    v_sename               productnew.sename%TYPE;
    v_name                 productnew.name%TYPE;
    v_summary              varchar(3999);
    v_freeground           productnew.freeground%TYPE;
    v_quantitydiscountid   productnew.quantitydiscountid%TYPE;
    v_sku                  productnew.sku%TYPE;
    v_price                productnew.price%TYPE;
    v_msrp                 productnew.msrp%TYPE;
    v_cost                 productnew.cost%TYPE;
    v_saleprice            productnew.saleprice%TYPE;
    v_weight               productnew.weight%TYPE;
    v_percase              productnew.percase%TYPE;
    v_relatedproducts      varchar(3999);
    v_hidepriceuntilcart   productnew.hidepriceuntilcart%TYPE;
    v_discontinued         productnew.discontinued%TYPE;
    v_discounttype         productnew.discounttype%TYPE;
    v_unitofmeasure        productnew.unitofmeasure%TYPE;
    v_replacement          productnew.replacement%TYPE;
    v_displayorder         productcategory.displayorder%TYPE;
    v_friendlyurl          urlmapper.friendlyurl%TYPE;
    v_sortby               varchar2(300);
    
BEGIN

    IF sortby IS NULL OR sortby = 'null' OR sortby = '' THEN
        v_sortby := '"p".Discontinued, "pc".DisplayOrder ';
    ELSIF sortby = 'PriceAscending' THEN
        v_sortby := '"p".discontinued, "p".price ';
    ELSIF sortby = 'PriceDescending' THEN
        v_sortby := '"p".discontinued, "p".price DESC ';
    ELSIF sortby = 'Name' THEN
        v_sortby := '"p".discontinued, "p".name ';
    ELSE
        v_sortby := '"p".discontinued, "pc".displayorder ';
    END IF;
        v_stmt_str := 'SELECT
                            "p".productid,
                            "pm".manufacturerid,
                            "p".sename,
                            "p".name,
                            to_char(substr("p".summary, 0, 3999)),
                            TO_NUMBER("p".freeground),
                            "p".quantitydiscountid,
                            "p".sku,
                            "p".price,
                            "p".msrp,
                            "p".cost,
                            "p".saleprice,
                            "p".weight,
                            "p".percase,
                            to_char(substr("p".relatedproducts, 0, 3999)),
                            "p".hidepriceuntilcart,
                            "p".discontinued,
                            "p".discounttype,
                            "p".unitofmeasure,
                            "p".replacement,
                            "pc".displayorder,
                            "um".friendlyurl
                        FROM
                            productnew            "p"
                            LEFT OUTER JOIN productcategory       "pc" ON "p".productid = "pc".productid
                            LEFT OUTER JOIN productmanufacturer   "pm" ON "p".productid = "pm".productid
                            LEFT OUTER JOIN urlmapper             "um" ON "p".productid = "um".productid
                        WHERE
                            ("pc".categoryid = ' || pcategoryid || ')
                        AND "p".productid in ('||commasepproductids||')
                                --and ("p".productid in ('||commasepproductids||') -- removed because I broke the actual original proc
                        AND ( "p".deleted = 0 )
                            AND "p".published = 1 ORDER BY '||v_sortby;
  
    s_counter := 0;
    apex_json.open_array;
    OPEN l_cursor FOR v_stmt_str;                   

        FETCH l_cursor into
            v_productid,
            v_manufacturerid,
            v_sename,
            v_name,
            v_summary,
            v_freeground,
            v_quantitydiscountid,
            v_sku,
            v_price,
            v_msrp,
            v_cost,
            v_saleprice,
            v_weight,
            v_percase,
            v_relatedproducts,
            v_hidepriceuntilcart,
            v_discontinued,
            v_discounttype,
            v_unitofmeasure,
            v_replacement,
            v_displayorder,
            v_friendlyurl;
      
            loop
             EXIT WHEN l_cursor%notfound;
                s_counter := 1;
                apex_json.open_object;
                apex_json.write('ProductID', v_productid);
                apex_json.write('ManufacturerID', v_manufacturerid);
                apex_json.write('SEName', v_sename);
                apex_json.write('Name', v_name);
                apex_json.write('Summary', v_summary);
                apex_json.write('FreeGround', v_freeground);
                apex_json.write('QuantityDiscountID', v_quantitydiscountid);
                apex_json.write('SKU', v_sku);
                apex_json.write('Price', v_price);
                apex_json.write('MSRP', v_msrp);
                apex_json.write('Cost', v_cost);
                apex_json.write('SalePrice', v_saleprice);
                apex_json.write('Weight', v_weight);
                apex_json.write('PerCase', v_percase);
                apex_json.write('RelatedProducts', v_relatedproducts);
                apex_json.write('HidePriceUntilCart', v_hidepriceuntilcart);
                apex_json.write('Discontinued', v_discontinued);
                apex_json.write('QuantityDiscountType', v_discounttype);
                apex_json.write('UnitOfMeasure', v_unitofmeasure);
                apex_json.write('Replacement', v_replacement);
                apex_json.write('DisplayOrder', v_displayorder);
                apex_json.write('FriendlyUrl', v_friendlyurl);
                apex_json.close_object;
        end loop;

    IF s_counter = 0 THEN
        apex_json.open_object;
        apex_json.write('ProductID', 0);
        apex_json.write('SEName', 'NOT FOUND');
        apex_json.write('Name', 'NOT FOUND');
        apex_json.write('Summary', 'NOT FOUND');
        apex_json.write('FreeGround', 'NOT FOUND');
        apex_json.write('QuantityDiscountID', 0);
        apex_json.write('SKU', 'NOT FOUND');
        apex_json.write('Price', 0);
        apex_json.write('MSRP', 0);
        apex_json.write('Cost', 0);
        apex_json.write('SalePrice', 0);
        apex_json.write('Weight', 0);
        apex_json.write('PerCase', 'NOT FOUND');
        apex_json.write('RelatedProducts', 'NOT FOUND');
        apex_json.write('HidePriceUntilCart', 'NOT FOUND');
        apex_json.write('Discontinued', 'NOT FOUND');
        apex_json.write('QuantityDiscountType', 'NOT FOUND');
        apex_json.write('UnitOfMeasure', 'NOT FOUND');
        apex_json.write('Replacement', 'NOT FOUND');
        apex_json.write('FriendlyUrl', 'NOT FOUND');
        apex_json.close_object;
    END IF;

    apex_json.close_all;
END get_categoryprods;

**新查询 1 **

create or replace PROCEDURE GET_CATEGORYPRODS2 
(
  COMMASEPPRODUCTIDS IN VARCHAR2 
, COMMASEPPRODUCTSKUS IN VARCHAR2 
, PCATEGORYID IN NUMBER 
, SORTBY IN VARCHAR2 
, PTOPCOUNT IN VARCHAR2 
) AS

    l_cursor               SYS_REFCURSOR;
    v_stmt_str             STRING(5000);
    s_counter              NUMBER;
    v_productid            productnew.productid%TYPE;
    v_manufacturerid       productmanufacturer.manufacturerid%TYPE;
    v_sename               productnew.sename%TYPE;
    v_name                 productnew.name%TYPE;
    v_summary              varchar(3999);
    v_freeground           productnew.freeground%TYPE;
    v_quantitydiscountid   productnew.quantitydiscountid%TYPE;
    v_sku                  productnew.sku%TYPE;
    v_price                productnew.price%TYPE;
    v_msrp                 productnew.msrp%TYPE;
    v_cost                 productnew.cost%TYPE;
    v_saleprice            productnew.saleprice%TYPE;
    v_weight               productnew.weight%TYPE;
    v_percase              productnew.percase%TYPE;
    v_relatedproducts      varchar(3999);
    v_hidepriceuntilcart   productnew.hidepriceuntilcart%TYPE;
    v_discontinued         productnew.discontinued%TYPE;
    v_discounttype         productnew.discounttype%TYPE;
    v_unitofmeasure        productnew.unitofmeasure%TYPE;
    v_replacement          productnew.replacement%TYPE;
    v_displayorder         productcategory.displayorder%TYPE;
    v_friendlyurl          urlmapper.friendlyurl%TYPE;
    v_sortby               varchar2(300);
    type t_categoryprods is table of categoryprod_typ;
    l_catprodrow            categoryprod_typ;
    l_catprods             t_categoryprods;
    
BEGIN
    IF sortby IS NULL OR sortby = 'null' OR sortby = '' THEN
        v_sortby := '"p".Discontinued, "pc".DisplayOrder ';
    ELSIF sortby = 'PriceAscending' THEN
        v_sortby := '"p".discontinued, "p".price ';
    ELSIF sortby = 'PriceDescending' THEN
        v_sortby := '"p".discontinued, "p".price DESC ';
    ELSIF sortby = 'Name' THEN
        v_sortby := '"p".discontinued, "p".name ';
    ELSE
        v_sortby := '"p".discontinued, "pc".displayorder ';
    END IF;
        v_stmt_str := 'SELECT
"p".productid,
"pm".manufacturerid,
"p".sename,
"p".name,
to_char(substr("p".summary, 0, 3999)),
to_number("p".freeground),
"p".quantitydiscountid,
"p".sku,
"p".price,
"p".msrp,
"p".cost,
"p".saleprice,
"p".weight,
"p".percase,
to_char(substr("p".relatedproducts, 0, 3999)),
"p".hidepriceuntilcart,
"p".discontinued,
"p".discounttype,
"p".unitofmeasure,
"p".replacement,
"pc".displayorder,
"um".friendlyurl
FROM
productnew "p"
LEFT OUTER JOIN productcategory "pc" ON "p".productid = "pc".productid
LEFT OUTER JOIN productmanufacturer "pm" ON "p".productid = "pm"
.productid
LEFT OUTER JOIN urlmapper "um" ON "p".productid = "um".productid
WHERE
"pc".categoryid = :pcategoryid
AND ( ( "p".productid IN (SELECT * FROM TABLE (string_to_table_num(:commasepproductids))) OR :commasepproductids IS NULL)
AND (lower("p".sku) IN (SELECT * FROM TABLE(string_to_table_varchar2(:commasepproductskus))) OR :commasepproductskus IS NULL ) )
AND  "p".deleted = 0 
AND "p".published = 1
ORDER BY :sortby';
     s_counter := 0;
    apex_json.open_array;
    OPEN l_cursor FOR v_stmt_str USING pcategoryid, commasepproductids, commasepproductids, commasepproductskus, commasepproductskus, v_sortby;
    
        FETCH l_cursor into
            v_productid,
            v_manufacturerid,
            v_sename,
            v_name,
            v_summary,
            v_freeground,
            v_quantitydiscountid,
            v_sku,
            v_price,
            v_msrp,
            v_cost,
            v_saleprice,
            v_weight,
            v_percase,
            v_relatedproducts,
            v_hidepriceuntilcart,
            v_discontinued,
            v_discounttype,
            v_unitofmeasure,
            v_replacement,
            v_displayorder,
            v_friendlyurl;
        loop
             EXIT WHEN l_cursor%notfound;
                s_counter := 1;
                apex_json.open_object;
                apex_json.write('ProductID', v_productid);
                apex_json.write('ManufacturerID', v_manufacturerid);
                apex_json.write('SEName', v_sename);
                apex_json.write('Name', v_name);
                apex_json.write('Summary', v_summary);
                apex_json.write('FreeGround', v_freeground);
                apex_json.write('QuantityDiscountID', v_quantitydiscountid);
                apex_json.write('SKU', v_sku);
                apex_json.write('Price', v_price);
                apex_json.write('MSRP', v_msrp);
                apex_json.write('Cost', v_cost);
                apex_json.write('SalePrice', v_saleprice);
                apex_json.write('Weight', v_weight);
                apex_json.write('PerCase', v_percase);
                apex_json.write('RelatedProducts', v_relatedproducts);
                apex_json.write('HidePriceUntilCart', v_hidepriceuntilcart);
                apex_json.write('Discontinued', v_discontinued);
                apex_json.write('QuantityDiscountType', v_discounttype);
                apex_json.write('UnitOfMeasure', v_unitofmeasure);
                apex_json.write('Replacement', v_replacement);
                apex_json.write('DisplayOrder', v_displayorder);
                apex_json.write('FriendlyUrl', v_friendlyurl);
                apex_json.close_object;
        end loop;
        
        IF s_counter = 0 THEN
        apex_json.open_object;
        apex_json.write('ProductID', 0);
        apex_json.write('SEName', 'NOT FOUND');
        apex_json.write('Name', 'NOT FOUND');
        apex_json.write('Summary', 'NOT FOUND');
        apex_json.write('FreeGround', 'NOT FOUND');
        apex_json.write('QuantityDiscountID', 0);
        apex_json.write('SKU', 'NOT FOUND');
        apex_json.write('Price', 0);
        apex_json.write('MSRP', 0);
        apex_json.write('Cost', 0);
        apex_json.write('SalePrice', 0);
        apex_json.write('Weight', 0);
        apex_json.write('PerCase', 'NOT FOUND');
        apex_json.write('RelatedProducts', 'NOT FOUND');
        apex_json.write('HidePriceUntilCart', 'NOT FOUND');
        apex_json.write('Discontinued', 'NOT FOUND');
        apex_json.write('QuantityDiscountType', 'NOT FOUND');
        apex_json.write('UnitOfMeasure', 'NOT FOUND');
        apex_json.write('Replacement', 'NOT FOUND');
        apex_json.write('FriendlyUrl', 'NOT FOUND');
        apex_json.close_object;
    END IF;

    apex_json.close_all;
END GET_CATEGORYPRODS2;

string_to_table_num

create or replace FUNCTION string_to_table_num (
    p VARCHAR2
)
   RETURN tab_number
   PIPELINED IS
BEGIN
   FOR cc IN (SELECT rtrim(regexp_substr(str, '[^,]*,', 1, level), ',') res
                FROM (SELECT p || ',' str FROM dual)
              CONNECT BY level <= length(str) 
                                  - length(replace(str, ',', ''))) LOOP
      PIPE ROW(lower(cc.res));
   END LOOP;
    
END;

string_to_table_varchar

create or replace FUNCTION string_to_table_varchar2(p VARCHAR2)
   RETURN tab_varchar2
   PIPELINED IS
BEGIN
   FOR cc IN (SELECT rtrim(regexp_substr(str, '[^,]*,', 1, level), ',') res
                FROM (SELECT p || ',' str FROM dual)
              CONNECT BY level <= length(str) 
                                  - length(replace(str, ',', ''))) LOOP
      PIPE ROW(lower(cc.res));
   END LOOP;
END;

然后我尝试用以下函数替换 string_to_table 函数

f_convert2

create or replace FUNCTION f_convert2(p_list IN STRING)
RETURN tab_number
    PIPELINED
   AS
      l_string       LONG := p_list || ',';
      l_comma_index  PLS_INTEGER;
      l_index        PLS_INTEGER := 1;
    BEGIN
      LOOP
       l_comma_index := INSTR(l_string, ',', l_index);
       EXIT WHEN l_comma_index = 0;
       PIPE ROW ( SUBSTR(l_string, l_index, l_comma_index - l_index) );
       l_index := l_comma_index + 1;
     END LOOP;
     RETURN ;
   END f_convert2;

f_convert

create or replace FUNCTION f_convert(p_list IN STRING)
RETURN tab_varchar2
    PIPELINED
   AS
      l_string       LONG := p_list || ',';
      l_comma_index  PLS_INTEGER;
      l_index        PLS_INTEGER := 1;
    BEGIN
      LOOP
       l_comma_index := INSTR(l_string, ',', l_index);
       EXIT WHEN l_comma_index = 0;
       PIPE ROW ( SUBSTR(l_string, l_index, l_comma_index - l_index) );
       l_index := l_comma_index + 1;
     END LOOP;
     RETURN;
   END f_convert;

如果我从 Postman 或我的 Node 应用程序调用该过程,它会超时或给出 Please check the SQL statement is correctly formed and executes without error. SQL Error Code: ORA-04036: PGA memory used by the instance exceeds PGA_AGGREGATE_LIMIT。如果我通过 SQL 开发人员将 apex_json.write 替换为 dbms_output.write_line 和 运行,它 运行 很快就可以了。

考虑在使用 table 函数时使用 DYNAMIC_SAMPLING 提示。 Oracle 不知道函数 STRING_TO_TABLE_NUM 返回了多少行,并且可能会使用 8168 行的猜测。动态采样提示指示 Oracle 花费一些额外的解析时间来计算行数,这有望比更好的执行计划节省的时间少得多。

例如,使用这个类型和函数:

create or replace type tab_number as table of number;

create or replace FUNCTION string_to_table_num (
    p VARCHAR2
)
   RETURN tab_number
   PIPELINED IS
BEGIN
   FOR cc IN (SELECT rtrim(regexp_substr(str, '[^,]*,', 1, level), ',') res
                FROM (SELECT p || ',' str FROM dual)
              CONNECT BY level <= length(str) 
                                  - length(replace(str, ',', ''))) LOOP
      PIPE ROW(lower(cc.res));
   END LOOP;
    
END;
/

下面的简单示例显示 Oracle 将行数高估为 8168,而不是实际值 9:

explain plan for select * from table(string_to_table_num('1,2,3,4,5,6,7,8,9'));
select * from table(dbms_xplan.display);

Plan hash value: 127161297
 
---------------------------------------------------------------------------------------------------------
| Id  | Operation                         | Name                | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                  |                     |  8168 | 16336 |    29   (0)| 00:00:01 |
|   1 |  COLLECTION ITERATOR PICKLER FETCH| STRING_TO_TABLE_NUM |  8168 | 16336 |    29   (0)| 00:00:01 |
---------------------------------------------------------------------------------------------------------

当我们添加动态采样提示时,Oracle 得到完美的基数:

explain plan for select /*+ dynamic_sampling(2) */ * from table(string_to_table_num('1,2,3,4,5,6,7,8,9'));
select * from table(dbms_xplan.display);

Plan hash value: 127161297
 
---------------------------------------------------------------------------------------------------------
| Id  | Operation                         | Name                | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                  |                     |     9 |    18 |    11   (0)| 00:00:01 |
|   1 |  COLLECTION ITERATOR PICKLER FETCH| STRING_TO_TABLE_NUM |     9 |    18 |    11   (0)| 00:00:01 |
---------------------------------------------------------------------------------------------------------
 
Note
-----
   - dynamic statistics used: dynamic sampling (level=2)

这个解决方案没有直接解决为什么 SQL 在不同的上下文中表现不同的问题。但如果你能解决两个巨大的基数问题,其他问题可能就不再重要了。