从 SELECT 生成 WITH clause/UNIONs

Generate a WITH clause/UNIONs from a SELECT

我想生成一个 WITH clause/UNIONs — 目的是轻松共享 small 数据样本(10- 20 行)。

我想在不创建 table 或插入行的情况下执行此操作。


示例:

进行 table 或这样的查询:

...并生成这个:

with cte as(
select 10 as asset_id, 1 as vertex_num, 118.56 as x, 3.8 as y from dual
union all
select 10 as asset_id, 2 as vertex_num, 118.62 as x, 1.03 as y from dual
union all
select 10 as asset_id, 3 as vertex_num, 121.93 as x, 1.03 as y from dual)

 --There are lots more rows. But it's too much work to write them all out.
   
select * from cte

使用 SQL,如何从结果集中自动生成 WITH clause/UNIONs?

您可以使用 dbms_sql to execute a query against your real table, interrogate the data types,并使用该信息生成 CTE 及其内部查询。

作为第一次尝试:

create or replace procedure print_cte (p_statement varchar2) as
  -- dbms_sql variables
  l_c pls_integer;
  l_col_cnt pls_integer;
  l_rows pls_integer;
  l_desc_t dbms_sql.desc_tab;

  l_first_row boolean := true;
  l_varchar2 varchar2(4000);
  l_number number;
  l_date date;
  -- etc.
begin
  -- ideally add some checks for p_statement being a sinple query

  l_c := dbms_sql.open_cursor;
  dbms_sql.parse(c => l_c, statement => p_statement, language_flag => dbms_sql.native);
  l_rows := dbms_sql.execute(c => l_c);

  dbms_sql.describe_columns(c => l_c, col_cnt => l_col_cnt, desc_t => l_desc_t);

  -- define columns, and output CTE columns at the same time
  dbms_output.put('with cte (');

  for i in 1..l_col_cnt loop
    case l_desc_t(i).col_type
      when 1 then
        dbms_sql.define_column(c => l_c, position=> i, column => l_varchar2, column_size => 4000);
      when 2 then
        dbms_sql.define_column(c => l_c, position=> i, column => l_number);
      when 12 then
        dbms_sql.define_column(c => l_c, position=> i, column => l_date);
      -- etc. plus else to skip or throw error for anything not handled
    end case;

    if i > 1 then
      dbms_output.put(', ');
    end if;
    dbms_output.put('"' || l_desc_t(i).col_name || '"');
  end loop;

  dbms_output.put(') as (');

  while dbms_sql.fetch_rows(c => l_c) > 0 loop
    if (l_first_row) then
      l_first_row := false;
    else
      dbms_output.put('  union all');
    end if;
    dbms_output.new_line;
 
    for i in 1..l_col_cnt loop
      if i = 1 then
        dbms_output.put('  select ');
      else
        dbms_output.put(', ');
      end if;
 
      case l_desc_t(i).col_type
        when 1 then
          dbms_sql.column_value(c => l_c, position => i, value => l_varchar2);
          dbms_output.put(q'[']' || l_varchar2 || q'[']');
        when 2 then
          dbms_sql.column_value(c => l_c, position => i, value => l_number);
          dbms_output.put(l_number);
        when 12 then
          dbms_sql.column_value(c => l_c, position => i, value => l_date);
          dbms_output.put(q'[to_date(']'
            || to_char(l_date, 'SYYYY-MM-DD-HH24:MI:SS')
            || q'[', 'SYYYY-MM-DD HH24:MI:SS')]');
        -- etc. plus else to skip or throw error for anything not handled
     end case;
    end loop;

    dbms_output.put(' from dual');
    dbms_output.new_line;
  end loop;

  dbms_output.put_line(')');
  dbms_output.put_line('select * from cte;');
  
  dbms_sql.close_cursor(c => l_c);
end print_cte;
/

然后你可以做:

begin
  print_cte('select * from your_table');
end;
/

产生:

with cte ("ASSET_ID", "VERTEX_NUM", "X", "Y") as (
  select 10, 1, 118.56, 3.8 from dual
  union all
  select 10, 2, 118.62, 1.03 from dual
  union all
  select 10, 3, 121.93, 1.03 from dual
)
select * from cte;

当然,您的客户端必须配置为处理 dbms_output。

如内联注释中所述,您应该检查 passed-in 语句不会做一些令人讨厌的事情;并且您需要添加对其他数据类型的处理。这只是一个起点。

db<>fiddle

xmltable 或(json_table for Oracle 12+)用于此类目的会更容易。

