import만 해도 sqlite3 DB를 사용할 수 있다고?

 

sqlite3 모듈은 파이썬 표준 라이브러리(파이썬이 설치될 때 기본적으로 설치되는 모듈)로 SQLite에 대한 인터페이스를 제공합니다. 표준 라이브러리인 sqlite3 모듈을 이용하면 SQLite 데이터베이스를 이용할 수 있습니다.

Python에서 SQLite 을 사용하기 위해서는 보통 pysqlite 으로 불리우는 Language binding (주: SQLite 라이브러리를 내부에서 사용)을 사용하는데, pysqlite 은 Python 2.5 이상에서 기본적으로 내장되어 있다.

pysqlite 를 import 하기 위해서는 "import sqlite3" 와 같이 import만 하면 된다.

 

파이썬 SQLite 라이브러리 불러오기 및 버전 확인

우선 라이브러리를 불러와서 “sqlite3 라이브러리”의 버전과 “SQLite(DB 엔진)” 버전을 각각 출력해보자.

import sqlite3
 
print(sqlite3.version)
print(sqlite3.sqlite_version)

 

DB 연결, 커서 획득

이제 진짜 DB를 생성하는 쿼리를 실행해보자.

# DB 생성 (오토 커밋)
conn = sqlite3.connect("test.db", isolation_level=None)
 
# 커서 획득
c = conn.cursor()
 
# 테이블 생성 (데이터 타입은 TEST, NUMERIC, INTEGER, REAL, BLOB 등)
c.execute("CREATE TABLE IF NOT EXISTS table1 \
(id integer PRIMARY KEY, name text, birthday text)")

일단 "test.db"라는 데이터베이스를 생성해봤다. 브라우저로 열어서 보면 잘 보일 거다. 엑셀에서 첫 행을 적어두었다 생각하면 된다.

c.execute("CREATE TABLE IF NOT EXISTS 테이블이름()")안에 문자열로 필드(열) 이름과 데이터 타입을 작성해주면 된다.

 

CREATE TABLE IF NOT EXITSTS 테이블_이름() 이라는 쿼리는 말 그대로 “테이블_이름이라는 테이블이 없으면 테이블을 생성해라”라는 의미다. 있으면 있는 걸 그대로 쓰는 거고. 그 안에 필드를 나열해주면 되는데 필드명 > 테이터 타입 순으로 입력한다. PRIMARY KEY는 테이블 내에 있는 레코드를 식별하는 고유 키를 말하며, 유일한 것이어야 한다.

개념 1. commit & rollback

위에서 sqlite3.connect() 괄호 안에 isolation_level=None이라고 명시했는데 이는 (실습을 위해) 쿼리문을 실행하여 DB에 즉시 반영, 즉시 자동 커밋을 하기 위함이다.

commit(커밋)은 “변경사항을 DB에 반영한다”는 뜻이라 commit을 하지 않으면 수정(추가/갱신/삭제 등) 작업에 대한 기록을 컴퓨터 메모리 같은 데 임시로 가지고 있을 뿐 실제로 DB에는 반영하지 않는다. 최종적으로 DB를 수정을 하려면 마지막에 반드시 conn.commit()이라는 명령을 실행해주어야 한다.

참고로 이것과 commit과 반대되는 개념으로 rollback(롤백)이 있다. 이전 이력으로 되돌린다는 뜻. conn.rollback()으로 명령한다.

개념 2. cursor

파이썬에서 파일을 읽고 쓰려면 커서를 가져와야 한다. 그래서 conn.cursor()로 일단 커서를 생성한다.

데이터 삽입하기

데이터 한 건을 넣으려면 c.execute(“INSERT INTO table1 VALUES()”)라고 해서 값을 직접 넣자.

# 데이터 삽입 방법 1
c.execute("INSERT INTO table1 \
VALUES(1, 'LEE', '1987-00-00')")

테이블 안에 데이터가 들어간 걸 확인할 수 있다. 그런데 이렇게 하는 건 필드명과 순서를 정확히 알고 있다는 전제 하에 가능한 거다.

만약 정석으로 데이터를 삽입하려면 아래와 같이 튜플로 넣어주는 게 좋다.

# 데이터 삽입 방법 2
c.execute("INSERT INTO table1(id, name, birthday) \
VALUES(?,?,?)", \
(2, 'KIM', '1990-00-00'))

이번엔 튜플이나 리스트 형태의 데이터 세트를 한 번에 삽입하는 방법도 알아야겠지. c.executemany()를 사용하면 된다.

test_tuple = (
(3, 'PARK', '1991-00-00'),
(4, 'CHOI', '1999-00-00'),
(5, 'JUNG', '1989-00-00')
)
 
c.executemany("INSERT INTO table1(id, name, birthday) VALUES(?,?,?)", test_tuple)

끝이다. 리스트로도 가능하다.

데이터 불러오기

이제 데이터들이 잘 있나 확인해야겠지. 우선 데이터를 모두 선택한 다음에 c.fetchone()으로 하나씩 출력해보자.

c.execute("SELECT * FROM table1")
print(c.fetchone())
print(c.fetchone())
print(c.fetchall())

c.fetchone()을 사용하면 한 줄씩 출력하는 걸 알 수 있다. 커서가 이동하기 때문이다. 그리고 이후에 c.fetchall()이라는 걸 사용해서 전체를 가져와서 출력하더라도 이미 읽은 지점 이후에 있는 것들만 출력된다. (참고로 fetch는 “가져오다”라는 뜻.)

그래서 만약 전체 데이터를 출력하고 싶다면 이렇게 전체를 다시 읽어 놓고 해야 한다.

c.execute("SELECT * FROM table1")
print(c.fetchall())

그리고 이렇게 가져온 데이터는 리스트 형태로 출력되는 걸 알 수 있는데, 결국 반복문을 돌 수 있다는 뜻이다.

# 방법 1
c.execute("SELECT * FROM table1")
for row in c.fetchall():
print(row)
 
# 방법 2
for row in c.execute("SELECT * FROM table1 ORDER BY id ASC"):
print(row)

방법 2가 한 줄 덜 쓰긴 하지만 가독성이 떨어져서 차라리 방법 1이 나은 것 같다.

데이터 조회하기 (필터링)

원하는 데이터만 찾아서 가져올 수도 있어야겠지. SQL에서 WHERE 문을 써야 한다.

그런데 이때 방법이 이것저것 있다. 포매팅을 할 수도 있고, 딕셔너리 형식으로 설정해서 가져올 수도 있다. 좀 복잡하니 해보면서 익숙한 방법을 사용하면 된다.

여기서는 id라는 변수가 어떤 녀석인 것들만 선택해서 가져올지 짜봤다.

# 방법 1
param1 = (1,)
c.execute('SELECT * FROM table1 WHERE id=?', param1)
print('param1', c.fetchone())
print('param1', c.fetchall())
 
# 방법 2
param2 = 1
c.execute("SELECT * FROM table1 WHERE id='%s'" % param2) # %s %d %f
print('param2', c.fetchone())
print('param2', c.fetchall())
 
# 방법 3
c.execute("SELECT * FROM table1 WHERE id=:Id", {"Id": 1})
print('param3', c.fetchone())
print('param3', c.fetchall())
 
# 방법 4
param4 = (1, 4)
c.execute('SELECT * FROM table1 WHERE id IN(?,?)', param4)
print('param4', c.fetchall())
 
# 방법 5
c.execute("SELECT * FROM table1 WHERE id In('%d','%d')" % (1, 4))
print('param5', c.fetchall())
 
# 방법 6
c.execute("SELECT * FROM table1 WHERE id=:id1 OR id=:id2", {"id1": 1, "id2": 4})
print('param6', c.fetchall())

처음엔 좀 어렵지만, 익숙해지면 어렵지 않게 쓸 수 있겠지.

데이터 수정하기

위에서 조회할 때 사용한 WHERE 문과 함께 UPDATE를 통해 갱신하는 거다.

id가 무엇인 녀석을 찾아서 name을 새로운 걸로 고쳐보자. 이렇게.

# 방법 1
c.execute("UPDATE table1 SET name=? WHERE id=?", ('NEW1', 1))
 
# 방법 2
c.execute("UPDATE table1 SET name=:name WHERE id=:id", {"name": 'NEW2', 'id': 3})
 
# 방법 3
c.execute("UPDATE table1 SET name='%s' WHERE id='%s'" % ('NEW3', 5))
 
# 확인
for row in c.execute('SELECT * FROM table1'):
print(row)

편한 방법으로 사용하자.

데이터 삭제하기

테이블에 있는 특정 데이터를 지우려면 WHERE과 DELETE를 조합하면 된다.

# 방법 1
c.execute("DELETE FROM table1 WHERE id=?", (1,))
 
# 방법 2
c.execute("DELETE FROM table1 WHERE id=:id", {'id': 3})
 
