2. PgDay.Seoul 2021
Ver. 14 새 SQL
● WITH RECURSIVE ... (...)
SEARCH DEPTH FIRST BY order_column SET column_name
SELECT … ORDER BY column_name
● WITH RECURSIVE ... (...)
CYCLE cycle_check_column SET is_cycle USING column_name
SELECT … WHERE is_cycle = false ORDER BY column_name
● jsonb_data['key1']['subkey2']::int + 1
● SELECT range_agg(range_type_column) FROM table
2
3. PgDay.Seoul 2021
Ver. 14 새 SQL - WITH RECURSIVE
3
WITH RECURSIVE t AS (
SELECT *, addrname AS conname
FROM addrcodes
WHERE addrid = '380704'
UNION ALL
SELECT a.*, a.addrname || ' ' || t.conname
FROM addrcodes a, t
WHERE a.addrid = t.upaddr
)
SEARCH DEPTH FIRST BY addrname SET path
CYCLE addrname SET is_cycle USING path2
SELECT conname
FROM t
WHERE is_cycle = false
ORDER BY path DESC
FETCH FIRST 1 ROW ONLY
5. PgDay.Seoul 2021
Ver. 14 새 SQL - multirange 자료형 1
5
이 초록 영역을 어떻게 구할 것인가? multirange!
dTS *multirange
6. PgDay.Seoul 2021
Ver. 14 새 SQL - multirange 자료형 2
6
postgres=# SELECT lower(unnest), upper(unnest) - 1
FROM (SELECT unnest(
'{[2021-08-01, 2021-10-01)}'::datemultirange
- range_agg(daterange(startdate, enddate, '[]')))
FROM place_term
WHERE startdate > '2021-08-01' AND enddate < '2021-09-30') AS t;
lower | ?column?
------------+------------
2021-08-01 | 2021-08-03
2021-09-27 | 2021-09-30
(2개 행)
7. PgDay.Seoul 2021
대한민국 구석구석 자료 소개
7
● 한국관광공사에서 제공하는 공공 그래프 데이터베이스 자료
출처: http://data.visitkorea.or.kr/linked_open_data
● 이것을 PostgreSQL용으로 변환하고, 관계형 데이터베이스 모델로 바꿨다.
● 엔터티 관계도:
https://github.com/i0seph/visitkorea_for_pg/blob/master/graph2rdbms/visitkor
ea-erd.pdf
● flask 로 만든 샘플 코드는 그 위에
8. PgDay.Seoul 2021
예제 구성 - 첫화면
8
검색: 인덱스를 사용하는 %검색%
분류 검색: 계층형 쿼리와 그 외
추천 장소: 임의 뽑기 최적화
오늘 행사 중인 축제: 범위 자료형 검색
9. PgDay.Seoul 2021
예제 구성 - 설명 화면
9
분류와 지역: 계층형 쿼리
각 항목: key-value 자료 구조처리
인근 추천 장소: 위경도 인덱스 탐색 및 거리
계산
10. PgDay.Seoul 2021
SQL 1: DISTINCT ON
10
● 1:N 관계에서 N 자료 가운데 하나만 뽑기
○ 전통적으로 row_number() 윈도우 함수를 이용한 인라인뷰를 만들고, 그 값이 1인 것을 뽑음
SELECT …. FROM (
SELECT …, row_number() OVER (PARTITION BY … ORDER BY …) as no
FROM …) AS t WHERE no = 1
○ 이 복잡한 쿼리가 DISTINCT ON으로 해결 가능함 (sql_search_list.py)
SELECT DISTINCT ON (a.place_name, a.place_id)
a.place_id, a.place_name, b.imgurl
FROM place a LEFT JOIN place_images b ON a.place_id = b.place_id
WHERE place_name ~* to_regexp('자연 휴양림')
ORDER BY a.place_name, a.place_id, b.imgurl
FETCH FIRST 50 ROWS ONLY
11. PgDay.Seoul 2021
SQL 2: 문자열 검색
11
● 전통 기법: LIKE, ILIKE
○ 인덱스를 사용하지 않아 테이블 전체 탐색을 함, WHERE place_name LIKE %자연휴양림%
○ 단어 분리 상황에서 AND 연산 중복 비용 발생, WHERE place_name LIKE %자연% AND place_name
LIKE %휴양림%
● pg_trgm 확장 모듈과 정규식을 이용한 문제 풀기 (sql_search_list.py)
1. CREATE EXTENSION pg_trgm
2. CREATE INDEX place_name_i ON place USING gist_trgm_ops (place_name)
3. CREATE OR REPLACE FUNCTION to_regexp(text) RETURNS text LANGUAGE sql
IMMUTABLE AS $function$
SELECT string_agg('(?=.*' || a || ')','') FROM
UNNEST(tsvector_to_array(to_tsvector('simple', $1))) a
$function$
4. SELECT ...
FROM place a LEFT JOIN place_images b ON a.place_id = b.place_id
WHERE a.place_name ~* to_regexp('자연 휴양림')
...
12. PgDay.Seoul 2021
SQL 3: 배열 검색
12
● 전통 기법: IN
○ WHERE col IN (1,2,3,4)
● pg의 또 다른 문법: ANY
○ WHERE col = ANY(ARRAY[1,2,3,4])
○ 더 복잡한 문법을 왜 쓰지? SQLAlchemy 모듈에서 변수 바인딩이 쉬워짐
○ sql_place_list.py 참조
SELECT addrid FROM addrcodes WHERE upaddr = ANY(:locastr)
query_string으로 받은 문자열 loca 값을 loca.split() 함수로 배열 locastr 으로 바꾸고
그것을 그대로 쿼리로 넘겨 넘겨주면, SQLAlchemy 모듈이 알아서 처리해줌
○ 대부분 응용 프로그램 pg용 DB 드라이버들이 이 배열처리를 편하게 쓸 수 있도록
각자의 방법을 제공함
13. PgDay.Seoul 2021
SQL 4: 실무 예제와 쿼리 최적화
13
SELECT DISTINCT ON (a.place_id, a.place_name)
a.place_id, a.place_name, c.imgurl
FROM place a,
(SELECT place_id FROM place
WHERE loca IN (SELECT addrid FROM addrcodes WHERE upaddr = ANY (ARRAY['380300']))
AND cate IN ( WITH RECURSIVE t AS (
SELECT * FROM tourism WHERE uptour = ANY (ARRAY['A05', 'B02'])
UNION ALL
SELECT a.* FROM tourism a, t WHERE a.uptour = t.tourid)
SELECT tourid FROM t WHERE length(tourid) = 9)
ORDER BY place_name FETCH FIRST 50 ROWS ONLY) b
LEFT JOIN place_images c ON b.place_id = c.place_id
WHERE a.place_id = b.place_id
ORDER BY a.place_name, a.place_id, c.imgurl;
● 전라남도 곡성군 음식점과 숙박 시설 찾는 쿼리
14. PgDay.Seoul 2021
SQL 5: TABLESAMPLE - 임의 자료 탐색
14
● 전통적인 ORDER BY random() LIMIT 10 구문은 테이블 전체를 탐색한다.
● TABLESAMPLE {SYSTEM|BERNOULLI} (퍼센트)
○ SYSTEM: 임의 블록으로 가서 그 해당 자료 수 만큼 추출 (퍼센트 값이 아주 작다면 하나의
블록 읽기만 하는데, 그 안에 비슷한 자료들이 몰려 있으면 임의 추출의 의미가 퇴색함)
○ BERNOULLI: 블록 기준으로 해당 퍼센트 확률만큼 블록을 선택하고 거기서 자료를 추출
(확률값이 낮을 수록 더 많은 블록을 뒤지게 된다. 반면 임의 추출 품질은 좋아짐)
● place_images 테이블(블록수: 약 1,400개) 대상 베르누이 샘풀링 확률 보면,
○ 1% = 18, 0.1% = 43, 0.01% = 600 정도의 블록을 읽음으로 예제 코드에서는 0.05로 설정함
○ 이처럼 베르누이 확률을 사용할 경우는 비용과 품질 사이 적정값을 찾아야 함
SELECT DISTINCT ON (place_id) place_id, imgurl
FROM ( SELECT place_id, imgurl FROM place_images
TABLESAMPLE BERNOULLI (0.05) LIMIT 20
) a ORDER BY place_id, imgurl LIMIT 10
15. PgDay.Seoul 2021
SQL 6: 범위 자료형 탐색 - 오늘 방문하면 되는 곳
15
● 전통적인 범위, 기간 검색은 시작값(lower)과 마침값(upper)을 저장하고, >=,
<, BETWEEN 연산으로 처리 함.
○ 시작날짜가 오늘보다 작거나 같고, 마침날짜가 오늘보다 크거나 같은 것
startdate <= current_date and enddate >= current_date
● 범위자료형을 쓰면
○ 행사기간들 중에 오늘이 포함된 것
term @> current_date
● 전통적인 모델링을 바꿀 수 없다면 (sql_festa_list.py)
○ 범위자료형 변환 함수를 사용하는 함수 기반 인덱스를 만들어 쓴다
CREATE INDEX term_range_i ON place_term USING gist (daterange(startdate, enddate, '[]'));
SELECT * FROM place_term
WHERE daterange(startdate, enddate, '[]') @> CURRENT_DATE
AND daterange(date_trunc('month' , current_timestamp)::date, (date_trunc('month' ,
current_timestamp) + interval '1 month')::date) @> daterange(startdate, enddate, '[]')
16. PgDay.Seoul 2021
SQL 7: key-value 자료 처리 - json
16
● 처음부터 jsonb, json 자료형으로 DB에 넣고 꺼내기
○ json 양식 검사 문제
○ jsonb 인 경우 key 정렬 문제
○ 해당 칼럼이 toast에 저장되는 문제
● DB에는 관계형으로 저장되고, 응용프로그램에서 json으로 다루기
○ 어디서 json으로 변환할 것인가?
■ DB측: json_build_*(), json_agg() 함수를 사용
■ APP측: DB result row의 유연한 json 변환 작업을 제공해야함
17. PgDay.Seoul 2021
SQL 7: key-value 자료 처리 - 예제
17
● 예제에서는 N:N 관계형 모델링 자료에 대한 처리를 다룬다.
● DB 측 ● 응용프로그램 측(python flask)
SELECT b.attname, a.v
FROM place_attrib a JOIN attnames b ON a.attid = b.attid
WHERE place_id = 130679;
attname | v
----------------------+------------------------------------
규모 | 지상 3층
신용카드사용여부 | 불가능
전화번호 | 033-462-2303
유모차대여서비스 | 불가능
주소 | 강원도 인제군 북면 만해로 91
이용요금 | 무료
애완동물동반가능여부 | 불가능
우편번호 | 24606
이용시간 | 09:00~17:00
쉬는날 | 매주 월요일, 1월 1일, 설/추석 당일
주차시설 | 주차 가능
@app.route('/ajax/getattrib/<int:place_id>')
def get_attribute(place_id):
mod = __import__('sql_get_attribute')
d = sql(mod.query, {'place_id': place_id})
return jsonify([dict(row) for row in d.fetchall()])
18. PgDay.Seoul 2021
SQL 8: 위경도 처리
18
● 위경도값 처리를 위한 작업
○ WGS84 좌표계를 사용하는 위경도값이라면,
가장 단순한 방법으로 earthdistance 확장 모듈을 사용하는 것이다.
CREATE EXTENSION earthdistance CASCADE;
ALTER TABLE place ADD position earth GENERATED ALWAYS AS (ll_to_earth(lat, long)) STORED;
-- INSERT 작업에서는 이 position 칼럼의 값으로 default를 쓴다
CREATE INDEX CONCURRENTLY place_position_i ON place USING gist (position);
-- 자료 찾기, :y1 = 현재 위도, :x1 = 현재 경도, :place_id = 현재 장소번호
-- 현재 위치에서 2Km 안에 있는 다른 장소들 찾기
SELECT place_id,
round((earth_distance(position, ll_to_earth(:y1, :x1))::numeric / 1000)::numeric, 2)::float as
distance
FROM place
WHERE earth_box(ll_to_earth(:y1, :x1), 2000) @> position AND a.place_id <> :place_id
19. PgDay.Seoul 2021
그 외 SQL 들
19
● 형변환자: ::
● {INSERT|UPDATE|DELETE} … RETURNING …
● JOIN UPDATE: UPDATE … FROM
● JOIN DELETE: DELETE FROM … USING …
● INSERT or UPDATE: INSERT INTO … ON CONFLICT …
● 명령어 VAULES: SELECT * FROM (VALUES(1,2,3)) t
● LATERAL 예약어: SELECT … FROM a, LATERAL (SELECT * FROM b where b.col =
a.col) …
● LISTEN & NOTIFY
20. PgDay.Seoul 2021
참고 자료
20
● python flask
○ flask.palletsprojects.com
● jquery
○ jquery.com
● PostgreSQL
○ postgresql.org & postgresql.kr
● 발표를 위한 샘플 코드
○ https://github.com/i0seph/visitkorea_for_pg
● PostgreSQL 한국 사용자 모임
○ https://www.facebook.com/groups/postgres.kr