示例xmltable

  1. 只需将所有需要的数据汇总到 xmltype: 你可以使用 xmltype(cursor(select...from...)):
select xmltype(cursor(select * from test)) xml from dual;

dbms_xmlgen.getxmltype(query_string):

select dbms_xmlgen.getxmltype('select * from test') xml from dual;
  1. 然后您可以使用返回的 XML 和
xmltable('/ROWSET/ROW' passing xmltype(your_xml) columns ...)

示例:

select *
from xmltable(
        '/ROWSET/ROW'
        passing xmltype(q'[<?xml version="1.0"?>
<ROWSET>
 <ROW>
  <ASSET_ID>10</ASSET_ID>
  <VERTEX_NUM>1</VERTEX_NUM>
  <X>118.56</X>
  <Y>3.8</Y>
 </ROW>
 <ROW>
  <ASSET_ID>10</ASSET_ID>
  <VERTEX_NUM>2</VERTEX_NUM>
  <X>118.62</X>
  <Y>1.03</Y>
 </ROW>
</ROWSET>
        ]')
    columns 
        asset_id,vertex_num,x,y
) test

DBFiddle: https://dbfiddle.uk/?rdbms=oracle_11.2&fiddle=036b718f2b18df898c3e3de722c97378

上的完整示例

我知道我的回答不纯粹 SQL 或 PLSQL。

但是我建议你使用Javascript生成CTE查询,因为数据量很小。

Javascript 比 SQL 或 PLSQL 更容易维护。

您可以随时在这里使用这个小脚本。 (不需要额外的编辑器) https://jsfiddle.net/pLvgr8oh/

或者,如果您担心安全问题,您可以 运行 使用 Chrome 浏览器编写脚本。 https://developer.chrome.com/docs/devtools/javascript/snippets/

function convertToSelect(tsv, firstRowIsColumn, columnsComma, typesComma) {
  function getCol(column, value, type) {
    // In case type is 'date', after colon is date format
    const [ typeNew, dateFormat ] = type.split(':')

    switch (typeNew) {
      case 'string': return `'${value}' as ${column}`
      case 'number': return `${value} as ${column}`
      case 'date': return `to_date('${value}', '${dateFormat}') as ${column}`
    }
  }

  const columns = columnsComma ? columnsComma.split(',') : []
  const types = typesComma ? typesComma.split(',') : []

  // Split row by '\r\n' or '\n'
  const list = tsv.split(/\r*\n/)
  const colCount = list[0].split(/\t/).length

  let columnsNew = []
  let typesNew = types

  // If first row is column name
  if (firstRowIsColumn) {
    columnsNew = list[0].split(/\t/);
    list.shift(0)
  }

  // If column name is specified then override column names in first row
  if (columns.length > 0) {
    columnsNew = columns
  }

  // If type is not specified set all type to 'string'
  if (typesNew.length === 0) {
    typesNew = [...Array(colCount)].map(t => 'string')
  }

  const rows = list.map(item => {
    // [ '2 as F_INT', '2.223 as F_LONG'... ]
    const cols = item
    .split(/\t/)
    .map((value, index) => {
      return getCol(columnsNew[index], value, typesNew[index])
    })
    .join(', ')

    // select 2 as F_INT, 2.223 as F_LONG... from dual
    return `    select ${cols} from dual`
  })

  const selectUnion = rows.join(`
union all
`)
  return `with cte as
(
${selectUnion}
)
select * from cte;
`
}

const tsv = `F_INT  F_LONG  F_VARCHAR   F_DATE
1   1.123   a   2022-12-01
2   2.223   b   2022-12-02
3   3.323   c   2022-12-03`
const firstRowIsColumn = true
const columnsComma = 'v_num,v_lng,v_str,v_date'
//const columnsComma = ''
const typesComma = 'number,number,string,date:YYYY-MM-DD'
//const typesComma = ''
const ret = convertToSelect(tsv, firstRowIsColumn, columnsComma, typesComma)
console.log(ret)

生成(如果代码段没有破坏制表符):

with cte as
(
    select 1 as v_num, 1.123 as v_lng, 'a' as v_str, to_date('2022-12-01', 'YYYY-MM-DD') as v_date from dual
union all
    select 2 as v_num, 2.223 as v_lng, 'b' as v_str, to_date('2022-12-02', 'YYYY-MM-DD') as v_date from dual
union all
    select 3 as v_num, 3.323 as v_lng, 'c' as v_str, to_date('2022-12-03', 'YYYY-MM-DD') as v_date from dual
)
select * from cte;