# 방법 3
c.execute("DELETE FROM table1 WHERE id='%s'" % 5)
 
# 확인
for row in c.execute('SELECT * FROM table1'):
print(row)

만약 테이블에 있는 데이터 전체를 지우려면 conn.execute() 안에 쿼리문을 써주면 된다.

# 방법 1
# conn.execute("DELETE FROM table1")
 
# 방법 2
print(conn.execute("DELETE FROM table1").rowcount)

뒤에 rowcount를 붙여주면 지운 행 개수를 돌려준다.

DB 연결 해제

데이터베이스를 연결해서 이런저런 수정을 했으면 마지막엔 그 연결을 해제해야 한다. 그래서 항상 conn.close() 명령으로 마무리한다.

DB 백업하기 (dump)

데이터베이스는 항상 dump를 통해 백업을 해놓는 게 중요하다. 그래야 나중에 다른 PC에서도 이걸 그대로 재구성할 수 있다.

with conn:
with open('dump.sql', 'w') as f:
for line in conn.iterdump():
f.write('%s\n' % line)
print('Completed.')

이래놓고 sql 파일을 열어보면 신기하게 아래와 같이 생겼다.

BEGIN TRANSACTION;
CREATE TABLE table1(id integer PRIMARY KEY, name text, birthday text);
INSERT INTO "table1" VALUES(1,'LEE','1987-00-00');
INSERT INTO "table1" VALUES(2,'KIM','1990-00-00');
INSERT INTO "table1" VALUES(3,'PARK','1991-00-00');
INSERT INTO "table1" VALUES(4,'CHOI','1999-00-00');
INSERT INTO "table1" VALUES(5,'JUNG','1989-00-00');
COMMIT;

실제로 DB를 아예 재구성하는 SQL 쿼리들이 작성되어 있다.

 

원문: https://hleecaster.com/python-sqlite3/

파이썬 소스코드를 보다보면 아래와 같이 if __name__ == "__main__": 구문을 많이 볼 수 있습니다.

if __name__ == "__main__":
    # Execute when the module is not initialized from an import statement.
    main()

위 문장이 어떤 의미인지 알아볼까요.

 

 __name__이란?

모듈의 이름을 담고 있는 파이썬 내장 변수입니다.

python.py 라는 파일이 있다면 __name__는 .py 확장자를 제외한 python이 됩니다.

 

__main__이란?

__main__은 최상위 코드가 실행되는 환경의 이름입니다. "최상위 코드"는 프로그램 실행 시 첫 번째로 실행되는 Python 모듈입니다. 프로그램 구동에 필요한 다른 모듈들을 가져오기(import) 때문에 "최상위"라고 합니다.

"최상위 코드"를 애플리케이션의 진입점(entry point)이라고도 합니다.

모듈은 __name__을 확인하여 최상위 환경에서 실행 중인지의 여부를 확인할 수 있습니다

모듈이 최상위 코드 환경에서 실행되면 해당 __name__은 '__main__' 문자열로 설정됩니다.

Python 모듈 또는 패키지를 imort하면 __name__은 모듈 이름으로 설정됩니다. 일반적으로 .py 확장자가 없는 파일 이름입니다.

 

<예제>

모듈이 메인 프로그램인 경우

first_module.py 파일을 만들고 아래와 같이 입력합니다.

##  first_module.py
print('__name__ value :', __name__)

아래와 같이 python first_module.py를 명령창에서 수행하면 __name__ 값이 '__main__'인 것을 알 수 있습니다. 이는 위에서 설명한 최상위 코드(main program)에서 실행했기 때문입니다.

$ python first_module.py
__name__ value : __main__

즉, 파이썬 인터프리터는 문자열 '__main__'을 __name__변수에 할당합니다.

# It's as if the interpreter inserts this at the top
# of your module when run as the main program.
__name__ = "__main__" 

 

모듈을 import하는 경우

다른 모듈이 위에서 작성한 모듈을 import 할 경우입니다. second_module.py 파일을 아래와 같이 생성합니다.

# second_module.py
import first_module

print('__name__ value :', __name__)

아래와 같이 python second_module.py를 명령창에서 수행하면 import한 first_module의 __name__ 값이 'first_module'인 것을 알 수 있습니다. 이는 위에서 설명한대로 .py 확장자를 제외한 파일명으로 설정됩니다. 

$ python second_module.py
__name__ value : first_module
__name__ value : __main__

파이썬 인터프리터는 first_module.py 파일을 검색하고 해당 모듈을 실행하기 전에 import  name문에서 __name__변수로 모듈 이름(확장자 .py를 제외한 이름)을 지정합니다.

# It's as if the interpreter inserts this at the top
# of your module when it's imported from another module.
__name__ = "first_module"

 

위의 예제에서처럼 import를 하게되면 import된 모듈이 실행이 됩니다. 위와같이 의도치 않게 import module이 실행되는 것을 막기 위해 

if __name__ == "__main__": 구문을 사용합니다.

즉, 메인 프로그램에서 실행했을 경우에만 수행을 하도록 제어하는 것입니다.

 

위에서 사용한 first_module.py과 second_module.py 파일을 아래와 같이 수정합니다.

##  first_module.py
if __name__ == "__main__": 
	print('__name__ value :', __name__)
# second_module.py
import first_module

if __name__ == "__main__": 
	print('__name__ value :', __name__)

아래와 같이 다시 second_module.py 를 실행합니다. first_module은 최상위 코드가 아니므로 if __name__ == "__main__":  구문에 의해 print 문장이 수행되지 않았습니다.

$ python second_module.py
__name__ value : __main__

이와같은 사유로 if __name__ == "__main__": 

구문을 습관적으로 사용해야 합니다.

 

가장 기본적인 사항만 설명하였으니, 상세 내용은 아래를 참조하세요. __name__과 __main__ 관련하여 위에서 설명한 것 이외에도 많은 내용들이 있습니다.

 

아래 내용은 파이썬 document의 내용입니다.

내용이 많고 복잡합니다.

https://docs.python.org/3/library/main.html?highlight=main

 

 

__main__ — Top-level code environment — Python 3.10.1 documentation

Both of these mechanisms are related to Python modules; how users interact with them and how they interact with each other. They are explained in detail below. If you’re new to Python modules, see the tutorial section Modules for an introduction. __name_

docs.python.org

Python에서 __main__이라는 이름은 다음 두 가지 구성에 사용됩니다.

  • 프로그램의 최상위 코드 환경 이름으로,  __name__ == '__main__' 표현식
  • Python 패키지의 __main__.py 파일

Python 모듈 또는 패키지를 가져올 때 __name__은 모듈의 이름으로 설정됩니다.

모듈이 최상위 코드 환경에서 실행되면 해당 __name__이 '__main__' 문자열로 설정됩니다.

 

Python 모듈 또는 패키지를 가져오면 __name__이 모듈 이름으로 설정됩니다. 일반적으로 .py 확장자가 없는 Python 파일 자체의 이름입니다.

 

"최상위 코드 환경"이란?
__main__은 최상위 코드가 실행되는 환경의 이름입니다. "최상위 코드"는 실행을 시작하는 첫 번째 Python 모듈입니다. 프로그램에 필요한 다른 모든 모듈을 가져오기 때문에 "최상위"라고 합니다. "최상위 코드"를 애플리케이션의 진입점이라고도 합니다.

 

결과적으로 모듈은 __name__을 확인하여 최상위 환경에서 실행 중인지 여부를 확인할 수 있습니다. 이는 모듈이 import 문에서 초기화되지 않을 때 조건부로 코드를 실행하는 일반적인 관용구를 허용합니다.

if __name__ == '__main__':
    # Execute when the module is not initialized from an import statement.
    ...

관용적 사용법
일부 모듈에는 명령줄 인수를 구문 분석하거나 표준 입력에서 데이터를 가져오는 것과 같이 스크립트 전용 코드가 포함되어 있습니다. 예를 들어 단위 테스트를 위해 이와 같은 모듈을 다른 모듈에서 가져온 경우 스크립트 코드도 의도하지 않게 실행됩니다.

여기서 if __name__ == '__main__' 코드 블록을 사용하면 편리합니다. 이 블록 내의 코드는 모듈이 최상위 환경에서 실행되지 않는 한 실행되지 않습니다.

if __name__ == '__main__' 아래 블록에 가능한 한 적은 수의 명령문을 넣으면 코드 명확성과 정확성이 향상될 수 있습니다. 대부분의 경우 main이라는 함수는 프로그램의 기본 동작을 캡슐화합니다. 

# echo.py

import shlex
import sys

def echo(phrase: str) -> None:
   """A dummy wrapper around print."""
   # for demonstration purposes, you can imagine that there is some
   # valuable and reusable logic inside this function
   print(phrase)

def main() -> int:
    """Echo the input arguments to standard output"""
    phrase = shlex.join(sys.argv)
    echo(phrase)
    return 0

