▣오늘의 주요 키워드▣
- SEQUENCE
- SYNONYM(동의어)
- INDEX
- COMMENT
- TRUNCATE
- 분석함수
★ sequence
- 자동일련번호를 생성하는 객체
- Sequence 객체를 생성하려면 create sequence 시스템권한이 필요
select * from session_privs; --sequence권한확인
> SEQUENCE 객체 생성
create sequence id_seq; -- sequence객체 생성
select * from user_sequences where sequence_name = 'ID_SEQ'; --sequence객체 확인
-시퀀스 기본값
create table seq_test(id number, name varchar2(30), day timestamp); --seq_test테이블 생성
insert into seq_test(id,name,day) values(id_seq.nextval,'홍길동', localtimestamp); --nextval : 가상컬럼
select * from seq_test;
insert into seq_test(id,name,day) values(id_seq.nextval,'박찬호', localtimestamp); --nextval : 가상컬럼
select * from seq_test;
select id_seq.currval from dual; --현재 사용한 번호를 리턴
- sequence이름.nextval : 가상컬럼, 현재사용가능한 번호를 리턴해준다.
- sequence이름.currval : 현재사용한 번호를 리턴해준다.
rollback; --롤백
select * from seq_test;
insert into seq_test(id,name,day) values(id_seq.nextval,'홍길동', localtimestamp); --nextval : 가상컬럼
select * from seq_test;
insert into seq_test(id,name,day) values(id_seq.nextval,'박찬호', localtimestamp); --nextval : 가상컬럼
select * from seq_test;
select id_seq.currval from dual; -- 현재 사용한 번호를 리턴
- rollback을 하여 다시 값을 넣었는데 id값이 1,2로 들어간 것이 아니라 3,4로 들어가고 현재사용한 번호는 4가 되었다. 즉 nextval을 사용하여 sequence를 수행하고 롤백해도 번호는 롤백이 되지않고 계속 count된다는 것을 알 수 있다.
> SEQUENCE 삭제
drop sequence id_seq;
select * from seq_test;
select * from user_sequences where sequence_name = 'ID_SEQ';
- sequence는 삭제됐지만 삽입한 값들은 그대로 있다.
create sequence id_seq
start with 1 -- 1부터 시작
maxvalue 3 -- 최대 3까지
increment by 1 -- 1씩 증가
nocycle -- cycle 하면 계속 123123반복
nocache -- 매번 nextval할 때마다 생성하겠다, cache 20
;
select * from user_sequences where sequence_name = 'ID_SEQ'; --last_number : 다음사용번호
insert into seq_test(id,name,day) values(id_seq.nextval,'나얼', localtimestamp); --행 추가
select * from seq_test;
select * from user_sequences where sequence_name = 'ID_SEQ';
- sequence 'id_seq'확인
- 다음번호를 last_number가 2로 알 수 있다.
insert into seq_test(id,name,day) values(id_seq.nextval,'윤건', localtimestamp);
select * from seq_test;
select * from user_sequences where sequence_name = 'ID_SEQ';
insert into seq_test(id,name,day) values(id_seq.nextval,'커리', localtimestamp);
select * from seq_test; --번호가 겹치게됨(키값이 충돌)
select * from user_sequences where sequence_name = 'ID_SEQ';
insert into seq_test(id,name,day) values(id_seq.nextval,'손흥민', localtimestamp);
select * from seq_test;
--nextval꽉차서 오류, maxvalue를 3으로 했기 때문에
> sequence 수정
alter sequence id_seq
maxvalue 100; --id_seq의 maxvalue를 100으로 변경
select * from user_sequences where sequence_name = 'ID_SEQ';
insert into seq_test(id,name,day) values(id_seq.nextval,'손흥민', localtimestamp);
select * from seq_test;
rollback;
> sequence 수정, start with 만 제외하고 다른 옵션들은 수정할 수 있다.
alter sequence id_seq
increment by 2
maxvalue 100
cache 20;
select * from user_sequences where sequence_name = 'ID_SEQ';
drop table seq_test;
drop sequence id_seq;
★ SYNONYM(동의어)
- 긴 객체 이름을 짧은 이름으로 사용하는 객체
- CREATE SYNONYM 시스템권한 필요하다.
- 모든 유저들이 사용할 수 있는 synonym을 생성하려면 create public synonym 시스템권한이 있어야 한다. (dba(sys)에게 권한을 받아야함)
select * from session_privs; --시스템 권한 확인
create table emp_copy_2022 as select * from employees;
select * from emp_copy_2022;
> synonym 생성
create synonym ec2 for emp_copy_2022; -- 나혼자만 사용할 수 있는 synonym, 다른 유저는 사용 불가(권한 필요)
select * from user_synonyms where table_name = 'EMP_COPY_2022';
select * from ec2;
select * from emp_copy_2022;
> synonym 삭제
drop synonym ec2;
select * from user_synonyms where table_name = 'EMP_COPY_2022';
select * from user_tab_privs; --테이블 시스템권한 확인
grant select on hr.employees to ora; --hr안써도 됨,ora유저에게 employees테이블 select할 수 있는 권한 주기
--select on hr.employees하는 유저들에게 내 synonym을 쓰게 하고 싶다?
grant select on hr.departments to ora; --ora유저에게 departments테이블 select할 수 있는 권한 주기
select * from user_tab_privs;
(ora유저 워크시트에서 진행)
select * from user_tab_privs;
select * from hr.employees;
select e.*, d.*
from hr.employees e, hr.departments d --소유자의 테이블을 사용할 때 너무 길지 않느냐
where e.department_id = d.department_id;
- ora유저는 departments, employees테이블을 select할 수 있는 권한을 받음, 그러나 synonym을 사용할 수는 없다.
(hr유저에서)
> public synonym 생성
create public synonym emp for hr.employees; --오류
create public synonym dept for hr.departments; --오류
- synonym을 다른 유저도 사용하도록 하고 싶은 때 create public synonym을 사용하면되는데 create public synonym 또한 권한이 있어야 하기때문에 dba(sys)에게 권한을 받아야한다.
(dba 워크시트에서)
grant create public synonym to hr; --hr유저에게 create public synonym 권한 부여
create public synonym emp for hr.employees; -- public synonym 생성
create public synonym dept for hr.departments; -- public synonym 생성
select * from user_synonyms; -- 내가 만든PUBLIC SYNONYM 정보 확인 불가
select * from all_synonyms where table_owner = 'HR'; -- 내가 만든PUBLIC SYNONYM 정보 확인
(ora 유저 워크시트에서)
select * from all_synonyms where table_owner = 'HR'; --synonym이 아직 남아있더라도 권한이 없으면 사용불가
select * from emp;
select * from dept;
select e.*, d.*
from emp e, dept d
where e.department_id = d.department_id;
(hr유저 워크시트)
> select 권한을 회수
revoke select on hr.employees from ora;
revoke select on hr.departments from ora;
(ora유저 워크시트)
select * from all_synonyms where table_owner = 'HR'; --synonym이 아직 남아있더라도 권한이 없으면 사용불가
select * from emp; --오류
select * from dept; --오류
(hr유저)
> public synonym 삭제
drop public synonym emp; -- drop을 수행하면 오류 발생(drop public synonym 시스템권한이 없기 때문에)
- drop public synonym 또한 마찬가지로 권한을 받아야 수행할 수 있다.
(dba유저)
grant drop public synonym to hr; -- hr에게 drop public synonym 권한주기
> public synonym 삭제
drop public synonym emp; -- drop을 수행하면 오류 발생(drop public synonym 시스템권한이 없기 때문에)
select * from session_privs;
drop public synonym emp; --emp synonym 삭제
drop public synonym dept; --dept synonym 삭제
select * from all_synonyms where table_owner = 'HR'; -- 내가 만든PUBLIC SYNONYM 정보 확인
※optimizer : SQL문을 수행하기 위한 실행계획을 만든다.
select *
from hr.employees
where employee_id = 100;
★ Data access method
책 = 테이블
페이지(page) = block(4k,(8k),16k,32k)
문장 = 행(row)
특정한 단어(오라클) 찾아야 한다.?
> full table scan (특정한 테이블에 특정한 단어를 찾을 때 첫번째 block에서부터 마지막block까지 access하는 방식)
- 테이블 첫 번째 행부터 마지막 행까지 access하는 방식
> rowid scan : 행의 물리적 주소를 가지고 찾는 방식, 데이터 access방법중에 가장 빠른 방법(예)집주소)
1) BY USER ROWID SCAN
select * from employees where rowid = 'AAAEAbAAEAAAADNAAA'; --f10
2) BY INDEX ROWID SCAN
select * from employees where employee_id = 100; --f10
select rowid, employee_id
from employees;
select * from employees where rowid = 'AAAEAbAAEAAAADNAAA';
사원번호 100번 사원의 행주소를 rowid('AAAEAbAAEAAAADNAAA')를 대신 기억해줄래?
rowid : 물리적 row 주소
1)
AAAEAb(6자리) : data object id
select * from user_objects; --DATA_OBJECT_ID가 NULL은 저장공간이 필요없는 테이블
2)
AAE(3자리) : file id
select * from dba_data_file; --오류(dba에서 수행)
3)
AAAADN(6자리) : block id
select * from user_segments where segment_name = 'EMPLOYEES';
select * from user_extents where segment_name = 'EMPLOYEES';
4)
AAA(3자리) : row slot id(찾고싶은 row가 있는 위치)
★ INDEX
- by index rowid scan 방식을 사용해서 검색속도를 높이기 위해서 사용되는 객체
- 인덱스를 이용해서 행을 검색하면 i/o(입력/출력)를 줄일 수 있다.
- 인덱스를 테이블과는 독립적으로 생성된다.
- 인덱스는 오라클이 자동으로 유지관리한다.
- primary key, unique 제약조건을 생성하면 unique index가 자동으로 생성된다.
- 수동으로 생성한다.
select * from user_indexes where table_name = 'EMPLOYEES'; -- 인덱스 확인
--( 예)책의 색인페이지에 장번호가 여러개면 nonunique, 하나면 unique)
--중복되는 값이 있으면 nonunique, 유일한 값이면 unique
select * from user_ind_columns where table_name = 'EMPLOYEES'; --인덱스 컬럼 확인
drop table emp purge; --기존 emp 테이블 삭제
create table emp as select * from employees; --emp 테이블 생성, employees테이블 복제(ctas)
desc emp -- 컬럼 , 타입, 값, not null만 복제됨
select * from user_constraints where table_name = 'EMP'; --제약조건 not null만 복제되고 나머지는 복제x
select * from user_cons_columns where table_name = 'EMP'; -- 제약조건 컬럼
select * from user_indexes where table_name = 'EMP'; --index는 복제되지 않음
select * from user_ind_columns where table_name = 'EMP';--index는 복제되지 않음
select * from emp where employee_id = 100;
--f10번키 누르고 options확인 -> full table scan발생
select rowid, employee_id from emp;
select * from emp where rowid = 'AAAFCSAAEAAAAHjAAA'; -- by user rowid scan (f10)
-full 스캔이 아닌 고유의rowid를 스캔하여 수행 속도가 아주 빠르다.
- primary key 제약조건을 추가 (unique)
alter table emp add constraint emp_id_pk primary key(employee_id);
select * from user_constraints where table_name = 'EMP';
select * from user_cons_columns where table_name = 'EMP';
select * from user_indexes where table_name = 'EMP';
select * from user_ind_columns where table_name = 'EMP';
select * from emp where employee_id = 100; --(f10) by index rowid scan
- 전과는 다르게 by index rowid로 옵션이 생겼다. index조건으로는 employee_id(primary key) = 100으로
EMP_ID_PK 값을 찾고 이 인덱스 값과 같은 ROWID만 EMP테이블에서 찾아 메모리에 저장하고 마지막에 출력하는 구조이다. 밑에서 조금 더 순서있게 설명할 것이다.
select employee_id, rowid
from emp
order by 1;
emp_id_pk 인덱스에는 아래와 같은 정보들이 입력되어 있다.
EMPLOYEE_ID ROWID
100 AAAFCFAAEAAAAI7AAA
101 AAAFCFAAEAAAAI7AAB
102 AAAFCFAAEAAAAI7AAC
103 AAAFCFAAEAAAAI7AAD
104 AAAFCFAAEAAAAI7AAE
105 AAAFCFAAEAAAAI7AAF
.......
select * from emp where employee_id = 100;
-- (f10)by index rowid scan (emp_id_pk가 by index rowid를 가지고 emp테이블을 스캔)
1) 100번에 해당하는 rowid를 emp_id_pk 인덱스에 가서 찾기
2) 찾은 rowid가지고 emp 테이블에 access하고 종료
select * from emp where last_name = 'King'; --full table scan
- full table scan은 테이블을 전부 탐색해서 찾는 구조이며 좋지 않은 방식이다.
> nonunique index 생성
create index emp_last_name_idx on emp(last_name); --last_name은 중복값이 있기에 nonunique조건이 된다.
select * from user_indexes where table_name = 'EMP';
select * from user_ind_columns where table_name = 'EMP';
select * from emp where last_name = 'King'; -- by index rowid scan
select last_name, rowid
from emp
order by 1;
select * from emp where last_name = 'King'; -- by index rowid scan
1) 'King'에 해당하는 rowid를 emp_last_name_idx에 가서 찾는다
2) 찾은 rowid가지고 emp 테이블에 찾아 간다.
select * from emp where rowid = 'AAAFCSAAEAAAAHjAAA';
3) 다시 'King'에 해당하는 rowid를 emp_last_name_idx에 가서 찾는다
4) 찾은 rowid가지고 emp 테이블에 찾아 간다.
select * from emp where rowid = 'AAAFCSAAEAAAAHjAA4';
5) 다시 'King'에 해당하는 rowid를 emp_last_name_idx에 가서 찾는다. 없으면 종료!
6) 결과 집합을 유저한테 전달
> primary key 제약조건 삭제
select * from user_constraints where table_name = 'EMP'; --제약조건 확인
select * from user_cons_columns where table_name = 'EMP';--제약조건 컬럼확인
select * from user_indexes where table_name = 'EMP'; --인덱스 확인
select * from user_ind_columns where table_name = 'EMP'; --인덱스 컬럼확인
alter table emp drop primary key; -- primary key 제약조건 삭제
- nonunique index 생성
create index emp_id_idx on emp(employee_id); -- employee_id의 인덱스 생성
select * from user_ind_columns where table_name = 'EMP';
select * from user_indexes where table_name = 'EMP';
select * from emp where employee_id = 100; -- index range scan
1) 100번에 해당하는 rowid를 emp_id_idx 인덱스에 가서 찾기
2) 찾은 rowid가지고 emp 테이블에 access
3) 다시 100번에 해당하는 rowid를 emp_id_idx 인덱스에 가서 찾아보고 없으면 종료
- index 삭제
drop index emp_id_idx;
select * from user_indexes where table_name = 'EMP';
> unique index 생성
> unique index 생성하게 되면 employee_id 컬럼의 값은 중복되는 값은 입력될 수 없다.
꼭 unique 제약조건을 만든 것 처럼 효과를 준다.
create unique index emp_id_idx on emp(employee_id); --중복되는 값이 있으면 안됨
select * from user_constraints where table_name = 'EMP';
select * from user_ind_columns where table_name = 'EMP';
select * from user_indexes where table_name = 'EMP';
select * from emp where employee_id = 100; -- (f10) by index rowid, unique scan
create table test(id number, name varchar2(30));
create unique index test_id_idx on test(id); --unique인덱스 생성
select * from user_constraints where table_name = 'TEST'; --제약조건을 만들지 않음
select * from user_indexes where table_name = 'TEST';
insert into test(id,name) values(1,user); --user함수는 현재 insert를 수행하는 오라클 유저
select * from test;
insert into test(id,name) values(1,user);
-- id컬럼의 unique index 생성되어 있기때문에 중복되는 값이 들어오면 오류 발생
rollback;
drop table test purge; -- test테이블 삭제
select * from user_indexes where table_name = 'TEST';
--테이블을 삭제하면 그 테이블과 연관된 인덱스도 자동으로 삭제됨
select * from user_ind_columns where table_name = 'EMPLOYEES';
-- EMP_NAME_IX는 인덱스가 2개? -> 조합인덱스
select *
from emp
where last_name = 'King' --인덱스 있음
and first_name = 'Steven'; -- 인덱스 없음
1) 'King'에 해당하는 rowid를 emp_last_name_idx에 가서 찾는다
2) 찾은 rowid가지고 emp 테이블에 찾아 간다.
select * from emp where rowid = 'AAAFCSAAEAAAAHjAAA' and first_name = 'Steven';
3) 이유는 first_name = 'Steven' 체크하기 위해서 emp테이블에 찾아가서 있으면
결과 집합에 그 데이터를 저장하고 없으면 다시 다음 단계를 진행해야 한다.
4) 다시 'King'에 해당하는 rowid를 emp_last_name_idx에 가서 찾는다
5) 찾은 rowid가지고 emp 테이블에 찾아 간다.
select * from emp where rowid = 'AAAFCSAAEAAAAHjAA4' and first_name = 'Steven';
6) 이유는 first_name = 'Steven' 체크하기 위해서 emp테이블에 찾아가서 있으면
결과 집합에 그 데이터를 저장하고 없으면 다시 다음 단계를 진행해야 한다.
7) 다시 'King'에 해당하는 rowid를 emp_last_name_idx에 가서 찾는다. 없으면 종료!
8) 결과 집합을 유저한테 전달
emp_last_name_idx
....
Khoo AAAFCFAAEAAAAI7AAP
King AAAFCFAAEAAAAI7AAA
King AAAFCFAAEAAAAI7AA4
Kochhar AAAFCFAAEAAAAI7AAB
Kumar AAAFCFAAEAAAAI7ABJ
Ladwig AAAFCFAAEAAAAI7AAl
....
select * from user_ind_columns where table_name = 'EMP';
drop index emp_last_name_idx;
> 조합인덱스
create index emp_last_first_name_idx on emp(last_name, first_name);
select * from user_ind_columns where table_name = 'EMP';
select * from user_indexes where table_name = 'EMP';
select last_name, first_name, rowid
from emp
order by 1,2;
emp_last_first_name_idx
....
Khoo Alexander AAAFCFAAEAAAAI7AAP
King Janette AAAFCFAAEAAAAI7AA4
King Steven AAAFCFAAEAAAAI7AAA
Kochhar Neena AAAFCFAAEAAAAI7AAB
....
select *
from emp
where last_name = 'King' --인덱스 있음
and first_name = 'Steven'; -- 인덱스 있음 (last_name, first_name) 조합인덱스
1) last_name = 'King'과 first_name = 'Steven' 값이 같이 존재하는
rowid를 emp_last_first_name_idx에 가서 찾는다
2) 찾은 rowid가지고 emp 테이블에 찾아 간다.
이유는 결과집합을 만들기 위해서 실제 테이블에 찾아간다. --(메모리에 저장 후 3)에서 메모리에 저장된 결과집합을 유저한테 전달)
select * from emp where rowid = 'AAAFCSAAEAAAAHjAAA';
3) 다시 last_name = 'King'과 first_name = 'Steven' 값이 같이 존재하는
rowid를 emp_last_first_name_idx에 가서 찾아보고 없으면 종료한 후 결과집합을 유저한테 전달
★ COMMENT(주석)
-테이블 생성하면 무조건 주석달기(업무에서 어떤테이블에 어떤건지, 주석만 봐도 이런 성격의 테이블이구나 알 정도로 자세히)
-테이블과 컬럼에 주석(설명) 만든다.
drop table emp purge;
create table emp as select * from employees;
- 테이블 주석 생성
comment on table emp is '사원정보 테이블';
- 컬럼 주석 생성
comment on column emp.employee_id is '사번';
comment on column emp.department_id is '부서코드';
- 테이블 주석 내용 확인
select * from user_tab_comments where table_name = 'EMP';
- 컬럼 주석 내용 확인
select * from user_col_comments where table_name = 'EMP';
- 테이블 주석 삭제
comment on table emp is ''; -- 빈문자열로 만들기
- 컬럼 주석 삭제
comment on column emp.employee_id is ''; -- 빈문자열로 만들기
select * from user_segments where segment_name = 'EMP'; --BYTES : 이 테이블의 용량
만약에 특정한 테이블의 데이터를 다 삭제할 것이라면 뭐하러 undo 공간에 저장을 하겠습니까?
★ truncate (현장에서 사용할때 주의하고 또 주의하고 또 주의하면서 사용)
- 테이블의 행을 다 삭제하는 행위는 DELETE 문과 비슷하지만
차이점은 TRUNCATE문은 저장공간을 초기상태로 만든다.
truncate문은 rollback을 할 수 없다.
truncate하는 대상 행은 undo공간에 입력하지 않습니다. 왜? rollback을 할 이유가 없기때문에
※주의
특정한 행을 삭제하려면 delete문을 이용해야 한다.
다시 테이블 전체 행을 삭제하려면 delete문, truncate문 둘 다 사용할 수 있다.
하지만 차이점은 undo 발생하느냐(delete), 안하느냐(truncate)
또 주의 하셔야 할 점은 truncate는 rollback이 안된다. truncate했으면 영구히 행을 삭제함
truncate table emp;
select * from emp;
★ 분석함수
sum(salary) over() : 총합
select sum(salary) --급여 총합 : 691416
from employees;
select employee_id, department_id, salary, 691416, salary - 691416
from employees;
select employee_id, department_id, salary, sum(salary) over(), salary -sum(salary) over()
from employees;
--sum(salary) over()에서 over()를 사용하지 않게되면 그룹핑을 하거나 오류가 발생하는데
over()을 사용하면 각각의 값을 계산할 수 있다.
select employee_id, department_id, salary, round(avg(salary) over()), round(salary -avg(salary) over())
from employees;
-평균함수도 사용가능
select employee_id, department_id, salary, max(salary) over(), salary - max(salary) over()
from employees;
-최대값도 가능
select employee_id, department_id, salary, min(salary) over(), salary - min(salary) over()
from employees;
-최소값도 가능
> 누적합
select employee_id, department_id, salary, sum(salary) over(order by employee_id) 누적합
-- employee_id순서로 누적합을 구하기
from employees;
select e2.employee_id, e2.department_id, e2.salary, e1.sumsal
from (select department_id, sum(salary) sumsal
from employees
group by department_id
order by 1) e1, employees e2
where e1.department_id = e2.department_id
order by 2;
-부서별로 각각의 급여의 누적합을 구하기
select employee_id, department_id, salary, sum(salary) over(partition by department_id) 부서별총합
-- partition by 그룹핑을해서 총합을 구하자
from employees;
select employee_id, department_id, salary, sum(salary) over(partition by department_id order by employee_id) 부서별총합 -- partition by 그룹핑을해서 총합을 구하자
from employees;
-위의 복잡한 식을 sum(salary) over(partition by department_id)로 간단히 구할 수 있다.
select employee_id, department_id, count(*) over(partition by department_id) 부서별인원수
from employees;
select employee_id, department_id, salary,
count(*) over(partition by department_id) 부서별인원수,
max(salary) over(partition by department_id) 부서별최고급여,
min(salary) over(partition by department_id) 부서별최저급여
from employees;
select employee_id, department_id, salary, sum(salary) over(order by employee_id) 누적합
from employees;
--employee_id 순서로 누적합을 구하기
TOP-N
최고 급여자 중에 10위까지 출력
1) 급여를 기준으로 내림차순 정렬
select employee_id, salary
from employees
order by salary desc;
2) 정렬한 결과를 가지고 10위까지 제한을 해야 한다.
- rownum : fetch번호를 리턴하는 가상컬럼
select rownum, employee_id
from employees;
select rownum, employee_id, salary --order by보다 where가 우선순위가 높아서 결과값이 틀리게 나온다
from employees
where rownum <= 10 -- 랜덤하게 10개 뽑아
order by salary desc; -- 랜덤하게 10개 뽑아낸 결과를 가지고 정렬
※주의 : 우리 회사에 똑같은 급여자가 없다라고 하면 아래와 같은 query문을 수행해도 되겠지만
동일한 급여를 받는 사원이 있다고 하면 절대 아래와 같은 query를 이용해서 top-n분석하면 큰일 난다.
올바른 코드
select *
from(select employee_id, salary
from employees
order by salary desc) e
where rownum <= 10;
- rank() : 순위를 구하는 함수, 동일한 순위가 있을 경우 다음 순위는 갭이 생길 수 있다.
- dense_rank() : 순위를 구하는 함수, 동일한 순위가 있더라도 연이은 순위를 구하는 함수
select rank() over(order by salary desc) "rank",
dense_rank() over(order by salary desc) "dense_rank",
employee_id, salary
from employees;
select rank() over(partition by department_id order by salary desc) "부서별 rank",
dense_rank() over(partition by department_id order by salary desc) "부서별 dense_rank",
department_id, employee_id, salary
from employees;
'SQL' 카테고리의 다른 글
2022.1.6 sql 끝 그리고 다시 시작 (0) | 2022.01.06 |
---|---|
2022.1.4 오늘의 공부 (0) | 2022.01.05 |
12.30 오늘의 공부 (0) | 2021.12.30 |
12.29 오늘의 공부 (0) | 2021.12.29 |
12.28 오늘의 공부 (0) | 2021.12.28 |