저번 포스팅에 이어서 서브쿼리(하위 질의)에 대해서 알아보겠습니다. 서브쿼리(Subquery) 서브쿼리는 다른 SQL 쿼리 안에 포함된 SELECT-FROM-WHERE 식 1. Nested Subquery 1.1 정의 중첩 쿼리(Nested Subquery) 는 다른 쿼리 안에 포함된 SELECT-FROM-WHERE 식 SELECT ... FROM ... WHERE column IN ( SELECT ... FROM ... WHERE ... ); 주요 용도는 다음과 같다. 집합 소속 여부 검사: IN , NOT IN 집합과 값 비교: SOME , ALL 결과 집합이 비었는지 검사 EXISTS , NOT EXISTS 결과의 중복 여부 검사 UNIQUE 계산한 결과를 임시 테이블이나 단일 값으로 사용 WITH 1.2 읽는 순서 일반적인 비상관 서브쿼리는 안쪽부터 읽으면 쉽다. 1. 안쪽 SELECT 실행 2. 안쪽 질의가 값 또는 집합 생성 3. 바깥 질의가 그 결과를 조건이나 입력으로 사용 2. IN 과 NOT IN 2.1 IN : 집합에 속하는가? 2017년 가을과 2018년 봄에 모두 개설된 과목을 찾는다. SELECT DISTINCT course_id FROM section WHERE semester = 'Fall' AND year = 2017 AND course_id IN ( SELECT course_id FROM section WHERE semester = 'Spring' AND year = 2018 ); 초록색이 2017년 가을에 개설된 과목 빨간색이 2018년 봄에 개설된 과목 파란색의 CS-101 이 공통인 과목이다. 처리 과정: 안쪽 질의가 2018년 봄에 개설된 course_id 집합을 만든다. 바깥 질의가 2017년 가을에 개설된 과목을 찾는다. 그중 안쪽 결과에도 포함된 과목만 남긴다. Fall 2017 과목 ∩ Spring 2018 과목 2.2 NOT IN : 집합에 속하지 않는가? 2017년 가을에는 개설되었지만 2018년 봄에는 개설되지 않은 과목을 찾는다. SELECT DISTINCT course_id FROM section WHERE semester = 'Fall' AND year = 2017 AND course_id NOT IN ( SELECT course_id FROM section WHERE semester = 'Spring' AND year = 2018 ); 초록색이 2017년 가을에 개설된 과목 빨간색이 2018년 봄에 개설된 과목 파란색의 CS-347 , PHY-101 이 조건을 만족하는 과목이다. Fall 2017 과목 - Spring 2018 과목 강의 예제 데이터의 결과: CS-347 , PHY-101 2.3 NOT IN 과 NULL 주의 서브쿼리 결과에 NULL 이 있으면 NOT IN 의 비교 결과가 UNKNOWN 이 되어 예상과 달리 행이 선택되지 않을 수 있다. 안전한 방법: 서브쿼리에서 NULL 을 제거한다. 또는 NOT EXISTS 를 사용한다. WHERE course_id NOT IN ( SELECT course_id FROM section WHERE course_id IS NOT NULL ); 3. 튜플 단위의 IN 교수 10101 이 담당한 분반을 수강한 서로 다른 학생 수를 구한다. SELECT COUNT(DISTINCT ID) FROM takes WHERE (course_id, sec_id, semester, year) IN ( SELECT course_id, sec_id, semester, year FROM teaches WHERE teaches.ID = 10101 ); 왜 네 속성을 함께 비교하는가? course_id 만으로는 특정 분반을 구별할 수 없다. 같은 과목이 여러 분반, 학기, 연도에 개설될 수 있기 때문이다. (course_id, sec_id, semester, year) 이 네 속성의 조합으로 특정 수업 분반을 식별한다. 처리 과정: teaches 에서 교수 10101 이 담당한 분반들의 복합 튜플을 구한다. takes 에서 그 튜플들에 해당하는 수강 기록을 찾는다. COUNT(DISTINCT ID) 로 중복 학생을 한 번만 센다. 4. 집합 비교: SOME 4.1 의미 SOME 은 서브쿼리 결과 중 적어도 하나 와 비교 조건을 만족하면 참이다. F <comp> SOME r <=> r의 원소 중 적어도 하나의 t에 대해 F <comp> t가 참 <comp> 에는 < , <= , > , >= , = , <> 등을 사용할 수 있다. 4.2 예제 CSE 학과 교수 중 적어도 한 명보다 급여가 높은 교수를 찾는다. SELECT name FROM instructor WHERE salary > SOME ( SELECT salary FROM instructor WHERE dept_name = 'CSE' ); 서브쿼리 결과가 {60000, 70000, 90000} 이라면, 급여가 60000보다 크기만 해도 조건을 만족한다. 4.3 자기 조인으로 표현한 같은 쿼리 SELECT DISTINCT T.name FROM instructor AS T, instructor AS S WHERE T.salary > S.salary AND S.dept_name = 'CSE'; SOME 을 사용하면 같은 의미를 더 직접적으로 표현할 수 있다. 4.4 SOME 의 중요한 등가 관계 = SOME <=> IN 하지만 다음은 성립하지 않는다. <> SOME != NOT IN 예를 들어 집합이 {0, 5} 일 때 5 $\neq$ SOME {0, 5} 는 5와 다른 원소인 0이 있으므로 참이다. 반면 5 NOT IN {0, 5} 는 5가 집합에 포함되어 있으므로 거짓이다. ANY 는 일반적으로 SOME 과 같은 의미다. 5. 집합 비교: ALL 5.1 의미 ALL 은 하위 질의 결과의 모든 값 과 비교 조건을 만족해야 참이다. F <comp> ALL r <=> r의 모든 원소 t에 대해 F <comp> t가 참 5.2 예제 Biology 학과 모든 교수보다 급여가 높은 교수를 찾는다. SELECT name FROM instructor WHERE salary > ALL ( SELECT salary FROM instructor WHERE dept_name = 'Biology' ); 하위 질의 결과가 {60000, 70000, 90000} 이라면 급여가 90000보다 커야 조건을 만족한다. 5.3 SOME 과 ALL 비교 표현 의미 x > SOME (집합) 집합의 값 중 하나보다만 크면 됨 x > ALL (집합) 집합의 모든 값보다 커야 함 x = SOME (집합) x IN (집합) 과 같음 5.4 빈 집합과 NULL 빈 집합에 대한 SOME 조건은 만족할 대상이 없으므로 거짓이다. 빈 집합에 대한 ALL 조건은 반례가 없으므로 참이다. 서브쿼리 결과에 NULL 이 있으면 3값 논리에 따라 결과가 UNKNOWN 이 될 수 있다. 6. EXISTS 와 NOT EXISTS 6.1 정의 EXISTS 는 서브쿼리 결과에 튜플이 하나라도 있으면 참 이다. NOT EXISTS 는 내부 결과가 비어 있으면 참 이다. EXISTS 에서는 실제 출력 값보다 행의 존재 여부가 중요하므로 보통 SELECT * 또는 SELECT 1 을 사용한다. WHERE EXISTS ( SELECT 1 FROM ... WHERE ... ); 7. Correlated Subquery 7.1 정의 상관 서브쿼리(Correlated Subquery) 는 하위 쿼리가 바깥 쿼리의 현재 튜플을 참조하는 질의다. 바깥 질의의 별칭을 correlation name 또는 correlation variable이라고 한다. 비상관 서브쿼리처럼 한 번만 독립 실행되는 것으로 이해하면 안 된다. 개념적으로 바깥 질의의 각 후보 행에 대해 하위 질의를 평가한다. 7.2 두 학기에 모두 개설된 과목 SELECT course_id FROM section AS S WHERE semester = 'Fall' AND year = 2017 AND EXISTS ( SELECT * FROM section AS T WHERE semester = 'Spring' AND year = 2018 AND S.course_id = T.course_id ); 여기서 S.course_id 가 바깥 질의의 현재 과목을 하위 질의에 전달한다. 처리 개념: 바깥 질의에서 2017년 가을 과목 S 를 하나 선택한다. 하위 질의에서 같은 course_id 가 2018년 봄에도 존재하는지 검사한다. 존재하면 해당 과목을 결과에 포함한다. 8. NOT EXISTS 로 "모두" 표현하기 Biology 학과에서 개설한 모든 과목을 수강한 학생을 찾는다. SELECT DISTINCT S.ID, S.name FROM student AS S WHERE NOT EXISTS ( (SELECT course_id FROM course WHERE dept_name = 'Biology') EXCEPT (SELECT T.course_id FROM takes AS T WHERE S.ID = T.ID) ); 8.1 집합으로 해석 A = Biology 학과의 전체 과목 집합 B = 현재 학생이 수강한 과목 집합 A EXCEPT B = 학생이 아직 수강하지 않은 Biology 과목 A EXCEPT B 가 비어 있으면 빠진 과목이 하나도 없다는 뜻이다. NOT EXISTS (A EXCEPT B) = 수강하지 않은 Biology 과목이 존재하지 않음 = Biology의 모든 과목을 수강함 8.2 이중 부정 패턴 SQL에서 "모든 X에 대해 조건을 만족"은 다음과 같은 이중 부정으로 자주 표현한다. 조건을 만족하지 않는 X가 존재하지 않는다. 9. 중복 존재 검사: UNIQUE UNIQUE(subquery) 는 서브쿼리 결과에 중복 튜플이 있는지 검사 한다. 중복이 없으면 참 중복이 있으면 거짓 빈 결과에 대해서도 참 2017년에 최대 한 번 개설된 과목을 찾는 예: SELECT T.course_id FROM course AS T WHERE UNIQUE ( SELECT R.course_id FROM section AS R WHERE T.course_id = R.course_id AND R.year = 2017 ); 각 과목에 대해 2017년의 개설 기록이 0개 또는 1개면 중복이 없으므로 참. 두 번 이상 개설되면 같은 course_id 가 반복되어 거짓. 강의 예제 데이터에서 2017년에 두 번 개설된 CS-190 만 결과에서 제외 UNIQUE(subquery) 술어의 지원 여부와 문법은 DBMS마다 다르다. 실제 환경에서는 GROUP BY ... HAVING COUNT(*) <= 1 또는 NOT EXISTS 를 이용한 대체 표현을 확인하는 것이 안전하다. 10. FROM 절의 서브쿼리 서브쿼리 결과를 하나의 임시 릴레이션처럼 FROM 에서 사용할 수 있다. 이를 derived table 또는 inline view라고도 한다. 학과별 평균 급여를 먼저 구한 뒤, 평균이 3,000,000보다 큰 학과를 찾는다. SELECT dept_name, avg_salary FROM ( SELECT dept_name, AVG(salary) AS avg_salary FROM instructor GROUP BY dept_name ) AS dept_avg WHERE avg_salary > 3000000; 처리 과정: 안쪽 쿼리가 학과별 평균 급여 릴레이션을 만든다. 이 결과에 dept_avg 라는 별칭을 붙인다. 바깥 쿼리가 avg_salary > 3000000 인 행만 선택한다. 열 이름까지 별칭으로 지정 SELECT dept_name, avg_salary FROM ( SELECT dept_name, AVG(salary) FROM instructor GROUP BY dept_name ) AS dept_avg(dept_name, avg_salary) WHERE avg_salary > 3000000; 많은 DBMS에서는 FROM 절의 서브쿼리에 별칭이 필요하다. 11. WITH 절과 CTE 11.1 정의 WITH 절은 현재 SQL 문 안에서만 사용할 수 있는 임시 결과에 이름을 붙인다. 이렇게 정의한 결과를 CTE(Common Table Expression) 라고 한다. 유효 범위: SQL 문 하나 ( ; 까지) WITH temporary_name AS ( SELECT ... ) SELECT ... FROM temporary_name; 복잡한 쿼리를 단계별로 나누어 읽기 쉽게 만들고, 같은 중간 결과를 재사용할 수 있다. 11.2 최대 예산을 가진 학과 WITH max_budget(value) AS ( SELECT MAX(budget) FROM department ) SELECT department.dept_name, department.budget FROM department, max_budget WHERE department.budget = max_budget.value; max_budget CTE가 전체 학과 중 최대 예산 하나를 구한다. department 에서 예산이 최대값과 같은 학과를 찾는다. 최대 예산이 같은 학과가 여러 개라면 모두 출력된다. 11.3 여러 CTE 연결 전체 학과의 총급여 평균 이상을 지출하는 학과를 찾는다. WITH dept_total(dept_name, value) AS ( SELECT dept_name, SUM(salary) FROM instructor GROUP BY dept_name ), dept_total_avg(value) AS ( SELECT AVG(value) FROM dept_total ) SELECT dept_total.dept_name FROM dept_total, dept_total_avg WHERE dept_total.value >= dept_total_avg.value; 처리 단계: 첫 번째 CTE dept_total 교수 데이터를 학과별로 묶어 학과별 총급여 를 계산합니다. 두 번째 CTE dept_total_avg 앞에서 만든 dept_total 을 사용해 학과별 총급여의 평균 을 계산합니다. 마지막 메인 쿼리 dept_total 과 dept_total_avg 를 비교해 총급여가 평균 이상인 학과 를 선택합니다. 12. Scalar Subquery 12.1 정의 스칼라 서브쿼리(Scalar Subquery) 는 단일 값 이 필요한 위치에서 사용하는 서브쿼리다. 결과가 정확히 1행 1열이면 그 값을 사용하는 구조 일반 서브쿼리의 결과가 0행이면 일반적으로 NULL 로 취급 COUNT(*) 와 같은 집계 함수는 대상 행이 없어도 값 0 을 가진 1행을 반환하므로 스칼라 값으로 사용 가능 결과가 2행 이상이면 단일 값을 결정할 수 없어 발생하는 실행 오류 예시1) 학과별 교수 수를 열로 출력 SELECT dept_name, ( SELECT COUNT(*) FROM instructor WHERE department.dept_name = instructor.dept_name ) AS num_instructors FROM department; 바깥 쿼리의 각 학과에 대해 같은 학과에 속한 교수 수를 하나의 값으로 계산한다. 예시 2) 급여와 학과 예산 비교 SELECT name FROM instructor WHERE salary * 10 > ( SELECT budget FROM department WHERE department.dept_name = instructor.dept_name ); 각 교수의 급여 10배가 소속 학과 예산보다 큰지 검사한다. department.dept_name 이 기본키라면 하위 쿼리는 최대 한 행만 반환한다. 13. 서브쿼리 종류 한눈에 보기 종류 결과 형태 또는 검사 대상 대표 문법 집합 소속 서브쿼리 값이 결과 집합에 포함되는지 IN , NOT IN 집합 비교 서브쿼리 하나 이상 또는 전체 값과 비교 SOME , ALL 존재 검사 결과가 비었는지 EXISTS , NOT EXISTS 상관 서브쿼리 바깥 질의의 현재 행을 참조 S.course_id = T.course_id FROM 서브쿼리 결과를 임시 릴레이션으로 사용 FROM (SELECT ...) AS x 스칼라 서브쿼리 결과를 단일 값으로 사용 salary > (SELECT AVG(...)) CTE 이름 붙인 임시 결과 WITH x AS (...) Reference: Database System Concept-7th Edition 건국대학교 김욱희 교수님 - Database 수업
멀쩡한 숫자를 버그로 의심했는데, 다른 게 고장나 있었다 노트북 하나만 있는 날 이날은 장비가 아무것도 없었다. 기기는 집에 있었고, 주문한 부품은 아직 안 왔고, 가진 건 노트북뿐이었다. 그래서 남은 미결을 다시 읽다가, "기기가 있어야 한다"고 적어둔 항목 하나를 쪼개봤다. 판정 기준이 네 가지였는데(실제 모델을 띄운다 / 진짜 초인종 소리를 넣는다 / 사람이 없는 상태로 만든다 / 화면과 기록 양쪽에서 차단을 확인한다), 기기가 있어야 하는 건 하나도 없었다. 모델은 노트북에 있고, 초인종 소리는 학습 데이터에 수백 개가 있고, "사람 없음"은 요청에 값을 넣으면 되고, 화면은 브라우저로 본다. 그렇게 노트북으로 모델을 띄우고 첫 요청을 쏘는 데까지 갔는데, 거기서 이 글의 이야기가 시작된다. 0.4 / 0.6 / 0.0 첫 요청의 점수가 이렇게 돌아왔다. 0.4 / 0.6 / 0.0 소수 첫째 자리로 딱 떨어지고, 하나는 정확히 0.0이다. 확률값이 이렇게 나오는 건 이상하다고 봤다. 며칠 전에 같은 모델로 쏴봤을 때는 소수점이 길게 나왔었다. 그래서 "표시가 뭉개지는 버그"라고 의심하고 파고들었다. 결과부터 쓰면 그 의심은 틀렸다. 서버가 점수를 소수 둘째 자리까지 반올림해서 내보내는 건 정상 동작이었고, 이 소리 파일의 값이 우연히 그렇게 떨어졌을 뿐이다. 버그를 찾으러 간 곳에 버그는 없었다. 그런데 파고드는 길에 다른 게 걸려 있었다. 반올림이 판정 앞에 있었다 반올림하는 자리를 찾아 올라가 보니, 그게 알림을 보낼지 판정하는 단계보다 앞 에 있었다. 지금: 점수 계산 → 반올림 → 알림 보낼지 판정 원칙: 점수 계산 → 알림 보낼지 판정 → (표시용으로만) 반올림 그러니까 "신뢰도 0.70 미만이면 알림을 보내지 않는다"는 기준이 원래 값이 아니라 반올림된 값 을 받고 있었다. 원래 값이 0.695에서 0.69999 사이면 반올림돼서 0.70이 되고, 문턱을 못 넘어야 할 게 통과한다. 여기서 뼈아팠던 건, 이게 내가 직접 세워둔 원칙의 위반 이라는 점이다. 몇 주 전에 "표시용 반올림이 판정 경계를 오염시키면 안 된다"고 문서에 못 박아뒀다. 그 원칙은 그때 만지던 파일에서만 지켜졌고, 다른 파일에서는 위반이 그대로 살아 있었다. 원칙을 적어두는 일과 그게 지켜지는지 확인하는 일은 별개인데, 나는 앞의 것만 했다. 다만 심각도는 따로 봐야 했다. 평가용 데이터 424개를 전부 돌려서 그 구간에 든 게 몇 개인지 셌더니 0건 이었다. 구조적으로 폭은 있는데 실제 사례는 없었다. 그래서 이건 "고쳐야 한다"가 아니라 "기록해두고 판단을 기다린다" 로 남겼다. 수정할지는 아직 정하지 않았다. 과잉 진단의 수지 이 하루를 정리하면 계산이 이렇게 된다. 내가 의심한 것: "표시가 뭉개진다" → 틀렸다 (정상 동작) 의심하며 파고든 길: 반올림 위치 → 원칙 위반이 걸렸다 (진짜) 이상한 숫자를 보고 "버그다" 하고 달려갔는데, 숫자는 정상이었고 다른 게 고장나 있었다. 과잉 진단은 비용이 싸다. 틀려도 잃는 건 시간이 조금이고, 가끔은 이렇게 공짜 수확을 준다. 다만 조건이 하나 있다. 결론을 확정하기 전에 원래 값을 직접 볼 것. 그러지 않고 "표시 버그"로 기록하고 끝냈다면, 거짓 진단이 문서에 남고 진짜 결함은 못 봤을 것이다. 아직 다 된 건 아니다 반올림 결함은 실제 사례가 0건 이다. "버그를 찾았다"가 아니라 "구조적 폭은 있으나 실사례는 없어 기록만 해뒀다"가 정확하다. 고칠지는 미정이다. 이날은 하루 종일 노트북만 썼고, 기기는 여전히 아무것도 안 했다. 노트북에서 한 확인은 기기가 실제로 보내는 경로를 검증한 게 아니다. 주문한 부품이 아직 안 왔고, 그게 안 오면 직접 녹음, 재학습, 분류 개선이 통째로 멈춘다. 진척이 좋아 보이는 하루였지만 이 병목은 그대로다. 마무리 이날 가장 값진 건 숫자 하나를 잘못 의심한 덕에 내가 세운 원칙이 어디서 안 지켜지고 있는지 보게 된 것이었다. 그리고 "장비가 없어서 못 한다"고 적어둔 일이 쪼개보니 노트북 일이었던 것도 같은 결이다. 이날 하루의 대부분은 새로 만드는 일이 아니라 적어둔 것을 다시 읽는 일 이었다. 띵동(Ddingdong)은 청각장애인 1인 가구를 위한 현관 부착형 소리 분류 알림 시스템 졸업작품입니다. 초인종 · 노크 · 화재경보를 구분해서 사진과 자막까지 스마트폰으로 보내주는 걸 목표로 만들고 있습니다.
같은 숫자를 네 번 재현했는데, 그 숫자가 부풀려 있었다 새 데이터도, 보드도 없는 하루 재학습 날이 다가오고 있었는데, 그날 쓸 새 데이터는 아직 도착하지 않은 상태였다. 노트북 하나로 할 수 있는 일이 뭘까 생각하다가, 그동안 났던 사고 기록을 쭉 모아 봤다. 모아 놓고 보니 전부 같은 모양이었다. 틀렸는데 에러 없이 계속 진행한 경우 였다. 그래서 하루를 이렇게 잡았다. 재학습 날 조용히 틀릴 것들을, 틀리면 그 자리에서 시끄럽게 죽도록 미리 바꿔 놓는 날. 새 기능은 하나도 만들지 않고, 이미 있는 숫자를 다른 방식으로 한 번 더 재 보는 작업이었다. 그 와중에 테스트 성적에 데이터 누수가 있다는 걸 찾았다. 전수 측정 도구를 만들었고, 네 번 재현됐다 먼저 도구 얘기를 해야 한다. 모델이 테스트 데이터 424건을 어떻게 분류하는지(알림이 나가는지, 막히는지, 오분류가 어디로 새는지) 보는 전수 측정은 원래 한 번 돌리고 버린 스크립트였다. 이걸 저장소에 남는 도구로 바꿨다. 넣은 장치는 이런 것들이다. 진짜 모델에 쏘고 있는지 스스로 확인한다. 메모리 사용량의 자릿수, 모델 라이브러리가 실제로 로드됐는지, 같은 입력을 두 번 넣었을 때 출력이 완전히 같은지를 본다. 기준 숫자와 전 칸이 일치해야 정상 종료한다. 일치 판정이 느슨하면 아무거나 통과하니, 일부러 한 칸만 틀리게 바꿔서 실제로 실패하는지도 확인했다. 결과는 424건 전수에서 정상 통과 367, 오알림 27, 막힘 30이었고, 정확도는 0.8868이었다. 이 값이 네 번째 재현까지 전부 일치 했다. 여기까지는 기분이 좋았다. 측정 경로가 안정적이라는 확신이 들었다. 그런데 이 일치가 증명한 건 딱 거기까지였다. 같은 숫자를 안정적으로 재현한다는 것과, 그 숫자가 옳다는 것은 다른 이야기였다. 같은 누수를 네 번 정확하게 재현했을 수도 있었다. 파일 이름이 같이 찍히자 보였다 도구는 결과를 낼 때 파일 이름을 같이 출력하게 만들어 두었다. 그 열이 생기자 눈에 걸리는 게 있었다. 서로 다른 화재경보 파일 17개의 첫 3초 확신도가 전부 0.9809193015098572 였다. 다음 3초도 17개가 전부 같은 값이었다. 확신도가 1.0 근처에서 겹치는 건 이상하지 않다. 숫자 표현이 그 근처에서는 촘촘하지 않기 때문이다. 그런데 0.98 언저리의 값이 소수점 16자리까지 겹치는 건 다른 문제다. 입력이 같았다는 뜻 이다. 파일 해시로 대조해 봤다. 항목 값 전처리 단계 파일 수 2,792개 고유한 내용 2,305개 잉여 487개 (17.4%) 중복 그룹 15개 가장 큰 세 그룹은 한 내용이 각각 108개, 56개, 30개 파일로 퍼져 있었다. 그리고 이 그룹들이 학습/검증/테스트 어디에 들어갔는지 보니 이랬다. 그룹 학습 / 검증 / 테스트 108개 그룹 71 / 20 / 17 56개 그룹 38 / 9 / 9 30개 그룹 19 / 4 / 7 중복 15그룹 중 10그룹이 경계를 가로질렀다. 같은 내용이 학습에도, 검증에도, 테스트에도 들어가 있었다. 나머지 5그룹은 학습 안에만 갇혀 있었고 전부 파일 2개짜리, 잉여 5개였다. 데이터량을 줄이는 문제는 사실상 없고, 문제는 경계를 넘은 쪽에 있었다. 결정타는 이거였다. 테스트 424건 중 79건이 학습·검증 데이터와 바이트 단위로 같았고, 전부 화재경보였다. 초인종과 노크는 0건이었다. (여러 파일이 앞부분을 공유하는 것으로 보이긴 하는데, 왜 그렇게 됐는지는 확정하지 못했다. 이 중복이 데이터를 모으는 단계에서 생겼는지 전처리 단계에서 생겼는지도 모른다.) 얼마나 부풀려졌나 — 그리고 아침에 내가 한 과장 이 숫자를 처음 봤을 때 나는 "정확도의 상당 부분이 암기일 수 있다"고 말했다. 실측해 보니 과한 표현이었다. 누수 79건의 정확도는 1.0000 , 전부 맞혔다. 나머지 청정 345건의 정확도는 0.8609 였다. 전체 정확도는 0.8868에서 청정 기준 0.8609로, 2.6%p 내려간다. 클래스 균형을 보는 지표(macro F1)는 0.8478에서 0.8367로, 0.011 내려간다. 화재경보만 흔들린다. 그 클래스의 F1은 0.927에서 0.893으로 내려가고, 평가 건수는 254에서 175로 줄어든다. 그리고 데모 주력인 초인종(F1 0.736)과 노크(0.881)는 소수점 아래까지 한 글자도 변하지 않았다. 누수가 그 두 클래스에 0건이었으니 당연한 결과다. 동시에 청정 값을 가려내는 계산이 맞게 돌았다는 검산이기도 했다. 건드리지 않아야 할 곳이 안 움직였다. 여기서 구분해 둘 게 있다. 79건이 전부 맞았다는 건 실측이다. 왜 전부 맞았는지, 그게 암기 때문인지는 아직 추정이다. 이 둘을 한 문장에 섞지 않으려고 했다. 그리고 누수가 있다 는 사실과 성적이 크게 틀렸다 는 주장도 별개였다. 아침에는 앞의 것을 보고 뒤의 것까지 말해 버렸고, 숫자를 보고서야 크기를 제대로 쟀다. 설계가 고장난 게 아니라, 범위 밖이었다 학습과 테스트를 나눌 때는 같은 원본에서 나온 조각이 양쪽으로 갈라지지 않게 파일 이름 기준으로 묶어서 나눈다. 이 방어는 설계대로 작동했다. 다만 내용이 같고 이름이 다른 파일 은 애초에 이 방어의 사정권 밖이었다. 그래서 기록에도 "설계 결함"이 아니라 "방어 범위 밖"이라고 적었다. 두 표현은 그 뒤의 대응을 다르게 이끈다. 결함이라고 하면 설계를 갈아엎는 쪽으로 가고, 범위 밖이라고 하면 방어를 한 겹 더 얹는 쪽으로 간다. 이번 경우에는 후자가 사실에 맞았다. 아직 다 된 건 아니다 누수는 발견과 정량까지다. 대응은 아직 정하지 않았다. 중복을 제거하고 다시 나눠 재학습할지, 누수를 고지하고 그대로 쓸지, 청정 값을 정본으로 바꿀지 모두 미결이다. 데이터는 한 줄도 건드리지 않았고 재학습도 하지 않았다. 청정 정확도 0.8609도 확정 숫자가 아니다. 발표에 어떤 숫자를 쓸지는 개발이 끝난 뒤 정한다. 증강본과 최종 데이터셋에 같은 중복이 있는지는 아직 보지 않았다. 이 측정은 노트북에서 서버 없이 모델만 직접 호출한 것이다. 보드는 쓰지 않았고 실제 알림 경로를 통과한 게 아니다. 기기와 시연 쪽 상태는 이날 달라진 게 없다. 79건이 전부 맞았다는 것과, 그 이유가 암기라는 것은 다른 층위다. 앞은 실측이고 뒤는 추정이다. 마무리 이날 한 일은 새 기능이 아니라 이미 있던 숫자를 다른 방식으로 한 번 더 재 본 것뿐이었다. 그랬더니 성적이 부풀려 있었다. 그중 가장 크게 남은 건 네 번 재현된다는 사실이 그 숫자가 옳다는 증거가 아니라는 것 이었다. 재현이 알려 주는 건 측정이 안정적이라는 사실이고, 무엇을 재고 있는지는 알려 주지 않는다. 이번에는 그걸 파일 이름 한 열이 알려 줬다. 띵동(Ddingdong)은 청각장애인 1인 가구를 위한 현관 부착형 소리 분류 알림 시스템 졸업작품입니다. 초인종 · 노크 · 화재경보를 구분해서 사진과 자막까지 스마트폰으로 보내주는 걸 목표로 만들고 있습니다.
고치지 못한 실험이 진척이었던 이야기 이날 "고쳤다"는 결과는 거의 없었다 마이크에는 오래 남아 있던 문제가 하나 있었다. 소리가 없는 순간에도 가끔 최대치로 튀는 값이 하나씩 끼어드는 현상이다. 원인 후보는 여럿이었고, 이날은 그중 하나를 결선을 건드리지 않고 가려 보기로 했다. 후보는 이렇다. 마이크의 데이터 선이 출력을 내보내지 않는 구간에서 어디에도 연결되지 않은 채 떠 있고, 그래서 값이 흔들린다는 가설이다. 같은 날 다른 것들도 재 봤다. 그런데 하루가 끝나고 정리해 보니 결과가 전부 비슷한 모양이었다. 무언가가 해결된 게 아니라 후보 하나가 약해졌거나, 어디까지는 아니라는 경계가 그어진 것들이었다. 이 글은 그중 마이크 실험 하나에 대한 기록이다. 후보를 지우는 실험은 이렇게 짰다 데이터 선이 떠 있어서 생기는 문제라면, 선을 약하게 눌러 주면 줄어들어야 한다. 칩 안에는 그런 용도로 쓸 수 있는 약한 내부 저항이 있었다. 납땜 없이 설정 하나로 켜고 끌 수 있다. 그래서 모드를 이렇게 나눴다. 기본 : 아무것도 바꾸지 않은 상태 저항 켬 : 데이터 선을 땅 쪽으로 약하게 눌러 주는 상태 센서 끔 / 센서 끔+저항 켬 : 사람 감지 센서의 측정을 멈춘 대조 조건. 튀는 값이 센서 쪽에서 오는지 보려는 것이다 측정 순서는 이랬다. 기본 → 저항 켬 → 기본 → 저항 켬 → 센서 끔 → 센서 끔 + 저항 켬 (각 블록마다 스냅샷 3회) 기본을 두 번 넣은 이유 가 이 설계의 핵심이다. 한 조건을 몰아서 연달아 재면 "조건 차이"와 "시간 차이"가 섞인다. 세션이 길어지면 환경도 달라지기 때문이다. 교대로 배치하면 그 둘을 가를 수 있다. 그리고 판정의 첫 관문은 비교가 아니라 재현 이었다. 기본 조건에서 튀는 값이 다시 나오지 않으면, 그 뒤의 비교는 아무것도 말해 주지 않는다. 교대로 두 번씩 재도 기본 조건 6번 모두 튀는 값이 나왔고, 그래서 비교가 성립했다. 결과: 줄어들지 않았다. 그런데 한 가지는 바뀌었다 스냅샷 18개가 전부 유효했고, 튀는 값의 개수는 이랬다. 조건 튀는 값 개수 (스냅샷별) 기본 (1차 / 2차) {1, 4, 5} / {2, 1, 5} 저항 켬 (1차 / 2차) {3, 5, 5} / {0, 1, 5} 센서 끔 0, 0, 0 센서 끔 + 저항 켬 0, 0, 0 저항을 켜도 개수는 기본과 같은 범위였다(0 5개 대 1 5개). 센서 측정을 끄면 저항과 상관없이 전부 0개였다. 그래서 "데이터 선이 떠 있어서 생긴다"는 후보는 이 실험으로는 지지되지 않았다. 그리고 튀는 값이 센서 측정과 같이 움직인다는 단서는 덤으로 얻었다. 그런데 한 가지는 달라졌다. 저항을 켜면 각 값의 끝자리 비트가 0으로 눌렸다. 끝자리가 0인 비트 수가 기본에서 5~6개이던 것이 저항을 켜면 7개 안팎(센서 끔+저항 켬에서는 8개)이 됐다. 약한 저항이 실제로 일을 하고 있었다는 뜻이다. 이 끝자리 비트들은 어차피 소리 값으로 변환하는 과정에서 버려지므로 소리 자체에는 영향이 없을 가능성이 높다. 이건 실증이 아니라 논증이다. 정리하면 "저항은 동작했고, 튀는 값은 별개의 경로일 수 있다"까지가 이 실험이 말해 주는 범위다. 여기서 조심할 게 있다. 칩 안쪽 저항은 값이 약하다. 문서가 권하는 외부 저항이었다면 결과가 달랐을지는 여전히 모른다. 그래서 "저항은 소용없다"로 읽히지 않도록 했다. 실험이 문제를 고치지는 못했지만 후보 목록에서 하나가 줄었고, 그게 이날의 진척이었다. 측정값에 섞여 들어온 것들 실험 설계가 깔끔했던 것과 별개로, 측정 현장은 깔끔하지 않았다. 시야에 물건이 있었다. 측정 전에 사람 감지 센서 값을 보니 앞쪽 물건 때문에 "감지됨/아님"이 2초마다 뒤집히고 있었다. 물건을 치우자 구역 수치가 7 8에서 0 4로 내려가 안정됐다. 이걸 확인하지 않고 쟀으면 센서 쪽 요동이 마이크 결과에 섞였을 것이다. 스냅샷 하나가 오염됐다. 튀는 값을 뺀 소리 크기가 다른 스냅샷의 약 6배였다. 그 순간 실제 소리가 들어갔다는 추정이다. 다만 무슨 소리였는지는 기록이 없다. 그래서 그 스냅샷의 개수는 참고용으로만 표시했다. 첫 시도에서는 스냅샷이 0줄이었다. 기록 파일을 열어 보니 소리 줄이 하나도 없어서 놀랐는데, 키 입력이 모니터 창을 클릭해 활성화하기 전에는 기기에 전달되지 않았다. 기록은 정상이었고 입력이 들어가지 않았을 뿐이었다. 기록 방식도 바꿨다. 화면을 복사해 붙이는 대신 터미널 기록 명령으로 파일에 실시간으로 이어 쓰게 했다. 소리 줄과 센서 줄이 섞여 나오는 출력이라 복사하다 빠뜨릴 위험이 컸다. 아직 다 된 건 아니다 이 실험은 후보 하나를 약하게 만든 것이지 원인 규명도 해결도 아니다. 튀는 값의 해결책은 아직 정해지지 않았다. 칩 안쪽의 약한 저항으로 한 실험이다. 외부 저항의 효과는 판정하지 못했다. 끝자리 비트가 소리 값에 영향이 없다는 건 논증이고, 실증하지 않았다. 스냅샷 하나의 오염은 추정이다. 그때 어떤 소리가 있었는지 모른다. 마무리 이날 마이크 쪽에서 얻은 건 "여기까지는 아니다"였다. 후보 목록에서 하나가 약해졌고, 튀는 값이 센서 측정과 같이 움직인다는 단서가 하나 생겼으며, 측정 현장에서 걸러야 할 오염이 몇 가지 드러났다. 고친 건 없다. 그래도 교대 배치로 시간 차이를 가르고, 먼저 문제가 재현되는지를 확인하고, 말할 수 있는 범위를 끝까지 좁혀 적고 나니 다음 후보로 넘어갈 수 있었다. 실험이 문제를 고치지 못해도, 설계가 맞았다면 후보 하나는 줄어든다. 띵동(Ddingdong)은 청각장애인 1인 가구를 위한 현관 부착형 소리 분류 알림 시스템 졸업작품입니다. 초인종 · 노크 · 화재경보를 구분해서 사진과 자막까지 스마트폰으로 보내주는 걸 목표로 만들고 있습니다.
들어가기 앞서 오늘은 개인프로젝트의 주제선정이 완전 종료된 이후 계속 진행하여 중간점검까지 진행하는 과정이였습니다. 주제 전역자 청년정책 가이드 챗봇 중간 점검 한 줄 상태 주제와 현재 상태(순조로움/지연/막힘) 선택 ᄂ 지연 - 수요일 마지막에 수정사항을 받은 뒤 해당하는 정책의 자료들을 찾다 지연0 여태까지 한 내용 / 앞으로 할 내용 체크리스트 완료/진행중/안함(이유) 표시 ᄂ 현재 7단계로 나누고 이후 세부항목으로 나눔 주제 확정 - 정책 주제 선정 → 완료 데이터 구축 - 7가지의 세부항목으로 나눔 온통청년 API 전체 수집 → 완료 서울, 중앙부처 정책 분리 → 완료 1단계 사용 정책 선정 → 완료 군 관련 특례 여부, 개수 확인 → 완료 군 관련 정책 공고문 수집 → 완료 정책 종료, 변경 안내문 수집 → 미진행 충청권, 전국 정책 확장 → 미진행 파싱, 정제, 청킹 - 정책, 문서 파싱, 정제, 청킹 → 완료 특례 규칙 - 전역자 특례 규칙 표 작성 → 완료 임베딩 - 임베딩, 백터 DB 적재 → 완료 평가 - 평가셋 완성 → 진행중 개선 - RAG 성능 비교 → 진행중 데이터 현황 - 수집한 문서와 출처 주요 데이터 수집 - 온통청년 api 이외의 정책과 출처 서울권 공고문 - 서울 특별시, 청년몽땅정보통 등 중앙부처 공고문 - 국토교통부, 한국문화예술위원회 웹페이지 본문 - 청년몽땅정보통 - 서울시, 제대군인지원센터 - 정부24 - 고용복지플러스센터, iMND(중앙부처) 참고 자료 - 온통청년 API 코드정의서 평가셋 문항 수 30 → 정답 20 애매 5 오답 5 정답과 근거 위치 함정문항 등 → 표 참조 숫자 표 GPT 빈손테스트(문서 학습 없이 질문 한 내용) ᄂ 각각 1, 9, 10번 내용 질문(GPT, Gemini) * 1번 둘다 정답 * 9번 둘다 정답 * 10번 GPT → 국토교통부 근거 → 국토교통부를 근거로 들고왔고, 해당 정책 pdf를 제시했지만 안에 전역군인 관련 정책은 없음 gemini 오답 → 서울주거포털 근거 → 서울주거포털이 제시하는 청년월세 정책과 국토교통부가 제시하는 청년월세정책은 엄연히 다름 ᄂ 그래서 질문도 국토교통부를 근거로 제시함 * 고치기 전 RAG ᄂ 첫 평가 결과는 정확 6 / 부분 정확 16 / 오답 8 → 원인을 정리해보니 제 코드에서 전제 확인 지침을 조건문에 빠뜨린 실수, 문화패스 14번의 연도별 기준 누락, 나이 미상 시 판단 통째 생략, 판정 사유 문장 오독, 세부 조건 누락 등이 발견 * 고치고 난 후 RAG ᄂ 정확 23 / 부분 정확 6 / 오답 1 → 오답 유도 문항은 5개 모두 정확 남은 오답 1개와 부분 정확 문항을 확인 오늘 후기 이제 개인프로젝트의 주제를 완전히 선정하고 이제 계속 프로젝트를 개발하며 중간 점검까지의 과정이였습니다. 우선 프로젝트를 선정하고나니 고려해야할 상황이나 수정 사항등 고칠부분과 고려할 부분이 많아 다소 지연되어 현재도 고칠 사항 몇몇이 계속 파악되고 있습니다. 이 부분은 다음주에도 계속 고쳐 중간점검까지 이어가겠습니다.
웹페이지의 모든 요소는 눈에 보이지 않는 사각형 박스로 구성 됩니다. 이 사각형 박스에 적용되는 규칙이 박스모델인데, 이 박스 모델(Box Model)은 웹 브라우저가 HTML의 모든 요소를 화면에 렌더링할 때 ‘네모난 상자’로 처리하는 기본 규칙 입니다. HTML 태그가 <div> 이든, <button> 이든, <h1> 이든 무엇이든 상관없이 모든 웹 요소는 중심부에서 외곽으로 확장되는 4개의 Layer(층) 구조로 이루어져 있습니다. [박스 모델 구성 요소 (4개의 Layer)] 1) Content (콘텐츠): 텍스트, 이미지, 아이콘 등 실제 내용물이 들어가는 영역 width와 height 속성으로 크기를 직접 조절 2) Padding (안쪽 여백): 콘텐츠 영역과 테두리(border) 사이의 내부 여백 요소를 클릭할 수 있는 영역 확장, 배경색/배경 이미지가 (이 영역까지) 적용됨 3) Border (테두리): Padding과 Margin 경계에 위치하는 외곽선 border: 1px solid black; 처럼 두께, 스타일, 색상 지정 4) Margin (바깥쪽 여백): 현재 요소와 다른 이웃 요소 사이의 외부 간격 투명한 영역이며, 요소 상하 마진이 겹칠 때 더 큰 마진으로 합쳐지는 '마진 병합(Margin Collapse)' 현상 발생 [박스 모델을 반드시 알아야 하는 3가지 이유] 박스 모델을 이해하지 못하면 웹 화면 레이아웃을 잡을 때 수많은 버그와 마주치게 됩니다. 1 요소를 원하는 크기로 정확히 제어하기 위해 (크기 계산 착오 방지) 기본적으로 CSS에서 width: 200px을 주고 padding: 20px, border: 2px를 추가하면 브라우저가 화면에 실제로 그려내는 전체 너비는 200 + 40 + 4 = 244px가 됩니다. 이 원리를 모르면 "너비를 분명 200px로 정했는데 왜 다른 200px 상자보다 더 크게 보이고 줄바꿈이 깨질까?"라는 문제를 겪게 됩니다. 2 안쪽 여백(Padding)과 바깥 여백(Margin)의 용도를 정확히 나누기 위해 Padding: 요소 내부 디자인을 가다듬을 때 씁니다. (예: 버튼 안의 글자와 버튼 테두리 사이가 너무 빽빽해서 답답할 때) Margin: 다른 요소와의 거리를 둘 때 씁니다. (예: 위쪽 카드가 아래쪽 카드와 너무 바짝 붙어있을 때) 이 둘을 헷갈려서 Margin을 줘야 할 곳에 Padding을 주면 배경색이 이상하게 확장되는 스타일 오작동이 일어납니다. 3 마진 병합(Margin Collapse) 현상을 해결하기 위해 두 개 이상의 세로로 인접한 요소가 각각 margin-top과 margin-bottom을 가질 때, 두 마진 값이 더해지는 것이 아니라 더 큰 쪽의 마진 하나만 덮어씌워져 적용되는 현상 이 발생합니다. 예: 위 상자의 margin-bottom: 30px과 아래 상자의 margin-top: 20px이 만나면 50px이 아닌 30px 간격만 생깁니다. 박스 모델의 원리를 알아야 이 현상을 버그로 오인하지 않고 대처할 수 있습니다. [박스 전체 크기 계산 (content-box 기준)] 실제 박스 너비 = Content + 좌우 Padding + 좌우 Border + 좌우 Margin 기본 CSS 동작(box-sizing: content-box)에서는 요소의 전체 너비가 width + padding + border의 합으로 계산되어 사용자가 지정한 width보다 실제 상자가 커지는 문제가 자주 생깁니다 . 이 때문에 실무에서는 padding과 border를 포함하여 지정한 width 크기로 상자를 고정해 주는 속성을 프로젝트 최상단에 적용하고 시작하는 것이 표준(box-sizing: border-box;) 입니다 [CSS 기술 예시] /* 실무 필수 기본 설정 - padding과 border를 포함 전체 넓이를 width로 기본 계산 / ` ` { box-sizing: border-box; } box-sizing: border-box 속성은 필히 CSS 최상단에 표기함을 기억해야 합니다. 참고로 '*' 는 전체 선택자로 이 선택자에 선언된 내용이 전체에 적용됨을 의미합니다.
주문 화면에 고객 이름을 표시하려고 합니다. 고객 이름은 고객 표에, 주문 번호는 주문 표에 들어 있습니다. 두 표를 연결하면 이름과 주문 번호를 한 줄에 함께 보여 줄 수 있습니다. DB(Database)는 자료를 저장하고 찾는 시스템입니다. 표의 한 줄은 행, 이름이나 번호처럼 같은 종류의 값을 담는 항목은 열이라고 부릅니다. SQL은 DB에 작업을 요청하는 언어입니다. 이번 글에서는 SQL의 JOIN으로 관련 행을 연결하고, 연결 조건에 따라 결과가 어떻게 달라지는지 확인하겠습니다. 앞선 Index 글 에서는 원하는 행을 찾는 보조 자료구조인 Index를 살펴봤습니다. 이번에는 찾은 행들이 어떤 짝으로 연결되는지에 집중하겠습니다. 고객 번호로 주문을 연결하기 JOIN은 여러 표의 관련 행을 묶어 읽는 작업입니다. 고객 표에는 세 명이 있고, 주문 표에는 세 건이 있다고 하겠습니다. 이름과 주문은 설명을 위해 만든 가상 자료입니다. 고객 번호 이름 1 가람 2 나래 3 다온 주문 번호 고객 번호 결제 상태 101 1 PAID 102 1 CANCELLED 103 2 PAID PAID 는 결제 완료, CANCELLED 는 취소를 뜻합니다. 고객 번호 1은 주문 101과 102에 모두 들어 있습니다. 따라서 가람의 행은 두 주문과 각각 연결됩니다. ON 은 행을 연결할 조건을 적는 부분입니다. 여기서는 고객 표의 번호와 주문 표의 고객 번호가 같을 때 연결합니다. INNER JOIN 은 조건에 맞는 짝을 결과로 내보냅니다. 고객 3은 연결할 주문이 없으므로 이 예제의 결과에는 고객 1과 2의 주문 세 건이 들어갑니다. SQLite JOIN 규칙 SQL로 세 쌍 확인하기 SQLite는 DB를 관리하는 프로그램입니다. 아래 코드는 SQLite의 새 Memory DB에서 실행했습니다. Memory는 실행 중에 자료를 담는 공간입니다. 예제 전용 공간에서 고객과 주문 표를 만들었습니다. CREATE TABLE customer ( id INTEGER PRIMARY KEY, name TEXT NOT NULL ); CREATE TABLE orders ( id INTEGER PRIMARY KEY, customer_id INTEGER NOT NULL, status TEXT NOT NULL ); INSERT INTO customer VALUES (1, '가람'), (2, '나래'), (3, '다온'); INSERT INTO orders VALUES (101, 1, 'PAID'), (102, 1, 'CANCELLED'), (103, 2, 'PAID'); SELECT c.id AS customer_id, c.name, o.id AS order_id FROM customer AS c INNER JOIN orders AS o ON c.id = o.customer_id ORDER BY c.id, o.id; CREATE TABLE 은 표를 만들고, INSERT 는 행을 넣습니다. INTEGER 는 정수, TEXT 는 글자 값을 담습니다. PRIMARY KEY 는 행을 구분하는 기본 열을 지정합니다. NOT NULL 은 값이 없음을 나타내는 NULL 의 입력을 제한합니다. SELECT 는 읽을 열을, FROM 은 읽을 표를 정합니다. AS 는 짧은 이름을 붙입니다. c 는 고객 표, o 는 주문 표를 가리킵니다. 따라서 c.id 는 고객의 번호이고, o.customer_id 는 주문에 적힌 고객 번호입니다. ORDER BY 는 출력 순서를 정합니다. 여기서는 고객 번호, 같은 고객 안에서는 주문 번호의 작은 순서로 출력합니다. 실제 반환 값은 다음과 같았습니다. customer_id name order_id 1 가람 101 1 가람 102 2 나래 103 주문이 없는 고객도 남기기 전체 고객 목록을 보려면 LEFT JOIN 을 사용할 수 있습니다. 왼쪽 표의 행을 남기고, 연결할 오른쪽 행이 있으면 그 값을 붙입니다. 연결할 주문이 없는 고객 3의 주문 열에는 NULL 이 들어갑니다. NULL 은 해당 값이 없거나 알 수 없음을 나타내는 표시입니다. SELECT c.id AS customer_id, c.name, o.id AS order_id FROM customer AS c LEFT JOIN orders AS o ON c.id = o.customer_id ORDER BY c.id, o.id; 이 조회는 앞의 세 행에 (3, 다온, NULL) 을 더해 네 행을 반환했습니다. 고객 1은 연결된 주문이 두 건이므로 여전히 두 행을 차지합니다. 그림 아래에서 화살표 하나는 고객과 주문의 연결 하나를 뜻합니다. 고객 1이 두 줄에 등장하는 이유는 연결된 주문이 두 건이기 때문입니다. 결과 행 수를 읽을 때는 고객 수와 연결된 짝의 수를 구분해야 합니다. 결제 조건을 어디에 적을까? “전체 고객을 보여 주고, 결제 완료 주문만 붙이기”라는 요청을 생각해보겠습니다. 결제 상태를 ON 에 적으면 연결할 주문을 고르는 조건이 됩니다. SELECT c.id AS customer_id, c.name, o.id AS order_id FROM customer AS c LEFT JOIN orders AS o ON c.id = o.customer_id AND o.status = 'PAID' ORDER BY c.id, o.id; AND 로 연결한 두 조건을 모두 만족하는 주문이 붙습니다. 결과는 (1, 가람, 101) , (2, 나래, 103) , (3, 다온, NULL) 입니다. 고객 3은 결제 완료 주문이 없어도 왼쪽 표의 행으로 남습니다. WHERE 는 결과에 남길 행의 조건을 적습니다. 같은 결제 조건을 WHERE 에 적으면 연결 결과에서 결제 완료인 행만 남깁니다. SELECT c.id AS customer_id, c.name, o.id AS order_id FROM customer AS c LEFT JOIN orders AS o ON c.id = o.customer_id WHERE o.status = 'PAID' ORDER BY c.id, o.id; 반환 값은 (1, 가람, 101) , (2, 나래, 103) 두 행입니다. 고객 3의 결제 상태는 NULL 입니다. 이 행은 PAID 라는 조건을 만족하지 않아 결과에서 빠집니다. LEFT JOIN에서 오른쪽 표의 조건을 적을 때는 모든 고객을 남길지, 조건을 만족하는 연결만 남길지 먼저 정해야 합니다. PostgreSQL도 DB 관리 프로그램입니다. 연결 규칙은 공식 문서를 참고했고, 위 네 조회는 SQLite에서 실행했습니다. PostgreSQL 18의 ON과 WHERE 예제 그림과 설명은 결과를 이해하기 위한 연결 규칙입니다. 실제로 자료를 읽는 순서는 DB가 선택한 실행 계획에 따라 달라집니다. 이 글에서는 반환 값을 확인했으며, 조회 시간은 따로 측정하지 않았습니다. 확인 문제 고객 1에게 주문 104를 추가하면 INNER JOIN의 전체 결과는 몇 행일까요? 주문 표의 모든 행을 지우면 LEFT JOIN에는 고객이 몇 명 남을까요? 전체 고객을 유지하면서 결제 완료 주문만 연결하려면 결제 조건을 어디에 적을까요? 답과 해설 네 행입니다. 고객 1은 세 주문과 연결되고, 고객 2는 한 주문과 연결됩니다. 세 명입니다. 각 고객의 주문 열에는 NULL이 들어갑니다. 연결한 주문이 있는지 확인할 때는 주문 번호 열을 읽을 수 있습니다. ON에 적습니다. 연결할 주문을 결제 완료로 제한하고, LEFT JOIN으로 전체 고객을 남깁니다. WHERE에 적은 결제 조건은 연결한 결과에서 조건을 만족하는 행만 남깁니다. 오늘은 SQL을 실행하기 전에 네 조회의 결과를 종이에 적어보겠습니다. 하루 뒤에는 고객 3에게 주문을 하나 추가하고 바뀌는 행을 예상해보겠습니다. 일주일 뒤에는 목록 화면의 행 하나가 고객 한 명인지, 고객과 주문의 한 쌍인지 설명해보면 좋겠습니다. 이어지는 학습 주제는 Composite Index, 즉 여러 열을 정해진 순서로 묶은 Index의 열 순서입니다. 자료 확인: 2026-10-07, SQLite 공식 문서와 PostgreSQL 18 공식 문서. 예제 검증 환경: 프로그램을 작성하는 언어인 Python 3.12.14, SQLite 3.53.1, 독립 :memory: DB. 위 네 조회의 반환 값과 주문 추가·빈 주문 표의 결과를 확인했습니다.
cnn 모델의 추론 loss신호가 불안정하거나 gan rl 학습이 조금 어렵다 diffusion rl value 를 띠리긴다 손실함수란? 모델이 예측한값과 실제값의 차이를 수치로 표현한 함수 클래식케이션 크로스 엔트로피 mse 우리가 정말 풀고싶은 문제 클래스피케이션 정답률 최대화 머신 트렌셀레이션 적확한 문장 생성 진짜 풀고싶은것 목적 함수 서로게이트로스 로스 펑션 미분 가능 그레디언트를 사용하는 역전파가 가능 방향이 정확하지 않을수있지만 어디로 가라라는 지시 자체는 확고하다 클래스피케이션 크로스 엔트로핀 0.5를 기준 그래드인트 디선트 ![] ![] 기울기가 0인 부분을 최솟값 미분해서 0이되는 지점을 찾아서 편미분 1억번 보통은 직접 미분보다는 그래디던트 디선트 w가 t1 아주작게 감소 시켯을 때 los를 구핧수있다 미분을 안하고도 기울기를 계산 할수있다 t2를 찍고 실제로스를 전체 데이터를 그레디언트를 계산하는게아니라 배치 데이터로 계산하는SGD 를 사용 이유는 계산 효율 메모리 절약 로컬 미니멈 비슷한 방향으로 업데이트
2026년 10월 7일 오전 11시 43분경 전골이 전담의 문워크를 따라하며 "호-호"라는 아담을 따라하는 말을 했습니다. 그러자 아담은 매우 화가 나서 초속 408M의 속도로 공중제비로 날아가서 전골에게 문워크 이단 옆차기로 전골을 단번에 쓰러트렸습니다. 그러자 전골은 "흐흐헿 역시 나는 잘생겼다"라는 조현병 같은 말을 했습니다. 그때 그 말을 듣고 화가 난 전세가 가오를 부리지 않고 바로 창문을 깨고 나와 전골에게 헤드락을 걸어 제압을 했습니다. 전골은 위기를 느끼고 입을 벌려 냄새로 전세와 전담을 도망가게 했습니다.