if __name__ == '__main__':
    sys.exit(main())  # next section explains the use of sys.exit

 

모듈이 main 함수 내부에 코드를 캡슐화하지 않고 대신 if __name__ == '__main__' 블록 내에 직접 넣는 경우, phrase 변수는 전체 모듈에 대해 global 변수가 됩니다. 모듈 내의 다른 함수가 의도하지 않게 local 변수 대신 global 변수를 사용할 수 있으므로 오류가 발생하기 쉽습니다. main 함수는 이 문제를 해결합니다.

main 함수를 사용하면 echo 함수 자체가 격리되고 다른 어느 곳에서나 import할 수 있다는 이점이 있습니다.

echo.py를 가져오면 echo와 main 함수가 정의되지만 둘 다 호출되지 않습니다.

왜냐하면 __name__ != '__main__' 때문입니다.

 

패키징 고려 사항
주요 기능은 콘솔 스크립트의 진입점으로 지정하여 명령줄 도구를 만드는 데 자주 사용됩니다. 이 작업이 완료되면 pip는 main의 반환 값이 sys.exit()에 전달되는 템플릿 스크립트에 함수 호출을 삽입합니다. 예를 들어:

sys.exit(main())
main에 대한 호출은 sys.exit()에 래핑되어 있으므로 함수가 sys.exit()에 대한 입력으로 허용되는 일부 값을 반환할 것입니다. 일반적으로 정수 또는 None(함수에 return 문이 없는 경우 암시적으로 반환됨)입니다.

이 규칙을 따랐다면 모듈은 나중에 pip-installable 패키지의 콘솔 스크립트 진입점으로 패키징할 때와 마찬가지로 직접 실행할 때(예: python3 echo.py) 동일한 동작을 갖게 됩니다.

특히 main 함수에서 문자열을 반환할 때 주의하십시오. sys.exit()는 문자열 인수를 실패 메시지로 해석하므로 프로그램은 실패를 나타내는 종료 코드 1을 가지며 문자열은 sys.stderr에 기록됩니다. 

 

__main__.py in Python Packages

일반적으로 __main__.py 파일은 패키지에 대한 명령줄 인터페이스를 제공하는 데 사용됩니다.

다음 패키지 "bandclass"를 가정해봅니다.

bandclass
  ├── __init__.py
  ├── __main__.py
  └── student.py

__main__.py는 패키지 자체가 -m 플래그를 사용하여 명령줄에서 직접 호출될 때 실행됩니다. 예를 들어:

$ python3 -m bandclass

이 명령은 __main__.py가 실행되도록 합니다. 이 메커니즘을 사용하는 방법은 작성 중인 패키지의 특성에 따라 다르지만 이 경우 교사가 학생을 검색하도록 허용하는 것이 합리적일 수 있습니다.

# bandclass/__main__.py

import sys
from .student import search_students

student_name = sys.argv[2] if len(sys.argv) >= 2 else ''
print(f'Found student: {search_students(student_name)}')

from .student import search_students는 상대적 import의 예입니다. 이 import 스타일은 패키지 내에서 모듈을 참조할 때 사용할 수 있습니다.

 

관용적 용법

__main__.py의 내용은 일반적으로 if __name__ == '__main__' 블록으로 구분되지 않습니다. 대신 해당 파일은 다른 모듈에서 실행하기 위해 짧게 유지됩니다. 그런 다음 이러한 다른 모듈은 단위 테스트를 쉽게 수행할 수 있으며 적절하게 재사용할 수 있습니다.

if __name__ == '__main__' 블록을 사용하면 패키지 내의 __main__.py 파일에 대해 예상대로 작동합니다. import가 되면 __name__ 속성에 패키지의 경로가 포함되기 때문입니다.

>>>
>>> import asyncio.__main__
>>> asyncio.__main__.__name__
'asyncio.__main__'

이것은 .zip 파일의 루트 디렉토리에 있는 __main__.py 파일에는 작동하지 않습니다. 따라서 일관성을 위해 위에서 언급한 venv와 같은  __main__.py가 더 좋습니다.

 

import __main__

Python 프로그램이 시작된 모듈에 관계없이 동일한 프로그램 내에서 실행되는 다른 모듈은 __main__ 모듈을 가져와서 최상위 환경의 범위(네임스페이스)를 가져올 수 있습니다. 이것은 __main__.py 파일을 가져오는 것이 아니라 특별한 이름 '__main__'을 받은 모듈을 가져옵니다.

다음은 __main__ 네임스페이스를 사용하는 예제 모듈입니다.

 

Here is an example module that consumes the __main__ namespace:

# namely.py

import __main__

def did_user_define_their_name():
    return 'my_name' in dir(__main__)

def print_user_name():
    if not did_user_define_their_name():
        raise ValueError('Define the variable `my_name`!')

    if '__file__' in dir(__main__):
        print(__main__.my_name, "found in file", __main__.__file__)
    else:
        print(__main__.my_name)

이 모듈의 사용 예는 다음과 같습니다

# start.py

import sys

from namely import print_user_name

# my_name = "Dinsdale"

def main():
    try:
        print_user_name()
    except ValueError as ve:
        return str(ve)

if __name__ == "__main__":
    sys.exit(main())

 

프로그램을 실행하면 결과는 다음과 같습니다.

$ python3 start.py
Define the variable `my_name`!

 

프로그램의 종료 코드는 오류를 나타내는 1이 됩니다. my_name = "Dinsdale"이 있는 줄의 주석 처리를 제거하면 프로그램이 수정되고 이제 성공을 나타내는 상태 코드 0으로 종료됩니다

$ python3 start.py
Dinsdale found in file /path/to/start.py

__main__을 가져오면 시작 모듈의 if __name__ == "__main__" 블록에 있는 스크립트 사용을 위한 최상위 코드를 의도하지 않게 실행하는 데 문제가 발생하지 않습니다. 왜 이런 식으로 작동할까요?

Python은 인터프리터 시작 시 sys.modules에 빈 __main__ 모듈을 삽입하고 최상위 코드를 실행합니다.

이 예에서는 한 줄씩 실행하고 namely를 import하는 시작 모듈입니다. 차례로 __main__(실제로 시작됨)을 import합니다.

이것이 import cycle입니다.

부분적으로 채워진 __main__ 모듈이 sys.modules에 있기 때문에 Python은 이를 namely로 전달합니다. 

Python REPL은 "최상위 환경"의 또 다른 예이므로 REPL에 정의된 모든 것이 __main__ 범위의 일부가 됩니다.

>>>
>>> import namely
>>> namely.did_user_define_their_name()
False
>>> namely.print_user_name()
Traceback (most recent call last):
...
ValueError: Define the variable `my_name`!
>>> my_name = 'Jabberwocky'
>>> namely.did_user_define_their_name()
True
>>> namely.print_user_name()
Jabberwocky

이 경우 __main__ 범위는 대화형이므로 __file__ 속성을 포함하지 않습니다.
__main__ 범위는 pdb 및 rlcompleter의 구현에 사용됩니다.

 

파이썬은 왜 프로그램의 시작점이 정해져 있지 않나요?

파이썬이 처음에 개발 될 당시에는 리눅스/유닉스에서 사용하는 스크립트 언어 기반이었기 때문에 프로그램의 시작점이 따로 정해져 있지 않았습니다. 보통 리눅스/유닉스의 스크립트 파일은 파일 한 개로 이루어진 경우가 많은데, 이 스크립트 파일 자체가 하나의 프로그램이다 보니 시작점이 따로 필요하지 않습니다. 하지만 C 언어나 자바같은 언어는 처음 만들어질 때부터 소스 파일을 여러 개 사용했기 때문에 여러 소스 파일의 함수들 중에서도 시작 함수(main)를 따로 정해 놓았습니다.

SQLite 설치

 

Widow에 SQLite 설치

 

