select звёздочка
Jul. 4th, 2011 12:49 am![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Иной раз возникает потребность вывести куда-нибудь результаты запроса. Например, Могучая Программа обработала сто миллионов тыщ строк и надо показать журнал работы — несложный запрос типа
select status, count(*) from log group by status.
Однако это легко только в SQL*Plus. А в PL/SQL работать не будет: будьте любезны процедурно открыть курсор, в цикле прочитать данные и вывести их в соответствии_с. Да, и курсор за собой закрыть.
Беда, конечно, небольшая, можно и написать. Но ведь обидно каждый раз делать и отлаживать одно и то же, зная, что есть Простое Решение из декларативного мира.
Итак, ставим задачу: нужна процедура, принимающая произвольный запрос и печатающая результат этого запроса. На эту тему у Оракла есть пакет dbms_sql, и с его помощью вырисовывается такая последовательность действий:
- open_cursor
- parse
- execute
- describe_columns — можем узнать столбцы запроса и их типы данных
- для каждого столбца: define_column — некий аналог into
- fetch_rows — построчная выборка данных
- для каждого столбца: column_value — переносим прочитанные данные в переменную
- close_cursor
И на этом можно было бы остановиться, если бы не Злобные Раундтрипы. Вот если, скажем, надо сходить в магазин за хлебом, молоком и сосиской, пойдём ли мы три раза за каждым продуктом отдельно? Вряд ли. Зачем, если они влезают в одну авоську? А зачем же тогда ходить до базы данных отдельно за каждой строкой?
К счастью, dbms_sql умеет делать и bulk collect. Для этого надо вместо define_column сказать define_array, подсунуть ему коллекцию и сообщить размер выборки. Тогда fetch_rows будет читать данные не построчно, а кусками указанного размера.
Таким образом, выборка данных побеждена на 99%. Мелкие пакости, например тип данных long, идут в сад.
Правда, ещё остаётся довольно крупная пакость, связанная с выводом прочитанного в виде таблички à la SQL*Plus: невозможно заранее узнать ширину колонки. Например, у нас есть столбец типа date. Какой ширины выводить колонку, чтобы любые данные этого столбца гарантированно в ней уместились? Да, это можно попробовать вычислить, распарсив NLS_DATE_FORMAT. Но взять и просто спросить у базы — нельзя. А ведь типов много, и у каждого свои заморочки.
Поэтому единственное разумное решение, которое приходит в голову, состоит в том, чтобы анализировать первые N строк и ограничивать ширину колонки по ним. Если же дальше встретятся более широкие данные — ну, не повезло, будем обрезать.
Ну и, наконец, собственно процедура.
- procedure print(
- p_sql varchar2
- )
- is
- c_prefetch constant integer := 100;
-
- -- место под читаемые данные
- subtype column_t is dbms_sql.varchar2_table;
- type table_t is table of column_t index by binary_integer;
- l_column column_t;
- l_table table_t;
-
- l_desc_tab dbms_sql.desc_tab;
- l_cursor integer;
- l_cols integer;
- l_res integer;
- l_rows integer;
- l_base_row integer;
-
- procedure calc_max_lens
- is
- l_max integer;
- begin
- for col in 1 .. l_cols loop
- l_max := l_desc_tab(col).col_name_len;
- if l_rows > 0 then
- for row in 1 .. l_rows loop
- l_max := greatest( l_max, nvl(length(l_table(col)(l_base_row + row)),0) );
- end loop;
- end if;
- l_desc_tab(col).col_name_len := l_max;
- end loop;
- end;
-
- procedure print_header
- is
- begin
- for col in 1 .. l_cols loop
- dbms_output.put( rpad( l_desc_tab(col).col_name, l_desc_tab(col).col_name_len+1 ) );
- end loop;
- dbms_output.new_line;
- for col in 1 .. l_cols loop
- dbms_output.put( rpad( '-', l_desc_tab(col).col_name_len, '-' ) || ' ' );
- end loop;
- dbms_output.new_line;
- end;
-
- procedure print_rows
- is
- l_cell varchar2(2000);
- begin
- if l_rows = 0 then
- return;
- end if;
- for row in 1 .. l_rows loop
- for col in 1 .. l_cols loop
- l_cell := nvl(l_table(col)(l_base_row + row),' ');
- dbms_output.put(
- case
- when length(l_cell) > l_desc_tab(col).col_name_len
- then substr( l_cell, 1, l_desc_tab(col).col_name_len ) || '>'
- else rpad( l_cell, l_desc_tab(col).col_name_len+1 )
- end
- );
- end loop;
- dbms_output.new_line;
- end loop;
- end;
-
- begin
- -- открываем, парсим, выполняем
- l_cursor := dbms_sql.open_cursor;
- dbms_sql.parse(l_cursor, p_sql, dbms_sql.native);
- l_res := dbms_sql.execute(l_cursor);
-
- -- получаем список столбцов
- dbms_sql.describe_columns(l_cursor, l_cols, l_desc_tab);
- for col in 1 .. l_cols loop
- l_table(col) := l_column; -- инициализация, без нее получим no_data_found
- dbms_sql.define_array(l_cursor, col, l_table(col), c_prefetch, 1);
- end loop;
-
- -- выборка данных
- l_base_row := 0;
- loop
- l_rows := dbms_sql.fetch_rows(l_cursor);
- for col in 1 .. l_cols loop
- l_table(col).delete;
- dbms_sql.column_value(l_cursor, col, l_table(col));
- end loop;
- if l_base_row = 0 then
- calc_max_lens;
- print_header;
- end if;
- print_rows;
- l_base_row := l_base_row + l_rows;
- exit when l_rows < c_prefetch;
- end loop;
- dbms_sql.close_cursor(l_cursor);
- end;
no subject
Date: 2011-07-04 06:15 am (UTC)no subject
Date: 2011-07-04 09:00 am (UTC)no subject
Date: 2011-07-04 01:34 pm (UTC)На углу двое юношей возились с каким-то механическим устройством. Один убежденно говорил: «Конструкторская мысль не может стоять на месте. Это закон развития общества. Мы изобретём его. Обязательно изобретём. Вопреки бюрократам вроде Чинушина и консерваторам вроде Твердолобова». Другой юноша нёс своё: «Я нашёл, как применить здесь нестирающиеся шины из полиструктурного волокна с вырожденными аминными связями и неполными кислородными группами. Но я не знаю пока, как использовать регенерирующий реактор на субтепловых нейтронах. Миша, Мишок! Как быть с реактором?» Присмотревшись к устройству, я без труда узнал велосипед.
no subject
Date: 2011-07-04 09:28 am (UTC)no subject
Date: 2011-07-04 01:41 pm (UTC)no subject
Date: 2011-07-04 03:08 pm (UTC)а зачем ? при выводе-то
"Что делать, если..."
least(l_max_reasonable_len, l_approximate_db_len)
l_approximate_db_len спросить у базы