-
Notifications
You must be signed in to change notification settings - Fork 0
/
null-hygene.qmd
390 lines (274 loc) · 17.2 KB
/
null-hygene.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# 데이터 결측과 위생
데이터베이스가 어떻게 결측 정보를 표현하는지 살펴보고,
결측 정보를 다룰 때, 3개 값을 가진 로직(three-valued logic) 데이터베이스 사용하여
결측 정보를 올바르게 처리하는 쿼리를 작성한다.
현실 세계 데이터는 결코 완전하지 않고 구멍은 항상 있다.
`null`로 불리는 특별한 값을 사용하여 데이터베이스는 구멍을 표현한다.
`null`는 0, `False`, 혹은 빈 문자열도 아니다."아무것도 없음(nothing here)"을 의미하는 특별한 값이다.
`null`을 다루는 것은 약간 특별한 기교와 신중한 생각을 요구한다.
기본적으로 SQLite는 출력에서 NULL 값을 표시하지 않는다.
`.nullvalue` 명령은 SQLite가 NULL에 대해 지정한 값을 표시하도록 한다.
NULL을 더 쉽게 볼 수 있도록 `-null-` 값을 사용한다.
\index{SQL!null}
```sql
.nullvalue -null-
```
시작으로 `Visited` 테이블을 살펴보자. 혹은 더 정확히 말하면 날짜가 `null`이다.
시작으로 `Visited` 테이블을 살펴보자.
레코드가 8개 있지만 #752은 날짜가 없다.
즉, 그 날짜는 `null`이다:
```sql
SELECT * FROM Visited;
```
| id | site | dated |
| ----- | ------ | ---------- |
| 619 | DR-1 | 1927-02-08 |
| 622 | DR-1 | 1927-02-10 |
| 734 | DR-3 | 1930-01-07 |
| 735 | DR-3 | 1930-01-12 |
| 751 | DR-3 | 1930-02-26 |
| 752 | DR-3 | \-null- |
| 837 | MSK-4 | 1932-01-14 |
| 844 | DR-1 | 1932-03-22 |
`Null` 다른 값과는 다르게 동작한다.
만약 1930년 이전 레코드를 선택한다면,
```sql
SELECT * FROM Visited WHERE dated < '1930-01-01';
```
| id | site | dated |
| ----- | ------ | ---------- |
| 619 | DR-1 | 1927-02-08 |
| 622 | DR-1 | 1927-02-10 |
결과 2개를 얻게 되고, 만약 1930년 동안 혹은 이후 레코드를 선택한다면,
```sql
SELECT * FROM Visited WHERE dated >= '1930-01-01';
```
| id | site | dated |
| ----- | ------ | ---------- |
| 734 | DR-3 | 1930-01-07 |
| 735 | DR-3 | 1930-01-12 |
| 751 | DR-3 | 1930-02-26 |
| 837 | MSK-4 | 1932-01-14 |
| 844 | DR-1 | 1932-03-22 |
결과를 5개 얻게 되지만, 레코드 #752은 결과값 어디에도 존재하지 않는다.
이유는 `null<'1930-00-00'` 평가결과가 참도 거짓도 아니기 때문이다.
`null` 이 의미하는 것은 "알수가 없다"는 것이다.
그리고 만약 비교 평가식의 왼쪽편 값을 알지 못한다면, 비교도 참인지 거짓인지 알수가 없다.
데이터베이스는 "알 수 없음"을 `null`로 표현하기 때문에, `null<'1930-00-00'`의 값도 사실 `null`이다.
`null>='1930-00-00'`도 또한 `null`인데 왜냐하면 질문에 답을 할 수 없기 때문이다.
그리고, `WHERE`절에 레코드는 테스트가 참인 것만 있기 때문에 레코드 #752은 어느 결과값에도 포함되지 않게 된다.
비교 연산 뿐만 아니라 다른 연산들도 null과 함께 이러한 방식으로 동작한다.
`1+null`도 `null`이고,
`5*null`도 `null`이고,
`log(null)`도 `null`이 된다.
특히, 무언가를 `=` 과 `!=` 으로 `null`과 비교하는 것도 `null`이 된다.
```sql
SELECT * FROM Visited WHERE dated = NULL;
```
출력을 생성하지 않으며, 다음도 마찬가지로 출력을 생성하지 않는다.
```sql
SELECT * FROM Visited WHERE dated != NULL;
```
`null` 인지 아닌지를 점검하기 위해서, 특별한 테스트 `IS NULL`을 사용해야 한다.
\index{SQL!is null}
| id | site | dated |
| ----- | ------ | ---------- |
| 752 | DR-3 | \-null- |
혹은, 역으로는 `IS NOT NULL`을 사용한다.
\index{SQL!is not null}
```sql
SELECT * FROM Visited WHERE dated IS NOT NULL;
```
| id | site | dated |
| ----- | ------ | ---------- |
| 619 | DR-1 | 1927-02-08 |
| 622 | DR-1 | 1927-02-10 |
| 734 | DR-3 | 1930-01-07 |
| 735 | DR-3 | 1930-01-12 |
| 751 | DR-3 | 1930-02-26 |
| 837 | MSK-4 | 1932-01-14 |
| 844 | DR-1 | 1932-03-22 |
`null` 값은 나타나는 곳마다 문제를 일으킬 수 있다.
예를 들어, Lake에 의해 측정되지 않은 모든 염도 측정을 찾고 싶다고 가정해보자.
쿼리를 다음과 같이 작성하는 것이 자연스럽다.
```sql
SELECT * FROM Survey WHERE quant = 'sal' AND person != 'lake';
```
| taken | person | quant | reading |
| ----- | ------ | ---------- | ------- |
| 619 | dyer | sal | 0\.13 |
| 622 | dyer | sal | 0\.09 |
| 752 | roe | sal | 41\.6 |
| 837 | roe | sal | 22\.5 |
하지만, 상기 쿼리 필터는 누가 측정을 했는지 모르는 레코드는 빠뜨린다.
다시 한번, 이유는 `person`이 `null`일 때, `!=`비교는 `null`값을 만들어서
레코드가 결과값에 있지 않게 된다. 만약 이런 레코드도 유지하려고 한다면,
명시적으로 검사를 추가할 필요가 있다.
```sql
SELECT * FROM Survey WHERE quant = 'sal' AND (person != 'lake' OR person IS NULL);
```
| taken | person | quant | reading |
| ----- | ------ | ---------- | ------- |
| 619 | dyer | sal | 0\.13 |
| 622 | dyer | sal | 0\.09 |
| 735 | \-null- | sal | 0\.06 |
| 752 | roe | sal | 41\.6 |
| 837 | roe | sal | 22\.5 |
여전히 이러한 접근법이 맞는 것인지 아닌 것인지 판단할 필요가 있다.
만약 절대적으로 결과에 Lake가 측정한 어떠한 값도 포함하지 않는다고 확신한다면,
누가 작업을 한 것인지 모르는 모든 레코드를 제외할 필요가 있다.
산술 또는 부울 연산자와 달리, `min`, `max` 또는 `avg`와 같은 여러 값을 결합하는 집계 함수는 `null` 값을 *무시한다*. 대부분의 경우, 이는 바람직한 결과이다.
예를 들어, 알려지지 않은 값들이 평균을 낼 때 `null`값은 데이터에 영향을 주지 않는다.
집계 함수는 다음 장에서 더 자세히 다룰 것이다.
## 데이터 위생
**데이터 위생(data hygiene)**은 특히 데이터 분석, 데이터베이스 관리 및 데이터 처리에서 중요한 개념으로, 오류가 적고 정확한 데이터를 보장하기 위한 일련의 방법론을 의미한다. 지금까지 조인이 어떻게 동작하는지 살펴봤으니, 왜 관계형 모델이 그렇게 유용한지 그리고 어떻게 가장 잘 사용할 수 있는지 살펴보자. 이를 위해, 데이터베이스 설계자들이 데이터를 어떻게 구조화하는지 살펴보자. \index{데이터 위생}
\index{data hygiene}
첫번째 규칙은 모든 값은 독립 요소로 분해될 수 없는 **원자(atomic)**적 속성을 지녀야 한다.
하나의 칼럼에 전체 이름을 넣는 대신에 별도로 구별되는 칼럼에 이름과 성을 저장해서 이름 컴포넌트를 뽑아내는 부분 문자열 연산(substring operation)을 사용할 필요가 없다.
좀더 중요하게는, 이름을 두 부분으로 저장한다.
왜냐하면, 공백으로 쪼개는 것은 신뢰성이 약하다.
"Eloise St. Cyr" 혹은 "Jan Mikkel Steubart" 같은 이름을 생각하면 쉽게 알 수 있다.
두번째 규칙은 모든 레코드는 유일한 기본키를 가져야한다.
내재적인 의미가 전혀없는 일련번호가 될 수도 있고, 레코드 값중의 하나 (`Person` 테이블의 `ident` 필드), 혹은 `Survey` 테이블에서 심지어 모든 측정값을 유일하게 식별하는 `(taken, person, quant)` 삼중값의 조합도 될 수 있다.
세번째 규칙은 불필요한 정보가 없어야 한다.
예를 들어, `Site`테이블을 제거하고 다음과 같이 `Visited` 테이블을 다시 작성할 수 있다.
| id | lat | long | dated |
| -------- | --------- | ---------- | ----------- |
| 619 | \-49.85 | \-128.57 | 1927-02-08 |
| 622 | \-49.85 | \-128.57 | 1927-02-10 |
| 734 | \-47.15 | \-126.72 | 1930-01-07 |
| 735 | \-47.15 | \-126.72 | 1930-01-12 |
| 751 | \-47.15 | \-126.72 | 1930-02-26 |
| 752 | \-47.15 | \-126.72 | \-null- |
| 837 | \-48.87 | \-123.40 | 1932-01-14 |
| 844 | \-49.85 | \-128.57 | 1932-03-22 |
사실, 스프레드쉬트와 마찬가지로 각 행에 각 측정값에 관한 모든 정보를 기록하는 하나의 테이블을 사용할 수도 있다.
문제는 이와 같은 방식으로 조직된 데이터를 일관성있게 관리하는 것은 매우 어렵다.
만약 특정한 사이트의 특정한 방문 날짜가 잘못된다면, 데이터베이스에 다수의 레코드를 변경해야한다.
더 안좋은 것은 다른 사이트도 그 날짜에 방문되었기 때문에 어느 레코드를 변경할지 추정해야하는 것이다.
네번째 규칙은 모든 값의 단위는 명시적으로 저장되어야 한다.
예제 데이터베이스는 그렇지 못해서 문제다.
로에리히(Roerich)의 염도 측정치는 다른 사람들보다 몇 배나 더 높지만, 천분율(parts per thousand) 대신 백만분율(parts per million)을 사용했는지, 아니면 1932년 그 사이트에서 실제로 염분 이상 현상이 있었는지 알 수 없습니다.
한걸음 물러나서 생각하자, 데이터와 저장하는데 사용되는 도구는 공생관계다.
테이블과 조인은 데이터가 특정 방식으로 잘 조직되었다면 매우 효과적이다.
하지만, 만약 특정 형태로 되어 있다면 효과적으로 다룰 수 있는 도구가 있기 때문에 데이터를 그와 같은 방식으로 조직하기도 한다.
인류학자가 말했듯이, 도구는 도구를 만드는 손을 만든다.
(the tool shapes the hand that shapes the tool) 즉, 도구(기술, 방법론 등)가 사용자(인간, 조직 등)에게 영향을 미치며, 동시에 사용자가 그 도구를 개선하거나 변형시키는 과정을 의미한다.
결과적으로, 도구와 사용자는 서로 영향을 주고받으며 발전해 나간다는 개념을 내포하고 있다.
## 세 종류 키 {#db-three-keys}
지금까지 데이터를 다중 연결된 테이블에 넣고 **키(keys)**를 사용하여 행을
연결하는 방식으로 데이터 모델을 생성했는데, 키와 관련된 몇몇 용어를
살펴볼 필요가 있다. 일반적으로 데이터베이스 모델에서 세가지 종류의 키가
사용된다.
- **논리 키(logical key)**는 "실제 세상"이 행을 찾기 위해서 사용하는
키다. 데이터 모델 예제에서, `name` 필드는 논리키다. 사용자에 대해서
`screen_name`이고, `name` 필드를 사용하여 프로그램에서 여러번 사용자
행을 찾을 수 있다. 논리 키에 `UNIQUE` 제약 사항을 추가하는 것이
의미있다는 것을 종종 이해하게 된다. 논리 키는 어떻게 바깥 세상에서
행을 찾는지 다루기 때문에, 테이블에 동일한 값을 가진 다중 행이
존재한다는 것은 의미가 없다.
- **기본 키(primary key)**는 통상적으로 데이터베이스에서 자동 대입되는
숫자다. 프로그램 밖에서는 일반적으로 의미가 없고, 단지 서로 다른
테이블에서 행을 열결할 때만 사용된다. 테이블에 행을 찾을 때,
통상적으로 주키를 사용해서 행을 찾는 것이 가장 빠르게 행을 찾는
방법이다. 주키는 정수형이어서, 매우 적은 저장공간을 차지하고 매우
빨리 비교 혹은 정렬할 수 있다. 이번에 사용된 데이터 모델에서 `id`
필드가 주키의 한 예가 된다.
- **외래 키(foreign key)**는 일반적으로 다른 테이블에 연관된 행의
주키를 가리키는 숫자다. 이번에 사용된 데이터 모델의 외부 키의 사례는
`from_id`다.
주키 `id`필드명을 호출하고, 항상 외부키에 임의 필드명에 접미사로 `_id`
붙이는 명명규칙을 사용한다. \index{기본 키(primary key)} \index{외래 키(foreign key)}
\index{논리 키(logical key)}
## 연습문제 {.unnumbered}
### 객관식 {.unnumbered}
1. **문제:** SQL에서 `NULL` 값을 처리할 때 사용되는 특별한 키워드는 무엇입니까?
- A. `EMPTY`
- B. `BLANK`
- C. `IS NULL`
- D. `NO VALUE`
- **정답:** C. `IS NULL`
2. **문제:** SQL에서 `NULL`과 다른 값의 비교 결과는 무엇입니까?
- A. 참(True)
- B. 거짓(False)
- C. `NULL`
- D. 오류(Error)
- **정답:** C. `NULL`
3. **문제:** `NULL` 값을 포함하는 열에 대해 평균을 계산할 때, SQL 함수는 `NULL` 값을 어떻게 처리합니까?
- A. `NULL`을 0으로 간주
- B. `NULL` 값을 무시
- C. 계산을 중지
- D. 오류를 반환
- **정답:** B. `NULL` 값을 무시
4. [원자 값 식별] 다음 중 어떤 것이 원자 값인가? 어떤 것이 아닌가? 그 이유는 무엇인가?
1. 뉴질랜드 (New Zealand)
1. 87 튜링 애비뉴 (87 Turing Avenue)
1. 1971년 1월 25일 (January 25, 1971)
1. XY 좌표 (0.5, 3.3)
::: {.content-visible when-format="revealjs"}
뉴질랜드는 명확한 원자 값이다.
주소와 XY 좌표는 별도로 저장되야 하는 여러 정보를 포함하고 있다.
- 주소, 거리명
- X 좌표, Y 좌표
날짜 항목은 월, 일, 연도 요소를 포함하고 있어 덜 명확하다.
그러나 SQL에는 `DATE` 데이터 유형이 있으며, 날짜는 이 형식을 사용하여 저장되어야 한다.
월, 일 또는 연도를 별도로 작업해야 하는 경우,
데이터베이스 소프트웨어에서 사용 가능한 SQL 함수를 사용할 수 있다(예: SQLite [`EXTRACT`](https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm) 또는 [`STRFTIME`](https://www.sqlite.org/lang_datefunc.html)).
:::
### 알려진 날짜별로 정렬 {.unnumbered}
날짜가 알려지지 않은 (즉 `null`) 항목은 빼고, 날짜 순으로 `Visited` 테이블에 있는 레코드를 정렬한 쿼리를 작성하시오.
::: {.content-visible when-format="revealjs"}
```sql
SELECT * FROM Visited WHERE dated IS NOT NULL ORDER BY dated ASC;
```
| id | site | dated |
| ----- | ------ | ---------- |
| 619 | DR-1 | 1927-02-08 |
| 622 | DR-1 | 1927-02-10 |
| 734 | DR-3 | 1930-01-07 |
| 735 | DR-3 | 1930-01-12 |
| 751 | DR-3 | 1930-02-26 |
| 837 | MSK-4 | 1932-01-14 |
| 844 | DR-1 | 1932-03-22 |
:::
### 집합에서 NULL {.unnumbered}
다음 쿼리가 어떤 결과를 생성할 것으로 예상하는가?
```sql
SELECT * FROM Visited WHERE dated IN ('1927-02-08', NULL);
```
실제로 어떤 결과를 생성하는가?
::: {.content-visible when-format="revealjs"}
위 쿼리는 '1927-02-08' 또는 NULL인 'dated' 행을 반환할 것으로 예상할 수 있다.
하지만 실제로는 '1927-02-08'인 행만 반환한다.
이는 다음과 같은 더 간단한 쿼리에서 얻을 수 있는 결과와 동일하다:
```sql
SELECT * FROM Visited WHERE dated IN ('1927-02-08');
```
이런 결과가 나타나는 이유는 `IN` 연산자가 *값들*의 집합과 작동하는데,
NULL은 정의상 값이 아니며 따라서 단순히 무시되기 때문이다.
실제로 NULL을 포함시키고자 한다면,
쿼리를 다음과 같이 IS NULL 조건을 사용하여 다시 작성해야 한다:
```sql
SELECT * FROM Visited WHERE dated = '1927-02-08' OR dated IS NULL;
```
이 쿼리는 'dated'가 '1927-02-08'이거나 NULL인 행을 모두 반환한다.
:::
### 표식값 장단점 {.unnumbered}
일부 데이터베이스 설계자들은 `null` 대신 표식값(sentienl value)을 사용하여 누락된 데이터를 표시하는 것을 선호한다. 예를 들어, 누락된 날짜에 "0000-00-00"을 사용하거나, 염도나 방사능 측정치가 누락된 경우 -1.0을 사용한다(실제 측정치는 음수가 될 수 없으므로).
이러한 접근방법이 단순화시킨 것은 무엇인가?
어떤 부담이나 위험을 도입하는가?
::: {.content-visible when-format="revealjs"}
표식값은 누락된 데이터의 존재를 명확히 표현한다.
`null`보다 직관적으로 이해될 수 있고, 특정 값으로 데이터를 표시하면 null 값을 다룰 때 발생할 수 있는 복잡성(예: IS NULL 조건)을 피할 수 있다.
표식값은 실제 데이터와 혼동될 위험이 있다.
예를 들어, "0000-00-00"이나 -1.0이 실제 측정치로 잘못 해석될 수 있다. 센티넬 값은 데이터 분석 시 추가적인 검증 단계를 필요로 한다. 예를 들어, 평균을 계산할 때 -1.0과 같은 표식값을 제외해야 한다. 센티넬 값의 사용은 데이터베이스 설계 및 유지 관리를 더 복잡하게 만들 수 있다. 모든 사용자 및 개발자가 표식값의 의미를 정확히 이해하고 있어야 한다.
:::
### 기본 키 식별하기 {.unnumbered}
다음 테이블에 기본 키는 무엇인가?
즉, 어떤 값 혹은 값들을 조합해야 레코드를 유일무이하게 식별해낼 수 있을까?
| latitude | longitude | date | temperature |
| -------- | --------- | ---------- | ----------- |
| 57\.3 | \-22.5 | 2015-01-09 | \-14.2 |
::: {.content-visible when-format="revealjs"}
위도, 경도 및 날짜는 모두 온도 기록을 고유하게 식별하는 데 필요하다.
:::