SQLite 홈페이지(https://www.sqlite.org/download.html) 에서 SQLite를 다운로드 한다.

자신의 PC에 맞는 버전을 다운로드하고 설치한다.

본 문서에서는 sqlite-tools-win32-x86 Binary 버전을 다운로드하여 설치한다.

 

본 문서에서는 D:\sqlite 디렉토리에 압축을 풀었다. 압축을 풀면 아래 3개의 파일이 생긴다.

 

sqldiff.exe :  SQLite 데이터베이스 간의 콘텐츠 차이를 보여주는 프로그램

                         <사용예> : sqldiff [options] database1.sqlite database2.sqlite

sqlite3.exe : SQLite 데이터베이스 또는 ZIP 아카이브에 대해 SQL 문을 입력하고 실행할 수 있도록 하는 프로그램

                         <사용예> : sqlite3 ex1.db (ex1.db 데이터베이스 생성)

sqlite3_analyzer.exe : 테이블과 인덱스가 공간을 측정하고 표시하는프로그램

                         <사용예> : sqlite3_analyzer database.sqlite

 

Linux에 SQLite 설치

SQLite 홈페이지(https://www.sqlite.org/download.html) 에서 SQLite를 다운로드하여 설치하거나,

sudo apt install sqlite3 명령어를 수행하여 설치한다.

$ sudo apt install sqlite3

 

SQLite 사용법

데이터베이스 생성

Linux 쉘이나 Dos 명령창에서 "sqlite3 first.db"를 수행하면 "first.db"라는 새로운 데이터베이스를 생성한다. 명령어를 수행한 경로에 first.db 파일이 생성된다. 즉 별도의 DB 생성 명령어는 존재하지 않고, sqlite3 명령어로 새로은 database 생성한다. 만약, 기존에 데이터베이스가 존재하면 기존 데이터베이스에 접속한다.

$ sqlite3 first.db
SQLite version 3.37.0 2021-11-27 14:13:22
Enter ".help" for usage hints.
sqlite>

 

SQLite 버전 확인

sqlite> select sqlite_version();
sqlite_version()
3.31.1

 

데이터베이스 목록 조회

sqlite> PRAGMA database_list;

seq|name|file
0|main|/mnt/d/sqlite/first.db
2|test|/mnt/d/sqlite/test.db
3|second|/mnt/d/sqlite/second.db

 

테이블생성

tbl1 테이블을 생성하고 데이터를 입력한 후 조회해 본다. 데이터는 '|' 기호로 분리되어 조회된다.

sqlite> create table tbl1(one varchar(10), two smallint);
sqlite> insert into tbl1 values('hello!',10);
sqlite> insert into tbl1 values('goodbye', 20);
sqlite> select * from tbl1;
one|two
hello!|10
goodbye|20
sqlite>

SQLite 커맨드라인 명령어는 . (점) 으로 시작하면 되고 .help를 치면 수행할 수 있는 명령어들이 화면에 표시된다.

 

한글입력

한글을 입력하기 위한 별도의 설정은 필요없다.

SQLite에서 TEXT 데이터 형으로 데이터를 입력하면 기본적으로 UTF-8로 저장된다.

 

테이블 목록 조회

SQLite의 커맨드라인으로 조회 : SQLite의 커맨드라인에서  .tables를 입력한다.

sqlite> .tables
tbl1

query로 table 목록 조회 
type, name, 테이블명, rootpage, sql 이 보여진다. 조회 시 header를 보이게 하려면 .headers on|off 를 사용한다.

아래 예제는 .headers on 을 사용한 결과이다.

sqlite> SELECT * FROM sqlite_master WHERE type='table';
type|name|tbl_name|rootpage|sql
table|tbl1|tbl1|2|CREATE TABLE tbl1(one varchar(10), two smallint)

-- table_name만 조회
sqlite> SELECT tbl_name FROM sqlite_master WHERE type='table';
tbl_name
tbl1

 

추가 DB 생성

생성하고자 하는 DB명으로 새로운 DB를 생성한다.

$ sqlite3 second.db
SQLite version 3.37.0 2021-11-27 14:13:22
Enter ".help" for usage hints.
sqlite>

 

transaction 제어(commit, rollback, savepoint 등)

데이터베이스에 액세스하는 모든 명령(기본적으로 일부 PRAGMA 문을 제외한 모든 SQL 명령)은 트랜잭션이 적용되지 않은 경우 자동으로 트랜잭션을 시작한다. 자동으로 시작된 트랜잭션은 마지막 SQL 문이 완료될 때 커밋된다.

트랜잭션은 BEGIN 명령을 사용하여 시작할 수 있다. 이러한 트랜잭션은 일반적으로 COMMIT 또는 ROLLBACK 명령까지 지속됩니다. 그러나 데이터베이스가 닫히거나 오류가 발생하고 ROLLBACK 충돌 해결 알고리즘이 지정된 경우 트랜잭션도 ROLLBACK됩니다. 
END TRANSACTION은 COMMIT과 동일하다. 아래 transaction에 대한 rollback과 commit 예제이다

-- transaction rollback
sqlite> create table trans(one varchar(10), two smallint);
sqlite> begin transaction;
sqlite> insert into trans values('hello',10);
sqlite> insert into trans values('goodbye', 20);
sqlite> rollback;
sqlite> select * from trans;
-- 입력한 데이터가 rollback 되어 한건도 조회되지 않는다.

-- transaction commit(= end transaction);
sqlite> begin transaction;
sqlite> insert into trans values('goodbye', 20);
sqlite> end transaction;
sqlite> select * from trans;
one|two
goodbye|20

sqlite3_get_autocommit() 인터페이스는 주어진 데이터베이스 연결이 자동 커밋 모드에 있으면 0이 아닌 값을, 그렇지 않은 경우에는 0을 반환한다. sqlite3 DB는 자동 커밋 모드가 기본적으로 켜져 있다. 자동 커밋 모드는 BEGIN 문에 의해 제어된다.

그러므로, 프로그램 작성 시 transaction을 제어하도록 해야 한다.

아래는 java 프로그램으로 transaction을 제어하는 예제이다.

SQLiteConnection con = new SQLiteConnection();
con.exec("BEGIN");
// do transaction work - auto-commit is disabled
con.exec("COMMIT");
// auto-commit is enabled again

 

 

DB 계정

사용자 이름/비밀번호는 sqlite/sqlite3 패키지에서 지원되지 않는다.

인증이 필요한 데이터베이스를 구성할려면 PHP와 별도로 SQLite를 컴파일해야 한다.(SQLite User Authentication)
자세한 내용은 SQLite 사용자 인증 문서를 참조한다.

 

DB접속 종료

sqlite> 프롬프트에서 .quit를 입력하여 프로그램을 종료하거나 .exit CODE 를 입력하면 return-code 로 프로그램을 종료한다.

여기서 CODE 부분은 sqlite 종료 시 리턴값으로 사용될 부분이므로 원하는 값으로 정의해서 사용하면 된다. 

Window인 경우에는 Control-C를 Linux인 경우에는  Control-D를 입력하여 sqlite3 프로그램을 종료할 수도 있다.

sqlite DB는 서버리스 DB이기 때문에 접속이 종료되면 process가 보이지 않는다. 

 

DB Dump

명령창에서 아래와 같이 SQLite 명령어를 치면 데이터베이스를 export할 수 있다.

Syntax : sqlite3 db명 .dump > dump파일명

$sqlite3 firstDB.db .dump > firstDB.sql

위의 명령은 firstDB.db 데이터베이스의 전체 내용을 SQLite 문으로 변환하고 ASCII 텍스트 파일 firstDB.sql로 덤프합니다. 생성된 firstDB.sql에서 다음과 같은 방법으로 복원을 수행할 수 있습니다.

$sqlite3 firstDB.db < dirstDB.sql

 

참조 : https://www.tutorialspoint.com/sqlite/sqlite_explain.htm

'SQLite' 카테고리의 다른 글

SQLite DB 관리 툴 - DB Browser for SQLite  (0) 2021.12.31
SQLite 란  (0) 2021.12.28

일반 DB인 경우

AWS Oracle RDS 에서는 X$ 테이블에 접근할 수 있는 권한이 없어, 기존에 hidden parameter 를 조회했던 아래 SQL 이 수행되지 않습니다. sys 계정으로 조회해야 하나, AWS Oracle RDS DB로 sys 계정 접근이 불가합니다.

AWS RDS가 아닌 일반 DB인 경우 아래 query를 수행하여 hidden parameter 를 조회합니다. hidden_parameter_name 부분에 조회를 원하는 파라미터명을 입력하면 됩니다.

SELECT KSPPINM
     , KSPPSTVL
  FROM X$KSPPI X, X$KSPPCV Y
 WHERE X.INDX = Y.INDX
   AND X.KSPPINM LIKE '%hidden_parameter_name%'
   AND SUBSTR(X.KSPPINM, 1, 1) = '_'

 

AWS Oracle RDS hidden parameter 조회

dbms_utility.get_parameter_value function을 이용해서 AWS Oracle RDS hidden parameter를 조회하는 프로시저입니다. 프로시저를 실행하면 hidden 파라미터 입력 프롬프트가 나타나고 원하는 hidden 파라미터명을 입력하면 hidden 파라미터값(value)이 출력됩니다.

  • 파라메터 값(Value) 이 숫자형 또는 Boolean형 이면 intval 에 출력, 문자형이면 strval 에 출력 또는 String인 경우에는 파라미터값의 길이(Boolean형 일경우 intval 이 1이면 True, 0이면 False )
  • RETURN ( INTEGER/BOOLEAN 파라미터면 0, string/file 파라미터면 1 )
set serveroutput on 
set lines 200

prompt Enter a value for parameter
DECLARE
	intval BINARY_INTEGER;
	strval VARCHAR2(256);
	partyp BINARY_INTEGER;
	intval2 VARCHAR2(12);

BEGIN
	dbms_output.put_line(chr(13));
	dbms_output.put_line('+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-');

	partyp := dbms_utility.get_parameter_value('&1',intval, strval);
	
	IF partyp = 1 THEN
		dbms_output.put('parameter value is: ');
		dbms_output.put_line(strval);
		dbms_output.put_line('parameter type is: STRING/FILE parameter');
		dbms_output.put('parameter value length is: ');
		dbms_output.put_line(intval);
	ELSE
		dbms_output.put('parameter value is: ');

		IF intval=0 THEN intval2:='FALSE';
			dbms_output.put_line(intval||'/'||intval2);
			dbms_output.put_line('parameter type is: INTEGER/BOOLEAN parameter');
		ELSIF intval=1 THEN intval2:='TRUE';
			dbms_output.put_line(intval||'/'||intval2);
			dbms_output.put_line('parameter type is: INTEGER/BOOLEAN parameter');
		ELSE 
			dbms_output.put_line(intval);
			dbms_output.put_line('parameter type is: INTEGER/BOOLEAN parameter');
		END IF;

	END IF;
	dbms_output.put_line('+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-');
	END;
/

 

간단한 프로시져로 v_parnam 에 조회하고자 하는 파라미터명을 입력한다.

아래 프로시저는 hidden 파라미터인 _cursor_obsolete_threshold  를 조회하는 예제이다.

declare

v_parnam varchar2(100) := '_cursor_obsolete_threshold' ;
v_intval binary_integer ;
v_strval varchar2(100) ;
v_ret varchar2(1000);

begin

v_ret := dbms_utility.get_parameter_value (
parnam => v_parnam,
intval => v_intval,
strval => v_strval
);

dbms_output.put_line(v_parnam||' : ');
dbms_output.put_line('  Type     : '||v_ret);
dbms_output.put_line('  INT Val  : '||v_intval);
dbms_output.put_line('  STR Val  : '||v_strval);

end;
/

'AWS > RDS' 카테고리의 다른 글

AWS aurora Serverless ACU Scaling  (0) 2023.10.06
AWS RDS Proxy란  (0) 2023.08.01
AWS RDS - Restore to point in time  (0) 2023.07.21

X Window 시스템 아키텍처는 X 서버와 X 클라이언트를 별도의 시스템에서 실행할 수 있도록 하며 WSL의 경우 X 서버는 OpenGL 가속을 제공하는 Windows 서버가 되며 클라이언트는 WSL 환경에서 실행되는 그래픽 Linux 응용 프로그램이 됩니다.

 

윈도우용 X서버 설치

아래 Windows용으로 사용 가능한 여러 X 서버 중 하나를 설치하고 실행합니다. 본문서에서는 MobaXTerm을 설치했습니다.

MobaXTerm을 설치 후 실행하면 아래 그림과 같이 좌측에 WSL-Ubuntu session이 보이는데,

더블 클릭하여 화면과 같이 WSL(X 클라이언트)로 접속합니다.

MobaXTerm WSL 접속화면

이제 Ubuntu(WSL)에서 실행할 그래픽 응용프로그램을 설치합니다. 본문서에서는 xclock을 설치합니다.

xclock은 DISPLAY 변수가 제대로 설정되었는지 테스트하는 편리한 도구이며, "xclock" 명령을 실행하면 GUI 기반 시계 화면이 나타납니다. 

xclock 설치

 WSL에는 xclock가 설치되어 있지 않으니,

WSL에 접속한 후 apt-get install x11-apps 명령어를 실행하여 Ubuntu용 X Window 응용프로그램인 xclock을 설치합니다.

sudo apt-get install x11-apps

xclock 실행

 WSL에는 xclock가 설치되었으니, xclock 명령어를 실행합니다. 화면에 그래픽 시계가 나타납니다.

xclock

X서버에 연결이 안될 때

그래픽 응용 프로그램이 실행 중인 X 서버에 연결할 수 없는 경우 응용 프로그램(xclock)을 실행하기 전에 터미널에 다음 명령을 붙여넣거나 ~/.bashrc에 추가하여 X 클라이언트 환경을 설정합니다.

export DISPLAY=:0 # in WSL 1
export DISPLAY=$(awk '/nameserver / {print $2; exit}' /etc/resolv.conf 2>/dev/null):0 # in WSL 2
export LIBGL_ALWAYS_INDIRECT=1

 

WSL에서 GUI 프로그램 실행

이제 WSL에서 그래픽 애플리케이션을 수행할 수 있는 환경을 확인했으니, Linux GUI 프로그램을 수행하여 GUI 프로그램을 사용할 수 있습니다.  본문서에서는 nautilus을 설치합니다.

sudo apt install nautilus

 

nautilus 명령어로 실행한 화면입니다. 실행하면서 몇 개의 오류 메시지가 뜨는데 아래와 같이 실행은 됩니다.

nautilus

 

 

참고 :

XServer(예: 저장된 config.xlaunch를 사용하는 vcxsrv.exe)를 시작하고 Windows DOS 배치 파일에서 XWindows 세션을 시작할 수 있습니다. 이 방법을 사용하면 DOS ipconfig를 사용하여 가상 이더넷 WSL2 IP를 결정하고 세션을 시작하기 전에 DISPLAY 환경 변수를 설정할 수 있습니다. 이것은 가상 이더넷 IP가 Windows를 재부팅할 때마다 변경되기 때문에 유용합니다(따라서 위의 DISPLAY에서 awk를 사용해야 함). 배치 파일의 이름을 지정하고(이 배치 파일 코드 주석 참고) config.xlaunch 파일이 있는 동일한 디렉토리(이 스크립트는 c:\bin\)에서 실행하세요.

@echo off

rem Works ONLY if Ethernet adapter vEthernet (WSL): is the LAST "adapter" in
rem the ipconfig output; that is, this for loop assigns the IPs from first to
rem last from the ipconfig printed output, so the WSL virtual adapter must be the 
rem last one in the ipconfig output for this to work correctly.
rem If you have excellent DOS batch file skills, you may find a better
rem way of doing this.

for /f "tokens=1-2 delims=:" %%a in ('ipconfig^|find "IPv4"') do set ip==%%b
set ipAddress=%ip:~1%

rem Trim whitespace

call :TRIM %ipAddress% ipAddress

rem Use this for testing

rem echo IP Address: %ipAddress%

rem Start up the xServer using saved xlaunch configuration
rem This configuration creates a single large window

start /B config_one_window.xlaunch

rem Start up PulseAudio server, will accept audio from external
rem source, i.e., from the X server 

start "" /B "C:\bin\pulseaudio-5.0-rev18\pulse\pulseaudio.exe"

rem Call ubuntu.exe, have it run what is essentially a startup 
rem script that sets two ENV variables using the ipAddress variable
rem created above, which is the virtual IP to which services
rem INSIDE the running X server can connect; these are the
rem link between WSL2 and Windows 10
rem Initiate an xfce4 session with dbus-launch
rem when the X server is killed, do cleanup by 
rem killing pulseadio and vcxsrv

<PATH_TO_UBUNTU_EXE>\ubuntu.exe run "export DISPLAY="%ipAddress%:0.0"; export PULSE_SERVER="%ipAddress%"; if [ -z \"$(pidof xfce4-session)\" ]; then  dbus-launch --exit-with-session ~/.xsession; pkill '(gpg|ssh)-agent'; taskkill.exe /IM pulseaudio.exe /F; taskkill.exe /IM vcxsrv.exe; fi;"

rem Function to trim white space or the export VARIABLE will fail

:TRIM
SET %2=%1
GOTO :EOF

 

 

'WSL' 카테고리의 다른 글

VS Code 아키텍처 - 수정  (0) 2022.01.02
Windows WSL2 아키텍처  (0) 2022.01.02
Remote - WSL  (0) 2022.01.02
Remote development in WSL  (0) 2022.01.02
WSL에 go 설치하기  (0) 2022.01.02

컬럼에 히스토그램을 삭제하고 자동 통계가 생성되지 않도록 하려면?

 

데이터 분포가 왜곡된 ID라는 VARCHAR2 열이 있는 테이블을 가지고 있다고 할 때 이 테이블에 대해 자동 통계 작업이 시작되면 거의 모든 SQL 문에서 사용되며 데이터 왜곡이 있기 때문에 이 열에 히스토그램이 자동으로 생성됩니다. 그러나 ID 열의 값은 매우 길고 각 ID의 처음 32자는 동일할 데이터라고 가정합니다.(데이터베이스는 Oracle Database 11g임).

Oracle이 VARCHAR2 열에 히스토그램을 생성할 때 Oracle Database 12c에서 열 값의 처음 64자 또는 Oracle Database 11g에서 열 값의 처음 32자만 고려합니다. 즉, 열 값의 처음 32/64바이트가 동일하면 실제로 값이 다르고 다른 히스토그램 버킷에 있는 것으로 가정하더라도 모든 값이 히스토그램의 단일 버킷에 포함됩니다. Optimizer는 이 히스토그램을 사용할 때 잠재적으로 카디널리티를 잘못 추정할 수 있으며, 이는 차선의 계획을 초래할 수 있습니다. 고유 값 및 기타 통계의 수는 정확하지만 히스토그램에만 이 문제가 있습니다. 이러한 이유로 현재 또는 미래에는 이 특정 열에 히스토그램을 사용하지 않는 것이 좋습니다. 그렇다면 기존 히스토그램을 삭제하고 향후 수집되지 않도록 하려면 어떻게 해야 할까요?

1. 먼저 ID 열에 히스토그램을 삭제합니다.

DBMS_STATS.DELETE_COLUMN_STATS 프로시저를 사용하여 col_stat_type 매개변수를 'HISTOGRAM'으로 설정하면 됩니다.

BEGIN
   dbms_stats.delete_column_stats(
         ownname=>'SH', tabname=>'SALES', colname=>'PROD_ID', col_stat_type=>'HISTOGRAM');
END;

 

2. DBMS_STATS.SET_TABLE_PREF 프로시저를 사용하여 PROD_ID 컬럼을 제외한 SALES 테이블의 모든 컬럼에 대해 히스토그램을 수집하도록 합니다. 아래 프로시저를 수행하면 PROD_ID 컬렁에 대한 히스토그램을 생성하지 않습니다. 하나의 버킷만을 생성하므로 통계 정보로써의 의미가 없도록 하는 것입니다.

BEGIN
   dbms_stats.set_table_prefs('SH', 'SALES',
   'METHOD_OPT', 'FOR ALL COLUMNS SIZE AUTO, FOR COLUMNS SIZE 1 PROD_ID');
END;
/

 

컬럼에 히스토그램을  자동 통계가 생성하도록 하려면?

 

 

자동 통계 수집 작업이 PORD_ID 컬럼에 히스토그램을 다시 생성도록 하려면 아래와 같이 DBMS_STATS.SET_TABLE_PREF 프로시저를 사용합니다. Oracle 이 자동으로 통계 정보에 대한 버킷을 생성하도록 합니다.

BEGIN
   dbms_stats.set_table_prefs('SH', 'SALES',
   'METHOD_OPT', 
   'FOR ALL COLUMNS SIZE AUTO, FOR COLUMNS SIZE AUTO PROD_ID');
END;
/

 

2021/12/28 오전 10:35분경 업무가 정상 처리가 안된다는 연락을 받고

최초 테이블 Lock을 의심해서 Lock 정보를 조회하려 DB에 접속 시도를 했으나, 접속이 되지 않았다.

몇 번의 접속 시도를 하는데 갑자기 접속이 되어 DB를 보니 다른 Muiti-Zone으로 failover가 된 상태였다.

DB 시작시간을 확인해 보니 2021/12/28 오전 10:39:09 였다. 장애 발생 이후 시간이다.

 

이후 AWS RDS의 모니터링과 Performnace Insights 를 살펴보았다.

특정 시점에서 평소 5미만인 Active session이 478까지 증가하였고,

그 시점의 SQL이 library cache lock, library cache : mutex X, regmgr CPU quantum, kksfbc child completion, CPU 지표들이 상당히 높계 계측되었다.

 

CloudWatch가 수집한 서버 모니터링 정보를 보니,

CPU, DB Connections, Write IOPS, read IOPS 등의 장애 시점에 평소 대비 높았다.

특히 Freeable Memory는 장애 시점에 0으로 여유 메모리가 없는 상황이 발생했다. 이런 상황에서 AWS에서 RDS를 장애로 판단하고 Failover 시킨 것으로 보인다.

 

일단, SQL 을 추출하여 Plan 정보를 확인해 보았다. SQL문은 Bind 변수를 사용하였다.

특별한 점은 없어 보였다.

library cache lock event 로 보아, 장애 시점에 DDL이 있었을 것이라고 판단하고 object 들의 생성일자와 수정일자를 살펴보았다. 그 시점에 생성이나 변경된 object는 보이지 않았다. ( 생성하고 삭제했을 경우에는 알수가 없을까?)

 

장애 원인으로 가정한 아래 세가지 상황은 아니다. 

1) 테이블 LOCK

2) library cache lock 이 발생한 SQL 관련 object의 생성이나 수정

3) 과도한 SQL 문장(bind 변수없이 literal 사용 포함)

 

그래서 library cache lock, library cache : mutex X  2가지 event를 추적해 보았다.

 

첫번째 왜 이런 이벤트(library cache lock, library cache : mutex X)가 발생하는가?

library cache lock 은 

  • SQL 구문을 파싱할 때나 LCO를 참조할 때 획득해야 하는 Lock으로 library cache lock 획득하지 못하여 대기하며 발생하는 이벤트이다.

library cache : mutex X

  • shared pool에서 cursor를 검색할 때 mutex를 획득하지 못하여 대기하며 발생하는 이벤트이다

결론은 child cursor가 너무 많아 검색 시간이 오래 걸리면서 다른 세션들이 library cache lock, library cache : mutex X 를 획득하는데 대기하는 이벤트가 발생한 것으로 판단해 볼 수 있다.

 

장애 시점의 child cursor 에 대한 상세 정보를 추적해 본다.

 

1. snap_id 확인

dba_hist_snapshot 에서 장애 발생 시점의 snap_id를 확인해 보았다.

 

- 장애 직전 snapshot : 649

- 장애 이후 snapshot : 650

DB 기동 시간 : 2021/12/28 AM 10:39:09.000

 

2. sql_id와 SQL 관련 정보 확인

SQL fulltext와 sql_id 등의 정보를 얻고자 v$sql 을 조회하려고 했으나 v$sql은 dynamic performance view로 DB가 기동된 이후의 정보만 조회할 수 있으므로 failover된 상태라 dba_hist_sqltext과 dba_hist_sqlstat 뷰를 조회했다.

dba_hist_sqltext 에서 sql_id를 추출했다.

추출한 sql_id로 dba_hist_sqlstat 을 조회했다. 특이사항을 확인해 본다.

plan_hash_value 가 0 인 경우가 많다.

 

메모리 부족현상이 있었으니 sharable_mem 컬럼과 주의깊게 살펴볼 필요가 있는 dba_hist_sqlstat 테이블의 컬럼(loaded_versions, version_count, fetches_total, fetches_delta, executions_total, executions_delta, invalidations_total, invalidations_delta, parse_calls_total, parse_calls_delta, buffer_gets_total, buffer_gets_delta, rows_processed_total, rows_processed_delta, elapsed_time_total, elapsed_time_delta) 등을 선정해 분석해 본다.

 

장애 시점에 sharable_mem 값이 상당이 높게 나왔다. 그러나 다른 날의 자료와 비교해 볼 때 상대적으로 특이사항은 보이지 않는다.

version_count 값을 조회해 보니 상당이 높게 나왔다. 다른 날 대비 약간의 이상 징후가 보인다.

parse_calls_delta 값의 추이도 특이사항이 보이지 않는다.

elapsed_time_delta 값의 추이도 장애 시점 상 특이사항이 보이지 않는다.

 

1차 dba_hist_sqlstat 으로 분석한 결과, version_count 에 대한 분석이 필요한 것으로 판단된다.

 

* 특이사항으로는 위 컬럼의 정보들이 수집되지 않았다. NULL 값들이 상당히 많다 --> 왜 정보를 수집하지 못했는지에 대한 확인이 필요해 보인다. 정보수집을 못했을 때에 version_count가 상대적으로 상당히 값이 크다.

 

<참고사항>

장애 시점에 서버 메모리 사용률이 100% 이었는데 이는 어떻게 설명되는가? 아래 이미지를 보면 이해할 수 있을 것이다. 

(1) 커서란?

공유 커서 (shared cursor) : 라이브러리 캐시에 공유돼 있는 Shared SQL Area
세션 커서 (session cursor) : Private SQL AREA에 저장된 커서
애플리케이션 커서 (application cursor) : 세션 커서를 가리키는 핸들

추가적으로, 대기 이벤트들이 발생하면서 Active session이 급격히 증가하였고

이는 서버 프로세스를 생성하며 메모리를 소진시키는 원인이 되었다. 

 

1차 결론은

version_count 때문에 장애가 발생한 것을 추정하고 이 값을 중심으로 분석해 본다.

version_count는 무엇인가?  왜 version_count가 높게 나오는 것일까? 

 

version_count란

SQL은 한번 파싱되면 실행계획을 생성하고 재사용하기 위해서 실행정보를 생성한다. ( 이를 LCO: library cache object 형태로 저장되며 shared cursor 라고 합니다.)

그리고 메모리(Shared pool 안의 library cache 안의 Shared SQL Area)에 저장합니다.

동일한 SQL 이더라도 여러가지 이유(주1)로 실행정보를 공유하지 못하면 추가적인 Chlid LCO ( child cursor )를 생성합니다.

즉, 새로운 버전의 child cursor 가 생성되는 것입니다. 이를 version count 가 증가한다고 표현합니다.

 

하나의 SQL(parent cursor)에 대해서 여러 child cursor 가 생기면 가장 문제가 되는 것은

SQL 수행시에 어떻게 실행할지 실행정보를 탐색하는데 오래걸리고,

경합이 발생할 수 있다는 점과 (이 때 발생하는 Wait Event를 library cache latch 라고 합니다.)

메모리의 공간을 차지한다는 점입니다.

 

아래 MOS 문서도 참고할 만합니다.

Troubleshooting: High Version Count Issues (Doc ID 296377.1)

https://support.oracle.com/epmos/faces/DocumentDisplay?id=296377.1

High SQL Version Counts - Script to determine reason(s) (Doc ID 438755.1)

https//support.oracle.com/epmos/faces/DocumentDisplay?id=438755.1

하나의 SQL 문장이 여러 개 Child 커서를 갖게 되는 이유

1. SQL에서 참조하는 오브젝트명이 같지만 SQL을 실행한 사용자에 따라 다른 오브젝트를 가리킬 때 - N/A

2. 참조 오브젝트가 변경돼 커서가 무효화되면 이후 그 커서를 처음 사용하려는 세션에 의해 다시 하드파싱돼야 하는데

   , 특정 세션이 아직 기존 커서를 사용 중(Pin)일 때 - N/A

3.  옵티마이저 모드를 비롯해 옵티마이저 관련 파라미터가 다를 때

Optimizer mismatch : The optimizer environment does not match the existing child cursor

 [ Optimizer Mode 외에 옵티마이져 관련 파라미터 불일치시 ]

4.  입력된 바인드 값의 길이가 크게 다를 때

5.  NLS 파라미터를 다르게 설정했을 때 - NA

6.  SQL trace를 활성화했을 때 - N/A

 

위의 사유 중 해당 사항을 찾기 위해

version_count가 높게 나온 이유를 파악하기 위해서 V$SQL_SHARED_CURSOR 조회한다.

mismatch 컬럼이 약 61개 정도라 일일이 'Y'인 값을 파악하는 것이 쉽지 않다.

 

아래 SQL을 수행하면 sql 문장이 출력되는데

출력된 SQL의  '&v_sql_id' 에 자신의 'sql_id' 를 입력한 후 query를 수행하면 mismatch 컬럼값이 'Y'인 것을 count 해서 보여준다.

select q'<select reason_not_sharing, count(*)
 from gv$sql_shared_cursor
 unpivot ( flag for reason_not_sharing in (>' || listagg(column_name, ', ') within group (order by null) || q'<) )
 where sql_id = nvl('>' || '&' || q'<v_sql_id', sql_id)
   and flag = 'Y'
  group by reason_not_sharing>' x
 from dba_tab_columns
 where owner = 'SYS'
   and table_name = 'V_$SQL_SHARED_CURSOR'
   and data_type = 'VARCHAR2'
   and data_length = 1;
   
   -------- Results ------
REASON_NOT_SHARING:COUNT(*)
USE_FEEDBACK_STATS:47775
BIND_EQUIV_FAILURE:47787
LOAD_OPTIMIZER_STATS:6

커서가 공유되지 않은 이유로 USE_FEEDBACK_STATS, BIND_EQUIV_FAILURE 로 조회되었다.

 

위의 query에서 찾은 컬럼으로 다시 아래 SQL로 reason까지 같이 조회해 본다.

SQL> 
select BIND_EQUIV_FAILURE, use_feedback_stats,  substr(s, instr(s, '<reason>')+8, instr(s, '</reason>') - (instr(s, '<reason>') + 8) ) res, count(*)
from 
(
select BIND_EQUIV_FAILURE, use_feedback_stats, DBMS_LOB.substr(reason, 100, 1) s
from v$sql_shared_cursor where sql_id = '69v32zxbv1mzk'
) 
group by BIND_EQUIV_FAILURE, use_feedback_stats, substr(s, instr(s, '<reason>')+8, instr(s, '</reason>') - (instr(s, '<reason>') + 8) )
order by 1;

------- Results --------------------------------
BIND_EQUIV_FAILURE:USE_FEEDBACK_STATS:RES:COUNT(*)
Y:N:Bind mismatch(24):6
Y:N:Bind mismatch(25):6
Y:Y:Bind mismatch(22):564
Y:Y:Bind mismatch(24):1145
Y:Y:Optimizer mismatch(13):46183

커서가 공유되지 않은 이유로 USE_FEEDBACK_STATS, BIND_EQUIV_FAILURE이고 reason은 Bind mismatch와 Optimizer mismatch 로 조회되었다.

 

우선 bind mismatch가 발생한 사유를 알아보자.

BIND_MISMATCH : The bind metadata does not match the existing child cursor.

Oracle은 varchar2 TYPE을 32, 128, 2000, 4000 구간으로 나누어 bind 값을 사용하기 때문에 bind값의 길이에 따라 bind mismatch 가 발생한다. 그러나, 우리의 경우에는 bind를 4개 사용했고 각각 varchar2(27), varchar2(27), varchar2(1000), varchar2(1000) 이었다. 경우의 수를 따져도 9개(1*1*3*3)이다. 즉, 순수 bind 값의 길이 때문에 child cursor 가 새로 생겼다고 보기는 어려울 것 같다.

 

커서가 공유되지 않은 원인으로 도출된 두 개의 값에 대해 분석해 본다.

BIND_EQUIV_FAILURE = 'Y'

USE_FEEDBACK_STATS = 'Y' 

 

이 두 값이 의미하는 것이 무엇인지 확인해 본다.

  • USE_FEEDBACK_STATS : 카디널리티 피드백. 카디널리티 피드백이 사용 중이고 현재 실행에 대한 새 계획이 생성될 수 있다.
  • BIND_EQUIV_FAILURE : 바인드 값의 선택도가 기존 자식 커서를 최적화하는 데 사용된 선택도와 일치하지 않습니다. adaptive cursor sharing이 설정되어 있고 커서가 바인드를 인식할 때 선택도가 현재 범위를 벗어나 새로운 Plan이 더 요구되는 경우 이전 Plan을 공유하지 않고 새로운 child cursor 생성합니다. 오라클 문서 836256.1을 참조하십시오.  
  •  
  •  
  • ... once the cursor is marked as bind aware and a second plan is seen then the following will be the resultant output:
    SQL_ID        ADDRESS          CHILD_ADDRESS    CHILD_NUMBER B
    ------------- ---------------- ---------------- ------------ -
    19sxt3v07nzm4 000000007A1C0DE0 000000007A1BF980            0 N
    19sxt3v07nzm4 000000007A1C0DE0 000000007A10DDB0            1 Y

    As can be seen, the new version is created due to BIND_EQUIV_FAILURE
  • select sql_id, address, child_address, child_number, BIND_EQUIV_FAILURE from v$sql_shared_cursor where sql_id='19sxt3v07nzm4';

v$sql_cs_selectivity 조회

select count(*)
from v$sql_cs_selectivity 
where sql_id='69v32zxbv1mzk' ;  

--> 102052


select predicate, range_id, low, high, count(*)
from V$SQL_CS_SELECTIVITY where sql_id = '69v32zxbv1mzk'
group by predicate, range_id, low, high;

PREDICATE RANGE_ID     LOW      HIGH     COUNT(*)
--------------------------------------------------
=1	        0	    0.301835	0.368910	2800
=1	        0	    0.296330	0.368910	1
=2	        0	    0.301827	0.368899	10633
=1	        0	    0.296330	0.368899	1
=1	        0	    0.296330	0.362181	32339
=2	        0	    0.296330	0.362181	32339
=2	        0	    0.296330	0.368910	1
=1	        0	    0.301827	0.368899	10633
=2	        0	    0.301835	0.368910	2800
=2	        0	    0.296330	0.368899	1

선택도 범위(V$SQL_CS_SELECTIVITY)가 많으며 겹친다. 

 

 

BIND_EQUIV_FAILURE 상세 설명

커서가 바인드를 인식하게 되면 이 커서가 실행될 때마다 확장 커서 공유 레이어 코드(Extended Cursor Sharing layer code)가 바인드 변수 값(본 문서의 경우에는 4개의 바인드 변수가 있음)을 보고(Peeking)  v$sql_cs_selectivity에 대한 select를 실행한다. 기존 child 커서가 Peeking한 바인드 변수의 선택도를 수용하고 있는지 확인하기 위해서이다. 자식 커서가 발견되면 공유하고 그렇지  않은 경우 새 child 커서가 바인드 변수 값 선택성의 새 범위와 함께 v$sql_cs_selectivity에 입력된다.
이 경우 확장 커서 공유 레이어 코드(Extended Cursor Sharing layer  code)가 v$sql_cs_selectivity에서 적절한 선택 범위(BIND_EQUIV_FAILURE)를 가진 자식 커서를 찾지 못하면 새로운 실행계획을 컴파일하고 다수의 "optimal" plan으로 v$sql 뷰를 채운다.

체크사항 :

  • V$SQLAREA에서 커서의 VERSION_COUNT가 높다.
  • 커서를 공유하지 않는 이유가 V$SQL_SHARED_CURSOR에 BIND_EQUIV_FAILURE로 표시된다.
  • 쿼리에는 커서 공유와 관련된 바인드 변수를 사용했다.
  • 선택도 범위(V$SQL_CS_SELECTIVITY)가 많으며 겹친다. 
  • 동시성이 높은 경우 문제가 "커서: 핀 S는 X에서 대기"로 표시되며 커서에 대해 대기한다.

위의 경우에 해당될 경우 버그로 의심할 수 있다. 그럴 경우 workaroud로 다음 설정을 권고한다. 본 문서가 오라클 공식 문서가 아니기에 적용 시 반드시 Oracle의 지원을 받아 진행해야 한다. 

Workaround : _optimizer_extended_cursor_sharing_rel = none    으로 설정

 

결국은 optimizer_adaptive_cursor_sharing 때문인 것 같다.

DB 전체에 대해 사용하지 않도록 설정하던지, 아니면 문제를 일으키는 SQL 관련 테이블의 컬럼에 대해서만 설정하던지.

일단, 후자를 선택해 진행해 본다.

대상 테이블 컬럼의 histogram 을 삭제하고, 더 이상 histogram을 생성하지 않도록 설정한다.

상기 장애와 관련하여 최선의 선택이 아닐까 한다. 물론 다른 부작용도 검토를 해야 한다.

 

 

Disable the Adaptive cursor sharing in Oracle

alter system set "_optimizer_adaptive_cursor_sharing"=false scope= both;

--Disable bind variable or other parameter
alter system set "cursor_sharing"=exact scope=both;
alter system set "_optimizer_extended_cursor_sharing_rel"=none scope=both;
alter system set "_optimizer_extended_cursor_sharing"=none scope= both;

 

Three 11g features of cost-based optimization and SQL execution to consider when upgrading from 10g:


Cardinality feedback. This feature, enabled by default in 11.2, is intended to improve execution plans for repeated executions. See Support note 1344937.1 and documentation for additional info. You can be disable cardinality feedback with an underscore parameter which can be set at session or instance level. Usual caveats on setting underscore parameters apply:

alter session set "_OPTIMIZER_USE_FEEDBACK"=FALSE;
This can also be done with a hint, therefore at statement level, using the opt_param syntax: /*+ opt_param('_OPTIMIZER_USE_FEEDBACK','FALSE') */


Adaptive cursor sharing. This was introduced in 11gR1 to address SQL performance consistency issues related to bind variable peeking.  See support note 740052.1 and documentation for details. For the purposes of this doc, if you want to get the 10g behavior with  an Oracle version higher than 10g and overall if you want to disable this feature, you may want to add the hint:
/*+ NO_BIND_AWARE */. 
According to support note 11657468.8 adaptive cursor sharing can be disabled by setting the following 2 parameters (say at session level): _optimizer_adaptive_cursor_sharing = false_optimizer_extended_cursor_sharing_rel = "none"

 

For more details on this bug, the Document ID of MOS is 14176247.8

https://support.oracle.com/epmos/faces/DocumentDisplay?_afrLoop=561615465291525&parent=EXTERNAL_SEARCH&sourceId=PROBLEM&id=1621829.1&_afrWindowMode=0&_adf.ctrl-state=j1i7joth3_4

참조 : https://www.linkedin.com/pulse/oracle-cursor-sharing-bug-diego-zucca

 

위에서 발생한

kksfbc child completion 이벤트에 대해 _kks_use_mutex_pin =  false 로 설정하라는 글들이 있는데

이  hidden parameter는 Oracle 동의가 있어야만 사용해야 하며 이 매개변수는 Oracle 12c에서 설정하면 안 됩니다." 라는 글도 있으니 참고하기 바랍니다. 본 문서에서는 이 이벤트는 다루지 않는다.

 

 

 

 

 

 

- Version Count가 큰 SQL 확인

 

SQL>SELECT SQL_ID , VERSION_COUNT ,  EXECUTIONS , INVALIDATIONS  

        FROM v$sqlarea 

        where sql_id = '위에서 찾은 sql_id'

        ORDER BY  version_count desc;

69v32zxbv1mzk 4097 54132 0

 

USE_FEEDBACK_STATS이 'Y' 인 경우, 

(Y|N) A hard parse is forced so that the optimizer can reoptimize the query with improved cardinality estimates

옵티마이저가 향상된 카디널리티 추정으로 쿼리를 다시 최적화할 수 있도록 하드 구문 분석이 강제 실행됩니다.

 

 

AWR Report 확인

AWS RDS Oracle Enterprise version에서만 제공한다.

 

ASH Report 확인

 

alert log 파일 확인

 

tarce 파일 확인

 

 

 

 

<참고>

semaphore vs mutex 란?

 

mutex : object, 자물쇠, locking mechanism, multiple program threads 이 single recource에만 접근 가능(동시 접근 불가), 오직 mutex를 acquire/release 할 수만 있다(Locked or unlocked)

화장실 열쇠

 

semaphore : sgnaling mechanism, 신호동, integer(couting semaphore), semaphore value는 update할수 있다.

signal  and wait.

 

 

 

 

 

 

 

카디널리티라는 용어는 두 가지 다른 의미로 사용됩니다. 데이터 모델링에서 사용할 때와 SQL 문에서 사용할 때입니다. 데이터 모델링에서 사용할 때 카디널리티는 한 테이블이 다른 테이블과 가질 수 있는 관계를 나타냅니다. 다대다, 다대일/일대다 또는 일대일의 테이블 간의 관계를 카디널리티라고 합니다. SQL에서 사용할 때는 해당 열에 대한 테이블에 나타나는 고유한 값의 수를 나타냅니다.

 

data modeling 에서의 cardinality 

회사에서 직원 정보를 저장하는 데 사용하는 세 개의 테이블(Employee 테이블, Employee_Salary 테이블 및 Department 테이블)이 있다고 가정합니다. 모든 직원은 하나의 부서에만 속할 수 있지만 부서는 여러 직원으로 구성될 수 있기 때문에 부서 테이블은 Employee 테이블과 일대다 관계를 갖습니다. 즉, 직원 테이블과 관련된 부서 테이블의 카디널리티는 일대다입니다. Employee_Salary 테이블과 관련된 Employee 테이블의 카디널리티는 직원이 하나의 급여만 가질 수 있고 그 반대도 마찬가지이므로 일대일입니다(예, 두 명의 직원이 동일한 급여를 가질 수 있지만 여전히 정확히 하나의 급여 항목이 있습니다. 다른 사람이 동일한 급여를 받는지 여부에 관계없이 각 직원에 대해).

만일, 한명의 직원이 여러 부서에 소속될 수 있다고 한다면 부서와 직원간의 관계는 다대다 관계입니다.

 

SQL에서의 Cardinality 

카디널리티의 다른 정의는 아마도 용어의 더 일반적으로 사용되는 버전일 것입니다.
SQL에서 주어진 테이블에 있는 열의 카디널리티는 해당 열에 대한 테이블에 나타나는 고유한 값의 수를 나타냅니다. 따라서 카디널리티는 숫자라는 것을 기억하십시오. 예를 들어 "남성"과 "여성"이라는 두 가지 가능한 값만 있는 "성별" 열이 있는 테이블이 있다고 가정해 보겠습니다. 그런 다음 해당 열에 나타날 수 있는 고유한 값은 남성과 여성이라는 두 가지뿐이므로 해당 "성별" 열의 카디널리티는 2입니다. Cardinality정보는 Execution Plan 정보에서 볼 수 있습니다.

primary key의 cardinality 

또는 다른 예로 10,000개의 행이 있는 테이블에 기본 키 열이 있다고 가정해 보겠습니다. 해당 열의 카디널리티는 무엇이라고 생각합니까? 자, 10,000입니다. 기본 키 열이기 때문에 열의 모든 값이 고유해야 함을 알고 있습니다. 그리고 10,000개의 행이 있기 때문에 열에 10,000개의 항목이 있다는 것을 알고 있으며, 이는 해당 열에 대해 10,000개의 카디널리티로 변환됩니다. 따라서 기본 키 열의 카디널리티는 항상 동일한 테이블의 레코드 수와 동일하다는 규칙을 생각해낼 수 있습니다

cardinality 가 0 이라는 의미는?

열의 카디널리티가 0이면 열에 고유한 값이 없음을 의미합니다. 이것은 열의 값이 NULL인  경우입니다.

이는 열이 실제로 사용되지 않았음을 의미합니다.

 

 

+ Recent posts